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

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

Log workflow transitions properly.

  • Property svn:keywords set to Id
File size: 47.5 KB
Line 
1## $Id: browser.py 12221 2014-12-14 06:14:39Z 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(_('Save'), style='primary')
417    def save(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        return
424
425
426class CustomerActivatePage(UtilityView, grok.View):
427    """ Activate customer account
428    """
429    grok.context(ICustomer)
430    grok.name('activate')
431    grok.require('waeup.manageCustomer')
432
433    def update(self):
434        self.context.suspended = False
435        self.context.writeLogMessage(self, 'account activated')
436        history = IObjectHistory(self.context)
437        history.addMessage('Customer account activated')
438        self.flash(_('Customer account has been activated.'))
439        self.redirect(self.url(self.context))
440        return
441
442    def render(self):
443        return
444
445
446class CustomerDeactivatePage(UtilityView, grok.View):
447    """ Deactivate customer account
448    """
449    grok.context(ICustomer)
450    grok.name('deactivate')
451    grok.require('waeup.manageCustomer')
452
453    def update(self):
454        self.context.suspended = True
455        self.context.writeLogMessage(self, 'account deactivated')
456        history = IObjectHistory(self.context)
457        history.addMessage('Customer account deactivated')
458        self.flash(_('Customer account has been deactivated.'))
459        self.redirect(self.url(self.context))
460        return
461
462    def render(self):
463        return
464
465
466class CustomerHistoryPage(IkobaPage):
467    """ Page to display customer history
468    """
469    grok.context(ICustomer)
470    grok.name('history')
471    grok.require('waeup.viewCustomer')
472    grok.template('customerhistory')
473    pnav = 4
474
475    @property
476    def label(self):
477        return _('${a}: History', mapping={'a':self.context.display_fullname})
478
479
480class CustomerRequestPasswordPage(IkobaAddFormPage):
481    """Captcha'd password request page for customers.
482    """
483    grok.name('requestpw')
484    grok.require('waeup.Anonymous')
485    grok.template('requestpw')
486    form_fields = grok.AutoFields(ICustomerRequestPW)
487    label = _('Request password for first-time login')
488
489    def update(self):
490        # Handle captcha
491        self.captcha = getUtility(ICaptchaManager).getCaptcha()
492        self.captcha_result = self.captcha.verify(self.request)
493        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
494        return
495
496    def _redirect(self, email, password, customer_id):
497        # Forward only email address to landing page in base package.
498        self.redirect(self.url(self.context, 'requestpw_complete',
499            data=dict(email=email)))
500        return
501
502    def _pw_used(self):
503        # XXX: False if password has not been used. We need an extra
504        #      attribute which remembers if customer logged in.
505        return True
506
507    @action(_('Send login credentials to email address'), style='primary')
508    def get_credentials(self, **data):
509        if not self.captcha_result.is_valid:
510            # Captcha will display error messages automatically.
511            # No need to flash something.
512            return
513        number = data.get('number','')
514        firstname = data.get('firstname','')
515        cat = getUtility(ICatalog, name='customers_catalog')
516        results = list(
517            cat.searchResults(reg_number=(number, number)))
518        if results:
519            customer = results[0]
520            if getattr(customer,'firstname',None) is None:
521                self.flash(_('An error occurred.'), type="danger")
522                return
523            elif customer.firstname.lower() != firstname.lower():
524                # Don't tell the truth here. Anonymous must not
525                # know that a record was found and only the firstname
526                # verification failed.
527                self.flash(_('No customer found.'), type="warning")
528                return
529            elif customer.password is not None and self._pw_used:
530                self.flash(_('Your password has already been set and used. '
531                             'Please proceed to the login page.'),
532                           type="warning")
533                return
534            # Store email address but nothing else.
535            customer.email = data['email']
536            notify(grok.ObjectModifiedEvent(customer))
537        else:
538            # No record found, this is the truth.
539            self.flash(_('No customer found.'), type="warning")
540            return
541
542        ikoba_utils = getUtility(IIkobaUtils)
543        password = ikoba_utils.genPassword()
544        mandate = PasswordMandate()
545        mandate.params['password'] = password
546        mandate.params['user'] = customer
547        site = grok.getSite()
548        site['mandates'].addMandate(mandate)
549        # Send email with credentials
550        args = {'mandate_id':mandate.mandate_id}
551        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
552        url_info = u'Confirmation link: %s' % mandate_url
553        msg = _('You have successfully requested a password for the')
554        if ikoba_utils.sendCredentials(IUserAccount(customer),
555            password, url_info, msg):
556            email_sent = customer.email
557        else:
558            email_sent = None
559        self._redirect(email=email_sent, password=password,
560            customer_id=customer.customer_id)
561        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
562        self.context.logger.info(
563            '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
564        return
565
566
567class CustomerCreateAccountPage(IkobaAddFormPage):
568    """Captcha'd account creation page for customers.
569    """
570    grok.name('createaccount')
571    grok.require('waeup.Anonymous')
572    grok.template('createaccount')
573    form_fields = grok.AutoFields(ICustomerCreate)
574    label = _('Create customer account')
575
576    def update(self):
577        # Handle captcha
578        self.captcha = getUtility(ICaptchaManager).getCaptcha()
579        self.captcha_result = self.captcha.verify(self.request)
580        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
581        return
582
583    def _redirect(self, email, password, customer_id):
584        # Forward only email address to landing page in base package.
585        self.redirect(self.url(self.context, 'requestpw_complete',
586            data=dict(email=email)))
587        return
588
589    @action(_('Send login credentials to email address'), style='primary')
590    def create_account(self, **data):
591        if not self.captcha_result.is_valid:
592            # Captcha will display error messages automatically.
593            # No need to flash something.
594            return
595        customer = createObject(u'waeup.Customer')
596        customer.firstname = data.get('firstname','')
597        customer.middlename = data.get('middlename','')
598        customer.lastname = data.get('lastname','')
599        customer.email = data.get('email','')
600        self.context['customers'].addCustomer(customer)
601        ikoba_utils = getUtility(IIkobaUtils)
602        password = ikoba_utils.genPassword()
603        mandate = PasswordMandate()
604        mandate.params['password'] = password
605        mandate.params['user'] = customer
606        site = grok.getSite()
607        site['mandates'].addMandate(mandate)
608        # Send email with credentials
609        args = {'mandate_id':mandate.mandate_id}
610        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
611        url_info = u'Confirmation link: %s' % mandate_url
612        msg = _('You have successfully created a customer account for the')
613        if ikoba_utils.sendCredentials(IUserAccount(customer),
614            password, url_info, msg):
615            email_sent = customer.email
616        else:
617            email_sent = None
618        self._redirect(email=email_sent, password=password,
619            customer_id=customer.customer_id)
620        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
621        self.context.logger.info(
622            '%s - %s - %s' % (ob_class, customer.customer_id, email_sent))
623        return
624
625
626class CustomerRequestPasswordEmailSent(IkobaPage):
627    """Landing page after successful password request.
628
629    """
630    grok.name('requestpw_complete')
631    grok.require('waeup.Public')
632    grok.template('requestpwmailsent')
633    label = _('Your request was successful.')
634
635    def update(self, email=None, customer_id=None, password=None):
636        self.email = email
637        self.password = password
638        self.customer_id = customer_id
639        return
640
641
642class CustomerFilesUploadPage(IkobaPage):
643    """ View to upload files by customer
644    """
645    grok.context(ICustomer)
646    grok.name('change_portrait')
647    grok.require('waeup.uploadCustomerFile')
648    grok.template('filesuploadpage')
649    label = _('Upload files')
650    pnav = 4
651
652    def update(self):
653        CUSTMANAGE_STATES = getUtility(
654            ICustomersUtils).CUSTMANAGE_CUSTOMER_STATES
655        if self.context.customer.state not in CUSTMANAGE_STATES:
656            emit_lock_message(self)
657            return
658        super(CustomerFilesUploadPage, self).update()
659        return
660
661# Pages for customers
662
663
664class CustomerBaseEditFormPage(IkobaEditFormPage):
665    """ View to edit customer base data
666    """
667    grok.context(ICustomer)
668    grok.name('edit_base')
669    grok.require('waeup.handleCustomer')
670    form_fields = grok.AutoFields(ICustomer).select(
671        'email', 'phone')
672    label = _('Edit base data')
673    pnav = 4
674
675    @action(_('Save'), style='primary')
676    def save(self, **data):
677        msave(self, **data)
678        return
679
680
681class CustomerChangePasswordPage(IkobaEditFormPage):
682    """ View to edit customer passords
683    """
684    grok.context(ICustomer)
685    grok.name('changepassword')
686    grok.require('waeup.handleCustomer')
687    grok.template('changepassword')
688    label = _('Change password')
689    pnav = 4
690
691    @action(_('Save'), style='primary')
692    def save(self, **data):
693        form = self.request.form
694        password = form.get('change_password', None)
695        password_ctl = form.get('change_password_repeat', None)
696        if password:
697            validator = getUtility(IPasswordValidator)
698            errors = validator.validate_password(password, password_ctl)
699            if not errors:
700                IUserAccount(self.context).setPassword(password)
701                self.context.writeLogMessage(self, 'saved: password')
702                self.flash(_('Password changed.'))
703            else:
704                self.flash(' '.join(errors), type="warning")
705        return
706
707class CustomerBasePDFFormPage(IkobaDisplayFormPage):
708    """ Page to display customer base data in pdf files.
709    """
710
711    def __init__(self, context, request, omit_fields=()):
712        self.omit_fields = omit_fields
713        super(CustomerBasePDFFormPage, self).__init__(context, request)
714
715    @property
716    def form_fields(self):
717        form_fields = grok.AutoFields(ICustomer)
718        for field in self.omit_fields:
719            form_fields = form_fields.omit(field)
720        return form_fields
721
722# Pages for customer documents
723
724class DocumentsBreadcrumb(Breadcrumb):
725    """A breadcrumb for the documents container.
726    """
727    grok.context(ICustomerDocumentsContainer)
728    title = _('Documents')
729
730
731class DocumentBreadcrumb(Breadcrumb):
732    """A breadcrumb for the customer container.
733    """
734    grok.context(ICustomerDocument)
735
736    @property
737    def title(self):
738        return self.context.document_id
739
740
741class DocumentsManageFormPage(IkobaEditFormPage):
742    """ Page to manage the customer documents
743
744    This manage form page is for both customers and customers officers.
745    """
746    grok.context(ICustomerDocumentsContainer)
747    grok.name('index')
748    grok.require('waeup.viewCustomer')
749    form_fields = grok.AutoFields(ICustomerDocumentsContainer)
750    grok.template('documentsmanagepage')
751    pnav = 4
752
753    @property
754    def manage_documents_allowed(self):
755        return checkPermission('waeup.editCustomerDocuments', self.context)
756
757    def unremovable(self, document):
758        usertype = getattr(self.request.principal, 'user_type', None)
759        if not usertype:
760            return False
761        if not self.manage_documents_allowed:
762            return True
763        return (self.request.principal.user_type == 'customer' and \
764            document.state in (VERIFIED, REJECTED, EXPIRED))
765
766    @property
767    def label(self):
768        return _('${a}: Documents',
769            mapping = {'a':self.context.__parent__.display_fullname})
770
771    @action(_('Add document'), validator=NullValidator, style='primary')
772    def addDocument(self, **data):
773        self.redirect(self.url(self.context, 'adddoc'))
774        return
775
776    @jsaction(_('Remove selected documents'))
777    def delDocument(self, **data):
778        form = self.request.form
779        if 'val_id' in form:
780            child_id = form['val_id']
781        else:
782            self.flash(_('No document selected.'), type="warning")
783            self.redirect(self.url(self.context))
784            return
785        if not isinstance(child_id, list):
786            child_id = [child_id]
787        deleted = []
788        for id in child_id:
789            # Customers are not allowed to remove used documents
790            document = self.context.get(id, None)
791            if document is not None and not self.unremovable(document):
792                del self.context[id]
793                deleted.append(id)
794        if len(deleted):
795            self.flash(_('Successfully removed: ${a}',
796                mapping = {'a': ', '.join(deleted)}))
797            self.context.writeLogMessage(
798                self,'removed: %s' % ', '.join(deleted))
799        self.redirect(self.url(self.context))
800        return
801
802
803class DocumentAddFormPage(IkobaAddFormPage):
804    """ Page to add a document
805
806    This add form page is for both customers and customers officers.
807    """
808    grok.context(ICustomerDocumentsContainer)
809    grok.name('adddoc')
810    grok.template('documentaddform')
811    grok.require('waeup.editCustomerDocuments')
812    label = _('Add document')
813    pnav = 4
814
815    form_fields = grok.AutoFields(ICustomerDocument)
816
817    @property
818    def selectable_doctypes(self):
819        doctypes = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT
820        return sorted(doctypes.items())
821
822    @action(_('Add document'), style='primary')
823    def createDocument(self, **data):
824        form = self.request.form
825        customer = self.context.__parent__
826        doctype = form.get('doctype', None)
827        # Here we can create various instances of CustomerDocument derived
828        # classes depending on the doctype parameter given in form.
829        document = createObject('waeup.%s' % doctype)
830        self.applyData(document, **data)
831        self.context.addDocument(document)
832        doctype = getUtility(ICustomersUtils).SELECTABLE_DOCTYPES_DICT[doctype]
833        self.flash(_('${a} added.', mapping = {'a': doctype}))
834        self.context.writeLogMessage(
835            self,'added: %s %s' % (doctype, document.document_id))
836        self.redirect(self.url(self.context))
837        return
838
839    @action(_('Cancel'), validator=NullValidator)
840    def cancel(self, **data):
841        self.redirect(self.url(self.context))
842
843
844class DocumentDisplayFormPage(IkobaDisplayFormPage):
845    """ Page to view a document
846    """
847    grok.context(ICustomerDocument)
848    grok.name('index')
849    grok.require('waeup.viewCustomer')
850    grok.template('documentpage')
851    pnav = 4
852
853    @property
854    def form_fields(self):
855        return grok.AutoFields(self.context.form_fields_interface)
856
857    @property
858    def label(self):
859        return _('${a}', mapping = {'a':self.context.title})
860
861
862class DocumentManageFormPage(IkobaEditFormPage):
863    """ Page to edit a document
864    """
865    grok.context(ICustomerDocument)
866    grok.name('manage')
867    grok.require('waeup.manageCustomer')
868    grok.template('documenteditpage')
869    pnav = 4
870    deletion_warning = _('Are you sure?')
871
872    @property
873    def form_fields(self):
874        return grok.AutoFields(self.context.form_fields_interface)
875
876    def update(self):
877        if not self.context.is_editable_by_manager:
878            emit_lock_message(self)
879            return
880        return super(DocumentManageFormPage, self).update()
881
882    @property
883    def label(self):
884        return _('${a}', mapping = {'a':self.context.title})
885
886    @action(_('Save'), style='primary')
887    def save(self, **data):
888        msave(self, **data)
889        return
890
891
892class DocumentEditFormPage(DocumentManageFormPage):
893    """ Page to edit a document
894    """
895    grok.name('edit')
896    grok.require('waeup.handleCustomer')
897
898    def update(self):
899        if not self.context.is_editable_by_customer:
900            emit_lock_message(self)
901            return
902        return super(DocumentEditFormPage, self).update()
903
904    @action(_('Save'), style='primary')
905    def save(self, **data):
906        msave(self, **data)
907        return
908
909    @action(_('Final Submit'), warning=WARNING)
910    def finalsubmit(self, **data):
911        msave(self, **data)
912        IWorkflowInfo(self.context).fireTransition('submit')
913        self.flash(_('Form has been submitted.'))
914        self.redirect(self.url(self.context))
915        return
916
917
918class DocumentTriggerTransitionFormPage(IkobaEditFormPage):
919    """ View to trigger customer document transitions
920    """
921    grok.context(ICustomerDocument)
922    grok.name('trigtrans')
923    grok.require('waeup.triggerTransition')
924    grok.template('trigtrans')
925    label = _('Trigger document transition')
926    pnav = 4
927
928    def update(self):
929        return super(IkobaEditFormPage, self).update()
930
931    def getTransitions(self):
932        """Return a list of dicts of allowed transition ids and titles.
933
934        Each list entry provides keys ``name`` and ``title`` for
935        internal name and (human readable) title of a single
936        transition.
937        """
938        wf_info = IWorkflowInfo(self.context)
939        allowed_transitions = [t for t in wf_info.getManualTransitions()]
940        return [dict(name='', title=_('No transition'))] +[
941            dict(name=x, title=y) for x, y in allowed_transitions]
942
943    @action(_('Save'), style='primary')
944    def save(self, **data):
945        form = self.request.form
946        if 'transition' in form and form['transition']:
947            transition_id = form['transition']
948            wf_info = IWorkflowInfo(self.context)
949            try:
950                wf_info.fireTransition(transition_id)
951            except InvalidTransitionError, error:
952                self.flash(error, type="warning")
953        return
954
955class PDFDocumentsOverviewPage(UtilityView, grok.View):
956    """Deliver an overview slip.
957    """
958    grok.context(ICustomerDocumentsContainer)
959    grok.name('documents_overview_slip.pdf')
960    grok.require('waeup.viewCustomer')
961    prefix = 'form'
962
963    omit_fields = ('suspended', 'sex',
964                   'suspended_comment',)
965
966    form_fields = None
967
968    @property
969    def label(self):
970        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
971        return translate(_('Documents of'),
972            'waeup.ikoba', target_language=portal_language) \
973            + ' %s' % self.context.customer.display_fullname
974
975    @property
976    def tabletitle(self):
977        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
978        tabletitle = []
979        tabletitle.append(translate(_('Customer Documents'), 'waeup.ikoba',
980            target_language=portal_language))
981        return tabletitle
982
983    def render(self):
984        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
985        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
986        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
987        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
988        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
989        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
990        tableheader = []
991        tabledata = []
992        contenttitle = []
993        for i in range(1,3):
994            tabledata.append(sorted(
995                [value for value in self.context.values()]))
996            tableheader.append([(Id, 'document_id', 2),
997                             (Title, 'title', 6),
998                             (Type, 'translated_class_name', 6),
999                             (State, 'translated_state', 2),
1000                             (LT, 'formatted_transition_date', 3),
1001                             ])
1002        customerview = CustomerBasePDFFormPage(self.context.customer,
1003            self.request, self.omit_fields)
1004        customers_utils = getUtility(ICustomersUtils)
1005        return customers_utils.renderPDF(
1006            self, 'overview_slip.pdf',
1007            self.context.customer, customerview,
1008            tableheader=tableheader,
1009            tabledata=tabledata,
1010            omit_fields=self.omit_fields)
1011
1012
1013class PDFDocumentSlipPage(UtilityView, grok.View):
1014    """Deliver pdf file including metadata.
1015    """
1016    grok.context(ICustomerDocument)
1017    grok.name('document_slip.pdf')
1018    grok.require('waeup.viewCustomer')
1019    prefix = 'form'
1020
1021    omit_fields = ('suspended', 'sex',
1022                   'suspended_comment',)
1023
1024    form_fields =()
1025
1026    @property
1027    def label(self):
1028        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1029        return '%s of %s\nTitle: %s' % (
1030            self.context.translated_class_name,
1031            self.context.customer.display_fullname,
1032            self.context.title)
1033
1034    def render(self):
1035        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1036        customerview = CustomerBasePDFFormPage(self.context.customer,
1037            self.request, self.omit_fields)
1038        customers_utils = getUtility(ICustomersUtils)
1039        return customers_utils.renderPDF(
1040            self, 'document_slip.pdf',
1041            self.context.customer, customerview,
1042            omit_fields=self.omit_fields)
1043
1044
1045class PDFMergeDocumentSlipPage(PDFDocumentSlipPage):
1046    """Deliver pdf file including metadata.
1047    """
1048    grok.context(ICustomerPDFDocument)
1049
1050    def render(self):
1051        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1052        customerview = CustomerBasePDFFormPage(self.context.customer,
1053            self.request, self.omit_fields)
1054        customers_utils = getUtility(ICustomersUtils)
1055        if self.context.state == VERIFIED:
1056            watermark_path = os.path.join(
1057                os.path.dirname(__file__), 'static', 'verified.pdf')
1058        else:
1059            watermark_path = os.path.join(
1060                os.path.dirname(__file__), 'static', 'unverified.pdf')
1061        watermark = open(watermark_path, 'rb')
1062        return customers_utils.renderPDF(
1063            self, 'pdfdocument_slip.pdf',
1064            self.context.customer, customerview,
1065            omit_fields=self.omit_fields,
1066            mergefiles=self.context.connected_files,
1067            watermark=watermark)
1068
1069# Pages for customer contracts
1070
1071class ContractsBreadcrumb(Breadcrumb):
1072    """A breadcrumb for the contracts container.
1073    """
1074    grok.context(IContractsContainer)
1075    title = _('Contracts')
1076
1077
1078class ContractBreadcrumb(Breadcrumb):
1079    """A breadcrumb for the customer container.
1080    """
1081    grok.context(IContract)
1082
1083    @property
1084    def title(self):
1085        return self.context.contract_id
1086
1087
1088class ContractsManageFormPage(IkobaEditFormPage):
1089    """ Page to manage the customer contracts
1090
1091    This manage form page is for both customers and officers.
1092    """
1093    grok.context(IContractsContainer)
1094    grok.name('index')
1095    grok.require('waeup.viewCustomer')
1096    form_fields = grok.AutoFields(IContractsContainer)
1097    grok.template('contractsmanagepage')
1098    pnav = 4
1099
1100    @property
1101    def manage_contracts_allowed(self):
1102        return checkPermission('waeup.editContracts', self.context)
1103
1104    def unremovable(self, contract):
1105        usertype = getattr(self.request.principal, 'user_type', None)
1106        if not usertype:
1107            return False
1108        if not self.manage_contracts_allowed:
1109            return True
1110        return (self.request.principal.user_type == 'customer' and \
1111            contract.state in (APPROVED, REJECTED, EXPIRED))
1112
1113    @property
1114    def label(self):
1115        return _('${a}: Contracts',
1116            mapping = {'a':self.context.__parent__.display_fullname})
1117
1118    @action(_('Add contract'), validator=NullValidator, style='primary')
1119    def addContract(self, **data):
1120        self.redirect(self.url(self.context, 'addcontract'))
1121        return
1122
1123    @jsaction(_('Remove selected contracts'))
1124    def delContract(self, **data):
1125        form = self.request.form
1126        if 'val_id' in form:
1127            child_id = form['val_id']
1128        else:
1129            self.flash(_('No contract selected.'), type="warning")
1130            self.redirect(self.url(self.context))
1131            return
1132        if not isinstance(child_id, list):
1133            child_id = [child_id]
1134        deleted = []
1135        for id in child_id:
1136            # Customers are not allowed to remove used contracts
1137            contract = self.context.get(id, None)
1138            if contract is not None and not self.unremovable(contract):
1139                del self.context[id]
1140                deleted.append(id)
1141        if len(deleted):
1142            self.flash(_('Successfully removed: ${a}',
1143                mapping = {'a': ', '.join(deleted)}))
1144            self.context.writeLogMessage(
1145                self,'removed: %s' % ', '.join(deleted))
1146        self.redirect(self.url(self.context))
1147        return
1148
1149
1150class ContractAddFormPage(IkobaAddFormPage):
1151    """ Page to add an contract
1152    """
1153    grok.context(IContractsContainer)
1154    grok.name('addcontract')
1155    grok.template('contractaddform')
1156    grok.require('waeup.editContracts')
1157    label = _('Add contract')
1158    pnav = 4
1159
1160    form_fields = grok.AutoFields(IContract).omit('product_object')
1161
1162    @property
1163    def selectable_contypes(self):
1164        contypes = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT
1165        return sorted(contypes.items())
1166
1167    @action(_('Add contract'), style='primary')
1168    def createContract(self, **data):
1169        form = self.request.form
1170        customer = self.context.__parent__
1171        contype = form.get('contype', None)
1172        # Here we can create various instances of Contract derived
1173        # classes depending on the contype parameter given in form.
1174        contract = createObject('waeup.%s' % contype)
1175        self.context.addContract(contract)
1176        contype = getUtility(ICustomersUtils).SELECTABLE_CONTYPES_DICT[contype]
1177        self.flash(_('${a} added.', mapping = {'a': contype}))
1178        self.context.writeLogMessage(
1179            self,'added: %s %s' % (contype, contract.contract_id))
1180        self.redirect(self.url(self.context))
1181        return
1182
1183    @action(_('Cancel'), validator=NullValidator)
1184    def cancel(self, **data):
1185        self.redirect(self.url(self.context))
1186
1187
1188class ContractDisplayFormPage(IkobaDisplayFormPage):
1189    """ Page to view a contract
1190    """
1191    grok.context(IContract)
1192    grok.name('index')
1193    grok.require('waeup.viewCustomer')
1194    grok.template('contractpage')
1195    pnav = 4
1196
1197    @property
1198    def form_fields(self):
1199        form_fields = grok.AutoFields(self.context.form_fields_interface)
1200        for field in form_fields:
1201            if field.__name__.endswith('_object'):
1202                form_fields[field.__name__].custom_widget = HREFDisplayWidget
1203        return form_fields
1204
1205    @property
1206    def label(self):
1207        return _('${a}', mapping = {'a':self.context.title})
1208
1209
1210class ContractManageFormPage(IkobaEditFormPage):
1211    """ Page to edit a contract
1212    """
1213    grok.context(IContract)
1214    grok.name('manage')
1215    grok.require('waeup.manageCustomer')
1216    grok.template('contracteditpage')
1217    pnav = 4
1218    deletion_warning = _('Are you sure?')
1219
1220    @property
1221    def form_fields(self):
1222        return grok.AutoFields(self.context.form_fields_interface)
1223
1224    @property
1225    def label(self):
1226        return _('${a}', mapping = {'a':self.context.title})
1227
1228    @action(_('Save'), style='primary')
1229    def save(self, **data):
1230        msave(self, **data)
1231        return
1232
1233
1234class ContractEditFormPage(ContractManageFormPage):
1235    """ Page to edit a contract
1236    """
1237    grok.name('edit')
1238    grok.require('waeup.handleCustomer')
1239
1240    @property
1241    def form_fields(self):
1242        return grok.AutoFields(self.context.edit_form_fields_interface)
1243
1244    def update(self):
1245        if not self.context.is_editable_by_customer:
1246            emit_lock_message(self)
1247            return
1248        return super(ContractEditFormPage, self).update()
1249
1250    @action(_('Save'), style='primary')
1251    def save(self, **data):
1252        msave(self, **data)
1253        return
1254
1255    @action(_('Apply now (final submit)'), warning=WARNING)
1256    def finalsubmit(self, **data):
1257        msave(self, **data)
1258        IWorkflowInfo(self.context).fireTransition('submit')
1259        self.flash(_('Form has been submitted.'))
1260        self.redirect(self.url(self.context))
1261        return
1262
1263
1264class ContractTriggerTransitionFormPage(IkobaEditFormPage):
1265    """ View to trigger customer contract transitions
1266    """
1267    grok.context(IContract)
1268    grok.name('trigtrans')
1269    grok.require('waeup.triggerTransition')
1270    grok.template('trigtrans')
1271    label = _('Trigger contract transition')
1272    pnav = 4
1273
1274    def update(self):
1275        return super(IkobaEditFormPage, self).update()
1276
1277    def getTransitions(self):
1278        """Return a list of dicts of allowed transition ids and titles.
1279
1280        Each list entry provides keys ``name`` and ``title`` for
1281        internal name and (human readable) title of a single
1282        transition.
1283        """
1284        wf_info = IWorkflowInfo(self.context)
1285        allowed_transitions = [t for t in wf_info.getManualTransitions()]
1286        return [dict(name='', title=_('No transition'))] +[
1287            dict(name=x, title=y) for x, y in allowed_transitions]
1288
1289    @action(_('Save'), style='primary')
1290    def save(self, **data):
1291        form = self.request.form
1292        if 'transition' in form and form['transition']:
1293            transition_id = form['transition']
1294            wf_info = IWorkflowInfo(self.context)
1295            try:
1296                wf_info.fireTransition(transition_id)
1297            except InvalidTransitionError, error:
1298                self.flash(error, type="warning")
1299        return
1300
1301class PDFContractsOverviewPage(UtilityView, grok.View):
1302    """Deliver an overview slip.
1303    """
1304    grok.context(IContractsContainer)
1305    grok.name('contracts_overview_slip.pdf')
1306    grok.require('waeup.viewCustomer')
1307    prefix = 'form'
1308
1309    omit_fields = ('suspended', 'sex',
1310                   'suspended_comment',)
1311
1312    form_fields = None
1313
1314    @property
1315    def label(self):
1316        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1317        return translate(_('Contracts of'),
1318            'waeup.ikoba', target_language=portal_language) \
1319            + ' %s' % self.context.customer.display_fullname
1320
1321    @property
1322    def tabletitle(self):
1323        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1324        tabletitle = []
1325        tabletitle.append(translate(_('Customer Contracts'), 'waeup.ikoba',
1326            target_language=portal_language))
1327        return tabletitle
1328
1329    def render(self):
1330        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1331        Id = translate(_('Id'), 'waeup.ikoba', target_language=portal_language)
1332        Title = translate(_('Title'), 'waeup.ikoba', target_language=portal_language)
1333        Type = translate(_('Type'), 'waeup.ikoba', target_language=portal_language)
1334        State = translate(_('State'), 'waeup.ikoba', target_language=portal_language)
1335        LT = translate(_('Last Transition'), 'waeup.ikoba', target_language=portal_language)
1336        tableheader = []
1337        tabledata = []
1338        contenttitle = []
1339        for i in range(1,3):
1340            tabledata.append(sorted(
1341                [value for value in self.context.values()]))
1342            tableheader.append([(Id, 'contract_id', 2),
1343                             (Title, 'title', 6),
1344                             (Type, 'translated_class_name', 6),
1345                             (State, 'translated_state', 2),
1346                             (LT, 'formatted_transition_date', 3),
1347                             ])
1348        customerview = CustomerBasePDFFormPage(self.context.customer,
1349            self.request, self.omit_fields)
1350        customers_utils = getUtility(ICustomersUtils)
1351        return customers_utils.renderPDF(
1352            self, 'overview_slip.pdf',
1353            self.context.customer, customerview,
1354            tableheader=tableheader,
1355            tabledata=tabledata,
1356            omit_fields=self.omit_fields)
1357
1358
1359class PDFContractSlipPage(UtilityView, grok.View):
1360    """Deliver pdf file including metadata.
1361    """
1362    grok.context(ISampleContract)
1363    grok.name('contract_slip.pdf')
1364    grok.require('waeup.viewCustomer')
1365    prefix = 'form'
1366
1367    omit_fields = ('suspended', 'sex',
1368                   'suspended_comment',)
1369
1370    form_fields = grok.AutoFields(ISampleContract)
1371
1372    @property
1373    def label(self):
1374        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1375        return '%s of %s\nTitle: %s' % (
1376            self.context.translated_class_name,
1377            self.context.customer.display_fullname,
1378            self.context.title)
1379
1380    def render(self):
1381        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
1382        customerview = CustomerBasePDFFormPage(self.context.customer,
1383            self.request, self.omit_fields)
1384        customers_utils = getUtility(ICustomersUtils)
1385        return customers_utils.renderPDF(
1386            self, 'contract_slip.pdf',
1387            self.context.customer, customerview,
1388            omit_fields=self.omit_fields)
Note: See TracBrowser for help on using the repository browser.