Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 6407)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 6408)
@@ -394,12 +394,54 @@
     return code
 
-def _fire_transition(info, arg, toward=False):
+def fire_transition(access_code, arg, toward=False):
+    """Fire workflow transition for access code.
+
+    The access code instance is looked up via `access_code` (a string
+    like ``APP-1-12345678``).
+
+    `arg` tells what kind of transition to trigger. This will be a
+    transition id like ``'use'`` or ``'init'``, or some transition
+    target like :var:`waeup.sirp.accesscodes.workflow.INITIALIZED`.
+
+    If `toward` is ``False`` (the default) you have to pass a
+    transition id as `arg`, otherwise you must give a transition
+    target.
+
+    :func:`fire_transition` might raise exceptions depending on the
+    reason why the requested transition cannot be performed.
+
+    The following exceptions can occur during processing:
+
+    :exc:`KeyError`:
+      signals not existent access code, batch or site.
+
+    :exc:`ValueError`:
+      signals illegal format of `access_code` string. The regular format is
+      ``APP-N-XXXXXXXX``.
+
+    :exc:`hurry.workflow.interfaces.InvalidTransitionError`:
+      the transition requested cannot be performed because the workflow
+      rules forbid it.
+
+    :exc:`Unauthorized`:
+      the current user is not allowed to perform the requested transition.
+
+    """
     try:
-        if toward:
-            info.fireTransitionToward(arg)
-        else:
-            info.fireTransition(arg)
-    except InvalidTransitionError:
-        return False
+        batch_id, ac_id = access_code.rsplit('-', 1)
+    except ValueError:
+        raise ValueError(
+            'Invalid access code format: %s (use: APP-N-XXXXXXXX)' % (
+                access_code,))
+    try:
+        ac = grok.getSite()['accesscodes'][batch_id].getAccessCode(access_code)
+    except TypeError:
+        raise KeyError(
+            'No site available for looking up accesscodes')
+    info = IWorkflowInfo(ac)
+    if toward:
+        info.fireTransitionToward(arg)
+    else:
+        info.fireTransition(arg)
     return True
 
@@ -407,9 +449,8 @@
     """Invalidate AccessCode denoted by string ``access_code``.
 
-    The access code that belongs to the passed string must exist.
-
     Fires an appropriate transition to perform the task.
 
-    Returns ``True`` if the ac was invalidated, ``False`` else.
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
     """
     ac = get_access_code(access_code)
