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

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

Add file upload and display components.

Adjust workflow.

File size: 21.0 KB
Line 
1## $Id: browser.py 11862 2014-10-21 07:07:04Z 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 IWorkflowInfo, IWorkflowState
34from waeup.ikoba.interfaces import MessageFactory as _
35from waeup.ikoba.interfaces import (
36    IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils,
37    IPasswordValidator, IUserAccount)
38from waeup.ikoba.browser.layout import (
39    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
40    IkobaForm, NullValidator, jsaction, action, UtilityView)
41from waeup.ikoba.browser.pages import ContactAdminForm
42from waeup.ikoba.browser.breadcrumbs import Breadcrumb
43from waeup.ikoba.browser.interfaces import ICaptchaManager
44from waeup.ikoba.utils.helpers import get_current_principal, to_timezone, now
45from waeup.ikoba.customers.interfaces import (
46    ICustomer, ICustomersContainer, ICustomerRequestPW
47    )
48from waeup.ikoba.customers.catalog import search
49
50grok.context(IIkobaObject)
51
52class CustomersBreadcrumb(Breadcrumb):
53    """A breadcrumb for the customers container.
54    """
55    grok.context(ICustomersContainer)
56    title = _('Customers')
57
58    @property
59    def target(self):
60        user = get_current_principal()
61        if getattr(user, 'user_type', None) == 'customer':
62            return None
63        return self.viewname
64
65
66class CustomerBreadcrumb(Breadcrumb):
67    """A breadcrumb for the customer container.
68    """
69    grok.context(ICustomer)
70
71    def title(self):
72        return self.context.display_fullname
73
74class CustomersContainerPage(IkobaPage):
75    """The standard view for customer containers.
76    """
77    grok.context(ICustomersContainer)
78    grok.name('index')
79    grok.require('waeup.viewCustomersContainer')
80    grok.template('containerpage')
81    label = _('Find customers')
82    search_button = _('Find customer(s)')
83    pnav = 4
84
85    def update(self, *args, **kw):
86        form = self.request.form
87        self.hitlist = []
88        if form.get('searchtype', None) == 'suspended':
89            self.searchtype = form['searchtype']
90            self.searchterm = None
91        elif 'searchterm' in form and form['searchterm']:
92            self.searchterm = form['searchterm']
93            self.searchtype = form['searchtype']
94        elif 'old_searchterm' in form:
95            self.searchterm = form['old_searchterm']
96            self.searchtype = form['old_searchtype']
97        else:
98            if 'search' in form:
99                self.flash(_('Empty search string'), type="warning")
100            return
101        if self.searchtype == 'current_session':
102            try:
103                self.searchterm = int(self.searchterm)
104            except ValueError:
105                self.flash(_('Only year dates allowed (e.g. 2011).'),
106                           type="danger")
107                return
108        self.hitlist = search(query=self.searchterm,
109            searchtype=self.searchtype, view=self)
110        if not self.hitlist:
111            self.flash(_('No customer found.'), type="warning")
112        return
113
114class CustomersContainerManagePage(IkobaPage):
115    """The manage page for customer containers.
116    """
117    grok.context(ICustomersContainer)
118    grok.name('manage')
119    grok.require('waeup.manageCustomer')
120    grok.template('containermanagepage')
121    pnav = 4
122    label = _('Manage customer section')
123    search_button = _('Find customer(s)')
124    remove_button = _('Remove selected')
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        if not 'entries' in form:
150            self.hitlist = search(query=self.searchterm,
151                searchtype=self.searchtype, view=self)
152            if not self.hitlist:
153                self.flash(_('No customer found.'), type="warning")
154            if 'remove' in form:
155                self.flash(_('No item selected.'), type="warning")
156            return
157        entries = form['entries']
158        if isinstance(entries, basestring):
159            entries = [entries]
160        deleted = []
161        for entry in entries:
162            if 'remove' in form:
163                del self.context[entry]
164                deleted.append(entry)
165        self.hitlist = search(query=self.searchterm,
166            searchtype=self.searchtype, view=self)
167        if len(deleted):
168            self.flash(_('Successfully removed: ${a}',
169                mapping = {'a':', '.join(deleted)}))
170        return
171
172class CustomerAddFormPage(IkobaAddFormPage):
173    """Add-form to add a customer.
174    """
175    grok.context(ICustomersContainer)
176    grok.require('waeup.manageCustomer')
177    grok.name('addcustomer')
178    form_fields = grok.AutoFields(ICustomer).select(
179        'firstname', 'middlename', 'lastname', 'reg_number')
180    label = _('Add customer')
181    pnav = 4
182
183    @action(_('Create customer record'), style='primary')
184    def addCustomer(self, **data):
185        customer = createObject(u'waeup.Customer')
186        self.applyData(customer, **data)
187        self.context.addCustomer(customer)
188        self.flash(_('Customer record created.'))
189        self.redirect(self.url(self.context[customer.customer_id], 'index'))
190        return
191
192class LoginAsCustomerStep1(IkobaEditFormPage):
193    """ View to temporarily set a customer password.
194    """
195    grok.context(ICustomer)
196    grok.name('loginasstep1')
197    grok.require('waeup.loginAsCustomer')
198    grok.template('loginasstep1')
199    pnav = 4
200
201    def label(self):
202        return _(u'Set temporary password for ${a}',
203            mapping = {'a':self.context.display_fullname})
204
205    @action('Set password now', style='primary')
206    def setPassword(self, *args, **data):
207        kofa_utils = getUtility(IIkobaUtils)
208        password = kofa_utils.genPassword()
209        self.context.setTempPassword(self.request.principal.id, password)
210        self.context.writeLogMessage(
211            self, 'temp_password generated: %s' % password)
212        args = {'password':password}
213        self.redirect(self.url(self.context) +
214            '/loginasstep2?%s' % urlencode(args))
215        return
216
217class LoginAsCustomerStep2(IkobaPage):
218    """ View to temporarily login as customer with a temporary password.
219    """
220    grok.context(ICustomer)
221    grok.name('loginasstep2')
222    grok.require('waeup.Public')
223    grok.template('loginasstep2')
224    login_button = _('Login now')
225    pnav = 4
226
227    def label(self):
228        return _(u'Login as ${a}',
229            mapping = {'a':self.context.customer_id})
230
231    def update(self, SUBMIT=None, password=None):
232        self.password = password
233        if SUBMIT is not None:
234            self.flash(_('You successfully logged in as customer.'))
235            self.redirect(self.url(self.context))
236        return
237
238class CustomerBaseDisplayFormPage(IkobaDisplayFormPage):
239    """ Page to display customer base data
240    """
241    grok.context(ICustomer)
242    grok.name('index')
243    grok.require('waeup.viewCustomer')
244    grok.template('basepage')
245    form_fields = grok.AutoFields(ICustomer).omit(
246        'password', 'suspended', 'suspended_comment')
247    pnav = 4
248
249    @property
250    def label(self):
251        if self.context.suspended:
252            return _('${a}: Base Data (account deactivated)',
253                mapping = {'a':self.context.display_fullname})
254        return  _('${a}: Base Data',
255            mapping = {'a':self.context.display_fullname})
256
257    @property
258    def hasPassword(self):
259        if self.context.password:
260            return _('set')
261        return _('unset')
262
263class ContactCustomerForm(ContactAdminForm):
264    grok.context(ICustomer)
265    grok.name('contactcustomer')
266    grok.require('waeup.viewCustomer')
267    pnav = 4
268    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
269
270    def update(self, subject=u'', body=u''):
271        super(ContactCustomerForm, self).update()
272        self.form_fields.get('subject').field.default = subject
273        self.form_fields.get('body').field.default = body
274        return
275
276    def label(self):
277        return _(u'Send message to ${a}',
278            mapping = {'a':self.context.display_fullname})
279
280    @action('Send message now', style='primary')
281    def send(self, *args, **data):
282        try:
283            email = self.request.principal.email
284        except AttributeError:
285            email = self.config.email_admin
286        usertype = getattr(self.request.principal,
287                           'user_type', 'system').title()
288        kofa_utils = getUtility(IIkobaUtils)
289        success = kofa_utils.sendContactForm(
290                self.request.principal.title,email,
291                self.context.display_fullname,self.context.email,
292                self.request.principal.id,usertype,
293                self.config.name,
294                data['body'],data['subject'])
295        if success:
296            self.flash(_('Your message has been sent.'))
297        else:
298            self.flash(_('An smtp server error occurred.'), type="danger")
299        return
300
301class CustomerBaseManageFormPage(IkobaEditFormPage):
302    """ View to manage customer base data
303    """
304    grok.context(ICustomer)
305    grok.name('manage_base')
306    grok.require('waeup.manageCustomer')
307    form_fields = grok.AutoFields(ICustomer).omit(
308        'customer_id', 'adm_code', 'suspended')
309    grok.template('basemanagepage')
310    label = _('Manage base data')
311    pnav = 4
312
313    def update(self):
314        super(CustomerBaseManageFormPage, self).update()
315        self.wf_info = IWorkflowInfo(self.context)
316        return
317
318    @action(_('Save'), style='primary')
319    def save(self, **data):
320        form = self.request.form
321        password = form.get('password', None)
322        password_ctl = form.get('control_password', None)
323        if password:
324            validator = getUtility(IPasswordValidator)
325            errors = validator.validate_password(password, password_ctl)
326            if errors:
327                self.flash( ' '.join(errors), type="danger")
328                return
329        changed_fields = self.applyData(self.context, **data)
330        # Turn list of lists into single list
331        if changed_fields:
332            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
333        else:
334            changed_fields = []
335        if password:
336            # Now we know that the form has no errors and can set password
337            IUserAccount(self.context).setPassword(password)
338            changed_fields.append('password')
339        fields_string = ' + '.join(changed_fields)
340        self.flash(_('Form has been saved.'))
341        if fields_string:
342            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
343        return
344
345class CustomerTriggerTransitionFormPage(IkobaEditFormPage):
346    """ View to manage customer base data
347    """
348    grok.context(ICustomer)
349    grok.name('trigtrans')
350    grok.require('waeup.triggerTransition')
351    grok.template('trigtrans')
352    label = _('Trigger registration transition')
353    pnav = 4
354
355    def getTransitions(self):
356        """Return a list of dicts of allowed transition ids and titles.
357
358        Each list entry provides keys ``name`` and ``title`` for
359        internal name and (human readable) title of a single
360        transition.
361        """
362        wf_info = IWorkflowInfo(self.context)
363        allowed_transitions = [t for t in wf_info.getManualTransitions()]
364        return [dict(name='', title=_('No transition'))] +[
365            dict(name=x, title=y) for x, y in allowed_transitions]
366
367    @action(_('Save'), style='primary')
368    def save(self, **data):
369        form = self.request.form
370        if 'transition' in form and form['transition']:
371            transition_id = form['transition']
372            wf_info = IWorkflowInfo(self.context)
373            wf_info.fireTransition(transition_id)
374        return
375
376class CustomerActivatePage(UtilityView, grok.View):
377    """ Activate customer account
378    """
379    grok.context(ICustomer)
380    grok.name('activate')
381    grok.require('waeup.manageCustomer')
382
383    def update(self):
384        self.context.suspended = False
385        self.context.writeLogMessage(self, 'account activated')
386        history = IObjectHistory(self.context)
387        history.addMessage('Customer account activated')
388        self.flash(_('Customer account has been activated.'))
389        self.redirect(self.url(self.context))
390        return
391
392    def render(self):
393        return
394
395class CustomerDeactivatePage(UtilityView, grok.View):
396    """ Deactivate customer account
397    """
398    grok.context(ICustomer)
399    grok.name('deactivate')
400    grok.require('waeup.manageCustomer')
401
402    def update(self):
403        self.context.suspended = True
404        self.context.writeLogMessage(self, 'account deactivated')
405        history = IObjectHistory(self.context)
406        history.addMessage('Customer account deactivated')
407        self.flash(_('Customer account has been deactivated.'))
408        self.redirect(self.url(self.context))
409        return
410
411    def render(self):
412        return
413
414class CustomerHistoryPage(IkobaPage):
415    """ Page to display customer history
416    """
417    grok.context(ICustomer)
418    grok.name('history')
419    grok.require('waeup.viewCustomer')
420    grok.template('customerhistory')
421    pnav = 4
422
423    @property
424    def label(self):
425        return _('${a}: History', mapping = {'a':self.context.display_fullname})
426
427class CustomerRequestPasswordPage(IkobaAddFormPage):
428    """Captcha'd registration page for applicants.
429    """
430    grok.name('requestpw')
431    grok.require('waeup.Anonymous')
432    grok.template('requestpw')
433    form_fields = grok.AutoFields(ICustomerRequestPW).select(
434        'firstname','number','email')
435    label = _('Request password for first-time login')
436
437    def update(self):
438        # Handle captcha
439        self.captcha = getUtility(ICaptchaManager).getCaptcha()
440        self.captcha_result = self.captcha.verify(self.request)
441        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
442        return
443
444    def _redirect(self, email, password, customer_id):
445        # Forward only email to landing page in base package.
446        self.redirect(self.url(self.context, 'requestpw_complete',
447            data = dict(email=email)))
448        return
449
450    def _pw_used(self):
451        # XXX: False if password has not been used. We need an extra
452        #      attribute which remembers if customer logged in.
453        return True
454
455    @action(_('Send login credentials to email address'), style='primary')
456    def get_credentials(self, **data):
457        if not self.captcha_result.is_valid:
458            # Captcha will display error messages automatically.
459            # No need to flash something.
460            return
461        number = data.get('number','')
462        firstname = data.get('firstname','')
463        cat = getUtility(ICatalog, name='customers_catalog')
464        results = list(
465            cat.searchResults(reg_number=(number, number)))
466        if results:
467            customer = results[0]
468            if getattr(customer,'firstname',None) is None:
469                self.flash(_('An error occurred.'), type="danger")
470                return
471            elif customer.firstname.lower() != firstname.lower():
472                # Don't tell the truth here. Anonymous must not
473                # know that a record was found and only the firstname
474                # verification failed.
475                self.flash(_('No customer record found.'), type="warning")
476                return
477            elif customer.password is not None and self._pw_used:
478                self.flash(_('Your password has already been set and used. '
479                             'Please proceed to the login page.'),
480                           type="warning")
481                return
482            # Store email address but nothing else.
483            customer.email = data['email']
484            notify(grok.ObjectModifiedEvent(customer))
485        else:
486            # No record found, this is the truth.
487            self.flash(_('No customer record found.'), type="warning")
488            return
489
490        kofa_utils = getUtility(IIkobaUtils)
491        password = kofa_utils.genPassword()
492        mandate = PasswordMandate()
493        mandate.params['password'] = password
494        mandate.params['user'] = customer
495        site = grok.getSite()
496        site['mandates'].addMandate(mandate)
497        # Send email with credentials
498        args = {'mandate_id':mandate.mandate_id}
499        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
500        url_info = u'Confirmation link: %s' % mandate_url
501        msg = _('You have successfully requested a password for the')
502        if kofa_utils.sendCredentials(IUserAccount(customer),
503            password, url_info, msg):
504            email_sent = customer.email
505        else:
506            email_sent = None
507        self._redirect(email=email_sent, password=password,
508            customer_id=customer.customer_id)
509        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
510        self.context.logger.info(
511            '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
512        return
513
514class CustomerRequestPasswordEmailSent(IkobaPage):
515    """Landing page after successful password request.
516
517    """
518    grok.name('requestpw_complete')
519    grok.require('waeup.Public')
520    grok.template('requestpwmailsent')
521    label = _('Your password request was successful.')
522
523    def update(self, email=None, customer_id=None, password=None):
524        self.email = email
525        self.password = password
526        self.customer_id = customer_id
527        return
528
529class CustomerFilesUploadPage(IkobaPage):
530    """ View to upload files by customer
531    """
532    grok.context(ICustomer)
533    grok.name('change_portrait')
534    grok.require('waeup.uploadCustomerFile')
535    grok.template('filesuploadpage')
536    label = _('Upload files')
537    pnav = 4
538
539    def update(self):
540        PWCHANGE_STATES = getUtility(ICustomersUtils).PWCHANGE_STATES
541        if self.context.customer.state not in PWCHANGE_STATES:
542            emit_lock_message(self)
543            return
544        super(CustomerFilesUploadPage, self).update()
545        return
546
547# Pages for customers only
548
549class CustomerBaseEditFormPage(IkobaEditFormPage):
550    """ View to edit customer base data
551    """
552    grok.context(ICustomer)
553    grok.name('edit_base')
554    grok.require('waeup.handleCustomer')
555    form_fields = grok.AutoFields(ICustomer).select(
556        'email', 'phone')
557    label = _('Edit base data')
558    pnav = 4
559
560    @action(_('Save'), style='primary')
561    def save(self, **data):
562        msave(self, **data)
563        return
564
565class CustomerChangePasswordPage(IkobaEditFormPage):
566    """ View to edit customer passords
567    """
568    grok.context(ICustomer)
569    grok.name('change_password')
570    grok.require('waeup.handleCustomer')
571    grok.template('change_password')
572    label = _('Change password')
573    pnav = 4
574
575    @action(_('Save'), style='primary')
576    def save(self, **data):
577        form = self.request.form
578        password = form.get('change_password', None)
579        password_ctl = form.get('change_password_repeat', None)
580        if password:
581            validator = getUtility(IPasswordValidator)
582            errors = validator.validate_password(password, password_ctl)
583            if not errors:
584                IUserAccount(self.context).setPassword(password)
585                self.context.writeLogMessage(self, 'saved: password')
586                self.flash(_('Password changed.'))
587            else:
588                self.flash( ' '.join(errors), type="warning")
589        return
590
Note: See TracBrowser for help on using the repository browser.