Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 11967)
@@ -19,15 +19,31 @@
 """
 
+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)
+    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 (
-    ICustomersContainer
+    ICustomer, ICustomersContainer, ICustomerRequestPW
     )
 from waeup.ikoba.customers.catalog import search
+
+grok.context(IIkobaObject)
 
 class CustomersBreadcrumb(Breadcrumb):
@@ -43,4 +59,13 @@
             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):
@@ -141,2 +166,359 @@
                 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
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/basepage.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/basepage.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/basepage.pt	(revision 11967)
@@ -0,0 +1,30 @@
+<table i18n:domain="waeup.ikoba" class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <tal:block content="widget/label"/>:
+        </td>
+        <td>
+          <input tal:replace="structure widget" />
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    </tr>
+  </tbody>
+  <tfoot>
+    <tr>
+      <td>
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render" />
+      </td>
+    </tr>
+  </tfoot>
+</table>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/customerhistory.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/customerhistory.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/customerhistory.pt	(revision 11967)
@@ -0,0 +1,5 @@
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep1.pt	(revision 11967)
@@ -0,0 +1,18 @@
+<form i18n:domain="waeup.kofa" method="post">
+  <p i18n:translate="login_as_intro_1">
+    Kofa is creating a temporary customer password which will replace the
+    original password chosen by the customer. The password will be valid
+    for
+    <span i18n:name="minutes"
+          tal:replace=view/context/temp_password_minutes></span>
+    minutes. The customer can't login
+    during this time period. After expiration the customer can login
+    with the original password again.
+  </p>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/loginasstep2.pt	(revision 11967)
@@ -0,0 +1,13 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <p i18n:translate="login_as_intro_2">
+    The random password
+    "<span i18n:name="password"
+          tal:replace=view/password></span>"
+    has been set. You can proceed by clicking the button below.
+  </p>
+  <input type="hidden" name="form.login" tal:attributes="value view/context/customer_id" />
+  <input type="hidden" name="form.password"   tal:attributes="value view/password" />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpw.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpw.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpw.pt	(revision 11967)
@@ -0,0 +1,28 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+  <p i18n:translate="">
+    Your student record will be looked up and
+    your login credentials will be sent to the email address given above.
+    To be able to proceed you must provide a valid email address!
+  </p>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/requestpwmailsent.pt	(revision 11967)
@@ -0,0 +1,47 @@
+<tal:only_email condition = "not: view/password" i18n:domain="waeup.kofa">
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p tal:condition = "view/email" i18n:translate="">
+    Please check your email account to proceed.
+  </p>
+  <p tal:condition = "not: view/email" i18n:translate="">
+    Email could not been sent. Please retry later.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:only_email>
+
+<tal:credentials condition = "view/password" i18n:domain="waeup.kofa">
+  <h3 i18n:translate="">Your login credentials are:</h3>
+  <table class="bordered-table span6">
+    <tr>
+      <td i18n:translate="">User Name:</td>
+      <td tal:content="view/applicant_id">ID</td>
+    <tr>
+    <tr>
+      <td i18n:translate="">Password:</td>
+      <td tal:content="view/password">PASSWORD</td>
+    <tr>
+  </table>
+  <p>
+    <span i18n:translate="">Print this page and proceed to the</span>
+    <a tal:attributes="href python: view.url(layout.site, 'login')">login form</a>.
+    <span i18n:translate="">Please note that passwords are case-sensitive,
+    <br />when entering your credentials, and keep your password secret!
+    </span>
+  </p>
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:credentials>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/trigtrans.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/trigtrans.pt	(revision 11967)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/trigtrans.pt	(revision 11967)
@@ -0,0 +1,18 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.ikoba" enctype="multipart/form-data"
+      autocomplete="off">
+
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py	(revision 11967)
@@ -190,5 +190,5 @@
         """
 
-    def export_student(student, filepath=None):
+    def export_customer(customer, filepath=None):
         """Export data for a given customer.
         """
