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

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

pep8

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