Index: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 11975)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 11977)
@@ -262,4 +262,7 @@
     login_button = label
 
+    def _comment(self, customer):
+        return getattr(customer, 'suspended_comment', None)
+
     def update(self, SUBMIT=None, camefrom=None):
         self.camefrom = camefrom
@@ -1613,13 +1616,12 @@
     def _searchUser(self, identifier, email):
         # Search customer
-        #cat = queryUtility(ICatalog, name='customers_catalog')
-        #results = cat.searchResults(
-            #reg_number=(identifier, identifier),
-        #    email=(email,email))
-        #for result in results:
-        #    if result.customer_id == identifier or \
-        #        result.reg_number == identifier or \
-        #        result.matric_number == identifier:
-        #        return result
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = cat.searchResults(
+            reg_number=(identifier, identifier),
+            email=(email,email))
+        for result in results:
+            if result.customer_id == identifier \
+                or result.reg_number == identifier:
+                return result
         # Search portal user
         user = grok.getSite()['users'].get(identifier, None)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 11975)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py	(revision 11977)
@@ -42,7 +42,8 @@
 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
+    ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils
     )
 from waeup.ikoba.customers.catalog import search
@@ -507,5 +508,5 @@
         self._redirect(email=email_sent, password=password,
             customer_id=customer.customer_id)
-        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
         self.context.logger.info(
             '%s - %s (%s) - %s' % (ob_class, number, customer.customer_id, email_sent))
