Index: /main/waeup.ikoba/trunk/CHANGES.txt
===================================================================
--- /main/waeup.ikoba/trunk/CHANGES.txt	(revision 13802)
+++ /main/waeup.ikoba/trunk/CHANGES.txt	(revision 13803)
@@ -4,4 +4,7 @@
 0.2.dev0 (unreleased)
 =====================
+
+* Enable temporary suspension of officer accounts. Plugins must be
+  updated after restart.
 
 * Do only allow one running export job.
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/authentication.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/authentication.py	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/authentication.py	(revision 13803)
@@ -210,4 +210,5 @@
         self.phone = phone
         self.public_name = public_name
+        self.suspended = False
         self.setPassword(password)
         self.setSiteRolesForPrincipal(roles)
@@ -226,4 +227,6 @@
         if not self.password:
             # unset/empty passwords do never match
+            return False
+        if self.suspended == True:
             return False
         passwordmanager = getUtility(IPasswordManager, 'SSHA')
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py	(revision 13803)
@@ -336,4 +336,18 @@
                                    type='warning')
                     return
+            # Display appropriate flash message if credentials are correct
+            # but officer has been deactivated.
+            login = self.request.form['form.login']
+            if login in grok.getSite()['users']:
+                user = grok.getSite()['users'][login]
+                password = self.request.form['form.password']
+                passwordmanager = getUtility(IPasswordManager, 'SSHA')
+                if user.password is not None and \
+                    passwordmanager.checkPassword(user.password, password):
+                    self.flash(_('Your user name and password are correct '
+                                 'but yor account has been temporarily '
+                                 'deactivated.'),
+                               type='warning')
+                    return
             self.flash(_('You entered invalid credentials.'), type='danger')
             return
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/usereditformpage.pt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/usereditformpage.pt	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/usereditformpage.pt	(revision 13803)
@@ -1,2 +1,9 @@
+<p class="alert alert-danger"  i18n:domain="waeup.kofa"
+   i18n:translate="officer_suspended_warning"
+   tal:condition="python: getattr(context, 'suspended', False)">
+  <strong>ATTENTION:</strong>
+  This account has been suspended. The officer can't login.
+</p>
+
 <form action="." tal:attributes="action request/URL" method="post"
       i18n:domain="waeup.ikoba" enctype="multipart/form-data"
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/userscontainerpage.pt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/userscontainerpage.pt	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/templates/userscontainerpage.pt	(revision 13803)
@@ -12,5 +12,8 @@
     <tr tal:repeat="account context/values">
       <td tal:content="account/name">USERNAME</td>
-      <td tal:content="account/title">TITLE</td>
+      <td>
+        <span tal:content="account/title">TITLE</span>
+        <span style="color:red" tal:condition="account/suspended">(suspended)</span>
+      </td>
       <td nowrap tal:content="structure python:view.getSiteRoles(account)">SITE ROLES</td>
       <td tal:content="structure python:view.getLocalRoles(account)">LOCAL ROLES</td>
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/tests/test_browser.py	(revision 13803)
@@ -30,5 +30,5 @@
 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
 from waeup.ikoba.app import Company
-from waeup.ikoba.interfaces import IJobManager
+from waeup.ikoba.interfaces import IJobManager, IUserAccount
 from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
 
@@ -160,2 +160,56 @@
         return
 
+
+class SupplementaryBrowserTests(CompanySetup):
+    # These are additional tests to browser.txt
+
+    def test_suspended_officer(self):
+        self.app['users'].addUser(
+            'officer', 'secret', title='Bob Officer', email='aa@aa.ng')
+        # Officer can't login if their password is not set
+        self.app['users']['officer'].password = None
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = 'secret'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        # We set the password again
+        IUserAccount(
+            self.app['users']['officer']).setPassword('secret')
+        # Officers can't login if their account is suspended/deactivated
+        self.app['users']['officer'].suspended = True
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = 'secret'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...but yor account has been temporarily deactivated...',
+            self.browser.contents)
+        self.assertFalse("Bob Officer" in self.browser.contents)
+        self.app['users']['officer'].suspended = False
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = 'secret'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        self.assertTrue("Bob Officer" in self.browser.contents)
+        self.browser.getLink("Logout").click()
+        # Suspended accounts are marked
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/users')
+        self.assertFalse('(suspended)' in self.browser.contents)
+        self.app['users']['officer'].suspended = True
+        self.browser.open('http://localhost/app/users')
+        self.assertTrue(
+            '<span style="color:red">(suspended)</span>'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/users/officer')
+        self.assertTrue(
+            'This account has been suspended.' in self.browser.contents)
+        self.app['users']['officer'].suspended = False
+        self.browser.open('http://localhost/app/users/officer')
+        self.assertFalse(
+            'This account has been suspended.' in self.browser.contents)
+        return
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/authentication.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/authentication.py	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/authentication.py	(revision 13803)
@@ -73,4 +73,7 @@
         return self.title
 
+    def suspended(self):
+        return self.context.suspended
+
     @property
     def failed_logins(self):
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py	(revision 13802)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/interfaces.py	(revision 13803)
@@ -482,9 +482,9 @@
     """
 
-    failed_logins = Attribute("""FailedLoginInfo for this account""")
+    failed_logins = Attribute('FailedLoginInfo for this account')
 
     name = schema.TextLine(
         title = _(u'User Id'),
-        description = u'Login name of user',
+        description = _(u'Login name of user'),
         required = True,)
 
@@ -495,6 +495,6 @@
     public_name = schema.TextLine(
         title = _(u'Public Name'),
-        description = u"Substitute for officer's real name "
-                       "in object histories.",
+        description = _(u"Substitute for officer's real name "
+                       "in student object histories."),
         required = False,)
 
@@ -522,4 +522,10 @@
         )
 
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        description = _(u'If set, the account is immediately blocked.'),
+        default = False,
+        required = False,
+        )
 
 
