## $Id: browser.py 11862 2014-10-21 07:07:04Z 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
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.utils.helpers import get_current_principal, to_timezone, now
from waeup.ikoba.customers.interfaces import (
    ICustomer, ICustomersContainer, ICustomerRequestPW
    )
from waeup.ikoba.customers.catalog import search

grok.context(IIkobaObject)

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):
        kofa_utils = getUtility(IIkobaUtils)
        password = kofa_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()
        kofa_utils = getUtility(IIkobaUtils)
        success = kofa_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

        kofa_utils = getUtility(IKofaUtils)
        password = kofa_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 kofa_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.kofa.','')
        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