Index: main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py	(revision 6756)
@@ -26,5 +26,10 @@
 from zope.component import getUtility
 from zope.password.interfaces import IPasswordManager
-from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.pluggableauth.interfaces import (
+    IAuthenticatorPlugin, ICredentialsPlugin)
+from zope.pluggableauth.plugins.session import (
+    SessionCredentialsPlugin, SessionCredentials)
+from zope.publisher.interfaces.http import IHTTPRequest
+from zope.session.interfaces import ISession
 from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
 from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
@@ -157,4 +162,80 @@
         return IUserAccount(student)
 
+class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
+                                      SessionCredentialsPlugin):
+    """A session credentials plugin that handles the case of a user
+       changing his/her own password.
+
+    When users change their own password they might find themselves
+    logged out on next request.
+
+    To avoid this, we support to use a 'change password' page a bit
+    like a regular login page. That means, on each request we lookup
+    the sent data for a login field called 'student_id' and a
+    password.
+
+    If both exist, this means someone sent new credentials.
+
+    We then look for the old credentials stored in the user session.
+    If the new credentials' login (the student_id) matches the old
+    one's, we set the new credentials in session _but_ we return the
+    old credentials for the authentication plugins to check as for the
+    current request (and for the last time) the old credentials apply.
+
+    No valid credentials are returned by this plugin if one of the
+    follwing circumstances is true
+
+    - the sent request is not a regular IHTTPRequest
+
+    - the credentials to set do not match the old ones
+
+    - no credentials are sent with the request
+
+    - no credentials were set before (i.e. the user has no session
+      with credentials set before)
+
+    Therefore it is mandatory to put this plugin in the line of all
+    credentials plugins _before_ other plugins, so that the regular
+    credentials plugins can drop in as a 'fallback'.
+
+    This plugin was designed for students to change their passwords,
+    but might be used to allow password resets for other types of
+    accounts as well.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('student_pw_change')
+
+    loginpagename = 'login'
+    loginfield = 'student_id'
+    passwordfield = 'form.password'
+
+    def extractCredentials(self, request):
+        if not IHTTPRequest.providedBy(request):
+            return None
+        login = request.get(self.loginfield, None)
+        password = request.get(self.passwordfield, None)
+        if not login or not password:
+            return None
+        session = ISession(request)
+        sessionData = session.get(
+            'zope.pluggableauth.browserplugins')
+        old_credentials = sessionData.get('credentials', None)
+        if old_credentials is None:
+            # Password changes for already authenticated users only!
+            return None
+        if old_credentials.getLogin() != login:
+            # Special treatment only for users that change their own pw.
+            return None
+        old_credentials = {
+            'login': old_credentials.getLogin(),
+            'password': old_credentials.getPassword()}
+
+        # Set new credentials in session. These will be active on next request
+        new_credentials = SessionCredentials(login, password)
+        sessionData['credentials'] = new_credentials
+
+        # Return old credentials for this one request only
+        return old_credentials
+
 class StudentsAuthenticatorSetup(grok.GlobalUtility):
     """Register or unregister student authentication for a PAU.
@@ -165,4 +246,8 @@
 
     def register(self, pau):
