Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/README.txt
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/README.txt	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/README.txt	(revision 5687)
@@ -0,0 +1,121 @@
+JAMB registration numbers
+*************************
+
+.. :doctest:
+
+JAMB data is stored in a JAMBDataTable.
+
+Creating JAMB Tables
+====================
+
+We can import JAMB registration data from a regular CSV file:
+
+    >>> datafile = 'jambsample.csv'
+    >>> open(datafile, 'wb').write(
+    ... '''reg_no,fst_sit_fname,lastname,firstname,middlenames,sex,date_of_birth,jamb_state,jamb_lga,course1,screening_date,screening_venue,entry_session,screening_type
+    ... 91100546DD,ISUOSUO MOSES ODOMERO,ISUOSUO,MOSES,ODOMERO,M,25/5/1982,DEL,ISO-S,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde
+    ... 91111834CC,DURUIHEOMA AUGUSTINA ADANNA,DURUIHEOMA,AUGUSTINA,ADANNA,F,15/4/1986,IMO,MBAIT,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde
+    ... 91109351AC,ARISERE EBIKEBUNA COMFORT,ARISERE,EBIKEBUNA,COMFORT,F,6/1/1984,EDO,OV-SW,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde
+    ... ''')
+
+JAMB CSV tables have to provide the following header fields in the
+first line:
+
+    >>> from pprint import pprint
+    >>> from waeup.sirp.jambtables.jambtables import JAMB_DATA_HEADERS
+    >>> JAMB_DATA_HEADERS
+    ['firstname', 'lastname', 'middlenames', 'screening_type',
+     'screening_venue', 'reg_no', 'sex', 'course1', 'jamb_state',
+     'screening_date', 'jamb_lga', 'fst_sit_fname', 'date_of_birth']
+
+Now we create a JAMBDataTable and import the data:
+
+    >>> from waeup.sirp.jambtables import JAMBDataTable
+    >>> table = JAMBDataTable()
+    >>> len(list(table))
+    0
+
+    >>> table.importFromCSV(datafile)
+    >>> len(list(table))
+    3
+
+Accessing JAMB Data
+===================
+
+Getting Entries
+---------------
+
+We can get the entries from the table as an iterator:
+
+    >>> from pprint import pprint
+    >>> entries = [x for x in table]
+    >>> pprint(entries)
+    [{'course1': u'BSCPOL',
+      'date_of_birth': datetime.date(1982, 5, 25),
+      'entry_session': '9',
+      'firstname': u'MOSES',
+      'fst_sit_fname': u'ISUOSUO MOSES ODOMERO',
+      'jamb_lga': u'ISO-S',
+      'jamb_state': u'DEL',
+      'lastname': u'ISUOSUO',
+      'middlenames': u'ODOMERO',
+      'reg_no': u'91100546DD',
+      'screening_date': u'2009/09/25 09:00:00 GMT+1',
+      'screening_type': u'pde',
+      'screening_venue': u'REPRINT SLIP AS FROM WED 23/09/2009',
+      'sex': u'M'},
+     {...},
+     {...}]
+
+Also a :meth:`keys` and :meth:`items` method are available. They both
+return an iterator.
+
+The :meth:`keys` method in action:
+
+     >>> pprint(list(table.keys()))
+     [u'91100546DD', u'91111834CC', u'91109351AC']
+
+The :meth:`items` method in action:
+
+     >>> pprint(list(table.items()))
+     [(u'91100546DD',
+      {'course1': u'BSCPOL',
+       'date_of_birth': datetime.date(1982, 5, 25),
+       'entry_session': '9',
+       'firstname': u'MOSES',
+       'fst_sit_fname': u'ISUOSUO MOSES ODOMERO',
+       'jamb_lga': u'ISO-S',
+       'jamb_state': u'DEL',
+       'lastname': u'ISUOSUO',
+       'middlenames': u'ODOMERO',
+       'reg_no': u'91100546DD',
+       'screening_date': u'2009/09/25 09:00:00 GMT+1',
+       'screening_type': u'pde',
+       'screening_venue': u'REPRINT SLIP AS FROM WED 23/09/2009',
+       'sex': u'M'}),
+     (u'91111834CC',
+      {...}),
+     (u'91109351AC',
+      {...})]
+
+A ``values()``-like behaviour cen be archieved by simply using the
+iterator.
+
+Getting JAMB Table Meta Data
+----------------------------
+
+We can also get a datetime stamp, that tells, when the table was
+imported. This could help with maintaining old tables:
+
+    >>> table.import_datetime
+    datetime.datetime(..., ..., ..., ..., ..., ..., ...)
+    
+
+We can delete the table:
+
+    >>> del JAMBDataTable
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink(datafile)
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/__init__.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/__init__.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/__init__.py	(revision 5687)
@@ -0,0 +1,17 @@
+# Make this a package.
+from waeup.sirp.jambtables.applicants import (
+    ResultEntry, Applicant, ApplicantFactory, ApplicantContainer,
+    )
+from waeup.sirp.jambtables.batching import ApplicantImporter
+from waeup.sirp.jambtables.jambtables import JAMBDataTable, JAMB_DATA_HEADERS
+
+
+__all__ = (
+    Applicant,
+    ApplicantContainer,
+    ApplicantFactory,
+    ApplicantImporter,
+    JAMBDataTable,
+    JAMB_DATA_HEADERS,
+    ResultEntry,
+    )
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/api.txt
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/api.txt	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/api.txt	(revision 5687)
@@ -0,0 +1,13 @@
+:mod:`waeup.sirp.jambtables` -- JAMBTables for WAeUP
+####################################################
+
+.. automodule:: waeup.sirp.jambtables
+   :members:
+
+.. automodule:: waeup.sirp.jambtables.jambtables
+   :members:
+
+Examples:
+*********
+
+.. include:: README.txt
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/applicants.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/applicants.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/applicants.py	(revision 5687)
@@ -0,0 +1,140 @@
+##
+## applicants.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jul 16 11:46:55 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
+##
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.schema.fieldproperty import FieldProperty
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+from waeup.sirp.jambtables.interfaces import (
+    IResultEntry, IApplicant, IApplicantContainer, IApplicantPDEEditData
+    )
+from waeup.sirp.widgets.passportwidget import PassportImage
+
+class ResultEntry(grok.Context):
+    grok.implements(IResultEntry)
+
+    def __init__(self, subject=None, score=None):
+        self.subject = subject
+        self.score = score
+    
+class Applicant(grok.Context):
+    # class Applicant(grok.Model):
+    grok.implements(IApplicant, IApplicantPDEEditData)
+    grok.provides(IApplicant)
+
+    fst_sit_results = FieldProperty(IApplicant['fst_sit_results'])
+    
+    def __init__(self):
+        self.reg_no = None
+        self.access_code = None
+        self.serial = None
+        self.course1 = None
+        self.course2 = None
+        self.course3 = None
+        self.firstname = None
+        self.middlenames = None
+        self.lastname = None
+        self.jamb_age = None
+        self.date_of_birth = None
+        self.jamb_state = None
+        self.jamb_lga = None
+        self.lga = None
+        self.sex = None
+        self.email = None
+        self.phone = None
+        self.passport = False
+        self.aos = None
+        self.subj1 = None
+        self.subj2 = None
+        self.subj3 = None
+        self.hq_matric_no = None
+        self.hq_type = None
+        self.hq_grade = None
+        self.hq_school = None
+        self.hq_session = None
+        self.hq_disc = None
+        self.fst_sit_fname = None
+        self.fst_sit_no = None
+        self.fst_sit_date = None
+        self.fst_sit_type = None
+        #self.fst_sit_results = []
+        self.fst_sit_results = None
+        #self.fst_sit_results = ResultEntry()
+        self.scd_sit_fname = None
+        self.scd_sit_no = None
+        self.scd_sit_date = None
+        self.scd_sit_type = None
+        self.scd_sit_results = None
+        self.eng_score = None
+        self.subj1score = None
+        self.subj2score = None
+        self.subj3score = None
+        self.application_date = None
+        self.status = None
+        self.screening_date = None
+        self.screening_type = None
+        self.screening_score = None
+        self.screening_venue = None
+        self.total_score = None
+        self.course_admitted = None
+        self.department = None
+        self.faculty = None
+        self.entry_session = None
+        self.notice = None
+        self.student_id = None
+        self.import_record_no = None
+        self.imported_by = None
+        self.import_date = None
+        self.import_from = None
+
+class ApplicantTraverser(grok.Traverser):
+    grok.context(IApplicant)
+    def traverse(self, name):
+        #passport_filename = self.context.passport.filename
+        passport_filename = getattr(self.context.passport, 'filename', None)
+        print "FILENAME: ", passport_filename
+        if name == passport_filename:
+            print "USE FILE", self.context.passport
+            return self.context.passport
+        if name == 'passport.jpg':
+            if self.context.passport is not None:
+                print "PP: ", self.context.passport
+                return self.context.passport
+            return PassportImage(None)
+
+class ApplicantFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    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)
+
+     
+class ApplicantContainer(grok.Container):
+    grok.implements(IApplicantContainer)
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/authentication.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/authentication.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/authentication.py	(revision 5687)
@@ -0,0 +1,260 @@
+##
+## authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jul 27 14:26:35 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
+##
+"""Special authentication for applicants.
+
+   XXX: This is work in progress, experimental code! Don't do that at home!
+"""
+import grok
+from zope.event import notify
+from zope.pluggableauth.factories import Principal
+from zope.pluggableauth.interfaces import (
+    ICredentialsPlugin, IAuthenticatorPlugin, IPrincipalInfo,
+    IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
+from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
+from zope.publisher.interfaces import IRequest
+from zope.publisher.interfaces.http import IHTTPRequest
+from zope.session.interfaces import ISession
+from waeup.sirp.authentication import Account, PrincipalInfo
+from waeup.sirp.jambtables.interfaces import (
+    IApplicantPrincipalInfo, IApplicantPrincipal, IApplicantSessionCredentials,
+    IJAMBApplicantSessionCredentials)
+from waeup.sirp.jambtables.util import get_applicant_data, application_exists
+from waeup.sirp.accesscodes import get_access_code
+
+class PortalUser(grok.Role):
+    """A role for applicants.
+    """
+    grok.name('waeup.Applicant')
+    grok.permissions('waeup.Public')
+
+class ApplicantPrincipalInfo(object):
+    """Infos about an applicant principal.
+    """
+    grok.implements(IApplicantPrincipalInfo)
+
+    def __init__(self, access_code, jamb_reg_no=None):
+        self.id = principal_id(access_code, jamb_reg_no)
+        self.title = u'Applicant'
+        self.description = u'An Applicant'
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+        self.reg_no = jamb_reg_no
+        self.access_code = access_code
+
+class ApplicantPrincipal(Principal):
+    """An applicant principal.
+
+    Applicant principals provide an extra `access_code` and `reg_no`
+    attribute extending ordinary principals.
+    """
+
+    grok.implements(IApplicantPrincipal)
+
+    def __init__(self, access_code, reg_no, prefix=None):
+        self.id = principal_id(access_code, reg_no)
+        if prefix is not None:
+            self.id = '%s.%s' % (prefix, self.id)
+        self.title = u'Applicant'
+        self.description = u'An applicant'
+        self.groups = []
+        self.reg_no = reg_no
+        self.access_code = access_code
+
+    def __repr__(self):
+        return 'ApplicantPrincipal(%r)' % self.id
+
+class AuthenticatedApplicantPrincipalFactory(grok.MultiAdapter):
+    """Creates 'authenticated' applicant principals.
+
+    Adapts (principal info, request) to an ApplicantPrincipal instance.
+
+    This adapter is used by the standard PAU to transform
+    PrincipalInfos into Principal instances.
+    """
+    grok.adapts(IApplicantPrincipalInfo, IRequest)
+    grok.implements(IAuthenticatedPrincipalFactory)
+
+    def __init__(self, info, request):
+        self.info = info
+        self.request = request
+
+    def __call__(self, authentication):
+        principal = ApplicantPrincipal(
+            self.info.access_code,
+            self.info.reg_no,
+            authentication.prefix,
+            )
+        notify(
+            AuthenticatedPrincipalCreated(
+                authentication, principal, self.info, self.request))
+        return principal
+
+
+#
+# Credentials plugins and related....
+#
+
+class ApplicantCredentials(object):
+    """Credentials class for ordinary applicants.
+    """
+    grok.implements(IApplicantSessionCredentials)
+
+    def __init__(self, access_code):
+        self.access_code = access_code
+
+    def getAccessCode(self):
+        """Get the access code.
+        """
+        return self.access_code
+
+    def getLogin(self):
+        """Stay compatible with non-applicant authenticators.
+        """
+        return None
+
+    def getPassword(self):
+        """Stay compatible with non-applicant authenticators.
+        """
+        return None
+    
+class JAMBApplicantCredentials(ApplicantCredentials):
+    """Credentials class for JAMB-screened applicants.
+    """
+    grok.implements(IJAMBApplicantSessionCredentials)
+
+    def __init__(self, access_code, jamb_reg_no):
+        self.access_code = access_code
+        self.jamb_reg_no = jamb_reg_no
+
+    def getJAMBRegNo(self):
+        """Get the JAMB registration no.
+        """
+        return self.jamb_reg_no
+
+class WAeUPApplicantCredentialsPlugin(grok.GlobalUtility,
+                                      SessionCredentialsPlugin):
+    """A credentials plugin that scans requests for applicant credentials.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('applicant_credentials')
+
+    loginpagename = 'login'
+    accesscode_prefix_field = 'form.prefix'
+    accesscode_series_field = 'form.ac_series'
+    accesscode_number_field = 'form.ac_number'
+    jamb_reg_no_field = 'form.jamb_reg_no'
+
+    def extractCredentials(self, request):
+        """Extracts credentials from a session if they exist.
+        """
+        if not IHTTPRequest.providedBy(request):
+            return None
+        session = ISession(request)
+        sessionData = session.get(
+            'zope.pluggableauth.browserplugins')
+        access_code_prefix = request.get(self.accesscode_prefix_field, None)
+        access_code_series = request.get(self.accesscode_series_field, None)
+        access_code_no = request.get(self.accesscode_number_field, None)
+        jamb_reg_no = request.get(self.jamb_reg_no_field, None)
+        access_code = '%s-%s-%s' % (
+            access_code_prefix, access_code_series, access_code_no)
+        if None in [access_code_prefix, access_code_series, access_code_no]:
+            access_code = None
+        credentials = None
+
+        if access_code and jamb_reg_no:
+            credentials = JAMBApplicantCredentials(
+                access_code, jamb_reg_no)
+        elif access_code:
+            credentials = ApplicantCredentials(access_code)
+        elif not sessionData:
+            return None
+        sessionData = session[
+            'zope.pluggableauth.browserplugins']
+        if credentials:
+            sessionData['credentials'] = credentials
+        else:
+            credentials = sessionData.get('credentials', None)
+        if not credentials:
+            return None
+        if not IApplicantSessionCredentials.providedBy(credentials):
+            # If credentials were stored in session from another
+            # credentials plugin then we cannot make assumptions about
+            # its structure.
+            return None
+        if not IJAMBApplicantSessionCredentials.providedBy(credentials):
+            # Entered credentials are ordinary applicant credentials,
+            # not JAMB-screened applicant credentials
+            return {'accesscode': credentials.getAccessCode()}
+        return {'accesscode': credentials.getAccessCode(),
+                'jambregno': credentials.getJAMBRegNo()}
+
+
+
+class ApplicantsAuthenticatorPlugin(grok.GlobalUtility):
+    """Authenticate applicants.
+
+    XXX: This plugin currently accepts any input and authenticates the
+    user as applicant.
+    """
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('applicants')
+
+    def authenticateCredentials(self, credentials):
+        if not isinstance(credentials, dict):
+            return None
+        accesscode = credentials.get('accesscode', None)
+        jambregno = credentials.get('jambregno', None)
+        if accesscode is None:
+            return None
+        applicant_data, ac = get_applicant_data(jambregno, accesscode)
+        appl_ac = getattr(applicant_data, 'access_code', None)
+        #print "AUTH", accesscode, jambregno
+        if ac is None:
+            return None
+        if jambregno is not None and applicant_data is None:
+            return None
+        if ac.invalidation_date is not None and appl_ac != ac.representation:
+            return None
+        if appl_ac is not None and appl_ac != ac.representation:
+            return None
+        return ApplicantPrincipalInfo(accesscode, jambregno)
+
+    def principalInfo(self, id):
+        """Returns an IPrincipalInfo object for the specified principal id.
+
+        This method is used by the stadard PAU to lookup for instance
+        groups. If a principal belongs to a group, the group is looked
+        up by the id.  Currently we always return ``None``,
+        indicating, that the principal could not be found. This also
+        means, that is has no effect if applicant users belong to a
+        certain group. They can not gain extra-permissions this way.
+        """
+        return None
+
+def principal_id(access_code, jamb_reg_no=None):
+    """Get a principal ID for applicants.
+
+    We need unique principal ids for appliants. As access codes must
+    be unique we simply return them.
+    """
+    return access_code
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/batching.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/batching.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/batching.py	(revision 5687)
@@ -0,0 +1,70 @@
+##
+## batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jul 23 15:42:46 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
+##
+"""Batch processing for applicants.
+"""
+import grok
+from zope.interface import Interface
+from waeup.sirp.interfaces import IBatchProcessor
+from waeup.sirp.utils.batching import BatchProcessor
+from waeup.sirp.jambtables.interfaces import IApplicantPDEImportData
+
+class ApplicantImporter(BatchProcessor):
+    """An importer for applicants.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applcation importer'
+    grok.name(util_name)
+
+    name = u'Application Importer'
+    mode = u'create'
+    iface = IApplicantPDEImportData
+
+    location_fields = ['reg_no',]
+    factory_name = 'waeup.Applicant'
+
+    def parentsExist(self, row, site):
+        return 'applications' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['reg_no'] in site['applications'].keys()
+
+    def getParent(self, row, site):
+        return site['applications']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['reg_no'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        reg_no = row['reg_no']
+        parent[reg_no] = obj
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['reg_no']]
+        return
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser.py	(revision 5687)
@@ -0,0 +1,223 @@
+##
+## browser.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 27 11:03:10 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
+##
+"""UI components for JAMB tables.
+"""
+import grok
+
+from zope.formlib.widgets import FileWidget
+from waeup.sirp.browser import (
+    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage,
+    WAeUPDisplayFormPage, NullValidator)
+from waeup.sirp.browser.pages import LoginPage
+from waeup.sirp.interfaces import IWAeUPObject
+from waeup.sirp.jambtables import JAMBDataTable
+from waeup.sirp.jambtables.util import get_applicant_data
+from waeup.sirp.jambtables.interfaces import (
+    IApplicant, IApplicantContainer, IApplicantPrincipal, IApplicantPDEEditData)
+from waeup.sirp.widgets.passportwidget import (
+    PassportWidget, PassportDisplayWidget
+    )
+#from zope.formlib.objectwidget import ObjectWidget
+from zope.formlib.sequencewidget import ListSequenceWidget, SequenceDisplayWidget
+from zope.formlib.widget import CustomWidgetFactory
+from waeup.sirp.jambtables.applicants import ResultEntry
+from waeup.sirp.widgets.objectwidget import (
+    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
+from waeup.sirp.widgets.multilistwidget import (
+    MultiListWidget, MultiListDisplayWidget)
+from waeup.sirp.image.browser.widget import (
+    ThumbnailWidget, EncodingImageFileWidget,
+    )
+
+results_widget = CustomWidgetFactory(
+    WAeUPObjectWidget, ResultEntry)
+
+results_display_widget = CustomWidgetFactory(
+    WAeUPObjectDisplayWidget, ResultEntry)
+
+#list_results_widget = CustomWidgetFactory(
+#    ListSequenceWidget, subwidget=results_widget)
+
+list_results_widget = CustomWidgetFactory(
+    MultiListWidget, subwidget=results_widget)
+
+list_results_display_widget = CustomWidgetFactory(
+    MultiListDisplayWidget, subwidget=results_display_widget)
+
+class ApplicationsPage(WAeUPPage):
+    grok.context(IApplicantContainer)
+    grok.name('index')
+    title = 'Applications'
+    pnav = 1
+    
+    def getApplications(self):
+        """Get a list of all stored applications.
+        """
+        for key, val in self.context.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key))
+
+class AddApplicant(WAeUPAddFormPage):
+    grok.context(IApplicantContainer)
+    grok.name('add')
+    form_fields = grok.AutoFields(IApplicant)
+    form_fields['fst_sit_results'].custom_widget = list_results_widget
+    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    label = 'Add Applicant'
+    title = 'Add Applicant'
+    pnav = 1
+
+    @grok.action('Add applicant')
+    def addApplicant(self, **data):
+        from waeup.sirp.jambtables.applicants import Applicant
+        applicant = Applicant()
+        self.applyData(applicant, **data)
+        # XXX: temporarily disabled.
+        #self.context[applicant.reg_no] = applicant
+        try:
+            self.context[applicant.access_code] = applicant
+        except KeyError:
+            self.flash('The given access code is already in use!')
+            return
+        self.redirect(self.url(self.context))
+
+class DisplayApplicant(WAeUPDisplayFormPage):
+    grok.context(IApplicant)
+    grok.name('index')
+    form_fields = grok.AutoFields(IApplicant)
+    form_fields['fst_sit_results'].custom_widget = list_results_display_widget
+    #form_fields['passport'].custom_widget = PassportDisplayWidget
+    form_fields['passport'].custom_widget = ThumbnailWidget
+    label = 'Applicant'
+    title = 'Applicant'
+    pnav = 1
+
+class EditApplicant(WAeUPEditFormPage):
+    grok.context(IApplicant)
+    grok.name('edit')
+    form_fields = grok.AutoFields(IApplicantPDEEditData)
+    #form_fields['passport'].custom_widget = FileWidget
+    #form_fields['passport'].custom_widget = PassportWidget
+    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    grok.template('form_edit_pde')
+
+    def update(self):
+        super(EditApplicant, self).update()
+        print self.request.form
+        return
+    
+    @property
+    def label(self):
+        # XXX: Use current/upcoming session
+        return 'Apply for Post UDE Screening Test (2009/2010)'
+    title = 'Edit Application'
+    pnav = 1
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.context._p_changed = True
+        return
+
+    @grok.action('Final Submit')
+    def finalsubmit(self, **data):
+        self.applyData(self.context, **data)
+        self.context._p_changed = True
+        # XXX: Lock the form for editing...
+        return
+
+class Login_PDE(LoginPage):
+    grok.context(IWAeUPObject)
+    grok.name('login_pde')
+
+    title = 'PDE Login'
+    pnav = 1
+
+    def update(self):
+        """Validate credentials and redirect or show error.
+
+        The real validation is done by an pluggable authentication
+        utility (PAU). Here we only check, whether correct credentials
+        were entered by looking up the principal status of the
+        request: If the user authenticated successfully, we get an
+        applicant principal. Otherwise we get the unauthenticated
+        principal.
+        """
+        formfields = ['form.ac_number', 'form.jamb_reg_no',
+                      'form.ac_series', 'form.prefix',]
+
+        self.reg_no = self.request.form.get('form.jamb_reg_no', '')
+        self.ac_series = self.request.form.get('form.ac_series', '')
+        self.ac_number = self.request.form.get('form.ac_number', '')
+
+        for required_field in formfields:
+            if required_field not in self.request.form.keys():
+                return
+        self.reg_no = self.request.form['form.jamb_reg_no']
+        self.ac_series = self.request.form['form.ac_series']
+        self.ac_number = self.request.form['form.ac_number']
+        principal = self.request.principal
+        if not IApplicantPrincipal.providedBy(principal):
+            self.flash('You entered invalid credentials')
+            return
+        if hasattr(principal, 'reg_no'):
+            if not principal.reg_no is None:
+                
+                site = grok.getSite()
+                applications = site['applications']
+                application = applications[principal.reg_no]
+                self.redirect(self.url(application, '@@edit'))
+        return
+
+    def getCurrentSession(self):
+        """Get the current session.
+
+        XXX: This should be computed or retrieved from elsewhere.
+
+        `session` here means an academic session, not a browser
+        session.
+        """
+        return u'2010/2011'
+
+    def getDeadline(self):
+        """Get application submission deadline.
+
+        XXX: This should be computed or retrieved from elsewhere.
+        """
+        return u"""Application submission deadline is at Midnight on Friday,
+                   01. October 2010. No application will be treated
+                   after the deadline."""
+
+    def getApplicantData(self, reg_no, ac):
+        """Validate credentials and return applicant data.
+
+        Returns tuple ``(<APPLICANT_ENTRY>, <ACCESSCODE>) on
+        successful validation and ``None`` else.
+
+        We expect a JAMB registration number and an access code in
+        format ``PUDE-XXX-XXXXXXXXXX``.
+
+        See
+        :func:`waeup.sirp.jambtables.util.get_applicant_data`
+        for details.
+        """
+        return get_applicant_data(reg_no, ac)
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/applicationspage.pt
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/applicationspage.pt	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/applicationspage.pt	(revision 5687)
@@ -0,0 +1,8 @@
+<div>
+  <div tal:repeat="entry view/getApplications">
+    <a href=""
+       tal:attributes="href entry/url"
+       tal:content="entry/name"
+       >Some Entry</a>
+  </div>
+</div>
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/form_edit_pde.pt
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/form_edit_pde.pt	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/form_edit_pde.pt	(revision 5687)
@@ -0,0 +1,89 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+
+    <div i18n:translate="" tal:content="view/status">
+      Form status summary
+    </div>
+
+    <ul class="errors" tal:condition="view/errors">
+      <li tal:repeat="error view/error_views">
+         <span tal:replace="structure error">Error Type</span>
+      </li>
+    </ul>
+  </div>
+
+  <table class="form-fields">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label" tal:define="hint widget/hint">
+            <label tal:condition="python:hint"
+                   tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required"
+              >*</span><span i18n:translate=""
+                             tal:content="widget/label">label</span>
+            </label>
+            <label tal:condition="python:not hint"
+                   tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required"
+              >*</span><span i18n:translate=""
+                             tal:content="widget/label">label</span>
+            </label>
+          </td>
+          <td class="field">
+            <div class="widget" tal:content="structure widget">
+              <input type="text" />
+            </div>
+            <div class="error" tal:condition="widget/error">
+              <span tal:replace="structure widget/error">error</span>
+            </div>
+          </td>
+        </tr>
+      </tal:block>
+      <tr>
+	<td colspan="2">
+	  <input type="checkbox" name="confirm" />
+	  <label for="confirm"></label>
+<span>
+	    I confirm that the Passport Photograph uploaded on this form
+            is a true picture of me.
+	  </span>
+	</td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div id="actionsView">
+    <span class="actionButtons" tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </span>
+  </div>
+</form>
+
+<br />
+
+<div class="block">
+  <h3>Instructions</h3>
+  <ul class="list">
+    <li>
+      Enter your JAMB Registration Number and the Application PIN.
+    </li>
+    <li>
+      Fill the form and upload your Passport Photograph with red background
+      in jpg format.
+    </li>
+    <li>
+      Save and preview picture.  * Press the 'Final Submit' button.
+    </li>
+  </ul>
+</div>
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/login_pde.pt
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/login_pde.pt	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/browser_templates/login_pde.pt	(revision 5687)
@@ -0,0 +1,56 @@
+<h3>
+  Apply for Post UDE Screening Test 
+  (<span tal:replace="view/getCurrentSession">2010/2011</span>)
+</h3>
+<div>
+  This Login is for applicants only, that went through PDE screening!
+</div>
+<form method="POST">
+  <table>
+    <tbody>
+      <tr>
+	<td width="30%"><label for="reg_no">JAMB Registration Number</label></td>
+	<td>
+	  <input type="text" name="form.jamb_reg_no"
+		 value="" tal:attributes="value view/reg_no" /></td>
+      </tr>
+      <tr>
+	<input type="hidden" name="form.prefix" value="PUDE" />
+	<td><label for="form.ac_series">Access Code</label></td>
+	<td>
+	  PUDE-<input name="form.ac_series" type="text" size="3" maxlength="3"
+	              value="" tal:attributes="value view/ac_series"
+	  />-<input name="form.ac_number" type="text" size="10" maxlength="10"
+	            value="" tal:attributes="value view/ac_number" />
+	</td>
+      </tr>
+      <tr>
+	<td colspan="2">
+	  <input type="submit" name="show" value="Show Application Record" />
+	</td>
+      </tr>
+    </tbody>
+  </table>
+</form>
+
+<div>
+  Instructions:
+  <ul class="list">
+    <li>
+      Enter your JAMB Registration Number and the Application PIN.
+    </li>
+    <li>
+      Fill the form and upload your Passport Photograph with red
+      background in jpg format.
+    </li>
+    <li>
+      Save and preview picture.
+    </li>
+    <li>
+      Press the 'Final Submit' button.
+    </li>
+  </ul>
+</div>
+<div class="error" tal:content="view/getDeadline">
+  The deadline is NOW
+</div>
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/interfaces.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/interfaces.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/interfaces.py	(revision 5687)
@@ -0,0 +1,764 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 27 11:06:23 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
+##
+"""Interfaces for JAMB data tables and related components.
+"""
+import os
+import waeup.sirp.browser
+from hurry.file import HurryFile
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope import schema
+from zope.app.file.interfaces import IImage
+from zope.interface import Interface, Attribute
+from zope.pluggableauth.interfaces import IPrincipalInfo
+from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
+from waeup.sirp.interfaces import IWAeUPObject
+from waeup.sirp.image.schema import ImageFile
+
+IMAGE_PATH = os.path.join(
+    os.path.dirname(waeup.sirp.browser.__file__),
+    'static'
+    )
+DEFAULT_PASSPORT_IMAGE_MALE = HurryFile(
+    'passport.jpg',
+    open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg')).read(),
+    )
+DEFAULT_PASSPORT_IMAGE_FEMALE = HurryFile(
+    'passport.jpg',
+    open(os.path.join(IMAGE_PATH, 'placeholder_f.jpg')).read(),
+    )
+
+class GenderSource(BasicSourceFactory):
+    """A gender source delivers basically a mapping
+       ``{'m': 'male', 'f': 'female'}``
+
+       Using a source, we make sure that the tokens (which are
+       stored/expected for instance from CSV files) are something one
+       can expect and not cryptic IntIDs.
+    """
+    def getValues(self):
+        return ['m', 'f']
+
+    def getToken(self, value):
+        return value[0].lower()
+        
+    def getTitle(self, value):
+        if value == 'm':
+            return 'male'
+        if value == 'f':
+            return 'female'
+
+class IJAMBDataTable(IWAeUPObject):
+    """A table containing JAMB data.
+    """
+    import_datetime = schema.Datetime(
+        title = u'Datetime of import of contained data.',
+        required = False,
+        )
+
+    importer_username = schema.TextLine(
+        title = u'Name of user who initiated import.',
+        required = False,
+        )
+
+    def __iter__():
+        """An iterator over all data elements.
+        """
+
+    def keys():
+        """Get iterator over all registration numbers of data.
+        """
+
+    def items():
+        """Get iterator over tuples of registration numbers and datasets.
+        """
+
+    def clear():
+        """Clear all data contained.
+
+        This will also erase any import data.
+        """
+
+    def importFromCSV(filepath, username=None):
+        """Import data from filepath.
+
+        `filepath` - the path to the CSV file to import data from.
+
+        `username` - the (optional) username of the importing person.
+        """
+        
+class IResultEntry(IWAeUPObject):
+    subject = schema.TextLine(
+        title = u'Subject',
+        description = u'The subject',
+        required=False,
+        )
+    score = schema.TextLine(
+        title = u'Score',
+        description = u'The score',
+        required=False,
+        )
+
+class IApplicantBaseData(IWAeUPObject):
+    """The data for an applicant.
+
+    This is a base interface with no field (except ``reg_no``)
+    required. For use with importers, forms, etc., please use one of
+    the derived interfaces below, which set more fields to required
+    state, depending on use-case.
+    """
+    reg_no = schema.TextLine(
+        title = u'JAMB Registration Number',
+        )
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        required = False,
+        )
+    serial = schema.TextLine(
+        title = u'Serial Number',
+        required = False,
+        )
+    course1 = schema.TextLine(
+        title = u'1st Choice Course of Study',
+        required = False,
+        )
+    course2 = schema.TextLine(
+        title = u'2nd Choice Course of Study',
+        required = False,
+        )
+    course3 = schema.TextLine(
+        title = u'3rd Choice Course of Study',
+        required = False,
+        )
+    firstname = schema.TextLine(
+        title = u'First Name',
+        required = False,
+        )
+    middlenames = schema.TextLine(
+        title = u'Middle Names',
+        required = False,
+        )
+    lastname = schema.TextLine(
+        title = u'Surname/Full Name',
+        required = False,
+        )
+    jamb_age = schema.Int(
+        title = u'Age (provided by JAMB)',
+        required = False,
+        )
+    date_of_birth = schema.Date(
+        title = u'Date of Birth',
+        required = False,
+        )
+    jamb_state = schema.TextLine(
+        title = u'State (provided by JAMB)',
+        required = False,
+        )
+    jamb_lga = schema.TextLine(
+        title = u'LGA (provided by JAMB)',
+        required = False,
+        )
+    lga = schema.TextLine(
+        # XXX: should be choice
+        title = u'State/LGA (confirmed by applicant)',
+        required = False,
+        )
+    sex = schema.Choice(
+        title = u'Sex',
+        source = GenderSource(),
+        default = u'm',
+        required = False,
+        )
+    email = schema.TextLine(
+        title = u'Email',
+        required = False,
+        )
+    phone = schema.TextLine(
+        title = u'Phone',
+        required = False,
+        )
+    #passport = schema.Bool(
+    #    title = u'Passport Photograph',
+    #    default = True,
+    #    required = False,
+    #    )
+    passport = ImageFile(
+        title = u'Passport Photograph',
+        default = DEFAULT_PASSPORT_IMAGE_MALE,
+        required = True,
+        )
+    aos = schema.TextLine(
+        # XXX: should be choice
+        title = u'Area of Specialisation',
+        required = False,
+        )
+    subj1 = schema.TextLine(
+        # XXX: should be choice
+        title = u'1st Choice of Study',
+        required = False,
+        )
+    subj2 = schema.TextLine(
+        # XXX: should be choice
+        title = u'2nd Choice of Study',
+        required = False,
+        )
+    subj3 = schema.TextLine(
+        # XXX: should be choice
+        title = u'3rd Choice of Study',
+        required = False,
+        )
+    #
+    # Higher Educational Data
+    #
+    hq_matric_no = schema.TextLine(
+        title = u'Former Matric Number',
+        required = False,
+        )
+    hq_type = schema.TextLine(
+        title = u'Higher Qualification',
+        required = False,
+        )
+    hq_grade = schema.TextLine(
+        title = u'Higher Qualification Grade',
+        required = False,
+        )
+    hq_school = schema.TextLine(
+        title = u'School Attended',
+        required = False,
+        )
+    hq_session = schema.TextLine(
+        title = u'Session Obtained',
+        required = False,
+        )
+    hq_disc = schema.TextLine(
+        title = u'Discipline',
+        required = False,
+        )
+    #
+    # First sitting data
+    #
+    fst_sit_fname = schema.TextLine(
+        title = u'Full Name',
+        required = False,
+        )
+    fst_sit_no = schema.TextLine(
+        title = u'Exam Number',
+        required = False,
+        )
+    fst_sit_date = schema.Date(
+        title = u'Exam Date (dd/mm/yyyy)',
+        required = False,
+        )
+    fst_sit_type = schema.TextLine(
+        # XXX: Should be choice
+        title = u'Exam Type',
+        required = False,
+        )
+    fst_sit_results = schema.List(
+        title = u'Results',
+        required = False,
+        value_type = schema.Object(
+            title = u'Entries',
+            schema = IResultEntry,
+            required = False,
+            )
+        )
+    scd_sit_fname = schema.TextLine(
+        title = u'Full Name',
+        required = False,
+        )
+    scd_sit_no = schema.TextLine(
+        title = u'Exam Number',
+        required = False,
+        )
+    scd_sit_date = schema.Date(
+        title = u'Exam Date (dd/mm/yyyy)',
+        required = False,
+        )
+    scd_sit_type = schema.TextLine(
+        # XXX: Should be choice
+        title = u'Exam Type',
+        required = False,
+        )
+    scd_sit_results = schema.TextLine(
+        # XXX: Should be nested list of choices
+        title = u'Results',
+        required = False,
+        )
+    #
+    # JAMB scores
+    #
+    eng_score = schema.TextLine(
+        title = u"'English' score",
+        required = False,
+        )
+    subj1score = schema.TextLine(
+        title = u'1st Choice of Study Score',
+        required = False,
+        )
+    subj2score = schema.TextLine(
+        title = u'2nd Choice of Study Score',
+        required = False,
+        )
+    subj3score = schema.TextLine(
+        title = u'3rd Choice of Study Score',
+        required = False,
+        )
+    # XXX: Total score???
+    
+    #
+    # Application Data
+    #
+    application_date = schema.Date(
+        title = u'Application Date',
+        required = False,
+        )
+    status = schema.TextLine(
+        # XXX: should be 'status' type
+        title = u'Application Status',
+        required = False,
+        )
+    screening_date = schema.Date(
+        title = u'Screening Date',
+        required = False,
+        )
+    screening_type = schema.TextLine(
+        # XXX: schould be choice
+        title = u'Screening Type',
+        required = False,
+        )
+    screening_score = schema.TextLine(
+        title = u'Screening Score',
+        required = False,
+        )
+    screening_venue = schema.TextLine(
+        title = u'Screening Venue',
+        required = False,
+        )
+    total_score = schema.TextLine(
+        title = u'Total Score',
+        required = False,
+        )
+    course_admitted = schema.TextLine(
+        # XXX: should be choice
+        title = u'Admitted Course of Study',
+        required = False,
+        )
+    department = schema.TextLine(
+        # XXX: if we have a course, dept. is not necessary
+        title = u'Department',
+        required = False,
+        )
+    faculty = schema.TextLine(
+        # XXX: if we have a course, faculty is not necessary
+        title = u'Faculty',
+        required = False,
+        )
+    entry_session = schema.TextLine(
+        # XXX: should be choice, should have sensible default: upcoming session
+        title = u'Entry Session',
+        required = False,
+        )
+    notice = schema.Text(
+        title = u'Notice',
+        required = False,
+        )
+    student_id = schema.TextLine(
+        title = u'Student ID',
+        required = False,
+        )
+    import_record_no = schema.TextLine(
+        title = u'Import Record No.',
+        required = False,
+        )
+    imported_by = schema.TextLine(
+        title = u'Imported By',
+        required = False,
+        )
+    import_date = schema.Datetime(
+        title = u'Import Date',
+        required = False,
+        )
+    import_from = schema.TextLine(
+        title = u'Import Source',
+        required = False,
+        )
+
+    
+class IApplicant(IApplicantBaseData):
+    """An applicant.
+
+    This is basically the applicant base data. Here we repeat the
+    fields from base data only with the `required` attribute of
+    required attributes set to True (which is the default).
+    """
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        )
+    course1 = schema.TextLine(
+        title = u'1st Choice Course of Study',
+        )
+    firstname = schema.TextLine(
+        title = u'First Name',
+        )
+    middlenames = schema.TextLine(
+        title = u'Middle Names',
+        )
+    lastname = schema.TextLine(
+        title = u'Surname/Full Name',
+        )
+    date_of_birth = schema.Date(
+        title = u'Date of Birth',
+        )
+    jamb_state = schema.TextLine(
+        title = u'State (provided by JAMB)',
+        )
+    jamb_lga = schema.TextLine(
+        title = u'LGA (provided by JAMB)',
+        )
+    lga = schema.TextLine(
+        # XXX: should be choice
+        title = u'State/LGA (confirmed by applicant)',
+        )
+    sex = schema.Choice(
+        title = u'Sex',
+        source = GenderSource(),
+        default = u'm',
+        )
+    #passport = schema.Bool(
+    #    title = u'Passport Photograph',
+    #    default = True,
+    #    )
+    passport = ImageFile(
+        title = u'Passport Photograph',
+        default = DEFAULT_PASSPORT_IMAGE_MALE,
+        required = True,
+        )
+    #
+    # Higher Educational Data
+    #
+
+    #
+    # First sitting data
+    #
+    fst_sit_fname = schema.TextLine(
+        title = u'Full Name',
+        )
+
+    #
+    # Second sitting data
+    #
+    scd_sit_fname = schema.TextLine(
+        title = u'Full Name',
+        )
+    #
+    # JAMB scores
+    #
+    
+    #
+    # Application Data
+    #
+    application_date = schema.Date(
+        title = u'Application Date',
+        )
+    status = schema.TextLine(
+        # XXX: should be 'status' type
+        title = u'Application Status',
+        )
+    screening_date = schema.Date(
+        title = u'Screening Date',
+        )
+    screening_type = schema.TextLine(
+        # XXX: schould be choice
+        title = u'Screening Type',
+        )
+    screening_score = schema.TextLine(
+        title = u'Screening Score',
+        )
+    entry_session = schema.TextLine(
+        # XXX: should be choice
+        # XXX: should have sensible default: upcoming session
+        title = u'Entry Session',
+        )
+    import_record_no = schema.TextLine(
+        title = u'Import Record No.',
+        )
+    imported_by = schema.TextLine(
+        title = u'Imported By',
+        )
+    import_date = schema.Datetime(
+        title = u'Import Date',
+        )
+    import_from = schema.TextLine(
+        title = u'Import Source',
+        )
+
+class IApplicantPDEEditData(IWAeUPObject):
+    """The data set presented to PDE applicants.
+    """
+    reg_no = schema.TextLine(
+        title = u'JAMB Registration Number',
+        readonly = True,
+        )
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        readonly = True,
+        )
+    course1 = schema.TextLine(
+        title = u'1st Choice Course of Study',
+        readonly = True,
+        )
+    course2 = schema.TextLine(
+        title = u'2nd Choice Course of Study',
+        required = False,
+        )
+    course3 = schema.TextLine(
+        title = u'3rd Choice Course of Study',
+        required = False,
+        )
+    lastname = schema.TextLine(
+        title = u'Name',
+        readonly = True,
+        )
+    jamb_age = schema.Int(
+        title = u'Age',
+        readonly = True,
+        )
+    date_of_birth = schema.Date(
+        title = u'Date of Birth',
+        required = True,
+        )
+    jamb_state = schema.TextLine(
+        title = u'State (provided by JAMB)',
+        readonly = True,
+        )
+    jamb_lga = schema.TextLine(
+        title = u'LGA (provided by JAMB)',
+        readonly = True,
+        )
+    lga = schema.TextLine(
+        # XXX: should be choice
+        title = u'State/LGA (confirmed by applicant)',
+        required = False,
+        )
+    email = schema.TextLine(
+        title = u'Email',
+        required = False,
+        )
+    phone = schema.TextLine(
+        title = u'Phone',
+        required = False,
+        )
+    aos = schema.TextLine(
+        # XXX: should be choice
+        title = u'Area of Specialisation',
+        required = False,
+        )
+    subj1 = schema.TextLine(
+        # XXX: should be choice
+        title = u'1st Choice of Study',
+        readonly = True,
+        )
+    subj2 = schema.TextLine(
+        # XXX: should be choice
+        title = u'2nd Choice of Study',
+        required = False,
+        )
+    subj3 = schema.TextLine(
+        # XXX: should be choice
+        title = u'3rd Choice of Study',
+        required = False,
+        )
+    
+    #
+    # Application Data
+    #
+    application_date = schema.Date(
+        title = u'Application Date',
+        readonly = True,
+        )
+    status = schema.TextLine(
+        # XXX: should be 'status' type
+        title = u'Application Status',
+        readonly = True,
+        )
+    screening_date = schema.Date(
+        title = u'Screening Date',
+        readonly = True,
+        )
+    #passport = schema.Bool(
+    #    title = u'Passport Photograph',
+    #    default = True,
+    #    required = False,
+    #    ),
+    #passport = schema.Bytes(
+    #    title = u'Passport Photograph',
+    #    required = True,
+    #    )
+    #passport = schema.Object(
+    #    title = u'Passport Photograph',
+    #    required = True,
+    #    schema = IImage)
+    passport = ImageFile(
+        title = u'Passport Photograph',
+        default = DEFAULT_PASSPORT_IMAGE_MALE,
+        required = True,
+        )
+
+class IApplicantPDEImportData(IApplicantBaseData):
+    """Data for applicants, that passed PDE screening.
+
+    This data set should be suitable for imports from JAMB tables. It
+    is also basicall the basic applicant data from
+    :class:`IApplicantBaseData` only with the required fields set.
+
+    """
+    firstname = schema.TextLine(
+        title = u'First Name',
+        required = True,
+        )
+    middlenames = schema.TextLine(
+        title = u'Middle Names',
+        required = True,
+        )
+    lastname = schema.TextLine(
+        title = u'Surname/Full Name',
+        required = True,
+        )
+    date_of_birth = schema.Date(
+        title = u'Date of Birth',
+        required = True,
+        )
+    jamb_state = schema.TextLine(
+        title = u'State (provided by JAMB)',
+        required = True,
+        )
+    jamb_lga = schema.TextLine(
+        title = u'LGA (provided by JAMB)',
+        required = True,
+        )
+    course1 = schema.TextLine(
+        title = u'1st Choice Course of Study',
+        required = True,
+        )
+    screening_date = schema.Date(
+        title = u'Screening Date',
+        required = True,
+        )
+    screening_type = schema.TextLine(
+        # XXX: schould be choice
+        title = u'Screening Type',
+        required = True,
+        )
+    screening_venue = schema.TextLine(
+        title = u'Screening Venue',
+        required = True,
+        )
+    entry_session = schema.TextLine(
+        # XXX: should be choice
+        # XXX: should have sensible default: upcoming session
+        title = u'Entry Session',
+        required = True,
+        )
+    
+class IApplicantContainer(IWAeUPObject):
+    """A container for applicants.
+    """
+
+class IApplicantPrincipalInfo(IPrincipalInfo):
+    """Infos about principals that are applicants.
+    """
+    reg_no = Attribute("The JAMB registration no. of the user")
+
+    access_code = Attribute("The Access Code the user purchased")
+
+class IApplicantPrincipal(IPrincipal):
+    """A principal that is an applicant.
+
+    This interface extends zope.security.interfaces.IPrincipal and
+    requires also an `id` and other attributes defined there.
+    """
+    reg_no = schema.TextLine(
+        title = u'Registration number',
+        description = u'The JAMB registration number',
+        required = True,
+        readonly = True)
+
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        description = u'The access code purchased by the user.',
+        required = True,
+        readonly = True)
+
+class IApplicantsFormChallenger(Interface):
+    """A challenger that uses a browser form to collect applicant
+       credentials.
+    """
+    loginpagename = schema.TextLine(
+        title = u'Loginpagename',
+        description = u"""Name of the login form used by challenger.
+
+        The form must provide an ``access_code`` input field.
+        """)
+
+    accesscode_field = schema.TextLine(
+        title = u'Access code field',
+        description = u'''Field of the login page which is looked up for
+                          access_code''',
+        default = u'access_code',
+        )
+
+class IJAMBApplicantsFormChallenger(IApplicantsFormChallenger):
+    """A challenger that uses a browser form to collect applicant
+       credentials for applicants in JAMB process.
+
+       JAMB-screened applicants have to provide an extra registration
+       no. provided by JAMB.
+    """
+    jamb_reg_no_field = schema.TextLine(
+        title = u'JAMB registration no.',
+        description = u'''Field of the login page which is looked up for
+                          the JAMB registration number.''',
+        default = u'jamb_reg_no',
+        )
+
+class IApplicantSessionCredentials(Interface):
+    """Interface for storing and accessing applicant credentials in a
+       session.
+    """
+
+    def __init__(access_code):
+        """Create applicant session credentials."""
+
+    def getAccessCode():
+        """Return the access code."""
+
+
+class IJAMBApplicantSessionCredentials(IApplicantSessionCredentials):
+    """Interface for storing and accessing JAMB applicant credentials in a
+       session.
+    """
+
+    def __init__(access_code, jamb_reg_no):
+        """Create credentials for JAMB screened applicants."""
+
+    def getJAMBRegNo():
+        """Return the JAMB registration no."""
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/jambtables.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/jambtables.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/jambtables.py	(revision 5687)
@@ -0,0 +1,181 @@
+##
+## jambtables.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jun 22 06:31:42 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
+##
+"""WAeUP SIRP support for JAMB tables.
+
+JAMB tables are datasets delivered by JAMB.
+"""
+import csv
+import os
+import shutil
+import tempfile
+import grok
+
+from datetime import datetime
+from BTrees.OOBTree import OOBTree
+from BTrees.Length import Length
+
+from waeup.sirp.jambtables.interfaces import IJAMBDataTable
+
+#: The header fields required for a valid JAMB table CSV file.
+JAMB_DATA_HEADERS = [
+    'firstname', 'lastname', 'middlenames', 'screening_type',
+    'screening_venue', 'reg_no', 'sex', 'course1', 'jamb_state',
+    'screening_date', 'jamb_lga', 'fst_sit_fname', 'date_of_birth']
+
+def filter_data(datadict):
+    """Filter items whose key contains 'ignore'
+
+    The function will remove all fields whose name start with
+    ``ignore``. All data will be convertetd to unicode except
+    ``data_of_birth``, which is turned into a `dateteime` object.
+    """
+    keys = datadict.keys()
+    for key in keys:
+        if not 'ignore' in key:
+            continue
+        del datadict[key]
+    for name in JAMB_DATA_HEADERS:
+        datadict[name] = unicode(datadict[name])
+    datadict['date_of_birth'] = datetime.strptime(
+        datadict['date_of_birth'],
+        '%d/%m/%Y'
+        ).date()
+    
+    return datadict
+
+
+class JAMBDataTable(grok.Model):
+    """A data table that contains JAMB data.
+
+    JAMB data tables are plain but fast as they store nearly no data
+    inside the ZODB. All data is held on-disk in CSV tables.
+
+    As a consequence these tables are read-only.
+    """
+    #: Implement :class:`IJAMBDataTable`
+    grok.implements(IJAMBDataTable)
+
+    #: The datetime when data was imported.
+    import_datetime = None
+
+    #: The username of the person that initiated the last import.
+    importer_username = None
+    
+    def __init__(self):
+        super(JAMBDataTable, self).__init__()
+        self._datafile_path = None
+        self._data_len = 0
+        self._temporary = False
+        return
+
+    def __del__(self):
+        self.clear()
+
+    def __iter__(self):
+        reader = None
+        if self._datafile_path is not None:
+            reader = csv.DictReader(open(self._datafile_path, 'rb'))
+        if reader is None:
+            raise StopIteration
+        for line in reader:
+            data_dict = filter_data(line)
+            yield data_dict
+
+    def keys(self):
+        """Get iterator over all registration numbers stored in table.
+        """
+        for item in self:
+            yield item['reg_no']
+
+    def items(self):
+        """Get tuples of registration number and datasets for each entry in
+           data table.
+        """
+        for item in self:
+            yield (item['reg_no'], item)
+            
+    def clear(self):
+        """Remove all existing entries.
+
+        Unsets also the :attr:`import_datetime` and
+        :attr:`importer_username` attributes.
+        """
+        self.import_datetime = None
+        self.importer_username = None
+        if self._datafile_path is None:
+            return
+        if self._temporary:
+            if not os.path.exists(self._datafile_path):
+                return
+            shutil.rmtree(os.path.dirname(self._datafile_path))
+        self._datafile_path = None
+            
+    def importFromCSV(self, filepath, username=None):
+        """Importing data from a CSV file means to copy the source to a safe
+           location.
+
+           If the username is set, it will be stored as well in
+           :attr:`importer_username`.
+        """
+        self.clear()
+        self.import_datetime = datetime.now()
+        self.importer_username = None
+        if username is not None:
+            self.importer_username = unicode(username)
+        self._copyDataFile(filepath)
+
+    def _copyDataFile(self, path):
+        """Copy file in path to the JAMBData storage.
+
+        See :meth:`_getJAMBTableStorage`.
+        """
+        storage = self._getJAMBTableStorage()
+        self._datafile_path = os.path.join(
+            storage, os.path.basename(path)
+            )
+        shutil.copy2(path, self._datafile_path)
+        return
+
+    def _getJAMBTableStorage(self):
+        """Get a path to store copies of datatables.
+
+        We normally store data in a ``jambdata`` subdir of datacenter,
+        but if none exists, we create a temporary dir and set
+        `temporary` to ``True``.
+
+        Any not existent directory is created.
+
+        Note, that temporary dirs will be deleted when the
+        JAMBDataTable object is destroyed.
+
+        Returns absolute path to the JAMB data storage.
+        """
+        site = grok.getSite()
+        if site is None:
+            jambtable_storage = tempfile.mkdtemp()
+            self._temporary = True
+        else:
+            datacenter = site['datacenter']
+            jambtable_storage = os.path.join(datacenter.storage, 'jambdata')
+        if not os.path.isdir(jambtable_storage):
+            os.mkdir(jambtable_storage)
+        return os.path.abspath(jambtable_storage)
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/__init__.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/__init__.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/__init__.py	(revision 5687)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_applicants.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_applicants.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_applicants.py	(revision 5687)
@@ -0,0 +1,149 @@
+##
+## 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 logging
+import unittest
+from StringIO import StringIO
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+from waeup.sirp.jambtables import (
+    ResultEntry, Applicant, ApplicantFactory, ApplicantContainer,
+    ApplicantsPlugin,
+    )
+from waeup.sirp.jambtables.interfaces import (
+    IResultEntry, IApplicant, IApplicantContainer,
+    )
+
+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(unittest.TestCase):
+
+    def setUp(self):
+        self.applicant = Applicant()
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IApplicant, Applicant)
+        verify.verifyObject(IApplicant, self.applicant)
+
+class ApplicantFactoryTest(unittest.TestCase):
+
+    def setUp(self):
+        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 ApplicantContainerTest(unittest.TestCase):
+
+    def setUp(self):
+        self.container = ApplicantContainer()
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IApplicantContainer, ApplicantContainer)
+        verify.verifyObject(IApplicantContainer, self.container)
+
+class ApplicantsPluginTest(unittest.TestCase):
+
+    def setUp(self):
+        self.plugin = ApplicantsPlugin()
+        self.site1 = dict()
+        self.site2 = dict(applications = True)
+        self.log_stream = StringIO()#.open('w')
+        handler = logging.StreamHandler(self.log_stream)
+        self.logger = logging.getLogger('waeup.sirp.testing')
+        self.logger.addHandler(handler)
+        self.logger.propagate = False
+        self.logger.setLevel(logging.DEBUG)
+        
+    def tearDown(self):
+        handlers = self.logger.handlers
+        for handler in handlers:
+            del handler
+        pass
+
+    def notest_interfaces(self):
+        verify.verifyClass(IWAeUPSIRPPluggable, ApplicantsPlugin)
+        verify.verifyObject(IWAeUPSIRPPluggable, self.plugin)
+
+    def test_setup(self):
+        self.plugin.setup(self.site1, 'some.site', self.logger)
+        assert 'Installed application container' in self.log_stream.getvalue()
+        assert 'applications' in self.site1.keys()
+
+    def test_update_empty_site(self):
+        self.plugin.update(self.site1, 'some.site', self.logger)
+        assert 'Updating site' in self.log_stream.getvalue()
+        assert 'Installing' in self.log_stream.getvalue()
+
+    def test_update_filled_site(self):
+        self.plugin.update(self.site2, 'some.site', self.logger)
+        assert 'Nothing to do' in self.log_stream.getvalue()
+        
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        ResultEntryTest, ApplicantTest, ApplicantFactoryTest,
+        ApplicantContainerTest, ApplicantsPluginTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_authentication.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_authentication.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_authentication.py	(revision 5687)
@@ -0,0 +1,347 @@
+##
+## test_authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Aug 20 08:18:58 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
+##
+import grok
+import unittest
+from zope.component import provideAdapter
+from zope.component.hooks import setSite
+from zope.interface import verify
+from zope.pluggableauth.interfaces import  IAuthenticatedPrincipalFactory
+from zope.publisher.browser import TestRequest
+from zope.publisher.interfaces import IRequest
+from zope.session.session import RAMSessionDataContainer
+from zope.session.interfaces import ISession
+from zope.site import LocalSiteManager
+from zope.site.folder import Folder
+from zope.site.testing import siteSetUp, siteTearDown
+from zope.testing import cleanup
+from waeup.sirp.jambtables.authentication import (
+    ApplicantsAuthenticatorPlugin, WAeUPApplicantCredentialsPlugin,
+    ApplicantCredentials, AuthenticatedApplicantPrincipalFactory,
+    ApplicantPrincipalInfo, ApplicantPrincipal)
+from waeup.sirp.jambtables.interfaces import(
+    IJAMBApplicantSessionCredentials,)
+
+class FakeBatch(dict):
+    def getAccessCode(self, id):
+        return self.get(id)
+
+class FakeAccessCode(object):
+    def __init__(self, repr, inv_date=None):
+        self.invalidation_date = inv_date
+        self.representation = repr
+
+class FakeInvAccessCode(object):
+    invalidation_date = True
+
+class FakeApplication(object):
+    def __init__(self, ac=None):
+        self.access_code = ac
+
+class FakeSite(grok.Application, grok.Container):
+    def __init__(self):
+        super(FakeSite, self).__init__()
+        self['applications'] = {
+            'APP-12345': FakeApplication('APP-12345'),
+            'APP-54321': FakeApplication('APP-54321'),
+            'APP-22222': FakeApplication('APP-OTHER'),
+            'APP-44444': FakeApplication(),
+            'APP-55555': FakeApplication('APP-OTHER'),
+            'JAMB1': FakeApplication(),
+            'JAMB2': FakeApplication('APP-12345'),
+            'JAMB3': FakeApplication('APP-54321'),
+            'JAMB4': FakeApplication('APP-44444'),
+            }
+        self['accesscodes'] = {
+            'APP': FakeBatch({
+                    'APP-12345': FakeAccessCode('APP-12345'),
+                    'APP-54321': FakeAccessCode('APP-54321', True),
+                    'APP-11111': FakeAccessCode('APP-11111'),
+                    'APP-22222': FakeAccessCode('APP-22222'),
+                    'APP-33333': FakeAccessCode('APP-33333', True),
+                    'APP-44444': FakeAccessCode('APP-44444', True),
+                    'APP-55555': FakeAccessCode('APP-55555', True),
+                    })
+            }
+
+class AuthenticatorPluginTest(unittest.TestCase):
+
+    def setUp(self):
+        self.root = Folder()
+        siteSetUp(self.root)
+        self.app = FakeSite() #grok.Application()
+        self.root['app'] = self.app
+        self.app.setSiteManager(LocalSiteManager(self.app))
+        self.plugin = ApplicantsAuthenticatorPlugin()
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        siteTearDown()
+        return
+
+    def test_invalid_credentials(self):
+        result = self.plugin.authenticateCredentials('not-a-dict')
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode=None, foo='blah'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(jambregno=None, foo='blah'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode=None, jambregno=None))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='Nonsense', jambregno='Nonsense'))
+        assert result is None
+
+        # The nine possible cases, where formal correct authentication
+        # data is not valid:
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-22222'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-33333'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-44444'))
+        assert result is None
+        
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-55555'))
+        assert result is None
+        
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345', jambregno='JAMB-NOT-EXISTENT'))
+        assert result is None
+        
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345', jambregno='JAMB3'))
+        assert result is None
+        
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321', jambregno='JAMB-NOT-EXISTENT'))
+        assert result is None
+        
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321', jambregno='JAMB1'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321', jambregno='JAMB2'))
+        assert result is None
+        
+        return
+
+    def test_valid_credentials(self):
+        """The six different cases where we allow login.
+
+        All other combinations should be forbidden.
+        """
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-11111'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345', jambregno='JAMB1'))
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345', jambregno='JAMB2'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321', jambregno='JAMB3'))
+        assert result is not None
+
+        # check the `principalInfo` method of authenticator
+        # plugin. This is only here to satisfy the coverage report.
+        assert self.plugin.principalInfo('not-an-id') is None
+        return
+
+session_data = {
+    'zope.pluggableauth.browserplugins': {}
+    }
+
+class FakeSession(dict):
+    def __init__(self, request):
+        pass
+
+    def get(self, key, default=None):
+        return self.__getitem__(key, default)
+    
+    def __getitem__(self, key, default=None):
+        return session_data.get(key, default)
+
+    def __setitem__(self, key, value):
+        session_data[key] = value
+        return
+
+class CredentialsPluginTest(unittest.TestCase):
+
+    def setUp(self):
+        self.request = TestRequest()
+        provideAdapter(FakeSession, (IRequest,), ISession)
+        self.plugin = WAeUPApplicantCredentialsPlugin()
+        self.full_request = TestRequest()
+        session_data['zope.pluggableauth.browserplugins'] = {}
+        return
+
+    def tearDown(self):
+        cleanup.tearDown()
+        return
+
+    def filled_request(self, form_dict):
+        request = TestRequest()
+        for key, value in form_dict.items():
+            request.form[key] = value
+        return request
+
+    def test_extractCredentials_invalid(self):
+        result = self.plugin.extractCredentials('not-a-request')
+        assert result is None
+        return
+
+    def test_extractCredentials_empty(self):
+        result = self.plugin.extractCredentials(self.request)
+        assert result is None
+        return
+
+    def test_extractCredentials_full_set(self):
+        request = self.filled_request({
+                'form.prefix': 'APP',
+                'form.ac_series': '1',
+                'form.ac_number': '1234567890',
+                'form.jamb_reg_no': 'JAMB_NUMBER',
+                })
+        result = self.plugin.extractCredentials(request)
+        self.assertEqual(result, {'jambregno': 'JAMB_NUMBER',
+                                  'accesscode': 'APP-1-1234567890'})
+        return
+
+    def test_extractCredentials_accesscode_only(self):
+        request = self.filled_request({
+                'form.prefix': 'APP',
+                'form.ac_series': '1',
+                'form.ac_number': '1234567890',
+                })
+        result = self.plugin.extractCredentials(request)
+        self.assertEqual(result, {'accesscode': 'APP-1-1234567890'})
+        return
+
+    def test_extractCredentials_from_empty_session(self):
+        session_data['zope.pluggableauth.browserplugins']['credentials'] = None
+        result = self.plugin.extractCredentials(self.request)
+        assert result is None
+        return
+
+    def test_extractCredentials_from_nonempty_session(self):
+        credentials = ApplicantCredentials('APP-1-12345')
+        session_data['zope.pluggableauth.browserplugins'][
+            'credentials'] = credentials
+        result = self.plugin.extractCredentials(self.request)
+        self.assertEqual(result, {'accesscode': 'APP-1-12345'})
+        return
+
+
+class ApplicantCredentialsTest(unittest.TestCase):
+
+    def setUp(self):
+        self.credentials = ApplicantCredentials('SOME_ACCESSCODE')
+        return
+
+    def tearDown(self):
+        return
+
+    def test_methods(self):
+        self.assertEqual(self.credentials.getAccessCode(), 'SOME_ACCESSCODE')
+        assert self.credentials.getLogin() is None
+        assert self.credentials.getPassword() is None
+        return
+
+class FakePluggableAuth(object):
+    prefix = 'foo'
+
+class PrincipalFactoryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.info = ApplicantPrincipalInfo('APP-1-1234567890')
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_principalFactory_interface(self):
+        verify.verifyClass(IAuthenticatedPrincipalFactory,
+                           AuthenticatedApplicantPrincipalFactory
+                           )
+        return
+
+    def test_principalFactory_create(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+
+        assert factory.info is self.info
+        assert factory.request is None
+        return
+
+    def test_principalFactory_call_w_prefix(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+        principal = factory(FakePluggableAuth())
+
+        assert isinstance(principal, ApplicantPrincipal)
+        self.assertEqual(principal.__repr__(),
+                         "ApplicantPrincipal('foo.APP-1-1234567890')")
+        self.assertEqual(principal.id, 'foo.APP-1-1234567890')
+        return
+
+    def test_principalFactory_call_wo_prefix(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+        fake_auth = FakePluggableAuth()
+        fake_auth.prefix = None
+        principal = factory(fake_auth)
+        self.assertEqual(principal.id, 'APP-1-1234567890')
+        return
+        
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        AuthenticatorPluginTest, CredentialsPluginTest,
+        ApplicantCredentialsTest, PrincipalFactoryTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_batching.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_batching.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_batching.py	(revision 5687)
@@ -0,0 +1,87 @@
+##
+## test_batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Aug 24 02:04:44 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
+##
+"""Unit tests for the applicant importer.
+"""
+import grok
+import unittest
+from zope.interface import verify
+from waeup.sirp.interfaces import IBatchProcessor
+from waeup.sirp.jambtables import ApplicantImporter
+
+class ApplicantImporterTest(unittest.TestCase):
+
+    def setUp(self):
+        self.importer = ApplicantImporter()
+        self.site1 = dict(
+            applications = {
+                'REG_NO_1': 'Application1',
+                }
+            )
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_interface(self):
+        """Make sure we fulfill the interface contracts."""
+        assert verify.verifyObject(IBatchProcessor, self.importer) is True
+        assert verify.verifyClass(IBatchProcessor, ApplicantImporter) is True
+
+    def test_parentsExist(self):
+        assert self.importer.parentsExist(None, dict()) is False
+        assert self.importer.parentsExist(None, self.site1) is True
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+            dict(reg_no='REG_NONE'), self.site1) is False
+        assert self.importer.entryExists(
+            dict(reg_no='REG_NO_1'), self.site1) is True
+
+    def test_getParent(self):
+        parent = self.importer.getParent(None, self.site1)
+        assert parent is self.site1['applications']
+
+    def test_getEntry(self):
+        assert self.importer.getEntry(
+            dict(reg_no='REG_NONE'), self.site1) is None
+        assert self.importer.getEntry(
+            dict(reg_no='REG_NO_1'), self.site1) == 'Application1'
+
+    def test_addEntry(self):
+        self.importer.addEntry(
+            'New application', dict(reg_no='REG_NO_99'), self.site1)
+        assert self.site1['applications']['REG_NO_99'] == 'New application'
+
+    def test_delEntry(self):
+        self.importer.delEntry(dict(reg_no='REG_NO_1'), self.site1)
+        assert 'REG_NO_1' not in self.site1['applications'].keys()
+        
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        ApplicantImporterTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_interfaces.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_interfaces.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_interfaces.py	(revision 5687)
@@ -0,0 +1,73 @@
+##
+## test_interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Aug 23 01:59:52 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 jambtable interfaces.
+"""
+import unittest
+from zc.sourcefactory.browser.source import FactoredTerms
+from zope.browser.interfaces import ITerms
+from zope.component import getMultiAdapter
+from zope.publisher.browser import TestRequest
+from waeup.sirp.jambtables.interfaces import GenderSource
+
+class InterfacesTest(unittest.TestCase):
+
+    def setUp(self):
+        self.source = GenderSource()
+        self.request = TestRequest()
+        self.terms = FactoredTerms(self.source, self.request)
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_GenderSource_list(self):
+        result = list(self.source)
+        self.assertEqual(result, ['m', 'f'])
+
+    def test_GenderSource_term_male(self):
+        term = self.terms.getTerm('m')
+        assert term.title == 'male'
+        assert term.token == 'm'
+        assert term.value == 'm'
+
+    def test_GenderSource_term_female(self):
+        term = self.terms.getTerm('f')
+        assert term.title == 'female'
+        assert term.token == 'f'
+        assert term.value == 'f'
+
+    def test_GernderSource_term_invalid(self):
+        term_inv = self.terms.getTerm('Invalid')
+        assert term_inv.title is None
+        assert term_inv.token == 'i'
+        
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        InterfacesTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
+
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_jambtables.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_jambtables.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_jambtables.py	(revision 5687)
@@ -0,0 +1,204 @@
+##
+## test_jambtables.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jun 24 08:08:12 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 JAMB tables.
+
+"""
+import os
+import shutil
+import tempfile
+import unittest
+
+import grok
+
+from datetime import datetime
+
+from zope.interface.verify import verifyClass, verifyObject
+from zope.site import LocalSiteManager
+from zope.site.hooks import setSite, clearSite
+
+from waeup.sirp.app import University
+from waeup.sirp.datacenter import DataCenter
+from waeup.sirp.jambtables import JAMBDataTable
+from waeup.sirp.jambtables.interfaces import IJAMBDataTable
+
+
+class JAMBTableTestCaseBase(unittest.TestCase):
+
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+        self.datafile = os.path.join(
+            self.tempdir, 'jambsample.csv'
+            )
+        open(self.datafile, 'wb').write(
+            'reg_no,fst_sit_fname,lastname,firstname,middlenames,sex,date_of_birth,jamb_state,jamb_lga,course1,screening_date,screening_venue,entry_session,screening_type,ignore_this_col\n' + 
+            '91100546DD,ISUOSUO MOSES ODOMERO,ISUOSUO,MOSES,ODOMERO,M,25/5/1982,DEL,ISO-S,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde,blah\n' +
+            '91111834CC,DURUIHEOMA AUGUSTINA ADANNA,DURUIHEOMA,AUGUSTINA,ADANNA,F,15/4/1986,IMO,MBAIT,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde,blah\n' +
+            '91109351AC,ARISERE EBIKEBUNA COMFORT,ARISERE,EBIKEBUNA,COMFORT,F,6/1/1984,EDO,OV-SW,BSCPOL,2009/09/25 09:00:00 GMT+1,REPRINT SLIP AS FROM WED 23/09/2009,9,pde,blah\n'
+            )
+        setSite()
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+        return
+
+class JAMBTableTestCase(JAMBTableTestCaseBase):
+    def test_createJAMBTable(self):
+        table = JAMBDataTable()
+        self.assertEqual(table._data_len, 0)
+        self.assertEqual(table._temporary, False)
+        self.assertEqual(table._datafile_path, None)
+        return
+
+    def test_iterator(self):
+        table = JAMBDataTable()
+        self.assertEqual(len(list(table)), 0)
+        table.importFromCSV(self.datafile)
+        self.assertEqual(len(list(table)), 3)
+
+    def test_import(self):
+        table = JAMBDataTable()
+        self.assertEqual(table.import_datetime, None)
+        table.importFromCSV(self.datafile)
+        self.assertNotEqual(table.import_datetime, None)
+
+    def test_clear(self):
+        table = JAMBDataTable()
+        table.importFromCSV(self.datafile)
+        path = table._datafile_path
+        table.clear()
+        exists = os.path.exists(path)
+        self.assertEqual(exists, False)
+        self.assertEqual(0, len(list(table)))
+
+    def test_clear_broken_path(self):
+        table = JAMBDataTable()
+        table.importFromCSV(self.datafile)
+        path = table._datafile_path
+        table._datafile_path = 'not-existent-path'
+        table.clear()
+        
+    def test_import_datetime(self):
+        table = JAMBDataTable()
+        self.assertTrue(table.import_datetime is None)
+        table.importFromCSV(self.datafile)
+        now = datetime.now()
+        diff = now - table.import_datetime
+        self.assertTrue(diff.seconds < 5)
+        table.clear()
+        self.assertTrue(table.import_datetime is None)
+
+    def test_importer_username(self):
+        table = JAMBDataTable()
+        self.assertTrue(table.importer_username is None)
+        table.importFromCSV(self.datafile, 'manfred')
+        self.assertEqual(table.importer_username, u'manfred')
+        self.assertTrue(isinstance(table.importer_username, unicode))
+        table.importFromCSV(self.datafile, 'fred')
+        self.assertEqual(table.importer_username, u'fred')
+        table.importFromCSV(self.datafile)
+        self.assertTrue(table.importer_username is None)
+        table.clear()
+        self.assertTrue(table.importer_username is None)
+
+    def test_keys(self):
+        table = JAMBDataTable()
+        self.assertFalse('91100546DD' in table.keys())
+        table.importFromCSV(self.datafile)
+        self.assertTrue('91100546DD' in table.keys())
+        self.assertFalse('91100546DE' in table.keys())
+
+    def test_items(self):
+        table = JAMBDataTable()
+        self.assertEqual([], list(table.items()))
+        table.importFromCSV(self.datafile)
+        self.assertNotEqual([], list(table.items()))
+
+    def test_interface_compliance(self):
+        self.assertTrue(verifyClass(IJAMBDataTable, JAMBDataTable))
+        table = JAMBDataTable()
+        self.assertTrue(verifyObject(IJAMBDataTable, table))
+        table.importFromCSV(self.datafile)
+        self.assertTrue(verifyObject(IJAMBDataTable, table))
+
+class JAMBTableFunctionalTestCase(JAMBTableTestCaseBase):
+    """A test case where we check JAMB tables inside a site.
+    """
+
+    def setUp(self):
+        super(JAMBTableFunctionalTestCase, self).setUp()
+        
+        # We need at least these to register factories for a University.
+        grok.testing.grok('waeup.sirp.student')
+        grok.testing.grok('waeup.sirp.hostel')
+        grok.testing.grok('waeup.sirp.university')
+
+        self.site = University()
+        sm = LocalSiteManager(self.site)
+        self.site.setSiteManager(sm)
+        self.datacenter_dir = tempfile.mkdtemp()
+        self.datacenter = self.site['datacenter']
+        self.datacenter.setStoragePath(self.datacenter_dir)
+        # If the old datacenter already had a jambtables dir, remove it...
+        if os.path.isdir(
+            os.path.join(self.datacenter_dir, 'jambdata')
+            ):
+            shutil.rmtree(os.path.join(self.datacenter_dir, 'jambdata'))
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+        shutil.rmtree(self.datacenter_dir)
+        # Unregister site...
+        setSite()
+        return
+
+    def test_createJAMBTableInSite(self):
+        table = JAMBDataTable()
+        self.assertEqual(table._data_len, 0)
+        self.assertEqual(table._temporary, False)
+        self.assertEqual(table._datafile_path, None)
+        table.importFromCSV(self.datafile)
+        return
+
+    def test_createJAMBTableOutsideSite(self):
+        clearSite(self.site)
+        table = JAMBDataTable()
+        self.assertEqual(table._data_len, 0)
+        self.assertEqual(table._temporary, False)
+        self.assertEqual(table._datafile_path, None)
+        table.importFromCSV(self.datafile)
+        return
+
+        
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        JAMBTableTestCase,
+        JAMBTableFunctionalTestCase,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_util.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_util.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/tests/test_util.py	(revision 5687)
@@ -0,0 +1,66 @@
+##
+## test_util.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Aug 22 19:56:52 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 jambtables and applicants helper tools.
+"""
+import unittest
+from zope.component.hooks import setSite
+from zope.site import LocalSiteManager
+from zope.site.folder import Folder
+from zope.site.testing import siteSetUp, siteTearDown
+from waeup.sirp.jambtables.tests.test_authentication import (
+    FakeSite)
+from waeup.sirp.jambtables.util import application_exists, get_applicant_data
+
+class HelperToolsTest(unittest.TestCase):
+
+    def setUp(self):
+        self.root = Folder()
+        siteSetUp(self.root)
+        self.app = FakeSite() #grok.Application()
+        self.root['app'] = self.app
+        self.app.setSiteManager(LocalSiteManager(self.app))
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        siteTearDown()
+        return
+
+    def test_application_exists(self):
+        result = application_exists('APP-99999')
+        assert result is False
+
+        result = application_exists('APP-44444')
+        assert result is True
+        
+        return
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        HelperToolsTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/util.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/util.py	(revision 5687)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/applicants/jambtables/util.py	(revision 5687)
@@ -0,0 +1,59 @@
+##
+## util.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jul 29 09:39:51 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
+##
+"""Little helpers for applicant specific stuff.
+"""
+import grok
+from waeup.sirp.accesscodes import get_access_code
+
+def get_applicant_data(reg_no, ac):
+    """Validate credentials and return applicant data.
+    
+    Returns tuple ``(<APPLICANT_ENTRY>, <ACCESSCODE>) on
+    successful validation and ``None`` else.
+
+    We expect a JAMB registration number and an access code in format
+    like ``PREFIX-XXX-YYYYYYYY`` where ``PREFIX`` is something like
+    ``APP`` or ``PUDE``, ``XXX`` the access code series and
+    ``YYYYYYYYYY`` the real accesscode number.
+    """
+    site = grok.getSite()
+    if reg_no is not None:
+        applicant_data = site['applications'].get(reg_no, None)
+    else:
+        # Non-JAMB-screened applicants are stored by ac as key...
+        applicant_data = site['applications'].get(ac, None)
+    access_code = get_access_code(ac)
+    return (applicant_data, access_code)
+
+def application_exists(identifier):
+    """Check whether an application for the given identifier already
+       exists.
+
+       `identifier` can be an access code or a JAMB registration
+       number. For JAMB screened applicants we use the JAMB
+       registration number as key, for other applicants we use the
+       access code.
+    """
+    site = grok.getSite()
+    if identifier in site['applications'].keys():
+        return True
+    return False
