source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py @ 12276

Last change on this file since 12276 was 12258, checked in by Henrik Bettermann, 10 years ago

Change contract_id generation algorithm. Use Universally Unique IDentifiers instead of consecutive numbers.

  • Property svn:keywords set to Id
File size: 48.1 KB
Line 
1## $Id: browser.py 12258 2014-12-18 14:44:30Z henrik $
2##
3## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for customers and related components.
19"""
20
21import sys
22import grok
23import pytz
24import os
25from urllib import urlencode
26from datetime import datetime
27from zope.event import notify
28from zope.i18n import translate
29from zope.catalog.interfaces import ICatalog
30from zope.component import queryUtility, getUtility, createObject
31from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
32from zope.formlib.textwidgets import BytesDisplayWidget
33from zope.security import checkPermission
34from hurry.workflow.interfaces import (
35    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
36from waeup.ikoba.interfaces import MessageFactory as _
37from waeup.ikoba.interfaces import (
38    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils,
39    IPasswordValidator, IUserAccount,
40    VERIFIED, REJECTED, EXPIRED, APPROVED)
41from waeup.ikoba.browser.layout import (
42    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
43    IkobaForm, NullValidator, jsaction, action, UtilityView)
44from waeup.ikoba.widgets.datewidget import (
45    FriendlyDateWidget, FriendlyDateDisplayWidget,
46    FriendlyDatetimeDisplayWidget)
47from waeup.ikoba.browser.pages import ContactAdminForm
48from waeup.ikoba.browser.breadcrumbs import Breadcrumb
49from waeup.ikoba.browser.interfaces import ICaptchaManager
50from waeup.ikoba.mandates.mandate import PasswordMandate
51from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget
52from waeup.ikoba.utils.helpers import get_current_principal, to_timezone, now
53from waeup.ikoba.customers.interfaces import (
54    ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils,
55    ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate,
56    ICustomerPDFDocument, IContractsContainer, IContract, IContractEdit,
57    ISampleContract,
58    )
59from waeup.ikoba.customers.catalog import search
60
61grok.context(IIkobaObject)
62
63WARNING = _('You can not edit your document after final submission.'
64            ' You really want to submit?')
65
66
67# Save function used for save methods in pages
68def msave(view, **data):
69    changed_fields = view.applyData(view.context, **data)
70    # Turn list of lists into single list
71    if changed_fields:
72        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
73    if 'product_object' in changed_fields and data['product_object'] is not None:
74        view.context.last_product_id = data['product_object'].product_id
75        notify(grok.ObjectModifiedEvent(view.context))
76    fields_string = ' + '.join(changed_fields)
77    view.flash(_('Form has been saved.'))
78    if fields_string:
79        view.context.writeLogMessage(
80            view, '%s - saved: %s' % (view.context.__name__, fields_string))
81    return
82
83
84def emit_lock_message(view):
85    """Flash a lock message.
86    """
87    view.flash(_('The requested form is locked (read-only).'), type="warning")
88    view.redirect(view.url(view.context))
89    return
90
91
92class CustomersBreadcrumb(Breadcrumb):
93    """A breadcrumb for the customers container.
94    """
95    grok.context(ICustomersContainer)
96    title = _('Customers')
97
98    @property
99    def target(self):
100        user = get_current_principal()
101        if getattr(user, 'user_type', None) == 'customer':
102            return None
103        return self.viewname
104
105
106class CustomerBreadcrumb(Breadcrumb):
107    """A breadcrumb for the customer container.
108    """
109    grok.context(ICustomer)
110
111    def title(self):
112        return self.context.display_fullname
113
114
115class CustomersContainerPage(IkobaPage):
116    """The standard view for customer containers.
117    """
118    grok.context(ICustomersContainer)
119    grok.name('index')
120    grok.require('waeup.viewCustomersContainer')
121    grok.template('containerpage')
122    label = _('Find customers')
123    search_button = _('Find customer(s)')
124    pnav = 4
125
126    def update(self, *args, **kw):
127        form = self.request.form
128        self.hitlist = []
129        if form.get('searchtype', None) == 'suspended':
130            self.searchtype = form['searchtype']
131            self.searchterm = None
132        elif 'searchterm' in form and form['searchterm']:
133            self.searchterm = form['searchterm']
134            self.searchtype = form['searchtype']
135        elif 'old_searchterm' in form:
136            self.searchterm = form['old_searchterm']
137            self.searchtype = form['old_searchtype']
138        else:
139            if 'search' in form:
140                self.flash(_('Empty search string'), type="warning")
141            return
142        if self.searchtype == 'current_session':
143            try:
144                self.searchterm = int(self.searchterm)
145            except ValueError:
146                self.flash(_('Only year dates allowed (e.g. 2011).'),
147                           type="danger")
148                return
149        self.hitlist = search(query=self.searchterm,
150            searchtype=self.searchtype, view=self)
151        if not self.hitlist:
152            self.flash(_('No customer found.'), type="warning")
153        return
154
155
156class CustomersContainerManagePage(IkobaPage):
157    """The manage page for customer containers.
158    """
159    grok.context(ICustomersContainer)
160    grok.name('manage')
161    grok.require('waeup.manageCustomer')
162    grok.template('containermanagepage')
163    pnav = 4
164    label = _('Manage customer section')
165    search_button = _('Find customer(s)')
166    remove_button = _('Remove selected')
167
168    def update(self, *args, **kw):
169        form = self.request.form
170        self.hitlist = []
171        if form.get('searchtype', None) == 'suspended':
172            self.searchtype = form['searchtype']
173            self.searchterm = None
174        elif 'searchterm' in form and form['searchterm']:
175            self.searchterm = form['searchterm']
176            self.searchtype = form['searchtype']
177        elif 'old_searchterm' in form:
178            self.searchterm = form['old_searchterm']
179            self.searchtype = form['old_searchtype']
180        else:
181            if 'search' in form:
182                self.flash(_('Empty search string'), type="warning")
183            return
184        if self.searchtype == 'current_session':
185            try:
186                self.searchterm = int(self.searchterm)
187            except ValueError:
188                self.flash(_('Only year dates allowed (e.g. 2011).'),
189                           type="danger")
190                return
191        if not 'entries' in form:
192            self.hitlist = search(query=self.searchterm,
193                searchtype=self.searchtype, view=self)
194            if not self.hitlist:
195                self.flash(_('No customer found.'), type="warning")
196            if 'remove' in form:
197                self.flash(_('No item selected.'), type="warning")
198            return
199        entries = form['entries']
200        if isinstance(entries, basestring):
201            entries = [entries]
202        deleted = []
203        for entry in entries:
204            if 'remove' in form:
205                del self.context[entry]
206                deleted.append(entry)
207        self.hitlist = search(query=self.searchterm,
208            searchtype=self.searchtype, view=self)
209        if len(deleted):
210            self.flash(_('Successfully removed: ${a}',
211                mapping={'a': ','.join(deleted)}))
212        return
213
214
215class CustomerAddFormPage(IkobaAddFormPage):
216    """Add-form to add a customer.
217    """
218    grok.context(ICustomersContainer)
219    grok.require('waeup.manageCustomer')
220    grok.name('addcustomer')
221    form_fields = grok.AutoFields(ICustomer).select(
222        'firstname', 'middlename', 'lastname', 'reg_number')
223    label = _('Add customer')
224    pnav = 4
225
226    @action(_('Create customer record'), style='primary')
227    def addCustomer(self, **data):
228        customer = createObject(u'waeup.Customer')
229        self.applyData(customer, **data)
230        self.context.addCustomer(customer)
231        self.flash(_('Customer created.'))
232        self.redirect(self.url(self.context[customer.customer_id], 'index'))
233        return
234
235
236class LoginAsCustomerStep1(IkobaEditFormPage):
237    """ View to temporarily set a customer password.
238    """
239    grok.context(ICustomer)
240    grok.name('loginasstep1')
241    grok.require('waeup.loginAsCustomer')
242    grok.template('loginasstep1')
243    pnav = 4
244
245    def label(self):
246        return _(u'Set temporary password for ${a}',
247            mapping={'a': self.context.display_fullname})
248
249    @action('Set password now', style='primary')
250    def setPassword(self, *args, **data):
251        ikoba_utils = getUtility(IIkobaUtils)
252        password = ikoba_utils.genPassword()
253        self.context.setTempPassword(self.request.principal.id, password)
254        self.context.writeLogMessage(
255            self, 'temp_password generated: %s' % password)
256        args = {'password': password}
257        self.redirect(self.url(self.context) +
258            '/loginasstep2?%s' % urlencode(args))
259        return
260
261
262class LoginAsCustomerStep2(IkobaPage):
263    """ View to temporarily login as customer with a temporary password.
264    """
265    grok.context(ICustomer)
266    grok.name('loginasstep2')
267    grok.require('waeup.Public')
268    grok.template('loginasstep2')
269    login_button = _('Login now')
270    pnav = 4
271
272    def label(self):
273        return _(u'Login as ${a}',
274            mapping={'a': self.context.customer_id})
275
276    def update(self, SUBMIT=None, password=None):
277        self.password = password
278        if SUBMIT is not None:
279            self.flash(_('You successfully logged in as customer.'))
280            self.redirect(self.url(self.context))
281        return
282
283
284class CustomerBaseDisplayFormPage(IkobaDisplayFormPage):
285    """ Page to display customer base data
286    """
287    grok.context(ICustomer)
288    grok.name('index')
289    grok.require('waeup.viewCustomer')
290    grok.template('basepage')
291    form_fields = grok.AutoFields(ICustomer).omit(
292        'password', 'suspended', 'suspended_comment')
293    pnav = 4
294
295    @property
296    def label(self):
297        if self.context.suspended:
298            return _('${a}: Base Data (account deactivated)',
299                mapping={'a': self.context.display_fullname})
300        return  _('${a}: Base Data',
301            mapping={'a': self.context.display_fullname})
302
303    @property
304    def hasPassword(self):
305        if self.context.password:
306            return _('set')
307        return _('unset')
308
309
310class ContactCustomerForm(ContactAdminForm):
311    grok.context(ICustomer)
312    grok.name('contactcustomer')
313    grok.require('waeup.viewCustomer')
314    pnav = 4
315    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
316
317    def update(self, subject=u'', body=u''):
318        super(ContactCustomerForm, self).update()
319        self.form_fields.get('subject').field.default = subject
320        self.form_fields.get('body').field.default = body
321        return
322
323    def label(self):
324        return _(u'Send message to ${a}',
325            mapping={'a': self.context.display_fullname})
326
327    @action('Send message now', style='primary')
328    def send(self, *args, **data):
329        try:
330            email = self.request.principal.email
331        except AttributeError:
332            email = self.config.email_admin
333        usertype = getattr(self.request.principal,
334                           'user_type', 'system').title()
335        ikoba_utils = getUtility(IIkobaUtils)
336        success = ikoba_utils.sendContactForm(
337                self.request.principal.title, email,
338                self.context.display_fullname, self.context.email,
339                self.request.principal.id,usertype,
340                self.config.name,
341                data['body'], data['subject'])
342        if success:
343            self.flash(_('Your message has been sent.'))
344        else:
345            self.flash(_('An smtp server error occurred.'), type="danger")
346        return
347
348
349class CustomerBaseManageFormPage(IkobaEditFormPage):
350    """ View to manage customer base data
351    """
352    grok.context(ICustomer)
353    grok.name('manage_base')
354    grok.require('waeup.manageCustomer')
355    form_fields = grok.AutoFields(ICustomer).omit(
356        'customer_id', 'adm_code', 'suspended')
357    grok.template('basemanagepage')
358    label = _('Manage base data')
359    pnav = 4
360
361    def update(self):
362        super(CustomerBaseManageFormPage, self).update()
363        self.wf_info = IWorkflowInfo(self.context)
364        return
365
366    @action(_('Save'), style='primary')
367    def save(self, **data):
368        form = self.request.form
369        password = form.get('password', None)
370        password_ctl = form.get('control_password', None)
371        if password:
372            validator = getUtility(IPasswordValidator)
373            errors = validator.validate_password(password, password_ctl)
374            if errors:
375                self.flash(' '.join(errors), type="danger")
376                return
377        changed_fields = self.applyData(self.context, **data)
378        # Turn list of lists into single list
379        if changed_fields:
380            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
381        else:
382            changed_fields = []
383        if password:
384            # Now we know that the form has no errors and can set password
385            IUserAccount(self.context).setPassword(password)
386            changed_fields.append('password')
387        fields_string = ' + '.join(changed_fields)
388        self.flash(_('Form has been saved.'))
389        if fields_string:
390            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
391        return
392
393
394class CustomerTriggerTransitionFormPage(IkobaEditFormPage):
395    """ View to trigger customer workflow transitions
396    """
397    grok.context(ICustomer)
398    grok.name('trigtrans')
399    grok.require('waeup.triggerTransition')
400    grok.template('trigtrans')
401    label = _('Trigger registration transition')
402    pnav = 4
403
404    def getTransitions(self):
405        """Return a list of dicts of allowed transition ids and titles.
406
407        Each list entry provides keys ``name`` and ``title`` for
408        internal name and (human readable) title of a single
409        transition.
410        """
411        wf_info = IWorkflowInfo(self.context)
412        allowed_transitions = [t for t in wf_info.getManualTransitions()]
413        return [dict(name='', title=_('No transition'))] +[
414            dict(name=x, title=y) for x, y in allowed_transitions]
415
416    @action(_('Apply now'), style='primary')
417    def apply(self, **data):
418        form = self.request.form
419        if 'transition' in form and form['transition']:
420            transition_id = form['transition']
421            wf_info = IWorkflowInfo(self.context)
422            wf_info.fireTransition(transition_id)
423            self.flash(_("Transition '%s' executed." % transition_id))
424            self.redirect(self.url(self.context))
425        return
426
427
428class CustomerActivatePage(UtilityView, grok.View):
429    """ Activate customer account
430    """
431    grok.context(ICustomer)
432    grok.name('activate')
433    grok.require('waeup.manageCustomer')
434
435    def update(self):
436        self.context.suspended = False
437        self.context.writeLogMessage(self, 'account activated')
438        history = IObjectHistory(self.context)
439        history.addMessage('Customer account activated')
440        self.flash(_('Customer account has been activated.'))
441        self.redirect(self.url(self.context))
442        return
443
444    def render(self):
445        return
446
447
448class CustomerDeactivatePage(UtilityView, grok.View):
449    """ Deactivate customer account
450    """
451    grok.context(ICustomer)
452    grok.name('deactivate')
453    grok.require('waeup.manageCustomer')
454
455    def update(self):
456        self.context.suspended = True
457        self.context.writeLogMessage(self, 'account deactivated')
458        history = IObjectHistory(self.context)
459        history.addMessage('Customer account deactivated')
460        self.flash(_('Customer account has been deactivated.'))
461        self.redirect(self.url(self.context))
462        return
463
464    def render(self):
465        return
466
467
468class CustomerHistoryPage(IkobaPage):
469    """ Page to display customer history
470    """
471    grok.context(ICustomer)
472    grok.name('history')
473    grok.require('waeup.viewCustomer')
474    grok.template('customerhistory')
475    pnav = 4
476
477    @property
478    def label(self):
479        return _('${a}: History', mapping={'a':self.context.display_fullname})
480
481
482class CustomerRequestPasswordPage(IkobaAddFormPage):
483    """Captcha'd password request page for customers.
484    """
485    grok.name('requestpw')
486    grok.require('waeup.Anonymous')
487    grok.template('requestpw')
488    form_fields = grok.AutoFields(ICustomerRequestPW)
489    label = _('Request password for first-time login')
490
491    def update(self):
492        # Handle captcha
493        self.captcha = getUtility(ICaptchaManager).getCaptcha()
494        self.captcha_result = self.captcha.verify(self.request)
495        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
496        return
497
498    def _redirect(self, email, password, customer_id):
499        # Forward only email address to landing page in base package.
500        self.redirect(self.url(self.context, 'requestpw_complete',
501            data=dict(email=email)))
502        return
503
504    def _pw_used(self):
505        # XXX: False if password has not been used. We need an extra
506        #      attribute which remembers if customer logged in.
507        return True
508
509    @action(_('Send login credentials to email address'), style='primary')
510    def get_credentials(self, **data):
511        if not self.captcha_result.is_valid:
512            # Captcha will display error messages automatically.
513            # No need to flash something.
514            return
515        number = data.get('number','')
516        firstname = data.get('firstname','')
517        cat = getUtility(ICatalog, name='customers_catalog')
518        results = list(
519            cat.searchResults(reg_number=(number, number)))
520        if results:
521            customer = results[0]
522            if getattr(customer,'firstname',None) is None:
523                self.flash(_('An error occurred.'), type="danger")
524                return
525            elif customer.firstname.lower() != firstname.lower():
526                # Don't tell the truth here. Anonymous must not
527                # know that a record was found and only the firstname
528                # verification failed.
529                self.flash(_('No customer found.'), type="warning")
530                return
531            elif customer.password is not None and self._pw_used:
532                self.flash(_('Your password has already been set and used. '
533                             'Please proceed to the login page.'),
534                           type="warning")
535                return
536            # Store email address but nothing else.
537            customer.email = data['email']
538            notify(grok.ObjectModifiedEvent(customer))
539        else:
540            # No record found, this is the truth.
541            self.flash(_('No customer found.'), type="warning")
542            return
543
544        ikoba_utils = getUtility(IIkobaUtils)
545        password = ikoba_utils.genPassword()
546        mandate = PasswordMandate()
547        mandate.params['password'] = password
548        mandate.params['user'] = customer
549        site = grok.getSite()
550        site['mandates'].addMandate(mandate)
551        # Send email with credentials
552        args = {'mandate_id':mandate.mandate_id}
553        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
554        url_info = u'Confirmation link: %s' % mandate_url
555        msg = _('You have successfully requested a password for the')
556        if ikoba_utils.sendCredentials(IUserAccount(customer),
557            password, url_info, msg):
558            email_sent = customer.email
559        else:
560            email_sent = None
561        self._redirect(email=email_sent, password=password,
562            customer_id=customer.customer_id)
563        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
564        self.context.logger.info(
565            '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
566        return
567
568
569class CustomerCreateAccountPage(IkobaAddFormPage):
570    """Captcha'd account creation page for customers.
571    """
572    grok.name('createaccount')
573    grok.require('waeup.Anonymous')
574    grok.template('createaccount')
575    form_fields = grok.AutoFields(ICustomerCreate)
576    label = _('Create customer account')
577
578    def update(self):
579        # Handle captcha
580        self.captcha = getUtility(ICaptchaManager).getCaptcha()
581        self.captcha_result = self.captcha.verify(self.request)
582        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
583        return
584
585    def _redirect(self, email, password, customer_id):
586        # Forward only email address to landing page in base package.
587        self.redirect(self.url(self.context, 'requestpw_complete',
588            data=dict(email=email)))
589        return
590
591    @action(_('Send login credentials to email address'), style='primary')
592    def create_account(self, **data):
593        if not self.captcha_result.is_valid:
594            # Captcha will display error messages automatically.
595            # No need to flash something.
596            return
597        customer = createObject(u'waeup.Customer')
598        customer.firstname = data.get('firstname','')
599        customer.middlename = data.get('middlename','')
600        customer.lastname = data.get('lastname','')
601        customer.email = data.get('email','')
602        self.context['customers'].addCustomer(customer)
603        ikoba_utils = getUtility(IIkobaUtils)
604        password = ikoba_utils.genPassword()
605        mandate = PasswordMandate()
606        mandate.params['password'] = password
607        mandate.params['user'] = customer
608        site = grok.getSite()
609        site['mandates'].addMandate(mandate)
610        # Send email with credentials
611        args = {'mandate_id':mandate.mandate_id}
612        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
613        url_info = u'Confirmation link: %s' % mandate_url
614        msg = _('You have successfully created a customer account for the')
615        if ikoba_utils.sendCredentials(IUserAccount(customer),
616            password, url_info, msg):
617            email_sent = customer.email
618        else:
619            email_sent = None
620        self._redirect(email=email_sent, password=password,
621            customer_id=customer.customer_id)
622        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
623        self.context.logger.info(
624            '%s - %s - %s' % (ob_class, customer.customer_id, email_sent))
625        return
626
627
628class CustomerRequestPasswordEmailSent(IkobaPage):
629    """Landing page after successful password request.
630
631    """
632    grok.name('requestpw_complete')
633    grok.require('waeup.Public')
634    grok.template('requestpwmailsent')
635    label = _('Your request was successful.')
636
637    def update(self, email=None, customer_id=None, password=None):
638        self.email = email
639        self.password = password
640        self.customer_id = customer_id
641        return
642
643
644class CustomerFilesUploadPage(IkobaPage):
645    """ View to upload files by customer
646
647    We use this page only to upload a passport picture (portrait).
648    """
649    grok.context(ICustomer)
650    grok.name('change_portrait')
651    grok.require('waeup.uploadCustomerFile')
652    grok.template('filesuploadpage')
653    label = _('Change portrait')
654    pnav = 4
655
656    def update(self):
657        CUSTMANAGE_STATES = getUtility(
658            ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES
659        if self.context.customer.state not in CUSTMANAGE_STATES:
660            emit_lock_message(self)
661            return
662        super(CustomerFilesUploadPage, self).update()
663        return
664
665# Pages for customers
666
667
668class CustomerBaseEditFormPage(IkobaEditFormPage):
669    """ View to edit customer base data
670    """
671    grok.context(ICustomer)
672    grok.name('edit_base')
673    grok.require('waeup.handleCustomer')
674    form_fields = grok.AutoFields(ICustomer).select(
675        'email', 'phone')
676    label = _('Edit base data')
677    pnav = 4
678
679    @action(_('Save'), style='primary')
680    def save(self, **data):
681        msave(self, **data)
682        return
683
684
685class CustomerChangePasswordPage(IkobaEditFormPage):
686    """ View to edit customer passords
687    """
688    grok.context(ICustomer)
689    grok.name('changepassword')
690    grok.require('waeup.handleCustomer')
691    grok.template('changepassword')
692    label = _('Change password')
693    pnav = 4
694
695    @action(_('Save'), style='primary')
696    def save(self, **data):
697        form = self.request.form
698        password = form.get('change_password', None)
699        password_ctl = form.get('change_password_repeat', None)
700        if password:
701            validator = getUtility(IPasswordValidator)
702            errors = validator.validate_password(password, password_ctl)
703            if not errors:
704                IUserAccount(self.context).setPassword(password)
705                self.context.writeLogMessage(self, 'saved: password')
706                self.flash(_('Password changed.'))
707            else:
708                self.flash(' '.join(errors), type="warning")
709        return
710
711class CustomerBasePDFFormPage(IkobaDisplayFormPage):
712    """ Page to display customer base data in pdf files.
713    """
714
715    def __init__(self, context, request, omit_fields=()):
716        self.omit_fields = omit_fields
717        super(CustomerBasePDFFormPage, self).__init__(context, request)
718
719    @property
720    def form_fields(self):
721        form_fields = grok.AutoFields(ICustomer)
722        for field in self.omit_fields:
723            form_fields = form_fields.omit(field)
724        return form_fields
725
726# Pages for customer documents
727
728class DocumentsBreadcrumb(Breadcrumb):
729    """A breadcrumb for the documents container.
730    """
731    grok.context(ICustomerDocumentsContainer)
732    title = _('Documents')
733
734
735class DocumentBreadcrumb(Breadcrumb):
736    """A breadcrumb for the customer container.
737    """
738    grok.context(ICustomerDocument)
739
740    @property
741    def title(self):
742        return self.context.document_id
743
744
745class DocumentsManageFormPage(IkobaEditFormPage):
746    """ Page to manage the customer documents
747
748    This manage form page is for both customers and customers officers.
749    """
750    grok.context(ICustomerDocumentsContainer)
751    grok.name('index')
752    grok.require('waeup.viewCustomer')
753    form_fields = grok.AutoFields(ICustomerDocumentsContainer)
754    grok.template('documentsmanagepage')
755    pnav = 4
756
757    @property
758    def manage_documents_allowed(self):
759        return checkPermission('waeup.editCustomerDocuments', self.context)
760
761    def unremovable(self, document):
762        usertype = getattr(self.request.principal, 'user_type', None)
763        if not usertype:
764            return False
765        if not self.manage_documents_allowed:
766            return True
767        return (self.request.principal.user_type == 'customer' and \
768            document.state in (VERIFIED, REJECTED, EXPIRED))
769
770    @property
771    def label(self):
772        return _('${a}: Documents',
773            mapping = {'a':self.context.__parent__.display_fullname})
774
775    @action(_('Add document'), validator=NullValidator, style='primary')
776    def addDocument(self, **data):
777        self.redirect(self.url(self.context, 'adddoc'))
778        return
779
780    @jsaction(_('Remove selected documents'))
781    def delDocument(self, **data):
782        form = self.request.form
783        if 'val_id' in form:
784            child_id = form['val_id']
785        else:
786            self.flash(_('No document selected.'), type="warning")
787            self.redirect(self.url(self.context))
788            return
789        if not isinstance(child_id, list):
790            child_id = [child_id]
791        deleted = []
792        for id in child_id:
793            # Customers are not allowed to remove used documents
794            document = self.context.get(id, None)
795            if document is not None and not self.unremovable(document):
796                del self.context[id]
797                deleted.append(id)
798        if len(deleted):
799            self.flash(_('Successfully removed: ${a}',
800                mapping = {'a': ', '.join(deleted)}))
801            self.context.writeLogMessage(
802                self,'removed: %s' % ', '.join(deleted))
803        self.redirect(self.url(self.context))
804        return
805
806
807class DocumentAddFormPage(IkobaAddFormPage):
808    """ Page to add a document
809
810    This add form page is for both customers and customers officers.
811    """
812    grok.context(ICustomerDocumentsContainer)
813    grok.name('adddoc')
814    grok.template('documentaddform')
815    grok.require('waeup.editCustomerDocuments')
816    label = _('Add document')
817    pnav = 4
818
819    form_fields = grok.AutoFields(ICustomerDocument).omit('document_id')
820
821    @property
822    def selectable_doctypes(self):
823        doctypes = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT
824        return sorted(doctypes.items())
825
826    @action(_('Add document'), style='primary')
827    def createDocument(self, **data):
828        form = self.request.form
829        customer = self.context.__parent__
830        doctype = form.get('doctype', None)
831        # Here we can create various instances of CustomerDocument derived
832        # classes depending on the doctype parameter given in form.
833        document = createObject('waeup.%s' % doctype)
834        self.applyData(document, **data)
835        self.context.addDocument(document)
836        doctype = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT[doctype]
837        self.flash(_('${a} added.', mapping = {'a': doctype}))
838        self.context.writeLogMessage(
839            self,'added: %s %s' % (doctype, document.document_id))
840        self.redirect(self.url(self.context))
841        return
842
843    @action(_('Cancel'), validator=NullValidator)
844    def cancel(self, **data):
845        self.redirect(self.url(self.context))
846
847
848class DocumentDisplayFormPage(IkobaDisplayFormPage):
849    """ Page to view a document
850    """
851    grok.context(ICustomerDocument)
852    grok.name('index')
853    grok.require('waeup.viewCustomer')
854    grok.template('documentpage')
855    pnav = 4
856
857    @property
858    def form_fields(self):
859        return grok.AutoFields(self.context.form_fields_interface)
860
861    @property
862    def label(self):
863        return _('${a}', mapping = {'a':self.context.title})
864
865
866class DocumentManageFormPage(IkobaEditFormPage):
867    """ Page to edit a document
868    """
869    grok.context(ICustomerDocument)
870    grok.name('manage')
871    grok.require('waeup.manageCustomer')
872    grok.template('documenteditpage')
873    pnav = 4
874    deletion_warning = _('Are you sure?')
875
876    @property
877    def form_fields(self):
878        return grok.AutoFields(
879            self.context.form_fields_interface).omit('document_id')
880
881    def update(self):
882        if not self.context.is_editable_by_manager:
883            emit_lock_message(self)
884            return
885        return super(DocumentManageFormPage, self).update()
886
887    @property
888    def label(self):
889        return _('${a}', mapping = {'a':self.context.title})
890
891    @action(_('Save'), style='primary')
892    def save(self, **data):
893        msave(self, **data)
894        return
895
896
897class DocumentEditFormPage(DocumentManageFormPage):
898    """ Page to edit a document
899    """
900    grok.name('edit')
901    grok.require('waeup.handleCustomer')
902
903    def update(self):
904        if not self.context.is_editable_by_customer:
905            emit_lock_message(self)
906            return
907        return super(DocumentEditFormPage, self).update()
908
909    @action(_('Save'), style='primary')
910    def save(self, **data):
911        msave(self, **data)
912        return
913
914    @action(_('Final Submit'), warning=WARNING)
915    def finalsubmit(self, **data):
916        msave(self, **data)
917        IWorkflowInfo(self.context).fireTransition('submit')
918        self.flash(_('Form has been submitted.'))
919        self.redirect(self.url(self.context))
920        return
921
922
923class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
924    """ View to trigger customer document transitions
925    """
926    grok.context(ICustomerDocument)
927    grok.name('trigtrans')
928    grok.require('waeup.triggerTransition')
929    grok.template('trigtrans')
930    label = _('Trigger document transition')
931    pnav = 4
932
933    def update(self):
934        return super(IkobaEditFormPage, self).update()
935
936    def getTransitions(self):
937        """Return a list of dicts of allowed transition ids and titles.
938
939        Each list entry provides keys ``name`` and ``title`` for
940        internal name and (human readable) title of a single
941        transition.
942        """
943        wf_info = IWorkflowInfo(self.context)
944        allowed_transitions = [t for t in wf_info.getManualTransitions()]
945        return [dict(name='', title=_('No transition'))] +[
946            dict(name=x, title=y) for x, y in allowed_transitions]
947
948    @action(_('Apply now'), style='primary')
949    def apply(self, **data):
950        form = self.request.form
951        if 'transition' in form and form['transition']:
952            transition_id = form['transition']
953            wf_info = IWorkflowInfo(self.context)
954            try:
955                wf_info.fireTransition(transition_id)
956                self.flash(_("Transition '%s' executed." % transition_id))
957            except InvalidTransitionError, error:
958                self.flash(error, type="warning")
959            self.redirect(self.url(self.context))
960        return
961
962class PDFDocumentsOverviewPage(UtilityView, grok.View):
963    """Deliver an overview slip.
964    """
965    grok.context(ICustomerDocumentsContainer)
966    grok.name('documents_overview_slip.pdf')
967    grok.require('waeup.viewCustomer')
968    prefix = 'form'
969
970    omit_fields = ('suspended', 'sex',
971                   'suspended_comment',)
972
973    form_fields = None
974
975    @property
976    def label(self):
977        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
978        return translate(_('Documents of'),
979            'waeup.ikoba', target_language=portal_language) \
980            + ' %s' % self.context.customer.display_fullname
981
982    @property
983    def tabletitle(self):
984        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
985        tabletitle = []
986        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
987            target_language=portal_language))
988        return tabletitle
989
990    def render(self):
991        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
992        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
993        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
994        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
995        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
996        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
997        tableheader = []
998        tabledata = []
999        contenttitle = []
1000        for i in range(1,3):
1001            tabledata.append(sorted(
1002                [value for value in self.context.values()]))
1003            tableheader.append([(Id, 'document_id', 2),
1004                             (Title, 'title', 6),
1005                             (Type, 'translated_class_name', 6),
1006                             (State, 'translated_state', 2),
1007                             (LT, 'formatted_transition_date', 3),
1008                             ])
1009        customerview = CustomerBasePDFFormPage(self.context.customer,
1010            self.request, self.omit_fields)
1011        customers_utils = getUtility(ICustomersUtils)
1012        return customers_utils.renderPDF(
1013            self, 'overview_slip.pdf',
1014            self.context.customer, customerview,
1015            tableheader=tableheader,
1016            tabledata=tabledata,
1017            omit_fields=self.omit_fields)
1018
1019
1020class PDFDocumentSlipPage(UtilityView, grok.View):
1021    """Deliver pdf file including metadata.
1022    """
1023    grok.context(ICustomerDocument)
1024    grok.name('document_slip.pdf')
1025    grok.require('waeup.viewCustomer')
1026    prefix = 'form'
1027
1028    omit_fields = ('suspended', 'sex',
1029                   'suspended_comment',)
1030
1031    form_fields =()
1032
1033    @property
1034    def label(self):
1035        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1036        return '%s of %s\nTitle: %s' % (
1037            self.context.translated_class_name,
1038            self.context.customer.display_fullname,
1039            self.context.title)
1040
1041    def render(self):
1042        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1043        customerview = CustomerBasePDFFormPage(self.context.customer,
1044            self.request, self.omit_fields)
1045        customers_utils = getUtility(ICustomersUtils)
1046        return customers_utils.renderPDF(
1047            self, 'document_slip.pdf',
1048            self.context.customer, customerview,
1049            omit_fields=self.omit_fields)
1050
1051
1052class PDFMergeDocumentSlipPage(PDFDocumentSlipPage):
1053    """Deliver pdf file including metadata.
1054    """
1055    grok.context(ICustomerPDFDocument)
1056
1057    def render(self):
1058        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1059        customerview = CustomerBasePDFFormPage(self.context.customer,
1060            self.request, self.omit_fields)
1061        customers_utils = getUtility(ICustomersUtils)
1062        if self.context.state == VERIFIED:
1063            watermark_path = os.path.join(
1064                os.path.dirname(__file__), 'static', 'verified.pdf')
1065        else:
1066            watermark_path = os.path.join(
1067                os.path.dirname(__file__), 'static', 'unverified.pdf')
1068        watermark = open(watermark_path, 'rb')
1069        return customers_utils.renderPDF(
1070            self, 'pdfdocument_slip.pdf',
1071            self.context.customer, customerview,
1072            omit_fields=self.omit_fields,
1073            mergefiles=self.context.connected_files,
1074            watermark=watermark)
1075
1076# Pages for customer contracts
1077
1078class ContractsBreadcrumb(Breadcrumb):
1079    """A breadcrumb for the contracts container.
1080    """
1081    grok.context(IContractsContainer)
1082    title = _('Contracts')
1083
1084
1085class ContractBreadcrumb(Breadcrumb):
1086    """A breadcrumb for the customer container.
1087    """
1088    grok.context(IContract)
1089
1090    @property
1091    def title(self):
1092        return self.context.contract_id
1093
1094
1095class ContractsManageFormPage(IkobaEditFormPage):
1096    """ Page to manage the customer contracts
1097
1098    This manage form page is for both customers and officers.
1099    """
1100    grok.context(IContractsContainer)
1101    grok.name('index')
1102    grok.require('waeup.viewCustomer')
1103    form_fields = grok.AutoFields(IContractsContainer)
1104    grok.template('contractsmanagepage')
1105    pnav = 4
1106
1107    @property
1108    def manage_contracts_allowed(self):
1109        return checkPermission('waeup.editContracts', self.context)
1110
1111    def unremovable(self, contract):
1112        usertype = getattr(self.request.principal, 'user_type', None)
1113        if not usertype:
1114            return False
1115        if not self.manage_contracts_allowed:
1116            return True
1117        return (self.request.principal.user_type == 'customer' and \
1118            contract.state in (APPROVED, REJECTED, EXPIRED))
1119
1120    @property
1121    def label(self):
1122        return _('${a}: Contracts',
1123            mapping = {'a':self.context.__parent__.display_fullname})
1124
1125    @action(_('Add contract'), validator=NullValidator, style='primary')
1126    def addContract(self, **data):
1127        self.redirect(self.url(self.context, 'addcontract'))
1128        return
1129
1130    @jsaction(_('Remove selected contracts'))
1131    def delContract(self, **data):
1132        form = self.request.form
1133        if 'val_id' in form:
1134            child_id = form['val_id']
1135        else:
1136            self.flash(_('No contract selected.'), type="warning")
1137            self.redirect(self.url(self.context))
1138            return
1139        if not isinstance(child_id, list):
1140            child_id = [child_id]
1141        deleted = []
1142        for id in child_id:
1143            # Customers are not allowed to remove used contracts
1144            contract = self.context.get(id, None)
1145            if contract is not None and not self.unremovable(contract):
1146                del self.context[id]
1147                deleted.append(id)
1148        if len(deleted):
1149            self.flash(_('Successfully removed: ${a}',
1150                mapping = {'a': ', '.join(deleted)}))
1151            self.context.writeLogMessage(
1152                self,'removed: %s' % ', '.join(deleted))
1153        self.redirect(self.url(self.context))
1154        return
1155
1156
1157class ContractAddFormPage(IkobaAddFormPage):
1158    """ Page to add an contract
1159    """
1160    grok.context(IContractsContainer)
1161    grok.name('addcontract')
1162    grok.template('contractaddform')
1163    grok.require('waeup.editContracts')
1164    label = _('Add contract')
1165    pnav = 4
1166
1167    form_fields = grok.AutoFields(IContract).omit(
1168        'product_object', 'contract_id')
1169
1170    @property
1171    def selectable_contypes(self):
1172        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
1173        return sorted(contypes.items())
1174
1175    @action(_('Add contract'), style='primary')
1176    def createContract(self, **data):
1177        form = self.request.form
1178        customer = self.context.__parent__
1179        contype = form.get('contype', None)
1180        # Here we can create various instances of Contract derived
1181        # classes depending on the contype parameter given in form.
1182        contract = createObject('waeup.%s' % contype)
1183        self.applyData(contract, **data)
1184        self.context.addContract(contract)
1185        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
1186        self.flash(_('${a} added.', mapping = {'a': contype}))
1187        self.context.writeLogMessage(
1188            self,'added: %s %s' % (contype, contract.contract_id))
1189        self.redirect(self.url(self.context))
1190        return
1191
1192    @action(_('Cancel'), validator=NullValidator)
1193    def cancel(self, **data):
1194        self.redirect(self.url(self.context))
1195
1196
1197class ContractDisplayFormPage(IkobaDisplayFormPage):
1198    """ Page to view a contract
1199    """
1200    grok.context(IContract)
1201    grok.name('index')
1202    grok.require('waeup.viewCustomer')
1203    grok.template('contractpage')
1204    pnav = 4
1205
1206    @property
1207    def form_fields(self):
1208        form_fields = grok.AutoFields(self.context.form_fields_interface)
1209        for field in form_fields:
1210            if field.__name__.endswith('_object'):
1211                form_fields[field.__name__].custom_widget = HREFDisplayWidget
1212        return form_fields
1213
1214    @property
1215    def label(self):
1216        return _('${a}', mapping = {'a':self.context.title})
1217
1218
1219class ContractManageFormPage(IkobaEditFormPage):
1220    """ Page to edit a contract
1221    """
1222    grok.context(IContract)
1223    grok.name('manage')
1224    grok.require('waeup.manageCustomer')
1225    grok.template('contracteditpage')
1226    pnav = 4
1227    deletion_warning = _('Are you sure?')
1228
1229    @property
1230    def form_fields(self):
1231        return grok.AutoFields(self.context.form_fields_interface).omit(
1232            'contract_id')
1233
1234    @property
1235    def label(self):
1236        return _('${a}', mapping = {'a':self.context.title})
1237
1238    @action(_('Save'), style='primary')
1239    def save(self, **data):
1240        msave(self, **data)
1241        return
1242
1243
1244class ContractEditFormPage(ContractManageFormPage):
1245    """ Page to edit a contract
1246    """
1247    grok.name('edit')
1248    grok.require('waeup.handleCustomer')
1249
1250    @property
1251    def form_fields(self):
1252        return grok.AutoFields(self.context.edit_form_fields_interface).omit(
1253            'contract_id')
1254
1255    def update(self):
1256        if not self.context.is_editable_by_customer:
1257            emit_lock_message(self)
1258            return
1259        return super(ContractEditFormPage, self).update()
1260
1261    @action(_('Save'), style='primary')
1262    def save(self, **data):
1263        msave(self, **data)
1264        return
1265
1266    @action(_('Apply now (final submit)'), warning=WARNING)
1267    def finalsubmit(self, **data):
1268        msave(self, **data)
1269        IWorkflowInfo(self.context).fireTransition('submit')
1270        self.flash(_('Form has been submitted.'))
1271        self.redirect(self.url(self.context))
1272        return
1273
1274
1275class ContractTriggerTransitionFormPage(IkobaEditFormPage):
1276    """ View to trigger customer contract transitions
1277    """
1278    grok.context(IContract)
1279    grok.name('trigtrans')
1280    grok.require('waeup.triggerTransition')
1281    grok.template('trigtrans')
1282    label = _('Trigger contract transition')
1283    pnav = 4
1284
1285    def update(self):
1286        return super(IkobaEditFormPage, self).update()
1287
1288    def getTransitions(self):
1289        """Return a list of dicts of allowed transition ids and titles.
1290
1291        Each list entry provides keys ``name`` and ``title`` for
1292        internal name and (human readable) title of a single
1293        transition.
1294        """
1295        wf_info = IWorkflowInfo(self.context)
1296        allowed_transitions = [t for t in wf_info.getManualTransitions()]
1297        return [dict(name='', title=_('No transition'))] +[
1298            dict(name=x, title=y) for x, y in allowed_transitions]
1299
1300    @action(_('Apply now'), style='primary')
1301    def apply(self, **data):
1302        form = self.request.form
1303        if 'transition' in form and form['transition']:
1304            transition_id = form['transition']
1305            wf_info = IWorkflowInfo(self.context)
1306            try:
1307                wf_info.fireTransition(transition_id)
1308                self.flash(_("Transition '%s' executed." % transition_id))
1309            except InvalidTransitionError, error:
1310                self.flash(error, type="warning")
1311            self.redirect(self.url(self.context))
1312        return
1313
1314class PDFContractsOverviewPage(UtilityView, grok.View):
1315    """Deliver an overview slip.
1316    """
1317    grok.context(IContractsContainer)
1318    grok.name('contracts_overview_slip.pdf')
1319    grok.require('waeup.viewCustomer')
1320    prefix = 'form'
1321
1322    omit_fields = ('suspended', 'sex',
1323                   'suspended_comment',)
1324
1325    form_fields = None
1326
1327    @property
1328    def label(self):
1329        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1330        return translate(_('Contracts of'),
1331            'waeup.ikoba', target_language=portal_language) \
1332            + ' %s' % self.context.customer.display_fullname
1333
1334    @property
1335    def tabletitle(self):
1336        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1337        tabletitle = []
1338        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
1339            target_language=portal_language))
1340        return tabletitle
1341
1342    def render(self):
1343        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1344        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
1345        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
1346        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
1347        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
1348        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
1349        tableheader = []
1350        tabledata = []
1351        contenttitle = []
1352        for i in range(1,3):
1353            tabledata.append(sorted(
1354                [value for value in self.context.values()]))
1355            tableheader.append([(Id, 'contract_id', 2),
1356                             (Title, 'title', 6),
1357                             (Type, 'translated_class_name', 6),
1358                             (State, 'translated_state', 2),
1359                             (LT, 'formatted_transition_date', 3),
1360                             ])
1361        customerview = CustomerBasePDFFormPage(self.context.customer,
1362            self.request, self.omit_fields)
1363        customers_utils = getUtility(ICustomersUtils)
1364        return customers_utils.renderPDF(
1365            self, 'overview_slip.pdf',
1366            self.context.customer, customerview,
1367            tableheader=tableheader,
1368            tabledata=tabledata,
1369            omit_fields=self.omit_fields)
1370
1371
1372class PDFContractSlipPage(UtilityView, grok.View):
1373    """Deliver pdf file including metadata.
1374    """
1375    grok.context(ISampleContract)
1376    grok.name('contract_slip.pdf')
1377    grok.require('waeup.viewCustomer')
1378    prefix = 'form'
1379
1380    omit_fields = ('suspended', 'sex',
1381                   'suspended_comment',)
1382
1383    form_fields = grok.AutoFields(ISampleContract)
1384
1385    @property
1386    def label(self):
1387        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1388        return '%s of %s\nTitle: %s' % (
1389            self.context.translated_class_name,
1390            self.context.customer.display_fullname,
1391            self.context.title)
1392
1393    def render(self):
1394        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1395        customerview = CustomerBasePDFFormPage(self.context.customer,
1396            self.request, self.omit_fields)
1397        customers_utils = getUtility(ICustomersUtils)
1398        return customers_utils.renderPDF(
1399            self, 'contract_slip.pdf',
1400            self.context.customer, customerview,
1401            omit_fields=self.omit_fields)
Note: See TracBrowser for help on using the repository browser.