Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/__init__.py	(revision 6632)
@@ -4,5 +4,5 @@
 
 Main content components are defined in
-:mod:`waeup.sirp.accesscodes.accesscodes`. This subpackage also
+:mod:`waeup.sirp.accesscodes.accesscode`. This subpackage also
 provides a _catalog_ to lookup any access code quickly and a
 _workflow_ to guarantee consistent states and transitions when dealing
@@ -16,5 +16,5 @@
 :mod:`waeup.sirp.accesscodes.interfaces`.
 """
-from waeup.sirp.accesscodes.accesscodes import (
+from waeup.sirp.accesscodes.accesscode import (
     get_access_code, invalidate_accesscode, disable_accesscode,
     reenable_accesscode, AccessCode, AccessCodeBatch, AccessCodeBatchContainer)
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.py	(revision 6632)
@@ -0,0 +1,520 @@
+"""Components to handle access codes.
+
+Access codes (aka PINs) in waeup sites are organized in batches. That
+means a certain accesscode must be part of a batch. As a site (or
+university) can hold an arbitrary number of batches, we also provide a
+batch container. Each university has one batch container that holds
+all access code batches of which each one can hold several thousands
+of access codes.
+"""
+import csv
+import grok
+import os
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from random import SystemRandom as random
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable, IObjectHistory
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
+    )
+from waeup.sirp.accesscodes.workflow import DISABLED, USED
+
+class ManageACBatches(grok.Permission):
+    grok.name('waeup.manageACBatches')
+
+class AccessCode(grok.Model):
+    """An access code (aka PIN).
+
+    Implements
+    :class:`waeup.sirp.accesscodes.interfaces.IAccessCode`. :class:`AccessCode`
+    instances are normally part of an :class:`AccessCodeBatch` so
+    their representation (or code) is built with the containing batch
+    involved.
+
+    `batch_serial`
+       the serial number of the new :class:`AccessCode` inside its batch.
+
+    `random_num`
+       a 10-digit number representing the main part of the code.
+
+    :class:`AccessCode` instances normally have a representation (or
+    code) like
+
+      ``APP-XXX-YYYYYYYYYY``
+
+    where ``APP`` is the prefix of the containing batch, ``XXX`` is
+    the batch number and ``YYYYYYYYYY`` is the real code. The complete
+    PIN is portal-wide unique.
+
+    Access code instances are far more than simple strings. They have
+    a state, a history (so that all changes can be tracked) and a
+    cost (given as a float number).
+
+    The state of an access code is something like 'used', 'disabled',
+    etc. and determined by the workflow defined in
+    :mod:`waeup.sirp.accesscodes.workflow`. This also means that
+    instead of setting the status of an access code directly (you
+    can't do that easily, and yes, that's intentionally), you have to
+    trigger a transition (that might fail, if the transition is not
+    allowed in terms of logic or permissions). See
+    :mod:`waeup.sirp.accesscodes.workflow` for details.
+
+    """
+    grok.implements(IAccessCode)
+
+    def __init__(self, batch_serial, random_num):
+        super(AccessCode, self).__init__()
+        self.batch_serial = batch_serial
+        self.random_num = random_num
+        IWorkflowInfo(self).fireTransition('init')
+
+    @property
+    def representation(self):
+        """A string representation of the :class:`AccessCode`.
+
+        It has format ``APP-XXX-YYYYYYYYYY`` as described above.
+        """
+        return '%s-%s-%s' % (
+            self.batch_prefix, self.batch_num, self.random_num)
+
+    @property
+    def batch(self):
+        """The batch this :class:`AccessCode` is contained.
+        """
+        return getattr(self, '__parent__', None)
+
+    @property
+    def batch_prefix(self):
+        """The prefix of the batch this :class:`AccessCode` belongs to.
+        """
+        if self.batch is None:
+            return ''
+        return self.batch.prefix
+
+    @property
+    def batch_num(self):
+        """The number of the batch this :class:`AccessCode` belongs to. A
+        read-only attribute.
+        """
+        if self.batch is None:
+            return ''
+        return self.batch.num
+
+    @property
+    def cost(self):
+        """A float representing the price or ``None``. A read-only attribute.
+        """
+        if self.batch is None:
+            return None
+        return self.batch.cost
+
+    @property
+    def state(self):
+        """The workflow state. A read-only attribute.
+        """
+        return IWorkflowState(self).getState()
+
+    @property
+    def history(self):
+        """A :class:`waeup.sirp.objecthistory.ObjectHistory` instance.
+        """
+        history = IObjectHistory(self)
+        return '||'.join(history.messages)
+
+class AccessCodeBatch(grok.Container):
+    """A batch of access codes.
+    """
+    grok.implements(IAccessCodeBatch)
+
+    def __init__(self, creation_date, creator, batch_prefix, cost,
+                 entry_num, num=None):
+        super(AccessCodeBatch, self).__init__()
+        self.creation_date = creation_date
+        self.creator = creator
+        self.prefix = batch_prefix.upper()
+        self.cost = cost
+        self.entry_num = entry_num
+        self.num = num
+        self.used_num = 0
+        self.disabled_num = 0
+
+    def _createEntries(self):
+        """Create the entries for this batch.
+        """
+        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
+            self.addAccessCode(num, pin)
+        return
+
+    def _getNewRandomNum(self, num=1):
+        """Create a set of ``num`` random numbers of 10 digits each.
+
+        The number is returned as string.
+        """
+        curr = 1
+        while curr <= num:
+            pin = ''
+            for x in range(10):
+                pin += str(random().randint(0, 9))
+            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
+                curr += 1
+                yield pin
+            # PIN already in use
+
+    def _getStoragePath(self):
+        """Get the directory, where we store all batch-related CSV files.
+        """
+        site = grok.getSite()
+        storagepath = site['datacenter'].storage
+        ac_storage = os.path.join(storagepath, 'accesscodes')
+        if not os.path.exists(ac_storage):
+            os.mkdir(ac_storage)
+        return ac_storage
+
+    def getAccessCode(self, ac_id):
+        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
+        """
+        return self[ac_id]
+
+    def addAccessCode(self, num, pin):
+        """Add an access-code.
+        """
+        ac = AccessCode(num, pin)
+        ac.__parent__ = self
+        self[ac.representation] = ac
+        return
+
+    def createCSVLogFile(self):
+        """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.
+        """
+        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
+        ac_storage = self._getStoragePath()
+        csv_path = os.path.join(
+            ac_storage, '%s-%s-%s-%s.csv' % (
+                self.prefix, self.num, date, self.creator)
+            )
+        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
+        writer.writerow(['serial', 'ac', 'cost'])
+        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
+
+        for value in sorted(self.values(),
+                            cmp=lambda x, y: cmp(
+                x.batch_serial, y.batch_serial)):
+            writer.writerow(
+                [str(value.batch_serial), str(value.representation)]
+                )
+        site = grok.getSite()
+        logger = site.logger
+        logger.info(
+            "Created batch %s-%s" % (self.prefix, self.num))
+        logger.info(
+            "Written batch CSV to %s" % csv_path)
+        return os.path.basename(csv_path)
+
+    def archive(self):
+        """Create a CSV file for archive.
+        """
+        ac_storage = self._getStoragePath()
+        now = datetime.now()
+        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
+        csv_path = os.path.join(
+            ac_storage, '%s-%s_archive-%s-%s.csv' % (
+                self.prefix, self.num, timestamp, self.creator)
+            )
+        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
+        writer.writerow(['prefix', 'serial', 'ac', 'state', 'history'])
+        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
+                         str(self.entry_num)])
+        for value in sorted(
+            self.values(),
+            cmp = lambda x, y: cmp(x.batch_serial, y.batch_serial)
+            ):
+            writer.writerow([
+                    self.prefix, value.batch_serial, value.representation,
+                    value.state, value.history
+                    ])
+        return os.path.basename(csv_path)
+
+@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
+def handle_batch_added(batch, event):
+    # A (maybe dirty?) workaround to make batch containers work
+    # without self-maintained acids: as batches should contain their
+    # set of data immediately after creation, but we cannot add
+    # subobjects as long as the batch was not added already to the
+    # ZODB, we trigger the item creation for the time after the batch
+    # was added to the ZODB.
+    batch._createEntries()
+    return
+
+
+class AccessCodeBatchContainer(grok.Container):
+    grok.implements(IAccessCodeBatchContainer)
+
+    def _getStoragePath(self):
+        """Get the directory, where batch import files are stored.
+
+        If the path does not exist yet, it is created. The path is
+        normally ``accesscodes/imports`` below the datacenter storage
+        path (see :data:`waeup.sirp.accesscodes.Datacenter.storage`).
+        """
+        site = grok.getSite()
+        storagepath = site['datacenter'].storage
+        ac_storage = os.path.join(storagepath, 'accesscodes')
+        import_path = os.path.join(ac_storage, 'imports')
+        for path in [ac_storage, import_path]:
+            if not os.path.exists(path):
+                os.mkdir(path)
+                site.logger.info('created path %s' % path)
+        return import_path
+
+    def addBatch(self, batch):
+        """Add an already created `batch`.
+        """
+        batch.num = self.getNum(batch.prefix)
+        key = "%s-%s" % (batch.prefix, batch.num)
+        self[key] = batch
+        self._p_changed = True
+
+    def createBatch(self, creation_date, creator, prefix, cost,
+                    entry_num):
+        """Create and add a batch.
+        """
+        batch_num = self.getNum(prefix)
+        batch = AccessCodeBatch(creation_date, creator, prefix,
+                                cost, entry_num, num=batch_num)
+        self.addBatch(batch)
+        return batch
+
+    def getNum(self, prefix):
+        """Get next unused num for given prefix.
+        """
+        num = 1
+        while self.get('%s-%s' % (prefix, num), None) is not None:
+            num += 1
+        return num
+
+    def getImportFiles(self):
+        """Return a generator with basenames of available import files.
+        """
+        path = self._getStoragePath()
+        for filename in sorted(os.listdir(path)):
+            yield filename
+
+    # This is temporary reimport solution. Access codes will be imported
+    # with state initialized no matter if they have been used before.
+    def reimport(self, filename, creator=u'UNKNOWN'):
+        """Reimport a batch given in CSV file.
+
+        CSV file must be of format as generated by createCSVLogFile
+        method.
+        """
+        path = os.path.join(self._getStoragePath(), filename)
+        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
+        entry = reader.next()
+        cost = float(entry['serial'])
+        num = int(entry['ac'])
+        prefix = entry['prefix']
+        batch_name = '%s-%s' % (prefix, num)
+        if batch_name in self.keys():
+            raise KeyError('Batch already exists: %s' % batch_name)
+        batch = AccessCodeBatch(
+            datetime.now(), creator, prefix, cost, 0, num=num)
+        num_entries = 0
+        self[batch_name] = batch
+        for row in reader:
+            pin = row['ac']
+            serial = int(row['serial'])
+            rand_num = pin.rsplit('-', 1)[-1]
+            batch.addAccessCode(serial, rand_num)
+            num_entries += 1
+        batch.entry_num = num_entries
+
+        batch.createCSVLogFile()
+        return
+
+    def getAccessCode(self, ac_id):
+        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
+        """
+        for batchname in self.keys():
+            batch = self[batchname]
+            try:
+                return batch.getAccessCode(ac_id)
+            except KeyError:
+                continue
+        return None
+
+    def disable(self, ac_id, comment=None):
+        """Disable the AC with ID ``ac_id``.
+
+        ``user_id`` is the user ID of the user triggering the
+        process. Already disabled ACs are left untouched.
+        """
+        ac = self.getAccessCode(ac_id)
+        if ac is None:
+            return
+        disable_accesscode(ac_id, comment)
+        return
+
+    def enable(self, ac_id, comment=None):
+        """(Re-)enable the AC with ID ``ac_id``.
+
+        This leaves the given AC in state ``unused``. Already enabled
+        ACs are left untouched.
+        """
+        ac = self.getAccessCode(ac_id)
+        if ac is None:
+            return
+        reenable_accesscode(ac_id, comment)
+        return
+
+class AccessCodePlugin(grok.GlobalUtility):
+    grok.name('accesscodes')
+    grok.implements(IWAeUPSIRPPluggable)
+
+    def setup(self, site, name, logger):
+        site['accesscodes'] = AccessCodeBatchContainer()
+        logger.info('Installed container for access code batches.')
+        return
+
+    def update(self, site, name, logger):
+        if not 'accesscodes' in site.keys():
+            logger.info('Updating site at %s. Installing access codes.' % (
+                    site,))
+            self.setup(site, name, logger)
+        else:
+            logger.info(
+                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
+                    site, ))
+        return
+
+def get_access_code(access_code):
+    """Get an access code instance.
+
+    An access code here is a string like ``PUDE-1-1234567890``.
+
+    Returns ``None`` if the given code cannot be found.
+
+    This is a convenicence function that is faster than looking up a
+    batch container for the approriate access code.
+    """
+    site = grok.getSite()
+    if not isinstance(access_code, basestring):
+        return None
+    try:
+        batch_id, ac_id = access_code.rsplit('-', 1)
+    except:
+        return None
+    batch = site['accesscodes'].get(batch_id, None)
+    if batch is None:
+        return None
+    try:
+        code = batch.getAccessCode(access_code)
+    except KeyError:
+        return None
+    return code
+
+def fire_transition(access_code, arg, toward=False, comment=None):
+    """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 :data:`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.
+
+    If `comment` is specified (default is ``None``) the given string
+    will be passed along as transition comment. It will appear in the
+    history of the changed access code. You can use this to place
+    remarks like for which object the access code was used or similar.
+
+    :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:
+        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, comment=comment)
+    else:
+        info.fireTransition(arg, comment=comment)
+    return True
+
+def invalidate_accesscode(access_code, comment=None):
+    """Invalidate AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    try:
+        fire_transition(access_code, 'use', comment=comment)
+        return True
+    except:
+        return False
+
+def disable_accesscode(access_code, comment=None):
+    """Disable AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(
+        access_code, DISABLED, toward=True, comment=comment)
+
+def reenable_accesscode(access_code, comment=None):
+    """Reenable AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(access_code, 'reenable', comment=comment)
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.txt	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscode.txt	(revision 6632)
@@ -0,0 +1,674 @@
+:mod:`waeup.sirp.accesscodes.accesscode` -- access codes (aka PINs)
+*******************************************************************
+
+.. module:: waeup.sirp.accesscodes.accesscode
+
+Components that represent access codes and related.
+
+.. :NOdoctest:
+.. :NOlayer: 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/accesscodes.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py	(revision 6631)
+++ 	(revision )
@@ -1,520 +1,0 @@
-"""Components to handle access codes.
-
-Access codes (aka PINs) in waeup sites are organized in batches. That
-means a certain accesscode must be part of a batch. As a site (or
-university) can hold an arbitrary number of batches, we also provide a
-batch container. Each university has one batch container that holds
-all access code batches of which each one can hold several thousands
-of access codes.
-"""
-import csv
-import grok
-import os
-from datetime import datetime
-from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
-from random import SystemRandom as random
-from waeup.sirp.interfaces import IWAeUPSIRPPluggable, IObjectHistory
-from waeup.sirp.accesscodes.interfaces import (
-    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
-    )
-from waeup.sirp.accesscodes.workflow import DISABLED, USED
-
-class ManageACBatches(grok.Permission):
-    grok.name('waeup.manageACBatches')
-
-class AccessCode(grok.Model):
-    """An access code (aka PIN).
-
-    Implements
-    :class:`waeup.sirp.accesscodes.interfaces.IAccessCode`. :class:`AccessCode`
-    instances are normally part of an :class:`AccessCodeBatch` so
-    their representation (or code) is built with the containing batch
-    involved.
-
-    `batch_serial`
-       the serial number of the new :class:`AccessCode` inside its batch.
-
-    `random_num`
-       a 10-digit number representing the main part of the code.
-
-    :class:`AccessCode` instances normally have a representation (or
-    code) like
-
-      ``APP-XXX-YYYYYYYYYY``
-
-    where ``APP`` is the prefix of the containing batch, ``XXX`` is
-    the batch number and ``YYYYYYYYYY`` is the real code. The complete
-    PIN is portal-wide unique.
-
-    Access code instances are far more than simple strings. They have
-    a state, a history (so that all changes can be tracked) and a
-    cost (given as a float number).
-
-    The state of an access code is something like 'used', 'disabled',
-    etc. and determined by the workflow defined in
-    :mod:`waeup.sirp.accesscodes.workflow`. This also means that
-    instead of setting the status of an access code directly (you
-    can't do that easily, and yes, that's intentionally), you have to
-    trigger a transition (that might fail, if the transition is not
-    allowed in terms of logic or permissions). See
-    :mod:`waeup.sirp.accesscodes.workflow` for details.
-
-    """
-    grok.implements(IAccessCode)
-
-    def __init__(self, batch_serial, random_num):
-        super(AccessCode, self).__init__()
-        self.batch_serial = batch_serial
-        self.random_num = random_num
-        IWorkflowInfo(self).fireTransition('init')
-
-    @property
-    def representation(self):
-        """A string representation of the :class:`AccessCode`.
-
-        It has format ``APP-XXX-YYYYYYYYYY`` as described above.
-        """
-        return '%s-%s-%s' % (
-            self.batch_prefix, self.batch_num, self.random_num)
-
-    @property
-    def batch(self):
-        """The batch this :class:`AccessCode` is contained.
-        """
-        return getattr(self, '__parent__', None)
-
-    @property
-    def batch_prefix(self):
-        """The prefix of the batch this :class:`AccessCode` belongs to.
-        """
-        if self.batch is None:
-            return ''
-        return self.batch.prefix
-
-    @property
-    def batch_num(self):
-        """The number of the batch this :class:`AccessCode` belongs to. A
-        read-only attribute.
-        """
-        if self.batch is None:
-            return ''
-        return self.batch.num
-
-    @property
-    def cost(self):
-        """A float representing the price or ``None``. A read-only attribute.
-        """
-        if self.batch is None:
-            return None
-        return self.batch.cost
-
-    @property
-    def state(self):
-        """The workflow state. A read-only attribute.
-        """
-        return IWorkflowState(self).getState()
-
-    @property
-    def history(self):
-        """A :class:`waeup.sirp.objecthistory.ObjectHistory` instance.
-        """
-        history = IObjectHistory(self)
-        return '||'.join(history.messages)
-
-class AccessCodeBatch(grok.Container):
-    """A batch of access codes.
-    """
-    grok.implements(IAccessCodeBatch)
-
-    def __init__(self, creation_date, creator, batch_prefix, cost,
-                 entry_num, num=None):
-        super(AccessCodeBatch, self).__init__()
-        self.creation_date = creation_date
-        self.creator = creator
-        self.prefix = batch_prefix.upper()
-        self.cost = cost
-        self.entry_num = entry_num
-        self.num = num
-        self.used_num = 0
-        self.disabled_num = 0
-
-    def _createEntries(self):
-        """Create the entries for this batch.
-        """
-        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
-            self.addAccessCode(num, pin)
-        return
-
-    def _getNewRandomNum(self, num=1):
-        """Create a set of ``num`` random numbers of 10 digits each.
-
-        The number is returned as string.
-        """
-        curr = 1
-        while curr <= num:
-            pin = ''
-            for x in range(10):
-                pin += str(random().randint(0, 9))
-            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
-                curr += 1
-                yield pin
-            # PIN already in use
-
-    def _getStoragePath(self):
-        """Get the directory, where we store all batch-related CSV files.
-        """
-        site = grok.getSite()
-        storagepath = site['datacenter'].storage
-        ac_storage = os.path.join(storagepath, 'accesscodes')
-        if not os.path.exists(ac_storage):
-            os.mkdir(ac_storage)
-        return ac_storage
-
-    def getAccessCode(self, ac_id):
-        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
-        """
-        return self[ac_id]
-
-    def addAccessCode(self, num, pin):
-        """Add an access-code.
-        """
-        ac = AccessCode(num, pin)
-        ac.__parent__ = self
-        self[ac.representation] = ac
-        return
-
-    def createCSVLogFile(self):
-        """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.
-        """
-        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
-        ac_storage = self._getStoragePath()
-        csv_path = os.path.join(
-            ac_storage, '%s-%s-%s-%s.csv' % (
-                self.prefix, self.num, date, self.creator)
-            )
-        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
-        writer.writerow(['serial', 'ac', 'cost'])
-        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
-
-        for value in sorted(self.values(),
-                            cmp=lambda x, y: cmp(
-                x.batch_serial, y.batch_serial)):
-            writer.writerow(
-                [str(value.batch_serial), str(value.representation)]
-                )
-        site = grok.getSite()
-        logger = site.logger
-        logger.info(
-            "Created batch %s-%s" % (self.prefix, self.num))
-        logger.info(
-            "Written batch CSV to %s" % csv_path)
-        return os.path.basename(csv_path)
-
-    def archive(self):
-        """Create a CSV file for archive.
-        """
-        ac_storage = self._getStoragePath()
-        now = datetime.now()
-        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
-        csv_path = os.path.join(
-            ac_storage, '%s-%s_archive-%s-%s.csv' % (
-                self.prefix, self.num, timestamp, self.creator)
-            )
-        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
-        writer.writerow(['prefix', 'serial', 'ac', 'state', 'history'])
-        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
-                         str(self.entry_num)])
-        for value in sorted(
-            self.values(),
-            cmp = lambda x, y: cmp(x.batch_serial, y.batch_serial)
-            ):
-            writer.writerow([
-                    self.prefix, value.batch_serial, value.representation,
-                    value.state, value.history
-                    ])
-        return os.path.basename(csv_path)
-
-@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
-def handle_batch_added(batch, event):
-    # A (maybe dirty?) workaround to make batch containers work
-    # without self-maintained acids: as batches should contain their
-    # set of data immediately after creation, but we cannot add
-    # subobjects as long as the batch was not added already to the
-    # ZODB, we trigger the item creation for the time after the batch
-    # was added to the ZODB.
-    batch._createEntries()
-    return
-
-
-class AccessCodeBatchContainer(grok.Container):
-    grok.implements(IAccessCodeBatchContainer)
-
-    def _getStoragePath(self):
-        """Get the directory, where batch import files are stored.
-
-        If the path does not exist yet, it is created. The path is
-        normally ``accesscodes/imports`` below the datacenter storage
-        path (see :data:`waeup.sirp.accesscodes.Datacenter.storage`).
-        """
-        site = grok.getSite()
-        storagepath = site['datacenter'].storage
-        ac_storage = os.path.join(storagepath, 'accesscodes')
-        import_path = os.path.join(ac_storage, 'imports')
-        for path in [ac_storage, import_path]:
-            if not os.path.exists(path):
-                os.mkdir(path)
-                site.logger.info('created path %s' % path)
-        return import_path
-
-    def addBatch(self, batch):
-        """Add an already created `batch`.
-        """
-        batch.num = self.getNum(batch.prefix)
-        key = "%s-%s" % (batch.prefix, batch.num)
-        self[key] = batch
-        self._p_changed = True
-
-    def createBatch(self, creation_date, creator, prefix, cost,
-                    entry_num):
-        """Create and add a batch.
-        """
-        batch_num = self.getNum(prefix)
-        batch = AccessCodeBatch(creation_date, creator, prefix,
-                                cost, entry_num, num=batch_num)
-        self.addBatch(batch)
-        return batch
-
-    def getNum(self, prefix):
-        """Get next unused num for given prefix.
-        """
-        num = 1
-        while self.get('%s-%s' % (prefix, num), None) is not None:
-            num += 1
-        return num
-
-    def getImportFiles(self):
-        """Return a generator with basenames of available import files.
-        """
-        path = self._getStoragePath()
-        for filename in sorted(os.listdir(path)):
-            yield filename
-
-    # This is temporary reimport solution. Access codes will be imported
-    # with state initialized no matter if they have been used before.
-    def reimport(self, filename, creator=u'UNKNOWN'):
-        """Reimport a batch given in CSV file.
-
-        CSV file must be of format as generated by createCSVLogFile
-        method.
-        """
-        path = os.path.join(self._getStoragePath(), filename)
-        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
-        entry = reader.next()
-        cost = float(entry['serial'])
-        num = int(entry['ac'])
-        prefix = entry['prefix']
-        batch_name = '%s-%s' % (prefix, num)
-        if batch_name in self.keys():
-            raise KeyError('Batch already exists: %s' % batch_name)
-        batch = AccessCodeBatch(
-            datetime.now(), creator, prefix, cost, 0, num=num)
-        num_entries = 0
-        self[batch_name] = batch
-        for row in reader:
-            pin = row['ac']
-            serial = int(row['serial'])
-            rand_num = pin.rsplit('-', 1)[-1]
-            batch.addAccessCode(serial, rand_num)
-            num_entries += 1
-        batch.entry_num = num_entries
-
-        batch.createCSVLogFile()
-        return
-
-    def getAccessCode(self, ac_id):
-        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
-        """
-        for batchname in self.keys():
-            batch = self[batchname]
-            try:
-                return batch.getAccessCode(ac_id)
-            except KeyError:
-                continue
-        return None
-
-    def disable(self, ac_id, comment=None):
-        """Disable the AC with ID ``ac_id``.
-
-        ``user_id`` is the user ID of the user triggering the
-        process. Already disabled ACs are left untouched.
-        """
-        ac = self.getAccessCode(ac_id)
-        if ac is None:
-            return
-        disable_accesscode(ac_id, comment)
-        return
-
-    def enable(self, ac_id, comment=None):
-        """(Re-)enable the AC with ID ``ac_id``.
-
-        This leaves the given AC in state ``unused``. Already enabled
-        ACs are left untouched.
-        """
-        ac = self.getAccessCode(ac_id)
-        if ac is None:
-            return
-        reenable_accesscode(ac_id, comment)
-        return
-
-class AccessCodePlugin(grok.GlobalUtility):
-    grok.name('accesscodes')
-    grok.implements(IWAeUPSIRPPluggable)
-
-    def setup(self, site, name, logger):
-        site['accesscodes'] = AccessCodeBatchContainer()
-        logger.info('Installed container for access code batches.')
-        return
-
-    def update(self, site, name, logger):
-        if not 'accesscodes' in site.keys():
-            logger.info('Updating site at %s. Installing access codes.' % (
-                    site,))
-            self.setup(site, name, logger)
-        else:
-            logger.info(
-                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
-                    site, ))
-        return
-
-def get_access_code(access_code):
-    """Get an access code instance.
-
-    An access code here is a string like ``PUDE-1-1234567890``.
-
-    Returns ``None`` if the given code cannot be found.
-
-    This is a convenicence function that is faster than looking up a
-    batch container for the approriate access code.
-    """
-    site = grok.getSite()
-    if not isinstance(access_code, basestring):
-        return None
-    try:
-        batch_id, ac_id = access_code.rsplit('-', 1)
-    except:
-        return None
-    batch = site['accesscodes'].get(batch_id, None)
-    if batch is None:
-        return None
-    try:
-        code = batch.getAccessCode(access_code)
-    except KeyError:
-        return None
-    return code
-
-def fire_transition(access_code, arg, toward=False, comment=None):
-    """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 :data:`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.
-
-    If `comment` is specified (default is ``None``) the given string
-    will be passed along as transition comment. It will appear in the
-    history of the changed access code. You can use this to place
-    remarks like for which object the access code was used or similar.
-
-    :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:
-        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, comment=comment)
-    else:
-        info.fireTransition(arg, comment=comment)
-    return True
-
-def invalidate_accesscode(access_code, comment=None):
-    """Invalidate AccessCode denoted by string ``access_code``.
-
-    Fires an appropriate transition to perform the task.
-
-    `comment` is a string that will appear in the access code
-    history.
-
-    See :func:`fire_transition` for possible exceptions and their
-    meanings.
-    """
-    try:
-        fire_transition(access_code, 'use', comment=comment)
-        return True
-    except:
-        return False
-
-def disable_accesscode(access_code, comment=None):
-    """Disable AccessCode denoted by string ``access_code``.
-
-    Fires an appropriate transition to perform the task.
-
-    `comment` is a string that will appear in the access code
-    history.
-
-    See :func:`fire_transition` for possible exceptions and their
-    meanings.
-    """
-    return fire_transition(
-        access_code, DISABLED, toward=True, comment=comment)
-
-def reenable_accesscode(access_code, comment=None):
-    """Reenable AccessCode denoted by string ``access_code``.
-
-    Fires an appropriate transition to perform the task.
-
-    `comment` is a string that will appear in the access code
-    history.
-
-    See :func:`fire_transition` for possible exceptions and their
-    meanings.
-    """
-    return fire_transition(access_code, 'reenable', comment=comment)
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt	(revision 6631)
+++ 	(revision )
@@ -1,674 +1,0 @@
-:mod:`waeup.sirp.accesscodes.accesscodes` -- access codes (aka PINs)
-********************************************************************
-
-.. module:: waeup.sirp.accesscodes.accesscodes
-
-Components that represent access codes and related.
-
-.. :NOdoctest:
-.. :NOlayer: 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/tests/test_accesscode.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 6632)
@@ -0,0 +1,473 @@
+##
+## test_accesscode.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 doctest
+import os
+import re
+import shutil
+import tempfile
+import unittest
+
+from datetime import datetime
+from hurry.workflow.interfaces import InvalidTransitionError, IWorkflowState
+from zope.app.testing.functional import (
+    FunctionalTestSetup, getRootFolder)
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyObject, verifyClass
+from zope.testing import renormalizing
+from waeup.sirp.app import University
+from waeup.sirp.interfaces import IObjectHistory, IWAeUPSIRPPluggable
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.accesscodes.accesscode import (
+    AccessCodeBatch, get_access_code, invalidate_accesscode, AccessCode,
+    disable_accesscode, reenable_accesscode, fire_transition,
+    AccessCodeBatchContainer, AccessCodePlugin)
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch,  IAccessCodeBatchContainer,)
+from waeup.sirp.accesscodes.workflow import INITIALIZED, USED, DISABLED
+
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+class AccessCodeHelpersTests(FunctionalTestCase):
+    # Tests for helpers like get_access_code, disable_accesscode, ...
+
+    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.state != USED
+        result = invalidate_accesscode('APP-1-11111111')
+        assert self.ac1.state == USED
+        assert result is True
+
+    def test_disable_accesscode_unused(self):
+        # we can disable initialized acs
+        assert self.ac1.state != USED
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1.state == DISABLED
+
+    def test_disable_accesscode_used(self):
+        # we can disable already used acs
+        assert self.ac1.state != DISABLED
+        invalidate_accesscode('APP-1-11111111')
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1.state == DISABLED
+
+    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.state != USED
+
+    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
+
+    def test_fire_transition_comment(self):
+        # when we request a comment, it will also appear in history
+        fire_transition('APP-1-11111111', 'use', comment='Hi there!')
+        history = IObjectHistory(self.ac1)
+        msgs = history.messages
+        assert 'Hi there!' in msgs[-1]
+
+    def test_fire_transition_no_comment(self):
+        # without comment, the history should be without trailing garbage
+        fire_transition('APP-1-11111111', 'use')
+        history = IObjectHistory(self.ac1)
+        msgs = history.messages
+        assert msgs[-1].endswith('AC used by system')
+
+class AccessCodeTests(FunctionalTestCase):
+    # Tests for AccessCode class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeTests, 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')
+
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        # AccessCodes fullfill their iface promises.
+        ac = AccessCode('1', '12345678')
+        assert verifyObject(IAccessCode, ac)
+        assert verifyClass(IAccessCode, AccessCode)
+
+    def test_history(self):
+        # Access codes have a history.
+        match = re.match(
+            '^....-..-.. ..:..:.. - AC initialized by system',
+            self.ac1.history)
+        assert match is not None
+
+    def test_cost(self):
+        # We get the cost set in batch
+        cost = self.ac1.cost
+        assert cost == 6.6
+
+class AccessCodeBatchTests(FunctionalTestCase):
+    # Tests for AccessCodeBatch class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeBatchTests, 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']
+
+        batch = AccessCodeBatch(    # create batch with zero entries
+            datetime.now(), 'testuser', 'FOO', 9.99, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        self.ac1 = AccessCode(0, '11111111')
+        self.ac2 = AccessCode(1, '22222222')
+        self.ac3 = AccessCode(2, '33333333')
+        batch['FOO-1-11111111'] = self.ac1
+        batch['FOO-1-22222222'] = self.ac2
+        batch['FOO-1-33333333'] = self.ac3
+        self.batch = batch
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeBatchTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        batch = AccessCodeBatch(
+            datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
+        assert verifyObject(IAccessCodeBatch, batch)
+        assert verifyClass(IAccessCodeBatch, AccessCodeBatch)
+
+    def test_csv_export(self):
+        # Make sure CSV export of accesscodes works
+        batch = self.batch
+        invalidate_accesscode('FOO-1-11111111', comment='comment with "quotes"')
+        disable_accesscode('FOO-1-33333333')
+        basename = batch.archive()
+        result_path = os.path.join(batch._getStoragePath(), basename)
+        expected = '''
+"prefix","serial","ac","state","history"
+"FOO","9.99","1","0"
+"FOO","0","FOO-1-11111111","used","<YYYY-MM-DD hh:mm:ss> - ..."
+"FOO","1","FOO-1-22222222","initialized","<YYYY-MM-DD hh:mm:ss> - ..."
+"FOO","2","FOO-1-33333333","disabled","<YYYY-MM-DD hh:mm:ss> - ..."
+'''[1:]
+        contents = open(result_path, 'rb').read()
+        self.assertMatches(expected, contents)
+
+class AccessCodeBatchContainerTests(FunctionalTestCase):
+    # Tests for AccessCodeContainer class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeBatchContainerTests, 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']
+
+        self.import_sample1_src = os.path.join(
+            os.path.dirname(__file__), 'sample_import.csv')
+
+        batch = AccessCodeBatch(    # create batch with zero entries
+            datetime.now(), 'testuser', 'BAR', 9.99, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        self.ac1 = AccessCode(0, '11111111')
+        self.ac2 = AccessCode(1, '22222222')
+        self.ac3 = AccessCode(2, '33333333')
+        batch['BAR-1-11111111'] = self.ac1
+        batch['BAR-1-22222222'] = self.ac2
+        batch['BAR-1-33333333'] = self.ac3
+        self.batch = batch
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeBatchContainerTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        accesscodes = AccessCodeBatchContainer()
+        assert verifyObject(IAccessCodeBatchContainer, accesscodes)
+        assert verifyClass(IAccessCodeBatchContainer, AccessCodeBatchContainer)
+
+    def test_csv_import(self):
+        # Make sure we can reimport sample data from local sample_import.csv
+        batchcontainer = self.app['accesscodes']
+        shutil.copyfile(        # Copy sample to import dir
+            os.path.join(os.path.dirname(__file__), 'sample_import.csv'),
+            os.path.join(batchcontainer._getStoragePath(), 'sample_import.csv')
+            )
+        batchcontainer.reimport('sample_import.csv')
+        batch = batchcontainer.get(u'FOO-1', None)
+        self.assertTrue(batch is not None)
+        keys = [x for x in batch.keys()]
+        self.assertEqual(
+            keys,
+            [u'FOO-1-11111111', u'FOO-1-22222222', u'FOO-1-33333333'])
+
+    def test_getAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        result1 = batchcontainer.getAccessCode('BAR-1-11111111')
+        result2 = batchcontainer.getAccessCode('BAR-1-not-existent')
+        assert isinstance(result1, AccessCode)
+        assert result2 is None
+
+    def test_disableAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        result1 = batchcontainer.disable('BAR-1-11111111')
+        result2 = batchcontainer.disable('BAR-1-not-existent')
+        assert self.ac1.state is DISABLED
+        assert result2 is None
+
+    def test_enableAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        batchcontainer.disable('BAR-1-11111111')
+        result1 = batchcontainer.enable('BAR-1-11111111')
+        result2 = batchcontainer.enable('BAR-1-not-existent')
+        assert self.ac1.state is INITIALIZED
+        assert result2 is None
+
+class AccessCodePluginTests(FunctionalTestCase):
+    # Tests for AccessCodeContainer class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodePluginTests, 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']
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodePluginTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        plugin = AccessCodePlugin()
+        assert verifyObject(IWAeUPSIRPPluggable, plugin)
+        assert verifyClass(IWAeUPSIRPPluggable, AccessCodePlugin)
+
+    def test_update_w_ac_container(self):
+        # The plugin changes nothing, if there is already a container
+        plugin = AccessCodePlugin()
+        site = self.app
+        logger = site.logger
+        accesscodes = site['accesscodes']
+        plugin.update(site, 'app', logger)
+        assert site['accesscodes'] is accesscodes
+
+    def test_update_wo_ac_container(self):
+        # The plugin creates a new accesscodes container if it is missing
+        plugin = AccessCodePlugin()
+        site = self.app
+        logger = site.logger
+        del site['accesscodes']
+        plugin.update(site, 'app', logger)
+        assert 'accesscodes' in site
+
+checker = renormalizing.RENormalizing([
+        (re.compile('[\d]{10}'), '<10-DIGITS>'),
+        ])
+
+def setUp(test):
+    FunctionalTestSetup().setUp()
+
+def tearDown(self, test=None):
+    FunctionalTestSetup().tearDown()
+
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        AccessCodeHelpersTests,
+        AccessCodeTests,
+        AccessCodeBatchTests,
+        AccessCodeBatchContainerTests,
+        AccessCodePluginTests,
+        ]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
+    for filename in [
+        #'accesscodes.txt',
+        'browser.txt'
+        ]:
+        path = os.path.join(
+            os.path.dirname(os.path.dirname(__file__)), filename)
+        test = doctest.DocFileSuite(
+            path,
+            module_relative=False,
+            setUp=setUp, tearDown=tearDown,
+            globs = dict(getRootFolder = getRootFolder),
+            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
+            checker = checker,
+            )
+        test.layer = FunctionalLayer
+        suite.addTest(test)
+    return suite
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 6631)
+++ 	(revision )
@@ -1,473 +1,0 @@
-##
-## 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 doctest
-import os
-import re
-import shutil
-import tempfile
-import unittest
-
-from datetime import datetime
-from hurry.workflow.interfaces import InvalidTransitionError, IWorkflowState
-from zope.app.testing.functional import (
-    FunctionalTestSetup, getRootFolder)
-from zope.component.hooks import setSite, clearSite
-from zope.interface.verify import verifyObject, verifyClass
-from zope.testing import renormalizing
-from waeup.sirp.app import University
-from waeup.sirp.interfaces import IObjectHistory, IWAeUPSIRPPluggable
-from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
-from waeup.sirp.accesscodes.accesscodes import (
-    AccessCodeBatch, get_access_code, invalidate_accesscode, AccessCode,
-    disable_accesscode, reenable_accesscode, fire_transition,
-    AccessCodeBatchContainer, AccessCodePlugin)
-from waeup.sirp.accesscodes.interfaces import (
-    IAccessCode, IAccessCodeBatch,  IAccessCodeBatchContainer,)
-from waeup.sirp.accesscodes.workflow import INITIALIZED, USED, DISABLED
-
-
-optionflags = (
-    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
-
-class AccessCodeHelpersTests(FunctionalTestCase):
-    # Tests for helpers like get_access_code, disable_accesscode, ...
-
-    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.state != USED
-        result = invalidate_accesscode('APP-1-11111111')
-        assert self.ac1.state == USED
-        assert result is True
-
-    def test_disable_accesscode_unused(self):
-        # we can disable initialized acs
-        assert self.ac1.state != USED
-        disable_accesscode('APP-1-11111111')
-        assert self.ac1.state == DISABLED
-
-    def test_disable_accesscode_used(self):
-        # we can disable already used acs
-        assert self.ac1.state != DISABLED
-        invalidate_accesscode('APP-1-11111111')
-        disable_accesscode('APP-1-11111111')
-        assert self.ac1.state == DISABLED
-
-    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.state != USED
-
-    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
-
-    def test_fire_transition_comment(self):
-        # when we request a comment, it will also appear in history
-        fire_transition('APP-1-11111111', 'use', comment='Hi there!')
-        history = IObjectHistory(self.ac1)
-        msgs = history.messages
-        assert 'Hi there!' in msgs[-1]
-
-    def test_fire_transition_no_comment(self):
-        # without comment, the history should be without trailing garbage
-        fire_transition('APP-1-11111111', 'use')
-        history = IObjectHistory(self.ac1)
-        msgs = history.messages
-        assert msgs[-1].endswith('AC used by system')
-
-class AccessCodeTests(FunctionalTestCase):
-    # Tests for AccessCode class
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(AccessCodeTests, 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')
-
-        self.ac1 = batch.getAccessCode('APP-1-11111111')
-        setSite(self.app)
-        return
-
-    def tearDown(self):
-        shutil.rmtree(self.dc_root)
-        super(AccessCodeTests, self).tearDown()
-        return
-
-    def test_iface(self):
-        # AccessCodes fullfill their iface promises.
-        ac = AccessCode('1', '12345678')
-        assert verifyObject(IAccessCode, ac)
-        assert verifyClass(IAccessCode, AccessCode)
-
-    def test_history(self):
-        # Access codes have a history.
-        match = re.match(
-            '^....-..-.. ..:..:.. - AC initialized by system',
-            self.ac1.history)
-        assert match is not None
-
-    def test_cost(self):
-        # We get the cost set in batch
-        cost = self.ac1.cost
-        assert cost == 6.6
-
-class AccessCodeBatchTests(FunctionalTestCase):
-    # Tests for AccessCodeBatch class
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(AccessCodeBatchTests, 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']
-
-        batch = AccessCodeBatch(    # create batch with zero entries
-            datetime.now(), 'testuser', 'FOO', 9.99, 0)
-        self.app['accesscodes'].addBatch(batch)
-
-        self.ac1 = AccessCode(0, '11111111')
-        self.ac2 = AccessCode(1, '22222222')
-        self.ac3 = AccessCode(2, '33333333')
-        batch['FOO-1-11111111'] = self.ac1
-        batch['FOO-1-22222222'] = self.ac2
-        batch['FOO-1-33333333'] = self.ac3
-        self.batch = batch
-
-        setSite(self.app)
-        return
-
-    def tearDown(self):
-        shutil.rmtree(self.dc_root)
-        super(AccessCodeBatchTests, self).tearDown()
-        return
-
-    def test_iface(self):
-        batch = AccessCodeBatch(
-            datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
-        assert verifyObject(IAccessCodeBatch, batch)
-        assert verifyClass(IAccessCodeBatch, AccessCodeBatch)
-
-    def test_csv_export(self):
-        # Make sure CSV export of accesscodes works
-        batch = self.batch
-        invalidate_accesscode('FOO-1-11111111', comment='comment with "quotes"')
-        disable_accesscode('FOO-1-33333333')
-        basename = batch.archive()
-        result_path = os.path.join(batch._getStoragePath(), basename)
-        expected = '''
-"prefix","serial","ac","state","history"
-"FOO","9.99","1","0"
-"FOO","0","FOO-1-11111111","used","<YYYY-MM-DD hh:mm:ss> - ..."
-"FOO","1","FOO-1-22222222","initialized","<YYYY-MM-DD hh:mm:ss> - ..."
-"FOO","2","FOO-1-33333333","disabled","<YYYY-MM-DD hh:mm:ss> - ..."
-'''[1:]
-        contents = open(result_path, 'rb').read()
-        self.assertMatches(expected, contents)
-
-class AccessCodeBatchContainerTests(FunctionalTestCase):
-    # Tests for AccessCodeContainer class
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(AccessCodeBatchContainerTests, 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']
-
-        self.import_sample1_src = os.path.join(
-            os.path.dirname(__file__), 'sample_import.csv')
-
-        batch = AccessCodeBatch(    # create batch with zero entries
-            datetime.now(), 'testuser', 'BAR', 9.99, 0)
-        self.app['accesscodes'].addBatch(batch)
-
-        self.ac1 = AccessCode(0, '11111111')
-        self.ac2 = AccessCode(1, '22222222')
-        self.ac3 = AccessCode(2, '33333333')
-        batch['BAR-1-11111111'] = self.ac1
-        batch['BAR-1-22222222'] = self.ac2
-        batch['BAR-1-33333333'] = self.ac3
-        self.batch = batch
-
-        setSite(self.app)
-        return
-
-    def tearDown(self):
-        shutil.rmtree(self.dc_root)
-        super(AccessCodeBatchContainerTests, self).tearDown()
-        return
-
-    def test_iface(self):
-        accesscodes = AccessCodeBatchContainer()
-        assert verifyObject(IAccessCodeBatchContainer, accesscodes)
-        assert verifyClass(IAccessCodeBatchContainer, AccessCodeBatchContainer)
-
-    def test_csv_import(self):
-        # Make sure we can reimport sample data from local sample_import.csv
-        batchcontainer = self.app['accesscodes']
-        shutil.copyfile(        # Copy sample to import dir
-            os.path.join(os.path.dirname(__file__), 'sample_import.csv'),
-            os.path.join(batchcontainer._getStoragePath(), 'sample_import.csv')
-            )
-        batchcontainer.reimport('sample_import.csv')
-        batch = batchcontainer.get(u'FOO-1', None)
-        self.assertTrue(batch is not None)
-        keys = [x for x in batch.keys()]
-        self.assertEqual(
-            keys,
-            [u'FOO-1-11111111', u'FOO-1-22222222', u'FOO-1-33333333'])
-
-    def test_getAccessCode(self):
-        batchcontainer = self.app['accesscodes']
-        result1 = batchcontainer.getAccessCode('BAR-1-11111111')
-        result2 = batchcontainer.getAccessCode('BAR-1-not-existent')
-        assert isinstance(result1, AccessCode)
-        assert result2 is None
-
-    def test_disableAccessCode(self):
-        batchcontainer = self.app['accesscodes']
-        result1 = batchcontainer.disable('BAR-1-11111111')
-        result2 = batchcontainer.disable('BAR-1-not-existent')
-        assert self.ac1.state is DISABLED
-        assert result2 is None
-
-    def test_enableAccessCode(self):
-        batchcontainer = self.app['accesscodes']
-        batchcontainer.disable('BAR-1-11111111')
-        result1 = batchcontainer.enable('BAR-1-11111111')
-        result2 = batchcontainer.enable('BAR-1-not-existent')
-        assert self.ac1.state is INITIALIZED
-        assert result2 is None
-
-class AccessCodePluginTests(FunctionalTestCase):
-    # Tests for AccessCodeContainer class
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(AccessCodePluginTests, 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']
-
-    def tearDown(self):
-        shutil.rmtree(self.dc_root)
-        super(AccessCodePluginTests, self).tearDown()
-        return
-
-    def test_iface(self):
-        plugin = AccessCodePlugin()
-        assert verifyObject(IWAeUPSIRPPluggable, plugin)
-        assert verifyClass(IWAeUPSIRPPluggable, AccessCodePlugin)
-
-    def test_update_w_ac_container(self):
-        # The plugin changes nothing, if there is already a container
-        plugin = AccessCodePlugin()
-        site = self.app
-        logger = site.logger
-        accesscodes = site['accesscodes']
-        plugin.update(site, 'app', logger)
-        assert site['accesscodes'] is accesscodes
-
-    def test_update_wo_ac_container(self):
-        # The plugin creates a new accesscodes container if it is missing
-        plugin = AccessCodePlugin()
-        site = self.app
-        logger = site.logger
-        del site['accesscodes']
-        plugin.update(site, 'app', logger)
-        assert 'accesscodes' in site
-
-checker = renormalizing.RENormalizing([
-        (re.compile('[\d]{10}'), '<10-DIGITS>'),
-        ])
-
-def setUp(test):
-    FunctionalTestSetup().setUp()
-
-def tearDown(self, test=None):
-    FunctionalTestSetup().tearDown()
-
-
-
-def test_suite():
-    suite = unittest.TestSuite()
-    for testcase in [
-        AccessCodeHelpersTests,
-        AccessCodeTests,
-        AccessCodeBatchTests,
-        AccessCodeBatchContainerTests,
-        AccessCodePluginTests,
-        ]:
-        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
-    for filename in [
-        #'accesscodes.txt',
-        'browser.txt'
-        ]:
-        path = os.path.join(
-            os.path.dirname(os.path.dirname(__file__)), filename)
-        test = doctest.DocFileSuite(
-            path,
-            module_relative=False,
-            setUp=setUp, tearDown=tearDown,
-            globs = dict(getRootFolder = getRootFolder),
-            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
-            checker = checker,
-            )
-        test.layer = FunctionalLayer
-        suite.addTest(test)
-    return suite
Index: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 6632)
@@ -32,5 +32,5 @@
 from waeup.sirp.interfaces import IQueryResultItem
 from waeup.sirp.testing import FunctionalLayer
-from waeup.sirp.accesscodes.accesscodes import (
+from waeup.sirp.accesscodes.accesscode import (
     AccessCode, AccessCodeBatch, invalidate_accesscode, disable_accesscode)
 from waeup.sirp.accesscodes.workflow import INITIALIZED, USED, DISABLED
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/__init__.py	(revision 6632)
@@ -2,5 +2,5 @@
 """
 # Make this a package.
-from waeup.sirp.applicants.applicants import (
+from waeup.sirp.applicants.applicant import (
     ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
     )
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicant.py	(revision 6632)
@@ -0,0 +1,122 @@
+##
+## applicants.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jul 16 11:46:55 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
+## 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 grok
+from grok import index
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.interfaces import IObjectHistory
+from waeup.sirp.app import University
+from waeup.sirp.applicants.interfaces import (
+    IResultEntry, IApplicant, IApplicantEdit, default_passport_image,
+    )
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class ResultEntry(grok.Context):
+    grok.implements(IResultEntry)
+
+    def __init__(self, subject=None, score=None):
+        self.subject = subject
+        self.score = score
+
+class Applicant(grok.Model):
+    grok.implements(IApplicant,IApplicantEdit)
+    grok.provides(IApplicant)
+
+    def __init__(self):
+        super(Applicant, self).__init__()
+        IWorkflowInfo(self).fireTransition('init')
+        self.application_date = None
+        return
+
+    #def getApplicantsRootLogger(self):
+    #    return grok.getSite()['applicants'].logger
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return grok.getSite()['applicants'].logger_info(ob_class,target,comment)
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+# Set all attributes of Applicant required in IApplicant as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+Applicant = attrs_to_fields(Applicant)
+
+class ApplicantCatalog(grok.Indexes):
+    """A catalog indexing :class:`Applicant` instances in the ZODB.
+    """
+    grok.site(University)
+    grok.name('applicants_catalog')
+    grok.context(IApplicant)
+
+    access_code = index.Field(attribute='access_code')
+
+class ApplicantTraverser(grok.Traverser):
+    """Get image of the context applicant.
+
+    Each applicant can provide a passport photograph which will be
+    returned by this traverser if:
+
+    - we request the exact filename of the picture or
+
+    - ask for a picture named 'passport.jpg'.
+
+    If no picture was stored yet, we get a placeholder image when
+    asking for `passport.jpg`.
+
+    If none of the above applies, we return ``None``, most probably
+    resulting a :exc:`NotFound` exception.
+
+    """
+    grok.context(IApplicant)
+    def traverse(self, name):
+        passport_filename = getattr(self.context.passport, 'filename', None)
+        if name == passport_filename:
+            return self.context.passport
+        if name == 'passport.jpg':
+            if self.context.passport is not None:
+                return self.context.passport
+        return
+
+class ApplicantFactory(grok.GlobalUtility):
+    """A factory for applicants.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Applicant')
+    title = u"Create a new applicant.",
+    description = u"This factory instantiates new applicant instances."
+
+    def __call__(self, *args, **kw):
+        return Applicant()
+
+    def getInterfaces(self):
+        return implementedBy(Applicant)
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicants.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/applicants.py	(revision 6631)
+++ 	(revision )
@@ -1,122 +1,0 @@
-##
-## applicants.py
-## Login : <uli@pu.smp.net>
-## Started on  Fri Jul 16 11:46:55 2010 Uli Fouquet
-## $Id$
-## 
-## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
-## 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 grok
-from grok import index
-from zope.component.interfaces import IFactory
-from zope.interface import implementedBy
-from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
-from waeup.sirp.interfaces import IObjectHistory
-from waeup.sirp.app import University
-from waeup.sirp.applicants.interfaces import (
-    IResultEntry, IApplicant, IApplicantEdit, default_passport_image,
-    )
-from waeup.sirp.utils.helpers import attrs_to_fields
-
-class ResultEntry(grok.Context):
-    grok.implements(IResultEntry)
-
-    def __init__(self, subject=None, score=None):
-        self.subject = subject
-        self.score = score
-
-class Applicant(grok.Model):
-    grok.implements(IApplicant,IApplicantEdit)
-    grok.provides(IApplicant)
-
-    def __init__(self):
-        super(Applicant, self).__init__()
-        IWorkflowInfo(self).fireTransition('init')
-        self.application_date = None
-        return
-
-    #def getApplicantsRootLogger(self):
-    #    return grok.getSite()['applicants'].logger
-
-    def loggerInfo(self, ob_class, comment=None):
-        target = self.__name__
-        return grok.getSite()['applicants'].logger_info(ob_class,target,comment)
-
-    @property
-    def state(self):
-        state = IWorkflowState(self).getState()
-        return state
-
-    @property
-    def history(self):
-        history = IObjectHistory(self)
-        return history
-
-# Set all attributes of Applicant required in IApplicant as field
-# properties. Doing this, we do not have to set initial attributes
-# ourselves and as a bonus we get free validation when an attribute is
-# set.
-Applicant = attrs_to_fields(Applicant)
-
-class ApplicantCatalog(grok.Indexes):
-    """A catalog indexing :class:`Applicant` instances in the ZODB.
-    """
-    grok.site(University)
-    grok.name('applicants_catalog')
-    grok.context(IApplicant)
-
-    access_code = index.Field(attribute='access_code')
-
-class ApplicantTraverser(grok.Traverser):
-    """Get image of the context applicant.
-
-    Each applicant can provide a passport photograph which will be
-    returned by this traverser if:
-
-    - we request the exact filename of the picture or
-
-    - ask for a picture named 'passport.jpg'.
-
-    If no picture was stored yet, we get a placeholder image when
-    asking for `passport.jpg`.
-
-    If none of the above applies, we return ``None``, most probably
-    resulting a :exc:`NotFound` exception.
-
-    """
-    grok.context(IApplicant)
-    def traverse(self, name):
-        passport_filename = getattr(self.context.passport, 'filename', None)
-        if name == passport_filename:
-            return self.context.passport
-        if name == 'passport.jpg':
-            if self.context.passport is not None:
-                return self.context.passport
-        return
-
-class ApplicantFactory(grok.GlobalUtility):
-    """A factory for applicants.
-    """
-    grok.implements(IFactory)
-    grok.name(u'waeup.Applicant')
-    title = u"Create a new applicant.",
-    description = u"This factory instantiates new applicant instances."
-
-    def __call__(self, *args, **kw):
-        return Applicant()
-
-    def getInterfaces(self):
-        return implementedBy(Applicant)
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 6632)
@@ -0,0 +1,199 @@
+##
+## test_applicant.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Aug 24 04:36:11 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 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
+##
+"""Tests for applicants and related.
+"""
+import unittest
+from StringIO import StringIO
+from hurry.file.interfaces import IFileRetrieval
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component import (
+    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
+from zope.component.hooks import setSite
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from zope.publisher.base import TestRequest
+from zope.publisher.interfaces import NotFound
+from waeup.sirp.app import University
+from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile
+from waeup.sirp.image.interfaces import IWAeUPImageFile
+from waeup.sirp.applicants import (
+    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
+    )
+from waeup.sirp.applicants.interfaces import (
+    IResultEntry, IApplicant,
+    )
+from waeup.sirp.imagestorage import ImageStorageFileRetrieval
+from waeup.sirp.testing import FunctionalLayer
+
+class FakeImageLocation(object):
+    implements(ILocation)
+    adapts(IWAeUPImageFile)
+    def __init__(self, context):
+        pass
+
+class ResultEntryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.result_entry = ResultEntry()
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IResultEntry, ResultEntry)
+        verify.verifyObject(IResultEntry, self.result_entry)
+
+    def test_resultentry(self):
+        entry = ResultEntry('Some subject', 3.7)
+        assert entry.subject == 'Some subject'
+        assert entry.score == 3.7
+
+class ApplicantTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantTest, self).setUp()
+        self.applicant = Applicant()
+        return
+
+    def tearDown(self):
+        super(ApplicantTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IApplicant, Applicant)
+        verify.verifyObject(IApplicant, self.applicant)
+        return
+
+    def test_passport_no_site(self):
+        # make sure we get a real image stored in passport attr
+        image = self.applicant.passport.file.read()
+        self.assertEqual(len(image), 2059)
+        return
+
+    def test_passport_insite(self):
+        # make sure we get a real image in passport attr when inside a site.
+        # When an applicant is created 'inside a site', its passport
+        # photograph should be put inside the images folder of the
+        # site, even if it is only a placeholder.
+        self.getRootFolder()['app'] = University()
+        app = self.getRootFolder()['app']
+        setSite(app)
+        applicant = Applicant()
+        image = self.applicant.passport.file.read()
+        self.assertEqual(len(image), 2059)
+        # The image contains a file_id instead of real image-data
+        self.assertEqual(self.applicant.passport.data,
+                         u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
+        assert u'b48a1d39bbcb32e955d9ff2dea4ed0e6' in app['images'].keys()
+        return
+
+class ApplicantFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        # Install a IFileRetrieval utility that returns WAeUPImageFiles.
+        storage = ImageStorageFileRetrieval()
+        provideUtility(storage, IFileRetrieval)
+        self.factory = ApplicantFactory()
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, ApplicantFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, Applicant)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IApplicant)
+
+
+class ApplicantTraverserTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantTraverserTest, self).setUp()
+        provideAdapter(FakeImageLocation)
+        self.applicant = Applicant()
+
+        self.request = TestRequest('')
+        return
+
+    def tearDown(self):
+        gsm = getGlobalSiteManager()
+        gsm.unregisterAdapter(FakeImageLocation)
+        super(ApplicantTraverserTest, self).tearDown()
+        return
+
+    def test_traverse_wo_passport(self):
+        # Ask for some attribute not provided
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.assertRaises(
+            NotFound,
+            traverser.publishTraverse, self.request, 'passport'
+            )
+        return
+
+    def test_traverse_wo_image_passport_jpg(self):
+        # Ask for applicant pic if we didn't provided one
+        # We get a placeholder.
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        result = traverser.publishTraverse(self.request, 'passport.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
+
+    def test_traverse_w_image_passport_jpg(self):
+        # Ask for applicant pic that's named 'passport.jpg'
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.applicant.passport = createWAeUPImageFile(
+            'nofile.jpg', StringIO('no-content'))
+        self.applicant.passport.filename = 'mypic.jpg'
+        result = traverser.publishTraverse(self.request, 'passport.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
+
+    def test_traverse_w_image_some_jpg(self):
+        # Ask for applicant pic by correct name
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.applicant.passport = WAeUPImageFile('nofile.jpg', '')
+        self.applicant.passport.filename = 'mypic.jpg'
+        result = traverser.publishTraverse(self.request, 'mypic.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicants.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_applicants.py	(revision 6631)
+++ 	(revision )
@@ -1,199 +1,0 @@
-##
-## test_applicants.py
-## Login : <uli@pu.smp.net>
-## Started on  Tue Aug 24 04:36:11 2010 Uli Fouquet
-## $Id$
-## 
-## Copyright (C) 2010 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
-##
-"""Tests for applicants and related.
-"""
-import unittest
-from StringIO import StringIO
-from hurry.file.interfaces import IFileRetrieval
-from zope.app.testing.functional import FunctionalTestCase
-from zope.component import (
-    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
-from zope.component.hooks import setSite
-from zope.component.interfaces import IFactory
-from zope.interface import verify, implements
-from zope.location.interfaces import ILocation
-from zope.publisher.base import TestRequest
-from zope.publisher.interfaces import NotFound
-from waeup.sirp.app import University
-from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile
-from waeup.sirp.image.interfaces import IWAeUPImageFile
-from waeup.sirp.applicants import (
-    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
-    )
-from waeup.sirp.applicants.interfaces import (
-    IResultEntry, IApplicant,
-    )
-from waeup.sirp.imagestorage import ImageStorageFileRetrieval
-from waeup.sirp.testing import FunctionalLayer
-
-class FakeImageLocation(object):
-    implements(ILocation)
-    adapts(IWAeUPImageFile)
-    def __init__(self, context):
-        pass
-
-class ResultEntryTest(unittest.TestCase):
-
-    def setUp(self):
-        self.result_entry = ResultEntry()
-        return
-
-    def tearDown(self):
-        pass
-
-    def test_interfaces(self):
-        verify.verifyClass(IResultEntry, ResultEntry)
-        verify.verifyObject(IResultEntry, self.result_entry)
-
-    def test_resultentry(self):
-        entry = ResultEntry('Some subject', 3.7)
-        assert entry.subject == 'Some subject'
-        assert entry.score == 3.7
-
-class ApplicantTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(ApplicantTest, self).setUp()
-        self.applicant = Applicant()
-        return
-
-    def tearDown(self):
-        super(ApplicantTest, self).tearDown()
-        return
-
-    def test_interfaces(self):
-        verify.verifyClass(IApplicant, Applicant)
-        verify.verifyObject(IApplicant, self.applicant)
-        return
-
-    def test_passport_no_site(self):
-        # make sure we get a real image stored in passport attr
-        image = self.applicant.passport.file.read()
-        self.assertEqual(len(image), 2059)
-        return
-
-    def test_passport_insite(self):
-        # make sure we get a real image in passport attr when inside a site.
-        # When an applicant is created 'inside a site', its passport
-        # photograph should be put inside the images folder of the
-        # site, even if it is only a placeholder.
-        self.getRootFolder()['app'] = University()
-        app = self.getRootFolder()['app']
-        setSite(app)
-        applicant = Applicant()
-        image = self.applicant.passport.file.read()
-        self.assertEqual(len(image), 2059)
-        # The image contains a file_id instead of real image-data
-        self.assertEqual(self.applicant.passport.data,
-                         u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
-        assert u'b48a1d39bbcb32e955d9ff2dea4ed0e6' in app['images'].keys()
-        return
-
-class ApplicantFactoryTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        # Install a IFileRetrieval utility that returns WAeUPImageFiles.
-        storage = ImageStorageFileRetrieval()
-        provideUtility(storage, IFileRetrieval)
-        self.factory = ApplicantFactory()
-
-    def tearDown(self):
-        pass
-
-    def test_interfaces(self):
-        verify.verifyClass(IFactory, ApplicantFactory)
-        verify.verifyObject(IFactory, self.factory)
-
-    def test_factory(self):
-        obj = self.factory()
-        assert isinstance(obj, Applicant)
-
-    def test_getInterfaces(self):
-        implemented_by = self.factory.getInterfaces()
-        assert implemented_by.isOrExtends(IApplicant)
-
-
-class ApplicantTraverserTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(ApplicantTraverserTest, self).setUp()
-        provideAdapter(FakeImageLocation)
-        self.applicant = Applicant()
-
-        self.request = TestRequest('')
-        return
-
-    def tearDown(self):
-        gsm = getGlobalSiteManager()
-        gsm.unregisterAdapter(FakeImageLocation)
-        super(ApplicantTraverserTest, self).tearDown()
-        return
-
-    def test_traverse_wo_passport(self):
-        # Ask for some attribute not provided
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.assertRaises(
-            NotFound,
-            traverser.publishTraverse, self.request, 'passport'
-            )
-        return
-
-    def test_traverse_wo_image_passport_jpg(self):
-        # Ask for applicant pic if we didn't provided one
-        # We get a placeholder.
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        result = traverser.publishTraverse(self.request, 'passport.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
-
-    def test_traverse_w_image_passport_jpg(self):
-        # Ask for applicant pic that's named 'passport.jpg'
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.applicant.passport = createWAeUPImageFile(
-            'nofile.jpg', StringIO('no-content'))
-        self.applicant.passport.filename = 'mypic.jpg'
-        result = traverser.publishTraverse(self.request, 'passport.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
-
-    def test_traverse_w_image_some_jpg(self):
-        # Ask for applicant pic by correct name
-        traverser = ApplicantTraverser(
-            self.applicant, self.request
-            )
-        self.applicant.passport = WAeUPImageFile('nofile.jpg', '')
-        self.applicant.passport.filename = 'mypic.jpg'
-        result = traverser.publishTraverse(self.request, 'mypic.jpg')
-        self.assertTrue(isinstance(result, FakeImageLocation))
-        return
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 6632)
@@ -36,5 +36,5 @@
 from waeup.sirp.app import University
 from waeup.sirp.applicants.container import ApplicantsContainer
-from waeup.sirp.applicants.applicants import Applicant
+from waeup.sirp.applicants.applicant import Applicant
 from waeup.sirp.university.faculty import Faculty
 from waeup.sirp.university.department import Department
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 6632)
@@ -36,5 +36,5 @@
 from waeup.sirp.app import University
 from waeup.sirp.applicants.container import ApplicantsContainer
-from waeup.sirp.applicants.applicants import Applicant
+from waeup.sirp.applicants.applicant import Applicant
 from waeup.sirp.testing import FunctionalLayer
 
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/__init__.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/__init__.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/__init__.py	(revision 6632)
@@ -2,5 +2,5 @@
 """
 # Make this a package.
-from waeup.sirp.students.students import (
+from waeup.sirp.students.student import (
     Student, StudentFactory
     )
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py	(revision 6632)
@@ -66,5 +66,5 @@
     IStudentPersonal, IStudentBase
     )
-from waeup.sirp.students.students import (
+from waeup.sirp.students.student import (
     Student,
     )
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/student.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/student.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/student.py	(revision 6632)
@@ -0,0 +1,56 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## 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
+##
+"""
+Container for the various objects owned by students.
+"""
+import grok
+from grok import index
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.students.interfaces import IStudent
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class Student(grok.Container):
+    """This is a student container for the various objects
+    owned by students.
+    """
+    grok.implements(IStudent)
+    grok.provides(IStudent)
+
+    def __init__(self):
+        super(Student, self).__init__()
+        #IWorkflowInfo(self).fireTransition('init')
+        return
+
+# Set all attributes of Student required in IStudent as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+Student = attrs_to_fields(Student)
+
+class StudentFactory(grok.GlobalUtility):
+    """A factory for students.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Student')
+    title = u"Create a new student.",
+    description = u"This factory instantiates new student instances."
+
+    def __call__(self, *args, **kw):
+        return Student()
+
+    def getInterfaces(self):
+        return implementedBy(Student)
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/students.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/students.py	(revision 6631)
+++ 	(revision )
@@ -1,56 +1,0 @@
-## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
-## 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
-##
-"""
-Container for the various objects owned by students.
-"""
-import grok
-from grok import index
-from zope.component.interfaces import IFactory
-from zope.interface import implementedBy
-from waeup.sirp.students.interfaces import IStudent
-from waeup.sirp.utils.helpers import attrs_to_fields
-
-class Student(grok.Container):
-    """This is a student container for the various objects
-    owned by students.
-    """
-    grok.implements(IStudent)
-    grok.provides(IStudent)
-
-    def __init__(self):
-        super(Student, self).__init__()
-        #IWorkflowInfo(self).fireTransition('init')
-        return
-
-# Set all attributes of Student required in IStudent as field
-# properties. Doing this, we do not have to set initial attributes
-# ourselves and as a bonus we get free validation when an attribute is
-# set.
-Student = attrs_to_fields(Student)
-
-class StudentFactory(grok.GlobalUtility):
-    """A factory for students.
-    """
-    grok.implements(IFactory)
-    grok.name(u'waeup.Student')
-    title = u"Create a new student.",
-    description = u"This factory instantiates new student instances."
-
-    def __call__(self, *args, **kw):
-        return Student()
-
-    def getInterfaces(self):
-        return implementedBy(Student)
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 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py	(revision 6632)
@@ -36,5 +36,5 @@
 from waeup.sirp.app import University
 from waeup.sirp.students.container import StudentsContainer
-from waeup.sirp.students.students import Student
+from waeup.sirp.students.student import Student
 from waeup.sirp.university.faculty import Faculty
 from waeup.sirp.university.department import Department
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_catalog.py	(revision 6631)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_catalog.py	(revision 6632)
@@ -28,5 +28,5 @@
 from waeup.sirp.interfaces import IQueryResultItem
 from waeup.sirp.testing import FunctionalLayer
-from waeup.sirp.students.students import Student
+from waeup.sirp.students.student import Student
 #from waeup.sirp.students.workflow import INITIALIZED, USED, DISABLED
 from waeup.sirp.students.catalog import StudentQueryResultItem, search
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_student.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_student.py	(revision 6632)
+++ main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_student.py	(revision 6632)
@@ -0,0 +1,75 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## 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
+##
+"""Tests for students and related.
+"""
+import unittest
+from StringIO import StringIO
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component import (
+    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
+from zope.component.hooks import setSite
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from zope.publisher.base import TestRequest
+from zope.publisher.interfaces import NotFound
+from waeup.sirp.students.student import (
+    Student, StudentFactory,
+    )
+from waeup.sirp.students.interfaces import (
+    IStudent,
+    )
+from waeup.sirp.testing import FunctionalLayer
+
+class StudentTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTest, self).setUp()
+        self.student = Student()
+        return
+
+    def tearDown(self):
+        super(StudentTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IStudent, Student)
+        verify.verifyObject(IStudent, self.student)
+        return
+
+class StudentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.factory = StudentFactory()
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, StudentFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, Student)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IStudent)
Index: main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_students.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_students.py	(revision 6631)
+++ 	(revision )
@@ -1,75 +1,0 @@
-## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
-## 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
-##
-"""Tests for students and related.
-"""
-import unittest
-from StringIO import StringIO
-from zope.app.testing.functional import FunctionalTestCase
-from zope.component import (
-    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
-from zope.component.hooks import setSite
-from zope.component.interfaces import IFactory
-from zope.interface import verify, implements
-from zope.location.interfaces import ILocation
-from zope.publisher.base import TestRequest
-from zope.publisher.interfaces import NotFound
-from waeup.sirp.students.students import (
-    Student, StudentFactory,
-    )
-from waeup.sirp.students.interfaces import (
-    IStudent,
-    )
-from waeup.sirp.testing import FunctionalLayer
-
-class StudentTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(StudentTest, self).setUp()
-        self.student = Student()
-        return
-
-    def tearDown(self):
-        super(StudentTest, self).tearDown()
-        return
-
-    def test_interfaces(self):
-        verify.verifyClass(IStudent, Student)
-        verify.verifyObject(IStudent, self.student)
-        return
-
-class StudentFactoryTest(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        self.factory = StudentFactory()
-
-    def tearDown(self):
-        pass
-
-    def test_interfaces(self):
-        verify.verifyClass(IFactory, StudentFactory)
-        verify.verifyObject(IFactory, self.factory)
-
-    def test_factory(self):
-        obj = self.factory()
-        assert isinstance(obj, Student)
-
-    def test_getInterfaces(self):
-        implemented_by = self.factory.getInterfaces()
-        assert implemented_by.isOrExtends(IStudent)
