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

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

Add browser components for customers. Tests will follow.

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