@@ -567,7 +568,7 @@
     """
     grok.context(ICustomer)
-    grok.name('change_password')
+    grok.name('changepassword')
     grok.require('waeup.handleCustomer')
-    grok.template('change_password')
+    grok.template('changepassword')
     label = _('Change password')
     pnav = 4
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/change_password.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/change_password.pt	(revision 11975)
+++ 	(revision )
@@ -1,29 +1,0 @@
-<form action="." tal:attributes="action request/URL" method="post"
-      i18n:domain="waeup.kofa" enctype="multipart/form-data">
-
-  <input type="hidden" name="student_id" value=""
-         tal:attributes="value context/student_id" />
-
-  <table class="form-table">
-    <tbody>
-      <tr>
-        <td class="fieldname" i18n:translate="">Password:</td>
-        <td>
-          <input name="change_password" class="form-control" type="password"  />
-        </td>
-      </tr>
-      <tr>
-        <td class="fieldname" i18n:translate="">Retype Password:</td>
-        <td>
-          <input name="change_password_repeat" class="form-control" type="password" />
-        </td>
-      </tr>
-    </tbody>
-  </table>
-
-  <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/changepassword.pt
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/changepassword.pt	(revision 11977)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/changepassword.pt	(revision 11977)
@@ -0,0 +1,29 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <input type="hidden" name="customer_id" value=""
+         tal:attributes="value context/customer_id" />
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input name="change_password" class="form-control" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input name="change_password_repeat" class="form-control" type="password" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <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/tests/test_authentication.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_authentication.py	(revision 11975)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_authentication.py	(revision 11977)
@@ -237,4 +237,4 @@
         self.assertRaises(
             Unauthorized,
-            self.browser.open, self.customer_path + '/change_password')
-        return
+            self.browser.open, self.customer_path + '/changepassword')
+        return
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py	(revision 11975)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py	(revision 11977)
@@ -498,2 +498,197 @@
             'mrofficer - customers.browser.LoginAsCustomerStep1 - K1000000 - '
             'temp_password generated: %s' % temp_password in logcontent)
+
+class CustomerUITests(CustomersFullSetup):
+    # Tests for Customer class views and pages
+
+    def test_customer_change_password(self):
+        # Customers can change the password
+        self.customer.personal_updated = datetime.utcnow()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        # Change password
+        self.browser.getLink("Change password").click()
+        self.browser.getControl(name="change_password").value = 'pw'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'pw'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password must have at least' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_passssword'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Passwords do not match' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_password'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password changed' in self.browser.contents)
+        # We are still logged in. Changing the password hasn't thrown us out.
+        self.browser.getLink("Base Data").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        # We can logout
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app/index')
+        # We can login again with the new password
+        self.browser.getLink("Login").click()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'new_password'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.customer_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        return
+
+    def test_customer_upload_passport(self):
+        # Customer cant login if their password is not set
+        IWorkflowInfo(self.customer).fireTransition('start')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Admitted customer can upload a passport picture
+        self.browser.open(self.customer_path + '/change_portrait')
+        ctrl = self.browser.getControl(name='passportuploadedit')
+        file_obj = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passportuploadedit').click()
+        self.assertTrue(
+            'src="http://localhost/app/customers/K1000000/passport.jpg"'
+            in self.browser.contents)
+
+    def test_customer_login(self):
+        # Customer cant login if their password is not set
+        self.customer.password = None
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        # We set the password again
+        IUserAccount(
+            self.app['customers'][self.customer_id]).setPassword('spwd')
+        # Customers can't login if their account is suspended/deactivated
+        self.customer.suspended = True
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">'
+            'Your account has been deactivated.</div>...', self.browser.contents)
+        # If suspended_comment is set this message will be flashed instead
+        self.customer.suspended_comment = u'Aetsch baetsch!'
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">Aetsch baetsch!</div>...',
+            self.browser.contents)
+        self.customer.suspended = False
+        # Customers can't login if a temporary password has been set and
+        # is not expired
+        self.app['customers'][self.customer_id].setTempPassword(
+            'anybody', 'temp_spwd')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...Your account has been temporarily deactivated...',
+            self.browser.contents)
+        # The customer can login with the temporary password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'temp_spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Customer can view the base data
+        self.browser.open(self.customer_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.customer_path)
+        # When the password expires ...
+        delta = timedelta(minutes=11)
+        self.app['customers'][self.customer_id].temp_password[
+            'timestamp'] = datetime.utcnow() - delta
+        self.app['customers'][self.customer_id]._p_changed = True
+        # ... the customer will be automatically logged out
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.customer_path)
+        # Then the customer can login with the original password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.customer_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+
+    def test_change_password_request(self):
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.identifier").value = '123'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('An email with' in self.browser.contents)
+
+class CustomerRequestPWTests(CustomersFullSetup):
+    # Tests for customer registration
+
+    layer = FunctionalLayer
+
+    def test_request_pw(self):
+        # Customer with wrong number can't be found.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.number").value = 'anynumber'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No customer record found.'
+            in self.browser.contents)
+        # Anonymous is not informed that firstname verification failed.
+        # It seems that the record doesn't exist.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").value = 'Johnny'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No customer record found.'
+            in self.browser.contents)
+        # Even with the correct firstname we can't register if a
+        # password has been set and used.
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your password has already been set and used.'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/requestpw')
+        self.app['customers'][self.customer_id].password = None
+        # The firstname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.firstname").value = 'aNNa'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        # Yeah, we succeded ...
+        self.assertTrue('Your password request was successful.'
+            in self.browser.contents)
+        # ... and  customer can be found in the catalog via the email address
+        cat = queryUtility(ICatalog, name='customers_catalog')
+        results = list(
+            cat.searchResults(
+            email=('new@yy.zz', 'new@yy.zz')))
+        self.assertEqual(self.customer,results[0])
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - customers.browser.CustomerRequestPasswordPage - '
+                        '123 (K1000000) - new@yy.zz' in logcontent)
+        return
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py	(revision 11975)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/customers/viewlets.py	(revision 11977)
@@ -234,2 +234,11 @@
         return "return window.confirm(%s);" % _(
             "'A history message will be added. Are you sure?'")
+
+class CustomerPasswordActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(ICustomer)
+    grok.view(CustomerBaseDisplayFormPage)
+    grok.require('waeup.handleCustomer')
+    icon = 'actionicon_key.png'
+    text = _('Change password')
+    target = 'changepassword'
