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

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

Do not allow to add contract without editing contract title.

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