Commits

Anonymous committed 92c483a

AccountManagerPlugin: Add interface for configurable user registration procedure, refs #874, #2707, #2897, #4651, #5295, #7577, #8076.

The current user registration process lacks flexibility, as can be witnessed by
the history of one of the oldest still pending tickets for this plugin: #874.

Comments (0)

Files changed (5)

accountmanagerplugin/trunk/acct_mgr/api.py

         """User verification has been requested."""
 
 
+class IAccountRegistrationInspector(Interface):
+    """An interface for Components, that wish to participate in examining
+    requests for account creation.
+
+    The check method is called not only by RegistrationModule but when adding
+    new users from the user editor in AccountManagerAdminPanels too.
+    """
+
+    def render_registration_fields(req):
+        """Emit one or multiple additional fields for registration form built.
+
+        Returns a dict containing a 'required' and/or 'optional' tuple of
+         * Genshi Fragment or valid XHTML markup for registration form
+         * template data object with default values or empty dict
+        If the return value is just a single tuple, its fragment or markup
+        will be inserted into the 'required' section.
+        """
+
+    def validate_registration(req):
+        """Check registration form input.
+
+        Returns a RegistrationError with error message, or None on success.
+        """
+
+
 class AccountManager(Component):
     """The AccountManager component handles all user account management methods
     provided by the IPasswordStore interface.

accountmanagerplugin/trunk/acct_mgr/register.py

 
 from genshi.core import Markup
 from genshi.builder import tag
+
 from trac import perm, util
 from trac.core import Component, TracError, implements
 from trac.config import Configuration, BoolOption, IntOption, Option, \
 from trac.web.main import IRequestHandler, IRequestFilter
 
 from acct_mgr.api import AccountManager, CommonTemplateProvider, \
-                         _, dgettext, ngettext, tag_
+                         IAccountRegistrationInspector, _, N_, dgettext, tag_
 from acct_mgr.model import email_associated, set_user_attribute
 from acct_mgr.util import containsAny, if_enabled, is_enabled
 
         set_user_attribute(env, username, attribute, value)
 
 
+class RegistrationError(TracError):
+    """Exception raised when a registration check fails."""
+
+    title = N_("Registration Error")
+
+
+class GenericRegistrationInspector(Component):
+    """Generic check class definitions.
+
+    'type' is a class property that matches registration form's fieldset name,
+    where field(s) should get inserted.
+    """
+
+    implements(IAccountRegistrationInspector)
+
+    abstract = True
+
+    def render_registration_fields(self, req):
+        """Emit one or multiple additional fields for registration form built.
+
+        Returns a dict containing a 'required' and/or 'optional' tuple of 
+         * Genshi Fragment or valid XHTML markup for registration form
+         * template data object with default values or empty dict
+        If the return value is just a single tuple, its fragment or markup
+        will be inserted into the 'required' section.
+        """
+        data = {}
+        template = ''
+        return template, data
+
+    def validate_registration(self, req):
+        """Check registration form input.
+
+        Returns a RegistrationError with error message, or None on success.
+        """
+        # Nicer than a plain NotImplementedError.
+        raise NotImplementedError, _(
+            "No check method 'validate_registration' defined in %(module)s",
+            module=self.__class__.__name__)
+
+
 class RegistrationModule(CommonTemplateProvider):
     """Provides users the ability to register a new account.
 
 
     implements(chrome.INavigationContributor, IRequestHandler)
 
+    _register_check = OrderedExtensionsOption(
+        'account-manager', 'register_check', IAccountRegistrationInspector,
+        include_missing=False,
+        doc="""Ordered list of IAccountRegistrationInspector's to use for
+        registration checks.""")
+
     def __init__(self):
         self.acctmgr = AccountManager(self.env)
         self._enable_check(log=True)
         data['verify_account_enabled'] = verify_enabled
         if req.method == 'POST' and action == 'create':
             try:
+                for inspector in self._register_check:
+                    inspector.validate_registration(req)
                 _create_user(req, self.env)
