Index: main/waeup.kofa/trunk/src/waeup/kofa/app.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/app.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/app.py	(revision 12926)
@@ -81,6 +81,8 @@
         """Lookup all plugins and call their `update()` method.
         """
-        # XXX: TBD: Remove update() in all plugins but documents
+        # XXX: TBD: Remove update() in all plugins but documents and users
         getUtility(IKofaPluggable, name='documents').update(
+            self, 'documents', self.logger)
+        getUtility(IKofaPluggable, name='users').update(
             self, 'documents', self.logger)
         return
Index: main/waeup.kofa/trunk/src/waeup/kofa/authentication.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/authentication.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/authentication.py	(revision 12926)
@@ -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.kofa/trunk/src/waeup/kofa/browser/pages.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/pages.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/pages.py	(revision 12926)
@@ -333,4 +333,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
@@ -578,5 +592,5 @@
     grok.name('add')
     grok.template('usereditformpage')
-    form_fields = grok.AutoFields(IUserAccount)
+    form_fields = grok.AutoFields(IUserAccount).omit('suspended')
     label = _('Add officer')
 
@@ -624,5 +638,5 @@
 
     def label(self):
-        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
+        return _("Edit officer ${a}", mapping = {'a':self.context.__name__})
 
     def setUpWidgets(self, ignore_request=False):
@@ -703,5 +717,5 @@
     grok.require('waeup.editUser')
     form_fields = grok.AutoFields(IUserAccount).omit(
-        'name', 'description', 'roles')
+        'name', 'description', 'roles', 'suspended')
     label = _(u"My Preferences")
 
Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/usereditformpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 12926)
@@ -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.kofa" enctype="multipart/form-data"
Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/userscontainerpage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 12926)
@@ -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.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py	(revision 12926)
@@ -30,5 +30,5 @@
 from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
 from waeup.kofa.app import University
-from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.interfaces import IJobManager, IUserAccount
 from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
 from waeup.kofa.university.faculty import Faculty
@@ -445,2 +445,53 @@
             'faculties']['fac1']['dep1'].certificates['CERT1'])
         return
+
+    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.kofa/trunk/src/waeup/kofa/interfaces.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py	(revision 12926)
@@ -549,9 +549,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,)
 
@@ -562,6 +562,6 @@
     public_name = schema.TextLine(
         title = _(u'Public Name'),
-        description = u"Substitute for officer's real name "
-                       "in student object histories.",
+        description = _(u"Substitute for officer's real name "
+                       "in student object histories."),
         required = False,)
 
@@ -589,4 +589,10 @@
         )
 
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        description = _(u'If set, the account is immediately blocked.'),
+        default = False,
+        required = False,
+        )
 
 
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/authentication.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/authentication.py	(revision 12921)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/authentication.py	(revision 12926)
@@ -71,4 +71,7 @@
         return self.title
 
+    def suspended(self):
+        return self.context.suspended
+
     @property
     def failed_logins(self):
