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

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

We need to protect also the manage form page of documents. Officers are only allowed to edit documents in state created.

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