+            except RegistrationError, e:
+                chrome.add_warning(req, e.message)
             except TracError, e:
                 data['registration_error'] = e.message
                 data['acctmgr'].update(getattr(e, 'account', ''))
                      You may log in as user %(user)s now.""",
                      user=tag.b(req.args.get('username')))))))
                 req.redirect(req.href.login())
+        # Collect additional fields from IAccountRegistrationInspector's.
+        fragments = dict(required=[], optional=[])
+        for inspector in self._register_check:
+            fragment, f_data = inspector.render_registration_fields(req)
+            if fragment:
+                try:
+                    if 'optional' in fragment.keys():
+                        fragments['optional'].append(fragment['optional'])
+                except AttributeError:
+                    # Not a dict, just append Genshi Fragment or str/unicode. 
+                    fragments['required'].append(fragment)
+                else:
+                    fragments['required'].append(fragment.get('required', ''))
+                finally:
+                    data.update(f_data)
+        data['required_fields'] = fragments['required']
+        data['optional_fields'] = fragments['optional']
+        # Deferred import required to aviod circular import dependencies.
         from acct_mgr.web_ui import AccountModule
         data['reset_password_enabled'] = AccountModule(self.env
                                                       ).reset_password_enabled

accountmanagerplugin/trunk/acct_mgr/templates/register.html

               <input type="text" name="email" class="textwidget" size="20"
                 value="${acctmgr.email}" />
             </label>
-            <p>The email address is required for Trac to send you a
-              verification token.
+            <p class="hint">The email address is required for Trac to send
+              you a verification token.
             </p>
-            <p py:if="reset_password_enabled">Entering your email address will
-              also enable you to reset your password if you ever forget it.
+            <p class="hint" py:if="reset_password_enabled">
+              Entering your email address will also enable you to reset your
+              password if you ever forget it.
             </p>
           </div>
+          <div py:for="field in required_fields">
+            <!--! Additional required fields can be included below. -->
+            ${field}
+          </div>
         </fieldset>
 
         <fieldset>
               <input type="text" name="email" class="textwidget" size="20"
                 value="${acctmgr.email}"/>
             </label>
-            <p py:if="reset_password_enabled">Entering your email address
-              will enable you to reset your password if you ever forget it.
+            <p class="hint" py:if="reset_password_enabled">
+              Entering your email address will enable you to reset your
+              password if you ever forget it.
             </p>
           </div>
+          <div py:for="field in optional_fields">
+            <!--! Additional optional fields can be included below. -->
+            ${field}
+          </div>
         </fieldset>
         <input type="submit"
                value="${dgettext('acct_mgr', 'Create account')}" />

accountmanagerplugin/trunk/acct_mgr/tests/__init__.py

     INCLUDE_FUNCTIONAL_TESTS = False
 
 def suite():
-    from acct_mgr.tests import htfile, db
+    from acct_mgr.tests import db, htfile, register
     suite = unittest.TestSuite()
+    suite.addTest(db.suite())
     suite.addTest(htfile.suite())
-    suite.addTest(db.suite())
+    suite.addTest(register.suite())
     if INCLUDE_FUNCTIONAL_TESTS:
         from acct_mgr.tests.functional import suite as functional_suite
         suite.addTest(functional_suite())

accountmanagerplugin/trunk/acct_mgr/tests/register.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.
+#
+# Author: Steffen Hoffmann <hoff.st@web.de>
+
+import shutil
+import tempfile
+import unittest
+
+from genshi.core  import Markup
+
+from trac.perm  import PermissionCache, PermissionSystem
+from trac.test  import EnvironmentStub, Mock
+
+from acct_mgr.admin  import AccountManagerAdminPanels
+from acct_mgr.api  import AccountManager, IAccountRegistrationInspector
+from acct_mgr.register  import GenericRegistrationInspector, \
+                               RegistrationError, RegistrationModule
+
+
+class _BaseTestCase(unittest.TestCase):
+    def setUp(self):
+        self.env = EnvironmentStub(
+                enable=['trac.*', 'acct_mgr.*'])
+        self.env.path = tempfile.mkdtemp()
+        self.perm = PermissionSystem(self.env)
+
+        # Register AccountManager actions.
+        self.ap = AccountManagerAdminPanels(self.env)
+        self.perm.grant_permission('admin', 'ACCTMGR_USER_ADMIN')
+        self.rmod = RegistrationModule(self.env)
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+
+
+class DummyRegInspectorTestCase(_BaseTestCase):
+    """Check GenericRegistrationInspector properties via child classes."""
+    def setUp(self):
+        _BaseTestCase.setUp(self)
+
+        class DummyRegistrationInspector(GenericRegistrationInspector):
+            def validate_registration(self, req):
+                if req.args.get('username') == 'dummy':
+                    raise RegistrationError('Dummy check error')
+                return
+
+        self.check = DummyRegistrationInspector(self.env)
+
+    def test_bad_check(self):
+        class BadRegistrationInspector(GenericRegistrationInspector):
+            """Bad example of a check without check method implementation."""
+
+        check = BadRegistrationInspector(self.env)
+        args = dict(username='', name='', email='')
+        req = Mock(authname='anonymous', args=args)
+        field_res = check.render_registration_fields(req)
+        self.assertEqual(len(field_res), 2)
+        self.assertEqual((Markup(field_res[0]), field_res[1]),
+                         (Markup(''), {}))
+        self.assertRaises(NotImplementedError, check.validate_registration,
+                          req)
+
+    def test_dummy_check(self):
+        args = dict(username='', name='', email='')
+        req = Mock(authname='anonymous', args=args)
+        self.assertEqual(self.check.validate_registration(req), None)
+
+    def test_check_error(self):
+        args = dict(username='dummy', name='', email='')
+        req = Mock(authname='anonymous', args=args)
+        self.assertRaises(RegistrationError, self.check.validate_registration,
+                          req)
+        try:
+            self.check.validate_registration(req)
+            # Shouldn't reach that point.
+            self.assertTrue(False)
+        except RegistrationError, e:
+            # Check error properties in detail.
+            self.assertEqual(e.message, 'Dummy check error')
+            self.assertEqual(e.title, 'Registration Error')
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(DummyRegInspectorTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')