## $Id: browser.py 11997 2014-11-19 17:02:48Z henrik $ ## ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """UI components for customers and related components. """ import sys import grok import pytz from urllib import urlencode from datetime import datetime from zope.event import notify from zope.i18n import translate from zope.catalog.interfaces import ICatalog from zope.component import queryUtility, getUtility, createObject from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing from zope.formlib.textwidgets import BytesDisplayWidget from zope.security import checkPermission from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState from waeup.ikoba.interfaces import MessageFactory as _ from waeup.ikoba.interfaces import ( IContactForm, IObjectHistory, IIkobaObject, IIkobaUtils, IPasswordValidator, IUserAccount) from waeup.ikoba.browser.layout import ( IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage, IkobaForm, NullValidator, jsaction, action, UtilityView) from waeup.ikoba.browser.pages import ContactAdminForm from waeup.ikoba.browser.breadcrumbs import Breadcrumb from waeup.ikoba.browser.interfaces import ICaptchaManager from waeup.ikoba.mandates.mandate import PasswordMandate from waeup.ikoba.utils.helpers import get_current_principal, to_timezone, now from waeup.ikoba.customers.interfaces import ( ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils ) from waeup.ikoba.customers.catalog import search grok.context(IIkobaObject) # Save function used for save methods in pages def msave(view, **data): changed_fields = view.applyData(view.context, **data) # Turn list of lists into single list if changed_fields: changed_fields = reduce(lambda x,y: x+y, changed_fields.values()) fields_string = ' + '.join(changed_fields) view.flash(_('Form has been saved.')) if fields_string: view.context.writeLogMessage(view, 'saved: %s' % fields_string) return def emit_lock_message(view): """Flash a lock message. """ view.flash(_('The requested form is locked (read-only).'), type="warning") view.redirect(view.url(view.context)) return class CustomersBreadcrumb(Breadcrumb): """A breadcrumb for the customers container. """ grok.context(ICustomersContainer) title = _('Customers') @property def target(self): user = get_current_principal() if getattr(user, 'user_type', None) == 'customer': return None return self.viewname class CustomerBreadcrumb(Breadcrumb): """A breadcrumb for the customer container. """ grok.context(ICustomer) def title(self): return self.context.display_fullname class CustomersContainerPage(IkobaPage): """The standard view for customer containers. """ grok.context(ICustomersContainer) grok.name('index') grok.require('waeup.viewCustomersContainer') grok.template('containerpage') label = _('Find customers') search_button = _('Find customer(s)') pnav = 4 def update(self, *args, **kw): form = self.request.form self.hitlist = [] if form.get('searchtype', None) == 'suspended': self.searchtype = form['searchtype'] self.searchterm = None elif 'searchterm' in form and form['searchterm']: self.searchterm = form['searchterm'] self.searchtype = form['searchtype'] elif 'old_searchterm' in form: self.searchterm = form['old_searchterm'] self.searchtype = form['old_searchtype'] else: if 'search' in form: self.flash(_('Empty search string'), type="warning") return if self.searchtype == 'current_session': try: self.searchterm = int(self.searchterm) except ValueError: self.flash(_('Only year dates allowed (e.g. 2011).'), type="danger") return self.hitlist = search(query=self.searchterm, searchtype=self.searchtype, view=self) if not self.hitlist: self.flash(_('No customer found.'), type="warning") return class CustomersContainerManagePage(IkobaPage): """The manage page for customer containers. """ grok.context(ICustomersContainer) grok.name('manage') grok.require('waeup.manageCustomer') grok.template('containermanagepage') pnav = 4 label = _('Manage customer section') search_button = _('Find customer(s)') remove_button = _('Remove selected') def update(self, *args, **kw): form = self.request.form self.hitlist = [] if form.get('searchtype', None) == 'suspended': self.searchtype = form['searchtype'] self.searchterm = None elif 'searchterm' in form and form['searchterm']: self.searchterm = form['searchterm'] self.searchtype = form['searchtype'] elif 'old_searchterm' in form: self.searchterm = form['old_searchterm'] self.searchtype = form['old_searchtype'] else: if 'search' in form: self.flash(_('Empty search string'), type="warning") return if self.searchtype == 'current_session': try: self.searchterm = int(self.searchterm) except ValueError: self.flash(_('Only year dates allowed (e.g. 2011).'), type="danger") return if not 'entries' in form: self.hitlist = search(query=self.searchterm, searchtype=self.searchtype, view=self) if not self.hitlist: self.flash(_('No customer found.'), type="warning") if 'remove' in form: self.flash(_('No item selected.'), type="warning") return entries = form['entries'] if isinstance(entries, basestring): entries = [entries] deleted = [] for entry in entries: if 'remove' in form: del self.context[entry] deleted.append(entry) self.hitlist = search(query=self.searchterm, searchtype=self.searchtype, view=self) if len(deleted): self.flash(_('Successfully removed: ${a}', mapping={'a': ','.join(deleted)})) return class CustomerAddFormPage(IkobaAddFormPage): """Add-form to add a customer. """ grok.context(ICustomersContainer) grok.require('waeup.manageCustomer') grok.name('addcustomer') form_fields = grok.AutoFields(ICustomer).select( 'firstname', 'middlename', 'lastname', 'reg_number') label = _('Add customer') pnav = 4 @action(_('Create customer record'), style='primary') def addCustomer(self, **data): customer = createObject(u'waeup.Customer') self.applyData(customer, **data) self.context.addCustomer(customer) self.flash(_('Customer record created.')) self.redirect(self.url(self.context[customer.customer_id], 'index')) return class LoginAsCustomerStep1(IkobaEditFormPage): """ View to temporarily set a customer password. """ grok.context(ICustomer) grok.name('loginasstep1') grok.require('waeup.loginAsCustomer') grok.template('loginasstep1') pnav = 4 def label(self): return _(u'Set temporary password for ${a}', mapping={'a': self.context.display_fullname}) @action('Set password now', style='primary') def setPassword(self, *args, **data): ikoba_utils = getUtility(IIkobaUtils) password = ikoba_utils.genPassword() self.context.setTempPassword(self.request.principal.id, password) self.context.writeLogMessage( self, 'temp_password generated: %s' % password) args = {'password': password} self.redirect(self.url(self.context) + '/loginasstep2?%s' % urlencode(args)) return class LoginAsCustomerStep2(IkobaPage): """ View to temporarily login as customer with a temporary password. """ grok.context(ICustomer) grok.name('loginasstep2') grok.require('waeup.Public') grok.template('loginasstep2') login_button = _('Login now') pnav = 4 def label(self): return _(u'Login as ${a}', mapping={'a': self.context.customer_id}) def update(self, SUBMIT=None, password=None): self.password = password if SUBMIT is not None: self.flash(_('You successfully logged in as customer.')) self.redirect(self.url(self.context)) return class CustomerBaseDisplayFormPage(IkobaDisplayFormPage): """ Page to display customer base data """ grok.context(ICustomer) grok.name('index') grok.require('waeup.viewCustomer') grok.template('basepage') form_fields = grok.AutoFields(ICustomer).omit( 'password', 'suspended', 'suspended_comment') pnav = 4 @property def label(self): if self.context.suspended: return _('${a}: Base Data (account deactivated)', mapping={'a': self.context.display_fullname}) return _('${a}: Base Data', mapping={'a': self.context.display_fullname}) @property def hasPassword(self): if self.context.password: return _('set') return _('unset') class ContactCustomerForm(ContactAdminForm): grok.context(ICustomer) grok.name('contactcustomer') grok.require('waeup.viewCustomer') pnav = 4 form_fields = grok.AutoFields(IContactForm).select('subject', 'body') def update(self, subject=u'', body=u''): super(ContactCustomerForm, self).update() self.form_fields.get('subject').field.default = subject self.form_fields.get('body').field.default = body return def label(self): return _(u'Send message to ${a}', mapping={'a': self.context.display_fullname}) @action('Send message now', style='primary') def send(self, *args, **data): try: email = self.request.principal.email except AttributeError: email = self.config.email_admin usertype = getattr(self.request.principal, 'user_type', 'system').title() ikoba_utils = getUtility(IIkobaUtils) success = ikoba_utils.sendContactForm( self.request.principal.title, email, self.context.display_fullname, self.context.email, self.request.principal.id,usertype, self.config.name, data['body'], data['subject']) if success: self.flash(_('Your message has been sent.')) else: self.flash(_('An smtp server error occurred.'), type="danger") return class CustomerBaseManageFormPage(IkobaEditFormPage): """ View to manage customer base data """ grok.context(ICustomer) grok.name('manage_base') grok.require('waeup.manageCustomer') form_fields = grok.AutoFields(ICustomer).omit( 'customer_id', 'adm_code', 'suspended') grok.template('basemanagepage') label = _('Manage base data') pnav = 4 def update(self): super(CustomerBaseManageFormPage, self).update() self.wf_info = IWorkflowInfo(self.context) return @action(_('Save'), style='primary') def save(self, **data): form = self.request.form password = form.get('password', None) password_ctl = form.get('control_password', None) if password: validator = getUtility(IPasswordValidator) errors = validator.validate_password(password, password_ctl) if errors: self.flash(' '.join(errors), type="danger") return changed_fields = self.applyData(self.context, **data) # Turn list of lists into single list if changed_fields: changed_fields = reduce(lambda x,y: x+y, changed_fields.values()) else: changed_fields = [] if password: # Now we know that the form has no errors and can set password IUserAccount(self.context).setPassword(password) changed_fields.append('password') fields_string = ' + '.join(changed_fields) self.flash(_('Form has been saved.')) if fields_string: self.context.writeLogMessage(self, 'saved: % s' % fields_string) return class CustomerTriggerTransitionFormPage(IkobaEditFormPage): """ View to manage customer base data """ grok.context(ICustomer) grok.name('trigtrans') grok.require('waeup.triggerTransition') grok.template('trigtrans') label = _('Trigger registration transition') pnav = 4 def getTransitions(self): """Return a list of dicts of allowed transition ids and titles. Each list entry provides keys ``name`` and ``title`` for internal name and (human readable) title of a single transition. """ wf_info = IWorkflowInfo(self.context) allowed_transitions = [t for t in wf_info.getManualTransitions()] return [dict(name='', title=_('No transition'))] +[ dict(name=x, title=y) for x, y in allowed_transitions] @action(_('Save'), style='primary') def save(self, **data): form = self.request.form if 'transition' in form and form['transition']: transition_id = form['transition'] wf_info = IWorkflowInfo(self.context) wf_info.fireTransition(transition_id) return class CustomerActivatePage(UtilityView, grok.View): """ Activate customer account """ grok.context(ICustomer) grok.name('activate') grok.require('waeup.manageCustomer') def update(self): self.context.suspended = False self.context.writeLogMessage(self, 'account activated') history = IObjectHistory(self.context) history.addMessage('Customer account activated') self.flash(_('Customer account has been activated.')) self.redirect(self.url(self.context)) return def render(self): return class CustomerDeactivatePage(UtilityView, grok.View): """ Deactivate customer account """ grok.context(ICustomer) grok.name('deactivate') grok.require('waeup.manageCustomer') def update(self): self.context.suspended = True self.context.writeLogMessage(self, 'account deactivated') history = IObjectHistory(self.context) history.addMessage('Customer account deactivated') self.flash(_('Customer account has been deactivated.')) self.redirect(self.url(self.context)) return def render(self): return class CustomerHistoryPage(IkobaPage): """ Page to display customer history """ grok.context(ICustomer) grok.name('history') grok.require('waeup.viewCustomer') grok.template('customerhistory') pnav = 4 @property def label(self): return _('${a}: History', mapping={'a':self.context.display_fullname}) class CustomerRequestPasswordPage(IkobaAddFormPage): """Captcha'd registration page for applicants. """ grok.name('requestpw') grok.require('waeup.Anonymous') grok.template('requestpw') form_fields = grok.AutoFields(ICustomerRequestPW).select( 'firstname','number','email') label = _('Request password for first-time login') def update(self): # Handle captcha self.captcha = getUtility(ICaptchaManager).getCaptcha() self.captcha_result = self.captcha.verify(self.request) self.captcha_code = self.captcha.display(self.captcha_result.error_code) return def _redirect(self, email, password, customer_id): # Forward only email to landing page in base package. self.redirect(self.url(self.context, 'requestpw_complete', data=dict(email=email))) return def _pw_used(self): # XXX: False if password has not been used. We need an extra # attribute which remembers if customer logged in. return True @action(_('Send login credentials to email address'), style='primary') def get_credentials(self, **data): if not self.captcha_result.is_valid: # Captcha will display error messages automatically. # No need to flash something. return number = data.get('number','') firstname = data.get('firstname','') cat = getUtility(ICatalog, name='customers_catalog') results = list( cat.searchResults(reg_number=(number, number))) if results: customer = results[0] if getattr(customer,'firstname',None) is None: self.flash(_('An error occurred.'), type="danger") return elif customer.firstname.lower() != firstname.lower(): # Don't tell the truth here. Anonymous must not # know that a record was found and only the firstname # verification failed. self.flash(_('No customer record found.'), type="warning") return elif customer.password is not None and self._pw_used: self.flash(_('Your password has already been set and used. ' 'Please proceed to the login page.'), type="warning") return # Store email address but nothing else. customer.email = data['email'] notify(grok.ObjectModifiedEvent(customer)) else: # No record found, this is the truth. self.flash(_('No customer record found.'), type="warning") return ikoba_utils = getUtility(IIkobaUtils) password = ikoba_utils.genPassword() mandate = PasswordMandate() mandate.params['password'] = password mandate.params['user'] = customer site = grok.getSite() site['mandates'].addMandate(mandate) # Send email with credentials args = {'mandate_id':mandate.mandate_id} mandate_url = self.url(site) + '/mandate?%s' % urlencode(args) url_info = u'Confirmation link: %s' % mandate_url msg = _('You have successfully requested a password for the') if ikoba_utils.sendCredentials(IUserAccount(customer), password, url_info, msg): email_sent = customer.email else: email_sent = None self._redirect(email=email_sent, password=password, customer_id=customer.customer_id) ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','') self.context.logger.info( '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent)) return class CustomerRequestPasswordEmailSent(IkobaPage): """Landing page after successful password request. """ grok.name('requestpw_complete') grok.require('waeup.Public') grok.template('requestpwmailsent') label = _('Your password request was successful.') def update(self, email=None, customer_id=None, password=None): self.email = email self.password = password self.customer_id = customer_id return class CustomerFilesUploadPage(IkobaPage): """ View to upload files by customer """ grok.context(ICustomer) grok.name('change_portrait') grok.require('waeup.uploadCustomerFile') grok.template('filesuploadpage') label = _('Upload files') pnav = 4 def update(self): PWCHANGE_STATES = getUtility(ICustomersUtils).PWCHANGE_STATES if self.context.customer.state not in PWCHANGE_STATES: emit_lock_message(self) return super(CustomerFilesUploadPage, self).update() return # Pages for customers only class CustomerBaseEditFormPage(IkobaEditFormPage): """ View to edit customer base data """ grok.context(ICustomer) grok.name('edit_base') grok.require('waeup.handleCustomer') form_fields = grok.AutoFields(ICustomer).select( 'email', 'phone') label = _('Edit base data') pnav = 4 @action(_('Save'), style='primary') def save(self, **data): msave(self, **data) return class CustomerChangePasswordPage(IkobaEditFormPage): """ View to edit customer passords """ grok.context(ICustomer) grok.name('changepassword') grok.require('waeup.handleCustomer') grok.template('changepassword') label = _('Change password') pnav = 4 @action(_('Save'), style='primary') def save(self, **data): form = self.request.form password = form.get('change_password', None) password_ctl = form.get('change_password_repeat', None) if password: validator = getUtility(IPasswordValidator) errors = validator.validate_password(password, password_ctl) if not errors: IUserAccount(self.context).setPassword(password) self.context.writeLogMessage(self, 'saved: password') self.flash(_('Password changed.')) else: self.flash(' '.join(errors), type="warning") return