@@ -198,2 +198,24 @@
         """
 
+class ICustomerRequestPW(ICustomer):
+    """Representation of an customer for first-time password request.
+
+    This interface is used when customers use the requestpw page to
+    login for the the first time.
+    """
+    number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = True,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/permissions.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/permissions.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/permissions.py	(revision 11967)
@@ -50,4 +50,7 @@
     grok.name('waeup.loginAsCustomer')
 
+class TriggerTransition(grok.Permission):
+    grok.name('waeup.triggerTransition')
+
 # Local role
 class CustomerRecordOwner(grok.Role):
@@ -76,5 +79,5 @@
                      'waeup.manageCustomer', 'waeup.viewCustomersContainer',
                      'waeup.payCustomer', 'waeup.uploadCustomerFile',
-                     'waeup.viewCustomersTab')
+                     'waeup.viewCustomersTab', 'waeup.triggerTransition')
 
 class CustomerImpersonator(grok.Role):
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py	(revision 11967)
@@ -18,4 +18,6 @@
 
 import grok
+from zope.i18n import translate
+from zope.interface import Interface
 from waeup.ikoba.interfaces import IIkobaObject
 from waeup.ikoba.interfaces import MessageFactory as _
@@ -25,6 +27,11 @@
     default_primary_nav_template, default_filedisplay_template,
     default_fileupload_template)
-
-grok.context(IIkobaObject) # Make IKofaObject the default context
+from waeup.ikoba.customers.interfaces import (
+    ICustomer, ICustomersContainer)
+from waeup.ikoba.customers.browser import (
+    CustomersContainerPage, CustomersContainerManagePage,
+    CustomerBaseDisplayFormPage)
+
+grok.context(IIkobaObject) # Make IIkobaObject the default context
 grok.templatedir('browser_templates')
 
@@ -99,2 +106,130 @@
             ]
         return targets
+
+class CustomerManageSidebar(grok.ViewletManager):
+    grok.name('left_customermanage')
+
+class CustomerManageLink(grok.Viewlet):
+    """A link displayed in the customer box which shows up for CustomerNavigation
+    objects.
+
+    """
+    grok.baseclass()
+    grok.viewletmanager(CustomerManageSidebar)
+    grok.view(Interface)
+    grok.order(5)
+    grok.require('waeup.viewCustomer')
+
+    link = 'index'
+    text = _(u'Base Data')
+
+    def render(self):
+        url = self.view.url(self.context.customer, self.link)
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        text = translate(self.text, 'waeup.kofa',
+            target_language=lang)
+        if not self.link:
+            return ''
+        return u'<li><a href="%s">%s</a></li>' % (
+                url, text)
+
+class CustomerManageBaseLink(CustomerManageLink):
+    grok.order(2)
+    link = 'index'
+    text = _(u'Base Data')
+
+class CustomerManageHistoryLink(CustomerManageLink):
+    grok.order(8)
+    link = 'history'
+    text = _(u'History')
+
+class CustomersContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomersContainer)
+    grok.view(CustomersContainerPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage customer section')
+
+class CustomersContainerAddActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(ICustomersContainer)
+    grok.view(CustomersContainerManagePage)
+    grok.require('waeup.manageCustomer')
+    text = _('Add customer')
+    target = 'addcustomer'
+
+class ContactActionButton(ManageActionButton):
+    grok.order(5)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    icon = 'actionicon_mail.png'
+    text = _('Send email')
+    target = 'contactcustomer'
+
+class CustomerBaseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Manage')
+    target = 'manage_base'
+
+class CustomerTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.triggerTransition')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Trigger transition')
+    target = 'trigtrans'
+
+class CustomerLoginAsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.loginAsCustomer')
+    icon = 'actionicon_mask.png'
+    text = _(u'Login as customer')
+    target = 'loginasstep1'
+
+class CustomerDeactivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Deactivate account')
+    target = 'deactivate'
+    icon = 'actionicon_traffic_lights_red.png'
+
+    @property
+    def target_url(self):
+        if self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+class CustomerActivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.manageCustomer')
+    text = _('Activate account')
+    target = 'activate'
+    icon = 'actionicon_traffic_lights_green.png'
+
+    @property
+    def target_url(self):
+        if not self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/workflow.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/workflow.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/workflow.py	(revision 11967)
@@ -25,5 +25,5 @@
 from waeup.ikoba.interfaces import (
     IObjectHistory, IIkobaWorkflowInfo, IIkobaUtils,
-    CREATED, REQUESTED, APPROVED)
+    STARTED, CREATED, REQUESTED, APPROVED)
 from waeup.ikoba.interfaces import MessageFactory as _
 from waeup.ikoba.workflow import IkobaWorkflow, IkobaWorkflowInfo
@@ -32,5 +32,5 @@
 
 
-IMPORTABLE_STATES = (REQUESTED, APPROVED)
+IMPORTABLE_STATES = (STARTED, REQUESTED, APPROVED)
 
 REGISTRATION_TRANSITIONS = (
@@ -44,9 +44,17 @@
 
     Transition(
+        transition_id = 'start',
+        title = _('Start registration'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Customer registration started'),
+        destination = STARTED),
+
+    Transition(
         transition_id = 'request',
         title = _('Request registration'),
         msg = _('Customer registration requested'),
-        source = REQUESTED,
-        destination = APPROVED),
+        source = STARTED,
+        destination = REQUESTED),
 
     Transition(
@@ -62,5 +70,5 @@
         msg = _('Reset to initial customer state'),
         source = APPROVED,
-        destination = CREATED),
+        destination = STARTED),
 
     Transition(
@@ -76,5 +84,5 @@
         msg = _("Reset to initial state"),
         source = REQUESTED,
-        destination = CREATED),
+        destination = STARTED),
 
     )
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py	(revision 11967)
@@ -46,4 +46,5 @@
 
 CREATED = 'created'
+STARTED = 'started'
 REQUESTED = 'requested'
 APPROVED = 'approved'
@@ -84,4 +85,5 @@
 registration_states_vocab = SimpleIkobaVocabulary(
     (_('created'), CREATED),
+    (_('started'), STARTED),
     (_('requested'), REQUESTED),
     (_('approved'), APPROVED),
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/permissions.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/permissions.py	(revision 11966)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/permissions.py	(revision 11967)
@@ -151,5 +151,4 @@
                      'waeup.importData',
                      'waeup.exportData',
-                     'waeup.viewTranscript',
                      'waeup.managePortalConfiguration',
                      'waeup.editUser',
@@ -159,4 +158,5 @@
                      'waeup.manageCustomer', 'waeup.viewCustomersContainer',
                      'waeup.payCustomer', 'waeup.uploadCustomerFile',
+                     'waeup.triggerTransition',
                      'waeup.viewCustomersTab'
                      )
@@ -176,5 +176,4 @@
                      #'waeup.importData',
                      'waeup.exportData',
-                     'waeup.viewTranscript',
                      'waeup.managePortalConfiguration',
                      #'waeup.editUser',
@@ -184,4 +183,5 @@
                      'waeup.manageCustomer', 'waeup.viewCustomersContainer',
                      'waeup.payCustomer', 'waeup.uploadCustomerFile',
+                     'waeup.triggerTransition',
                      'waeup.viewCustomersTab'
                      )
