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

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

Replace kofa by ikoba.

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