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

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

Rename button and redirect after transition to context page.

Remove restwidget.py.

  • Property svn:keywords set to Id
File size: 48.0 KB
Line 
1## $Id: browser.py 12246 2014-12-15 12:49:01Z 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)
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(self.context.form_fields_interface)
879
880    def update(self):
881        if not self.context.is_editable_by_manager:
882            emit_lock_message(self)
883            return
884        return super(DocumentManageFormPage, self).update()
885
886    @property
887    def label(self):
888        return _('${a}', mapping = {'a':self.context.title})
889
890    @action(_('Save'), style='primary')
891    def save(self, **data):
892        msave(self, **data)
893        return
894
895
896class DocumentEditFormPage(DocumentManageFormPage):
897    """ Page to edit a document
898    """
899    grok.name('edit')
900    grok.require('waeup.handleCustomer')
901
902    def update(self):
903        if not self.context.is_editable_by_customer:
904            emit_lock_message(self)
905            return
906        return super(DocumentEditFormPage, self).update()
907
908    @action(_('Save'), style='primary')
909    def save(self, **data):
910        msave(self, **data)
911        return
912
913    @action(_('Final Submit'), warning=WARNING)
914    def finalsubmit(self, **data):
915        msave(self, **data)
916        IWorkflowInfo(self.context).fireTransition('submit')
917        self.flash(_('Form has been submitted.'))
918        self.redirect(self.url(self.context))
919        return
920
921
922class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
923    """ View to trigger customer document transitions
924    """
925    grok.context(ICustomerDocument)
926    grok.name('trigtrans')
927    grok.require('waeup.triggerTransition')
928    grok.template('trigtrans')
929    label = _('Trigger document transition')
930    pnav = 4
931
932    def update(self):
933        return super(IkobaEditFormPage, self).update()
934
935    def getTransitions(self):
936        """Return a list of dicts of allowed transition ids and titles.
937
938        Each list entry provides keys ``name`` and ``title`` for
939        internal name and (human readable) title of a single
940        transition.
941        """
942        wf_info = IWorkflowInfo(self.context)
943        allowed_transitions = [t for t in wf_info.getManualTransitions()]
944        return [dict(name='', title=_('No transition'))] +[
945            dict(name=x, title=y) for x, y in allowed_transitions]
946
947    @action(_('Apply now'), style='primary')
948    def apply(self, **data):
949        form = self.request.form
950        if 'transition' in form and form['transition']:
951            transition_id = form['transition']
952            wf_info = IWorkflowInfo(self.context)
953            try:
954                wf_info.fireTransition(transition_id)
955                self.flash(_("Transition '%s' executed." % transition_id))
956            except InvalidTransitionError, error:
957                self.flash(error, type="warning")
958            self.redirect(self.url(self.context))
959        return
960
961class PDFDocumentsOverviewPage(UtilityView, grok.View):
962    """Deliver an overview slip.
963    """
964    grok.context(ICustomerDocumentsContainer)
965    grok.name('documents_overview_slip.pdf')
966    grok.require('waeup.viewCustomer')
967    prefix = 'form'
968
969    omit_fields = ('suspended', 'sex',
970                   'suspended_comment',)
971
972    form_fields = None
973
974    @property
975    def label(self):
976        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
977        return translate(_('Documents of'),
978            'waeup.ikoba', target_language=portal_language) \
979            + ' %s' % self.context.customer.display_fullname
980
981    @property
982    def tabletitle(self):
983        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
984        tabletitle = []
985        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
986            target_language=portal_language))
987        return tabletitle
988
989    def render(self):
990        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
991        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
992        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
993        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
994        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
995        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
996        tableheader = []
997        tabledata = []
998        contenttitle = []
999        for i in range(1,3):
1000            tabledata.append(sorted(
1001                [value for value in self.context.values()]))
1002            tableheader.append([(Id, 'document_id', 2),
1003                             (Title, 'title', 6),
1004                             (Type, 'translated_class_name', 6),
1005                             (State, 'translated_state', 2),
1006                             (LT, 'formatted_transition_date', 3),
1007                             ])
1008        customerview = CustomerBasePDFFormPage(self.context.customer,
1009            self.request, self.omit_fields)
1010        customers_utils = getUtility(ICustomersUtils)
1011        return customers_utils.renderPDF(
1012            self, 'overview_slip.pdf',
1013            self.context.customer, customerview,
1014            tableheader=tableheader,
1015            tabledata=tabledata,
1016            omit_fields=self.omit_fields)
1017
1018
1019class PDFDocumentSlipPage(UtilityView, grok.View):
1020    """Deliver pdf file including metadata.
1021    """
1022    grok.context(ICustomerDocument)
1023    grok.name('document_slip.pdf')
1024    grok.require('waeup.viewCustomer')
1025    prefix = 'form'
1026
1027    omit_fields = ('suspended', 'sex',
1028                   'suspended_comment',)
1029
1030    form_fields =()
1031
1032    @property
1033    def label(self):
1034        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1035        return '%s of %s\nTitle: %s' % (
1036            self.context.translated_class_name,
1037            self.context.customer.display_fullname,
1038            self.context.title)
1039
1040    def render(self):
1041        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1042        customerview = CustomerBasePDFFormPage(self.context.customer,
1043            self.request, self.omit_fields)
1044        customers_utils = getUtility(ICustomersUtils)
1045        return customers_utils.renderPDF(
1046            self, 'document_slip.pdf',
1047            self.context.customer, customerview,
1048            omit_fields=self.omit_fields)
1049
1050
1051class PDFMergeDocumentSlipPage(PDFDocumentSlipPage):
1052    """Deliver pdf file including metadata.
1053    """
1054    grok.context(ICustomerPDFDocument)
1055
1056    def render(self):
1057        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1058        customerview = CustomerBasePDFFormPage(self.context.customer,
1059            self.request, self.omit_fields)
1060        customers_utils = getUtility(ICustomersUtils)
1061        if self.context.state == VERIFIED:
1062            watermark_path = os.path.join(
1063                os.path.dirname(__file__), 'static', 'verified.pdf')
1064        else:
1065            watermark_path = os.path.join(
1066                os.path.dirname(__file__), 'static', 'unverified.pdf')
1067        watermark = open(watermark_path, 'rb')
1068        return customers_utils.renderPDF(
1069            self, 'pdfdocument_slip.pdf',
1070            self.context.customer, customerview,
1071            omit_fields=self.omit_fields,
1072            mergefiles=self.context.connected_files,
1073            watermark=watermark)
1074
1075# Pages for customer contracts
1076
1077class ContractsBreadcrumb(Breadcrumb):
1078    """A breadcrumb for the contracts container.
1079    """
1080    grok.context(IContractsContainer)
1081    title = _('Contracts')
1082
1083
1084class ContractBreadcrumb(Breadcrumb):
1085    """A breadcrumb for the customer container.
1086    """
1087    grok.context(IContract)
1088
1089    @property
1090    def title(self):
1091        return self.context.contract_id
1092
1093
1094class ContractsManageFormPage(IkobaEditFormPage):
1095    """ Page to manage the customer contracts
1096
1097    This manage form page is for both customers and officers.
1098    """
1099    grok.context(IContractsContainer)
1100    grok.name('index')
1101    grok.require('waeup.viewCustomer')
1102    form_fields = grok.AutoFields(IContractsContainer)
1103    grok.template('contractsmanagepage')
1104    pnav = 4
1105
1106    @property
1107    def manage_contracts_allowed(self):
1108        return checkPermission('waeup.editContracts', self.context)
1109
1110    def unremovable(self, contract):
1111        usertype = getattr(self.request.principal, 'user_type', None)
1112        if not usertype:
1113            return False
1114        if not self.manage_contracts_allowed:
1115            return True
1116        return (self.request.principal.user_type == 'customer' and \
1117            contract.state in (APPROVED, REJECTED, EXPIRED))
1118
1119    @property
1120    def label(self):
1121        return _('${a}: Contracts',
1122            mapping = {'a':self.context.__parent__.display_fullname})
1123
1124    @action(_('Add contract'), validator=NullValidator, style='primary')
1125    def addContract(self, **data):
1126        self.redirect(self.url(self.context, 'addcontract'))
1127        return
1128
1129    @jsaction(_('Remove selected contracts'))
1130    def delContract(self, **data):
1131        form = self.request.form
1132        if 'val_id' in form:
1133            child_id = form['val_id']
1134        else:
1135            self.flash(_('No contract selected.'), type="warning")
1136            self.redirect(self.url(self.context))
1137            return
1138        if not isinstance(child_id, list):
1139            child_id = [child_id]
1140        deleted = []
1141        for id in child_id:
1142            # Customers are not allowed to remove used contracts
1143            contract = self.context.get(id, None)
1144            if contract is not None and not self.unremovable(contract):
1145                del self.context[id]
1146                deleted.append(id)
1147        if len(deleted):
1148            self.flash(_('Successfully removed: ${a}',
1149                mapping = {'a': ', '.join(deleted)}))
1150            self.context.writeLogMessage(
1151                self,'removed: %s' % ', '.join(deleted))
1152        self.redirect(self.url(self.context))
1153        return
1154
1155
1156class ContractAddFormPage(IkobaAddFormPage):
1157    """ Page to add an contract
1158    """
1159    grok.context(IContractsContainer)
1160    grok.name('addcontract')
1161    grok.template('contractaddform')
1162    grok.require('waeup.editContracts')
1163    label = _('Add contract')
1164    pnav = 4
1165
1166    form_fields = grok.AutoFields(IContract).omit('product_object')
1167
1168    @property
1169    def selectable_contypes(self):
1170        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
1171        return sorted(contypes.items())
1172
1173    @action(_('Add contract'), style='primary')
1174    def createContract(self, **data):
1175        form = self.request.form
1176        customer = self.context.__parent__
1177        contype = form.get('contype', None)
1178        # Here we can create various instances of Contract derived
1179        # classes depending on the contype parameter given in form.
1180        contract = createObject('waeup.%s' % contype)
1181        self.context.addContract(contract)
1182        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
1183        self.flash(_('${a} added.', mapping = {'a': contype}))
1184        self.context.writeLogMessage(
1185            self,'added: %s %s' % (contype, contract.contract_id))
1186        self.redirect(self.url(self.context))
1187        return
1188
1189    @action(_('Cancel'), validator=NullValidator)
1190    def cancel(self, **data):
1191        self.redirect(self.url(self.context))
1192
1193
1194class ContractDisplayFormPage(IkobaDisplayFormPage):
1195    """ Page to view a contract
1196    """
1197    grok.context(IContract)
1198    grok.name('index')
1199    grok.require('waeup.viewCustomer')
1200    grok.template('contractpage')
1201    pnav = 4
1202
1203    @property
1204    def form_fields(self):
1205        form_fields = grok.AutoFields(self.context.form_fields_interface)
1206        for field in form_fields:
1207            if field.__name__.endswith('_object'):
1208                form_fields[field.__name__].custom_widget = HREFDisplayWidget
1209        return form_fields
1210
1211    @property
1212    def label(self):
1213        return _('${a}', mapping = {'a':self.context.title})
1214
1215
1216class ContractManageFormPage(IkobaEditFormPage):
1217    """ Page to edit a contract
1218    """
1219    grok.context(IContract)
1220    grok.name('manage')
1221    grok.require('waeup.manageCustomer')
1222    grok.template('contracteditpage')
1223    pnav = 4
1224    deletion_warning = _('Are you sure?')
1225
1226    @property
1227    def form_fields(self):
1228        return grok.AutoFields(self.context.form_fields_interface)
1229
1230    @property
1231    def label(self):
1232        return _('${a}', mapping = {'a':self.context.title})
1233
1234    @action(_('Save'), style='primary')
1235    def save(self, **data):
1236        msave(self, **data)
1237        return
1238
1239
1240class ContractEditFormPage(ContractManageFormPage):
1241    """ Page to edit a contract
1242    """
1243    grok.name('edit')
1244    grok.require('waeup.handleCustomer')
1245
1246    @property
1247    def form_fields(self):
1248        return grok.AutoFields(self.context.edit_form_fields_interface)
1249
1250    def update(self):
1251        if not self.context.is_editable_by_customer:
1252            emit_lock_message(self)
1253            return
1254        return super(ContractEditFormPage, self).update()
1255
1256    @action(_('Save'), style='primary')
1257    def save(self, **data):
1258        msave(self, **data)
1259        return
1260
1261    @action(_('Apply now (final submit)'), warning=WARNING)
1262    def finalsubmit(self, **data):
1263        msave(self, **data)
1264        IWorkflowInfo(self.context).fireTransition('submit')
1265        self.flash(_('Form has been submitted.'))
1266        self.redirect(self.url(self.context))
1267        return
1268
1269
1270class ContractTriggerTransitionFormPage(IkobaEditFormPage):
1271    """ View to trigger customer contract transitions
1272    """
1273    grok.context(IContract)
1274    grok.name('trigtrans')
1275    grok.require('waeup.triggerTransition')
1276    grok.template('trigtrans')
1277    label = _('Trigger contract transition')
1278    pnav = 4
1279
1280    def update(self):
1281        return super(IkobaEditFormPage, self).update()
1282
1283    def getTransitions(self):
1284        """Return a list of dicts of allowed transition ids and titles.
1285
1286        Each list entry provides keys ``name`` and ``title`` for
1287        internal name and (human readable) title of a single
1288        transition.
1289        """
1290        wf_info = IWorkflowInfo(self.context)
1291        allowed_transitions = [t for t in wf_info.getManualTransitions()]
1292        return [dict(name='', title=_('No transition'))] +[
1293            dict(name=x, title=y) for x, y in allowed_transitions]
1294
1295    @action(_('Apply now'), style='primary')
1296    def apply(self, **data):
1297        form = self.request.form
1298        if 'transition' in form and form['transition']:
1299            transition_id = form['transition']
1300            wf_info = IWorkflowInfo(self.context)
1301            try:
1302                wf_info.fireTransition(transition_id)
1303                self.flash(_("Transition '%s' executed." % transition_id))
1304            except InvalidTransitionError, error:
1305                self.flash(error, type="warning")
1306            self.redirect(self.url(self.context))
1307        return
1308
1309class PDFContractsOverviewPage(UtilityView, grok.View):
1310    """Deliver an overview slip.
1311    """
1312    grok.context(IContractsContainer)
1313    grok.name('contracts_overview_slip.pdf')
1314    grok.require('waeup.viewCustomer')
1315    prefix = 'form'
1316
1317    omit_fields = ('suspended', 'sex',
1318                   'suspended_comment',)
1319
1320    form_fields = None
1321
1322    @property
1323    def label(self):
1324        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1325        return translate(_('Contracts of'),
1326            'waeup.ikoba', target_language=portal_language) \
1327            + ' %s' % self.context.customer.display_fullname
1328
1329    @property
1330    def tabletitle(self):
1331        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1332        tabletitle = []
1333        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
1334            target_language=portal_language))
1335        return tabletitle
1336
1337    def render(self):
1338        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1339        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
1340        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
1341        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
1342        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
1343        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
1344        tableheader = []
1345        tabledata = []
1346        contenttitle = []
1347        for i in range(1,3):
1348            tabledata.append(sorted(
1349                [value for value in self.context.values()]))
1350            tableheader.append([(Id, 'contract_id', 2),
1351                             (Title, 'title', 6),
1352                             (Type, 'translated_class_name', 6),
1353                             (State, 'translated_state', 2),
1354                             (LT, 'formatted_transition_date', 3),
1355                             ])
1356        customerview = CustomerBasePDFFormPage(self.context.customer,
1357            self.request, self.omit_fields)
1358        customers_utils = getUtility(ICustomersUtils)
1359        return customers_utils.renderPDF(
1360            self, 'overview_slip.pdf',
1361            self.context.customer, customerview,
1362            tableheader=tableheader,
1363            tabledata=tabledata,
1364            omit_fields=self.omit_fields)
1365
1366
1367class PDFContractSlipPage(UtilityView, grok.View):
1368    """Deliver pdf file including metadata.
1369    """
1370    grok.context(ISampleContract)
1371    grok.name('contract_slip.pdf')
1372    grok.require('waeup.viewCustomer')
1373    prefix = 'form'
1374
1375    omit_fields = ('suspended', 'sex',
1376                   'suspended_comment',)
1377
1378    form_fields = grok.AutoFields(ISampleContract)
1379
1380    @property
1381    def label(self):
1382        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1383        return '%s of %s\nTitle: %s' % (
1384            self.context.translated_class_name,
1385            self.context.customer.display_fullname,
1386            self.context.title)
1387
1388    def render(self):
1389        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1390        customerview = CustomerBasePDFFormPage(self.context.customer,
1391            self.request, self.omit_fields)
1392        customers_utils = getUtility(ICustomersUtils)
1393        return customers_utils.renderPDF(
1394            self, 'contract_slip.pdf',
1395            self.context.customer, customerview,
1396            omit_fields=self.omit_fields)
Note: See TracBrowser for help on using the repository browser.