+        plugins = list(pau.credentialsPlugins)
+        # this plugin must come before the regular credentials plugins
+        plugins.insert(0, 'student_pw_change')
+        pau.credentialsPlugins = tuple(plugins)
         plugins = list(pau.authenticatorPlugins)
         plugins.append('students')
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py	(revision 6756)
@@ -63,5 +63,5 @@
     WAeUPObjectWidget, WAeUPObjectDisplayWidget)
 from waeup.sirp.students.interfaces import (
-    IStudentsContainer, IStudent, IStudentClearance,
+    IStudentsContainer, IStudent, IStudentClearance, IStudentPasswordSetting,
     IStudentPersonal, IStudentBase, IStudentStudyCourse,
     IStudentPayments, IStudentAccommodation, IStudentNavigation,
@@ -160,5 +160,5 @@
         self.acseries = self.request.form.get('form.acseries', None)
         self.acnumber = self.request.form.get('form.acnumber', None)
-        
+
         if SUBMIT is None:
             return
@@ -294,5 +294,5 @@
     grok.require('waeup.viewStudent')
     grok.template('basepage')
-    form_fields = grok.AutoFields(IStudentBase)  #.omit('password')
+    form_fields = grok.AutoFields(IStudentBase).omit('password')
     pnav = 4
     title = 'Base Data'
@@ -541,4 +541,29 @@
     target = 'bedit'
 
+class StudentPasswordSetting(grok.Adapter):
+    """Adapt IStudent to data needed for password settings.
+
+    We provide password getters/setters for the attached context (an
+    IStudent object) that cooperate seamless with the usual
+    formlib/form techniques.
+    """
+    grok.context(IStudent)
+    grok.provides(IStudentPasswordSetting)
+
+    def __init__(self, context):
+        self.name = context.name
+        self.password_repeat = context.password
+        self.context = context
+        return
+
+    def getPassword(self):
+        return self.context.password
+
+    def setPassword(self, password):
+        IUserAccount(self.context).setPassword(password)
+        return
+
+    password = property(getPassword, setPassword)
+
 class StudentBaseEditFormPage(WAeUPEditFormPage):
     """ View to edit student base data by student
@@ -547,6 +572,7 @@
     grok.name('bedit')
     grok.require('waeup.handleStudent')
-    form_fields = grok.AutoFields(IStudentBaseEdit).omit(
-        'student_id', 'reg_number', 'matric_number')
+    #form_fields = grok.AutoFields(IStudentBaseEdit).omit(
+    #    'student_id', 'reg_number', 'matric_number')
+    form_fields = grok.AutoFields(IStudentPasswordSetting)
     grok.template('baseeditpage')
     label = 'Change password'
@@ -555,24 +581,39 @@
 
     def update(self):
-        datepicker.need() # Enable jQuery datepicker in date fields.
         super(StudentBaseEditFormPage, self).update()
         self.wf_info = IWorkflowInfo(self.context)
         return
 
-    @grok.action('Save')
+    def onFailure(self, action, data, errors):
+        new_status = []
+        other_errors = False
+        for error in errors:
+            msg = getattr(error, 'message', '')
+            if isinstance(msg, basestring) and msg != '':
+                new_status.append(msg)
+            else:
+                other_errors = True
+        if other_errors:
+            if new_status:
+                new_status.append('see below for further errors')
+            else:
+                new_status.append('See below for details.')
+        if new_status:
+            self.status = u'There were errors: %s' % ', '.join(new_status)
+        return
+
+    @grok.action('Save', failure=onFailure)
     def save(self, **data):
         form = self.request.form
         ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
-        if form.has_key('password') and form['password']:
-            if form['password'] != form['control_password']:
-                self.flash('Passwords do not match.')
-                return
-            IUserAccount(self.context).setPassword(form['password'])
+        changed_fields = self.applyData(self.context, **data)
+        # Turn list of lists into single list
+        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        changed_fields = [x for x in changed_fields
+                          if not x.startswith('password')]
+        if form.get('form.password', u'') != u'':
             self.context.loggerInfo(ob_class, 'password changed')
-        changed_fields = self.applyData(self.context, **data)
-        changed_fields = changed_fields.values()
-        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
-        self.context._p_changed = True
-        self.flash('Form has been saved.')
+            self.flash('Form has been saved.')
+        fields_string = ' + '.join(changed_fields)
         if fields_string:
             self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
@@ -614,5 +655,6 @@
     grok.name('cedit')
     grok.require('waeup.handleStudent')
-    form_fields = grok.AutoFields(IStudentClearanceEdit).omit('clearance_locked')
+    form_fields = grok.AutoFields(
+        IStudentClearanceEdit).omit('clearance_locked')
     #grok.template('clearanceeditpage')
     label = 'Edit clearance data'
@@ -667,9 +709,9 @@
     pnav = 4
     buttonname = 'Start'
-    
 
     def update(self, SUBMIT=None):
-        # We must not use form.ac_series and form.ac_number in forms since these
-        # are interpreted as applicant credentials in the applicants package
+        # We must not use form.ac_series and form.ac_number in forms
+        # since these are interpreted as applicant credentials in the
+        # applicants package
         self.acseries = self.request.form.get('form.acseries', None)
         self.acnumber = self.request.form.get('form.acnumber', None)
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/browser_templates/baseeditpage.pt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/browser_templates/baseeditpage.pt	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/browser_templates/baseeditpage.pt	(revision 6756)
@@ -9,9 +9,11 @@
     tal:define="status view/status"
     tal:condition="status">
-    Form Status:
     <span i18n:translate="" tal:content="view/status">
       Form status summary
     </span>
   </div>
+
+  <input type="hidden" name="student_id" value=""
+         tal:attributes="value context/student_id" />
 
   <table class="form-fields zebra">
@@ -38,16 +40,4 @@
         </tr>
       </tal:block>
-      <tr>
-        <td class="label"><label>Password:</label></td>
-        <td>
-          <input name="password" type="password"/>
-        </td>
-      </tr>
-      <tr>
-        <td class="label"><label>Retype password:</label></td>
-        <td>
-          <input name="control_password" type="password" />
-        </td>
-      </tr>
     </tbody>
   </table>
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py	(revision 6756)
@@ -1,5 +1,6 @@
 ##
 ## interfaces.py
-from zope.interface import Interface, Attribute
+from zope.interface import Interface, Attribute, invariant
+from zope.interface.exceptions import Invalid
 from zope import schema
 from waeup.sirp.interfaces import IWAeUPObject
@@ -58,22 +59,7 @@
         """
 
-class IStudentBase(IWAeUPObject):
-    """Representation of student base data.
-    """
-    history = Attribute('Object history, a list of messages.')
-    state = Attribute('Returns the registration state of a student')
-    #student_id = Attribute('Randomly generated id')
-    password = Attribute('Encrypted password of a student')
-    adm_code = Attribute('Admission checking access code')
-
-    def loggerInfo(ob_class, comment):
-        """Adds an INFO message to the log file
-        """
-
-    student_id = schema.TextLine(
-        title = u'Student ID',
-        required = True,
-        )
-
+class IStudentPasswordSetting(IWAeUPObject):
+    """Data needed for password setting.
+    """
     name = schema.TextLine(
         title = u'Full Name',
@@ -82,4 +68,48 @@
         )
 
+    password = schema.Password(
+        title = u'New password',
+        required = False,
+        )
+
+    password_repeat = schema.Password(
+        title = u'Retype new password',
+        required = False,
+        )
+
+    @invariant
+    def passwords_match(obj):
+        if obj.password == obj.password_repeat:
+            return
+        raise Invalid('passwords do not match')
+
+class IStudentBase(IWAeUPObject):
+    """Representation of student base data.
+    """
+    history = Attribute('Object history, a list of messages.')
+    state = Attribute('Returns the registration state of a student')
+    password = Attribute('Encrypted password of a student')
+    adm_code = Attribute('Admission checking access code')
+
+    password = schema.Password(
+        title = u'Password',
+        required = False,
+        )
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    student_id = schema.TextLine(
+        title = u'Student ID',
+        required = True,
+        )
+
+    name = schema.TextLine(
+        title = u'Full Name',
+        default = u'Nobody',
+        required = True,
+        )
+
     reg_number = schema.TextLine(
         title = u'Registration Number',
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_authentication.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_authentication.py	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_authentication.py	(revision 6756)
@@ -27,4 +27,5 @@
 from zope.password.interfaces import IPasswordManager
 from zope.pluggableauth import PluggableAuthentication
+from zope.security.interfaces import Unauthorized
 from zope.securitypolicy.role import Role
 from zope.securitypolicy.interfaces import IRole, Allow
@@ -33,5 +34,6 @@
 from waeup.sirp.students.authentication import (
     StudentsAuthenticatorSetup, StudentAccount)
-
+from waeup.sirp.students.tests.test_browser import StudentsFullSetup
+from waeup.sirp.testing import FunctionalLayer
 
 class StudentsAuthenticatorSetupTests(unittest.TestCase):
@@ -152,2 +154,23 @@
         self.assertEqual(self.account.roles, ['waeup.test.Role'])
         return
+
+
+
+class FunctionalStudentAuthTests(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalStudentAuthTests, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalStudentAuthTests, self).tearDown()
+        return
+
+    def test_reset_protected_anonymous(self):
+        # anonymous users cannot reset others passwords
+        self.assertRaises(
+            Unauthorized,
+            self.browser.open, self.student_path + '/bedit')
+        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py	(revision 6755)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py	(revision 6756)
@@ -279,6 +279,7 @@
         # Change password
         self.browser.getLink("Change password").click()
-        self.browser.getControl(name="password").value = 'new_password'
-        self.browser.getControl(name="control_password").value = 'new_password'
+        self.browser.getControl(name="form.password").value = 'new_password'
+        self.browser.getControl(
+            name="form.password_repeat").value = 'new_password'
         self.browser.getControl("Save").click()
         self.assertTrue('Form has been saved' in self.browser.contents)
@@ -294,5 +295,5 @@
         self.browser.open(self.login_path)
         self.browser.getControl(name="form.login").value = self.test_student_id
-        self.browser.getControl(name="password").value = 'new_password'
+        self.browser.getControl(name="form.password").value = 'new_password'
         self.browser.getControl("Login").click()
         self.assertEqual(self.browser.url, self.student_path)