@@ -423,24 +464,18 @@
     """Disable AccessCode denoted by string ``access_code``.
 
-    The access code that belongs to the passed string must exist.
-
     Fires an appropriate transition to perform the task.
 
-    Returns ``True`` if the ac was disabled, ``False`` else.
-    """
-    ac = get_access_code(access_code)
-    info = IWorkflowInfo(ac)
-    return _fire_transition(info, DISABLED, toward=True)
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(access_code, DISABLED, toward=True)
 
 def reenable_accesscode(access_code):
     """Reenable AccessCode denoted by string ``access_code``.
 
-    The access code that belongs to the passed string must exist.
-
     Fires an appropriate transition to perform the task.
 
-    Returns ``True`` if the ac was reenabled, ``False`` else.
-    """
-    ac = get_access_code(access_code)
-    info = IWorkflowInfo(ac)
-    return _fire_transition(info, 'reenable')
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(access_code, 'reenable')
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt	(revision 6408)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt	(revision 6408)
@@ -0,0 +1,672 @@
+:mod:`waeup.sirp.accesscodes.accesscodes` -- access codes (aka PINs)
+********************************************************************
+
+.. module:: waeup.sirp.accesscodes.accesscodes
+
+Components that represent access codes and related.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+About access-codes
+==================
+
+Access codes are ids used to grant first-time access to the system for
+students.
+
+They are normally not generated by third-party components but always
+part of batches.
+
+An access-code consists of three parts::
+
+    APP-12-0123456789
+    ^^^ ^^ ^^^^^^^^^^
+     A  B  C
+
+where ``A`` tells about the purpose of the code, ``B`` gives the
+number of batch the code belongs to (1 to 3 digits), and ``C`` is a
+unique random number of 10 digits.
+
+For the generation of the random number :mod:`waeup.sirp` requires a
+'urandom' entropy provider which is available with most standard
+Unix/Linux systems. This makes the generated numbers relatively
+secure, especially when compared with recent PHP-based applications.
+
+
+AccessCode
+==========
+
+.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
+
+   You normally shouldn't create standalone access-codes. Use
+   instances of :class:`AccessCodeBatch` instead as they generate them
+   (in masses) and care for them.
+
+   Note, that :class:`AccessCode` instances are not persistent on
+   themselves. They have to be stored inside a persistent object (like
+   :class:`AccessCodeBatch`) to be kept.
+
+   Access-codes can have three states: unused, used, and
+   disabled. While still unused but enabled access-codes are reflected
+   by an empty ``invalidation_date`` (set to ``None``), an already
+   used (invalidated) code provides an invalidation date.
+
+   In case of misuse or similar cases access-codes can also be
+   completely disabled by setting the ``disabled`` attribute to
+   ``True``.
+
+   The class implements
+   :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`:
+
+    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
+    >>> from waeup.sirp.accesscodes.accesscodes import AccessCode
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IAccessCode, AccessCode)
+    True
+
+   .. attribute:: representation
+
+      The 'full' id of an access-code as described above. Something
+      like ``'APP-12-0123456789'``.
+
+      Read-only attribute.
+
+   .. attribute:: batch_serial
+
+      Serial number of this access-code inside the batch.
+
+      .. note:: XXX: Do we really need this?
+
+         Subject to be dropped.
+
+   .. attribute:: student_id
+
+      A string or ``None``. Set when an access-code is
+      invalidated. ``None`` by default.
+
+   .. attribute:: batch_prefix
+
+      The prefix of the batch the access-code belongs to.
+
+      Read-only attribute.
+
+   .. attribute:: batch_num
+
+      The number of the batch the access-code belongs to.
+
+      Read-only attribute.
+
+   .. attribute:: cost
+
+      What the access-code costs. A float.
+
+      Read-only attribute.
+
+   .. attribute:: invalidation_date
+
+      Python datetime when the access code was invalidated, or
+      ``None``. ``None`` by default.
+
+      If an access-code is disabled, this attribute contains the
+      datetime of disabling.
+
+      Read-only attribute. Only batches are supposed to set this value.
+
+   .. attribute:: disabled
+
+      Boolean, ``False`` by default. When set to ``True``, this
+      access-code should not be used any more.
+
+      Read-only attribute. Only batches are supposed to set this value.
+
+   Access codes that are not part of a batch, will give strange
+   representations:
+
+    >>> ac = AccessCode(None, '9999999999')
+    >>> ac.representation
+    '--<10-DIGITS>'
+
+   Also the ``cost`` will not be set:
+
+    >>> ac.cost is None
+    True
+
+
+AccessCodeBatch
+===============
+
+.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
+
+   Create a batch of access-codes.
+
+   :param creation_date: python datetime
+   :param creator: creators user id
+   :type creator: string
+   :param batch_prefix: prefix of this batch
+   :param cost: cost per access code
+   :type cost: float
+   :param entry_num: number of access codes to create
+   :param num: number of this batch
+
+   A persistent :class:`grok.Model`. It implements
+   :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`.
+
+   When creating a batch, all entries (access-codes) are generated as
+   well.
+
+   .. attribute:: creation_date
+
+      The datetime when the batch was created.
+
+   .. attribute:: creator
+
+      String with user id of the user that generated the batch.
+
+   .. attribute:: cost
+
+      Float representing the costs for a single access-code. All
+      entries inside the batch share the same cost.
+
+   .. attribute:: entry_num
+
+      Number of entries (access-codes) inside the batch.
+
+   .. attribute:: invalidated_num
+
+      Number of entries that were already invalidated.
+
+   .. attribute:: prefix
+
+      Prefix of the batch. This tells about the purpose of this batch.
+
+   .. attribute:: num
+
+      Number of this batch. For a certain prefix there can exist
+      several batches, which are numbered in increasing order. The
+      number is normally computed by the
+      :class:`AccessCodeBatchContainer` in which batches are
+      stored.
+
+      .. seealso:: :class:`AccessCodeBatchContainer`
+
+   .. method:: entries()
+
+      Get all accesscodes stored in the batch.
+
+      Returns a generator over all stored entries.
+
+   .. method:: getAccessCode(acesscode_id)
+
+      Get the :class:`AccessCode` object for the given
+      ``accesscode_id``.
+
+      Certain single access codes can be accessed inside a batch by
+      their representation (i.e. something like ``'APP-12-0123456789'``.
+
+      When a code cannot be found :exc:`KeyError` is raised.
+
+   .. method:: getAccessCodeForStudentId(student_id)
+
+      Get the :class:`AccessCode` object for the given
+      ``student_id``.
+
+      Certain single access codes can be accessed inside a batch by
+      their student_id. The student_id must be some string; ``None``
+      is not a valid value.
+
+      When a code cannot be found :exc:`KeyError` is raised.
+
+   .. method:: addAccessCode(serial_num, random_num)
+
+      Add an access code to the batch.
+
+      ``serial_num`` denotes the serial number of the new access-code
+      inside the batch. ``random_num`` is a string of 10 digits unique
+      in this batch.
+
+   .. method:: invalidate(ac_id[, student_id=None])
+
+      Invalidate the access-code with ID ``ac_id``.
+
+      Sets also the ``student_id`` attribute of the respective
+      :class:`AccessCode` entry.
+
+   .. method:: disable(ac_id, user_id)
+
+      Disable the access-code with ID ``ac_id``.
+
+     ``user_id`` is the user ID of the user triggering the
+      process. Already disabled ACs are left untouched.
+
+      Sets also the ``student_id`` and ``invalidation_date``
+      attributes of the respective :class:`AccessCode` entry. While
+      ``student_id`` is set to the given ``user_id``,
+      ``invalidation_date`` is set to current datetime.
+
+      Disabled access codes are supposed not to be used any more at
+      all.
+
+   .. method:: enable(ac_id)
+
+      (Re-)enable the access-code with ID ``ac_id``.
+
+      This leaves the given AC in state ``unused``. Already enabled
+      ACs are left untouched.
+
+      Sets ``student_id`` and ``invalidation_date`` values of the
+      respective :class:`AccessCode` entry to ``None``.
+
+   .. method:: createCSVLogFile()
+
+      Create a CSV file with data in batch.
+
+      Data will not contain invalidation date nor student ids.  File
+      will be created in ``accesscodes`` subdir of data center storage
+      path.
+
+      Returns name of created file.
+
+   .. method:: archive()
+
+      Create a CSV file for archive. Archive files contain also
+      ``student_id`` and ``invalidation_date``.
+
+      Returns name of created file.
+
+   .. method:: search(searchterm, searchtype)
+
+      Search the batch for entries that comply with ``searchterm`` and
+      ``searchtype``.
+
+      ``searchtype`` must be one of ``serial``, ``pin``, or
+      ``stud_id`` and specifies, what kind of data is contained in the
+      ``searchterm``.
+
+      - ``serial`` looks for the AC with the ``serial_batch`` set to
+        the given (integer) number in ``searchterm``. Here
+        ``searchterm`` must be an int number.
+
+      - ``pin`` looks for the AC with the ``representation`` set to
+        the given string in ``searchterm``. Here ``searchterm`` must
+        be a string containing the full AC-ID like
+        ``APP-1-0123456789``.
+
+      - ``stud_id`` looks for the AC with the ``student_id`` set to
+        the given string in ``searchterm``. Here ``searchterm`` must
+        be a string containing the full student ID.
+
+      Lookup is done via local BTrees and therefore pretty fast.
+
+      Returns a list of access-codes found or empty list.
+
+
+Examples
+--------
+
+:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
+
+    >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch
+    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
+    True
+
+Creating a batch of three access-codes, with a cost of ``12.12`` per
+code, the batch prefix ``APP``, batch number ``10``, creator ID
+``Fred`` and some arbitrary creation date:
+
+    >>> import datetime
+    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
+    >>> batch = AccessCodeBatch(
+    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
+
+Getting all access-codes from a batch:
+
+    >>> ac_codes = batch.entries()
+    >>> ac_codes
+    <generator object...at 0x...>
+
+    >>> [x.representation for x in ac_codes]
+    ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>']
+
+    >>> [x for x in batch.entries()]
+    [<waeup.sirp...AccessCode object at 0x...>, ...]
+
+Getting a single entry from the batch:
+
+    >>> ac_id = list(batch.entries())[0].representation
+    >>> ac = batch.getAccessCode(ac_id)
+    >>> ac
+    <waeup.sirp...AccessCode object at 0x...>
+
+    >>> ac is list(batch.entries())[0]
+    True
+
+Trying to get a single not-existent entry from a batch:
+
+    >>> batch.getAccessCode('blah')
+    Traceback (most recent call last):
+    ...
+    KeyError: 'blah'
+
+Invalidating an entry:
+
+    >>> batch.invalidated_num
+    0
+
+    >>> str(ac.invalidation_date), str(ac.student_id)
+    ('None', 'None')
+
+    >>> batch.invalidate(ac_id)
+    >>> batch.invalidated_num
+    1
+
+    >>> ac.invalidation_date , str(ac.student_id)
+    (datetime.datetime(...), 'None')
+
+    >>> batch.invalidate(ac_id, 'some_user_id')
+    >>> ac.student_id
+    'some_user_id'
+
+Getting a single entry by student_id:
+
+    >>> batch.getAccessCodeForStudentId('some_user_id')
+    <waeup.sirp...AccessCode object at 0x...>
+
+Non-existent values will cause a :exc:`KeyError`:
+
+    >>> batch.getAccessCodeForStudentId('non-existing')
+    Traceback (most recent call last):
+    ...
+    KeyError: 'non-existing'
+
+Already enabled entries will be left untouched when trying to renable
+them:
+
+    >>> batch.enable(ac_id)
+    >>> ac.student_id
+    'some_user_id'
+
+Disabling an entry:
+
+    >>> batch.disabled_num
+    0
+
+    >>> ac.disabled
+    False
+
+    >>> batch.disable(ac_id, 'some userid')
+    >>> ac.disabled
+    True
+
+    >>> ac.student_id
+    'some userid'
+
+    >>> batch.disabled_num
+    1
+
+Already disabled entries will not be disabled again:
+
+    >>> batch.disable(ac_id, 'other userid')
+    >>> ac.student_id
+    'some userid'
+
+Reenabling an entry:
+
+    >>> batch.enable(ac_id)
+    >>> ac.disabled
+    False
+
+    >>> ac.student_id is None
+    True
+
+    >>> ac.invalidation_date is None
+    True
+
+Access codes get their ``cost`` from the batch they belong to. Note,
+that it is advisable to print costs always using a format, as Python
+floats are often represented by irritating values:
+
+    >>> ac.cost
+    12.119999999999999
+
+    >>> print "%0.2f" % ac.cost
+    12.12
+
+Searching for serials:
+
+    >>> result = batch.search(0, 'serial')
+    >>> result
+    [<waeup.sirp...AccessCode object at 0x...>]
+
+    >>> result[0].batch_serial
+    0
+
+Searching for AC-IDs:
+
+    >>> result = batch.search(ac.representation, 'pin')
+    >>> result[0].representation
+    'APP-...-...'
+
+Searching for student IDs:
+
+    >>> batch.invalidate(ac_id, 'some_new_user_id')
+    >>> result = batch.search('some_new_user_id', 'stud_id')
+    >>> result[0].student_id
+    'some_new_user_id'
+
+Searching for not existing entries will return empty lists:
+
+    >>> batch.search(12, 'serial')
+    []
+
+    >>> batch.search('not-a-valid-pin', 'pin')
+    []
+
+    >>> batch.search('not-a-student-id', 'stud_id')
+    []
+
+    >>> batch.search('blah', 'not-a-valid-searchtype')
+    []
+
+AccessCodeBatchContainer
+========================
+
+.. class:: AccessCodeBatchContainer()
+
+   A container for access code batches.
+
+   .. method:: addBatch(batch)
+
+      Add a batch in this container. You should make sure, that
+      entries in the given batch are set correctly.
+
+   .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num)
+
+      Create a batch inside this container. Returns the batch created.
+
+      :param creation_date: python datetime
+      :param creator: creators user id
+      :type creator: string
+      :param batch_prefix: prefix of this batch
+      :param cost: cost per access code
+      :type cost: float
+      :param entry_num: number of access codes to create
+
+   .. method:: getNum(prefix)
+
+      Get next unused num for a new batch and a given prefix.
+
+      Batches for a given prefix are numerated. Whenever a new batch
+      is created and other batches inside the container already have
+      the same prefix, the new one will get the lowest unused number.
+
+   .. method:: getImportFiles()
+
+      Get a list of basenames of available import files, suitable for
+      feeding :meth:`reimport`.
+
+   .. method:: reimport(filename, creator=u'UNKNOWN')
+
+      Reimport a CSV log file of previously created AC batch.
+
+      ``filename`` is the name (basename) of the file residing in the
+      accesscode storage's ``import`` directory.``creator`` is the
+      user ID of the current user.
+
+   .. method:: search(search_term, search_type)
+
+      Look in all contained batches for access codes that comply with
+      the given parameters.
+
+      ``search_type`` must be one of ``serial``, ``pin``, or
+      ``stud_id`` and specifies, what kind of data is contained in the
+      ``search_term``.
+
+      - ``serial`` looks for the AC with the ``serial_batch`` set to
+        the given (integer) number in ``search_term``. Here
+        ``search_term`` can be an integer or a string. If it is a
+        string it will be converted to an int.
+
+      - ``pin`` looks for the AC with the ``representation`` set to
+        the given string in ``search_term``. Here ``search_term`` must
+        be a string containing the full AC-ID like
+        ``APP-1-0123456789``.
+
+      - ``stud_id`` looks for the AC with the ``student_id`` set to
+        the given string in ``search_term``. Here ``search_term`` must
+        be a string containing the full student ID.
+
+Examples:
+---------
+
+Creating a batch container:
+
+    >>> from waeup.sirp.accesscodes.accesscodes import (
+    ...   AccessCodeBatchContainer)
+    >>> container = AccessCodeBatchContainer()
+
+Creating batches inside the container:
+
+    >>> from datetime import datetime
+    >>> batch1 = container.createBatch(
+    ...   datetime.now(), 'some userid', 'FOO', 10.12, 5)
+
+    >>> batch2 = container.createBatch(
+    ...   datetime.now(), 'other userid', 'BAR', 1.95, 5)
+
+Searching the container for batch serials:
+
+    >>> container.search(1, 'serial')
+    [<waeup.sirp...AccessCode object at 0x...>,
+     <waeup.sirp...AccessCode object at 0x...>]
+
+    >>> container.search('not-a-number', 'serial')
+    []
+
+    >>> result = container.search('1', 'serial')
+    >>> result
+    [<waeup.sirp...AccessCode object at 0x...>,
+     <waeup.sirp...AccessCode object at 0x...>]
+
+Searching for ACs:
+
+    >>> ac = result[0]
+    >>> container.search(ac.representation, 'pin')
+    [<waeup.sirp...AccessCode object at 0x...>]
+
+Searching for student IDs:
+
+    >>> ac.__parent__.invalidate(
+    ...   ac.representation, 'some_user')
+    >>> container.search('some_user', 'stud_id')
+    [<waeup.sirp...AccessCode object at 0x...>]
+
+
+Access code plugin
+==================
+
+.. class:: AccessCodePlugin
+
+  A `waeup.sirp` plugin that updates existing WAeUP SIRP university
+  instances so that they provide support for access-codes.
+
+  .. attribute:: grok.implements(IWAeUPSIRPPluggable)
+  .. attribute:: grok.name('accesscodes')
+
+  .. method:: setup(site, name, logger)
+
+     Create an accesscodebatch container in ``site``. Any events are
+     logged to ``logger``.
+
+  .. method:: update(site, name, logger)
+
+     Check if ``site`` contains an accesscodebatch container and add
+     it if missing. Any events are logged to ``logger``.
+
+  The AccessCodePlugin is available as a global named utility for the
+  IWAeUPSIRPPluggable interface named ``accesscodes``.
+
+  It is looked up by a university instance when created, so that this
+  instance has nothing to know about accesscodes at all and can
+  although provide support for them.
+
+    >>> from zope.component import getUtility
+    >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+    >>> plugin = getUtility(IWAeUPSIRPPluggable, name='accesscodes')
+    >>> plugin
+    <waeup.sirp.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
+
+  It provides a `setup()` and an `update()` method. Both have to be
+  fed with a site object (the `IUniversity` instance to modify), a
+  logger and a name.
+
+  We create a faked site and a logger:
+
+    >>> import logging
+    >>> faked_site = dict()
+    >>> logger = logging.getLogger('ac_test')
+    >>> logger.setLevel(logging.DEBUG)
+    >>> ch = logging.FileHandler('ac_tests.log', 'w')
+    >>> ch.setLevel(logging.DEBUG)
+    >>> logger.addHandler(ch)
+
+  Now we can install our stuff in the faked site:
+
+    >>> plugin.setup(faked_site, 'blah', logger)
+
+  The faked site now has an access code container:
+
+    >>> faked_site.keys()
+    ['accesscodes']
+
+  The action is described in the log:
+
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+
+  We can also update an existing site, by calling `update()`:
+
+    >>> plugin.update(faked_site, 'blah', logger)
+
+  There was nothing to do for the updater:
+
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
+
+  But if we remove the created batch container and call the updater, it
+  will create a new one:
+
+    >>> del faked_site['accesscodes']
+    >>> plugin.update(faked_site, 'blah', logger)
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
+    Updating site at {}. Installing access codes.
+    Installed container for access code batches.
+
+  Clean up:
+
+    >>> import os
+    >>> os.unlink('ac_tests.log')
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/api.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/api.txt	(revision 6408)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/api.txt	(revision 6408)
@@ -0,0 +1,17 @@
+:mod:`waeup.sirp.accesscodes` -- Access Codes
+*********************************************
+
+.. module:: waeup.sirp.accesscodes
+
+Components that represent access codes and related components.
+
+Submodules
+==========
+
+The :mod:`waeup.sirp.accesscodes` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   browser.txt
+   accesscodes.txt
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6408)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6408)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py	(revision 6408)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscodes.py	(revision 6408)
@@ -0,0 +1,164 @@
+##
+## test_accesscodes.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 12 13:07:58 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import shutil
+import tempfile
+import unittest
+from hurry.workflow.interfaces import InvalidTransitionError, IWorkflowState
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component.hooks import setSite, clearSite
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.accesscodes.accesscodes import (
+    AccessCodeBatch, get_access_code, invalidate_accesscode,
+    disable_accesscode, reenable_accesscode, fire_transition)
+from waeup.sirp.accesscodes.workflow import INITIALIZED, USED, DISABLED
+
+class AccessCodeHelpersTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeHelpersTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        # Create batch
+        batch = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        # Fill batch with accesscodes
+        batch.addAccessCode(0, '11111111')
+        batch.addAccessCode(1, '22222222')
+        batch.addAccessCode(2, '33333333')
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        self.ac2 = batch.getAccessCode('APP-1-22222222')
+        self.ac3 = batch.getAccessCode('APP-1-33333333')
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeHelpersTests, self).tearDown()
+        return
+
+    def test_get_access_code(self):
+        ac = get_access_code('APP-1-11111111')
+        assert ac is self.ac1
+
+    def test_get_access_code_not_string(self):
+        ac = get_access_code(object())
+        assert ac is None
+
+    def test_get_access_code_no_proper_pin(self):
+        ac = get_access_code('APP-without_pin')
+        assert ac is None
+
+    def test_get_access_code_invalid_batch_num(self):
+        ac = get_access_code('APP-invalid-11111111')
+        assert ac is None
+
+    def test_get_access_code_invalid_pin(self):
+        ac = get_access_code('APP-1-notexistent')
+        assert ac is None
+
+    def test_invalidate_accesscode(self):
+        assert self.ac1._invalidation_date is None
+        result = invalidate_accesscode('APP-1-11111111')
+        assert self.ac1._invalidation_date is not None
+        assert result is True
+
+    def test_disable_accesscode_unused(self):
+        # we can disable initialized acs
+        assert self.ac1._disabled is False
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1._disabled is True
+
+    def test_disable_accesscode_used(self):
+        # we can disable already used acs
+        assert self.ac1._disabled is False
+        invalidate_accesscode('APP-1-11111111')
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1._disabled is True
+
+    def test_reenable_accesscode(self):
+        # we can reenable disabled acs
+        disable_accesscode('APP-1-11111111')
+        result = reenable_accesscode('APP-1-11111111')
+        assert result is True
+        assert self.ac1._disabled is False
+
+    def test_fire_transition(self):
+        # we can fire transitions generally
+        fire_transition('APP-1-11111111', 'use')
+        assert IWorkflowState(self.ac1).getState() is USED
+
+    def test_fire_transition_toward(self):
+        # the `toward` keyword is respected
+        fire_transition('APP-1-11111111', DISABLED, toward=True)
+        assert IWorkflowState(self.ac1).getState() is DISABLED
+
+    def test_fire_transition_no_site(self):
+        # when no site is available, we will get a TypeError
+        clearSite()
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-11111111', 'use')
+
+    def test_fire_transition_broken_ac_id(self):
+        # if we get an invalid access code id (of wrong format) we get
+        # ValueErrors
+        self.assertRaises(
+            ValueError,
+            fire_transition, '11111111', 'use')
+
+    def test_fire_transition_invalid_batch_id(self):
+        # if we request a non-existent batch_id, we'll get a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'FOO-1-11111111', 'use')
+
+    def test_fire_transition_invalid_ac(self):
+        # if we request a non-exitent access-code, we'll get a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-NONSENSE', 'use')
+
+    def test_fire_transition_undef_trans_id(self):
+        # asking for undefined transition id means a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-11111111', 'nonsense')
+
+    def test_fire_transition_invalid_transition(self):
+        # asking for a forbidden transition will result in
+        # InvalidTransitionError
+        self.assertRaises(
+            InvalidTransitionError,
+            fire_transition, 'APP-1-11111111', 'init') # already initialized
