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

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

Add file viewlets to documents.

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