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

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

propset svn:keywords "Id"

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