Index: /main/waeup.sirp/branches/henrik-experimental-layout/README.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/README.txt	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/README.txt	(revision 5330)
@@ -15,5 +15,5 @@
 ============
 
-.. note:: ``waeup.sirp`` does not work with Python > 2.5
+.. note:: ``waeup.sirp`` might not work with Python > 2.5
           currently. Use of Python 2.5 is recommended.
 
@@ -30,4 +30,12 @@
 
 .. _Zope: http://www.zope.org/
+
+Both, Grok and Zope, are written in Python (with parts written in
+C). You therefore need `Python`_ installed.
+
+.. _Python: http://www.python.org/
+
+Note, that you also need the Python header files and a compiler to
+compile the parts written in C.
 
 To deploy ``waeup.sirp`` most easily, we use `zc.buildout`_
@@ -48,5 +56,5 @@
 Bootstrap the buildout environment::
 
-    $ python2.4 bootstrap/bootstrap.py
+    $ python2.5 bootstrap/bootstrap.py
 
 and run the buildout command::
@@ -73,8 +81,8 @@
 If you now connect to port 8080 and log in with username 'grok',
 password 'grok', you should be able to add the grok-based applications
-(such as ``waeup``) from the menu.
+(such as ``University``) from the menu.
 
-Add an instance of ``waeup`` and click on the link next to the then
-visible entry in the list of installed applications.
+Add an instance of ``University`` and click on the link next to the
+then visible entry in the list of installed applications.
 
 Running the tests
Index: /main/waeup.sirp/branches/henrik-experimental-layout/buildout.cfg
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/buildout.cfg	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/buildout.cfg	(revision 5330)
@@ -5,5 +5,6 @@
 #newest = false
 #extends= http://grok.zope.org/releaseinfo/grok-1.0.cfg
-extends = http://svn.zope.org/*checkout*/groktoolkit/trunk/grok.cfg
+extends= http://grok.zope.org/releaseinfo/grok-1.1.cfg
+#extends = http://svn.zope.org/*checkout*/groktoolkit/trunk/grok.cfg
 versions = versions
 
@@ -18,4 +19,8 @@
 #hurry.zoperesource = 0.4
 megrok.layout = 1.0.2
+Sphinx = 0.6.4
+hurry.resource = 0.4.1  # We need to pin this as hurry.resource 0.9
+                        # requires extra packages (WebOb) we don't
+                        # like to depend on now.
 
 
@@ -107,3 +112,3 @@
 default.css =
 layout.html = 
-
+extensions = sphinx.ext.autodoc
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/batchprocessing.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/batchprocessing.txt	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/batchprocessing.txt	(revision 5330)
@@ -85,5 +85,6 @@
     >>> importerselect = browser.getControl(name='importer')
     >>> importerselect.displayOptions
-    ['CertificateCourse importer', 
+    ['Application Importer',
+     'CertificateCourse importer', 
      'Certificate importer', 
      'Course importer', 
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/browser.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/browser.txt	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/browser/browser.txt	(revision 5330)
@@ -763,5 +763,5 @@
 
   >>> sorted(os.listdir(uploadpath))
-  ['finished', 'logs', 'unfinished']
+  ['finished', 'jambdata', 'logs', 'unfinished']
 
 
@@ -804,5 +804,5 @@
 
   >>> sorted(os.listdir(uploadpath))
-  ['finished', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
+  ['finished', 'jambdata', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
 
 We create and upload also a CSV file containing departments:
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/index.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/index.txt	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/index.txt	(revision 5330)
@@ -19,4 +19,5 @@
    catalog
    accesscodes/api.txt
+   jambtables/api.txt
    university/api.txt
    utils/api.txt
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/README.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/README.txt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/README.txt	(revision 5330)
@@ -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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/__init__.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/__init__.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/__init__.py	(revision 5330)
@@ -0,0 +1,2 @@
+# Make this a package.
+from waeup.sirp.jambtables.jambtables import JAMBDataTable, JAMB_DATA_HEADERS
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/api.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/api.txt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/api.txt	(revision 5330)
@@ -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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/applicants.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/applicants.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/applicants.py	(revision 5330)
@@ -0,0 +1,143 @@
+##
+## 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.schema.fieldproperty import FieldProperty
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+from waeup.sirp.jambtables.interfaces import (
+    IResultEntry, IApplicant, IApplicantContainer
+    )
+
+class ResultEntry(grok.Context):
+    grok.implements(IResultEntry)
+
+    def __init__(self, subject=None, score=None):
+        self.subject = subject
+        self.score = score
+    
+
+class Applicant(grok.Context):
+    grok.implements(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 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(Appliant)
+
+        
+class ApplicantContainer(grok.Container):
+    grok.implements(IApplicantContainer)
+
+class AccessCodePlugin(grok.GlobalUtility):
+    grok.name('applications')
+    grok.implements(IWAeUPSIRPPluggable)
+
+    def setup(self, site, name, logger):
+        site['applications'] = ApplicantContainer()
+        logger.info('Installed application container.')
+        return
+
+    def update(self, site, name, logger):
+        if not 'applications' in site.keys():
+            logger.info('Updating site at %s. Installing applications.' % (
+                    site,))
+            self.setup(site, name, logger)
+        else:
+            logger.info(
+                'ApplicantsPlugin: Updating site at %s: Nothing to do.' % (
+                    site, ))
+        return
+
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/batching.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/batching.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/batching.py	(revision 5330)
@@ -0,0 +1,68 @@
+##
+## 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.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applcation importer'
+    grok.name(util_name)
+
+    name = u'Application Importer'
+    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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser.py	(revision 5330)
@@ -0,0 +1,184 @@
+##
+## 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 waeup.sirp.browser import (
+    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage,
+    WAeUPDisplayFormPage, NullValidator)
+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
+
+#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)
+
+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
+    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)
+        self.context[applicant.reg_no] = applicant
+        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
+    label = 'Applicant'
+    title = 'Applicant'
+    pnav = 1
+
+class EditApplicant(WAeUPEditFormPage):
+    grok.context(IApplicant)
+    grok.name('edit')
+    form_fields = grok.AutoFields(IApplicant)
+    form_fields['fst_sit_results'].custom_widget = list_results_widget
+    label = 'Edit Application'
+    title = 'Edit Application'
+    pnav = 1
+
+    @grok.action('Save')
+    def save(self, **data):
+        print "DATA: ", data
+        self.applyData(self.context, **data)
+        print "ENTRY: ", self.context.fst_sit_results
+        self.context._p_changed = True
+        return
+
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class Login_PDE(WAeUPPage):
+    grok.context(IWAeUPObject)
+    grok.name('login_pde')
+
+    title = 'PDE Login'
+    pnav = 1
+
+    def update(self, reg_no=None, ac_series=None, ac_number=None):
+        """Validate credentials and redirect or show error.
+
+        XXX: log things happening here
+        XXX: consider real login procedure with single applicant user.
+        """
+        self.reg_no = reg_no
+        self.ac_series = ac_series
+        self.ac_number = ac_number
+        for param in [reg_no, ac_series, ac_number]:
+            if param is None:
+                return
+        ac = "PUDE-%s-%s" % (ac_series, ac_number)
+        data = self.getApplicantData(reg_no, ac)
+        if data is None:
+            self.flash('Invalid data entered')
+            return
+        applicant_data, access_code = data
+        app_page = self.url(applicant_data, '@@edit')
+        # XXX: Invalidate ACCESS_CODE
+        self.redirect(app_page)
+        return
+
+    def getCurrentSession(self):
+        """Get the current session.
+
+        XXX: This should be computed or retrieved from elsewhere.
+        """
+        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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/applicationspage.pt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/applicationspage.pt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/applicationspage.pt	(revision 5330)
@@ -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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/login_pde.pt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/login_pde.pt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/browser_templates/login_pde.pt	(revision 5330)
@@ -0,0 +1,55 @@
+<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="reg_no"
+		 value="" tal:attributes="value view/reg_no" /></td>
+      </tr>
+      <tr>
+	<td><label for="ac_series">Access Code</label></td>
+	<td>
+	  PUDE-<input name="ac_series" type="text" size="3" maxlength="3"
+	              value="" tal:attributes="value view/ac_series"
+	  />-<input name="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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/interfaces.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/interfaces.py	(revision 5330)
@@ -0,0 +1,543 @@
+##
+## 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.
+"""
+from waeup.sirp.interfaces import IWAeUPObject
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope.interface import Interface
+from zope import schema
+
+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,
+        )
+    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
+        # XXX: 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,
+        )
+    #
+    # 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 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.
+    """
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/jambtables.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/jambtables.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/jambtables.py	(revision 5330)
@@ -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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/__init__.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/__init__.py	(revision 5330)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/test_jambtables.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/test_jambtables.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/tests/test_jambtables.py	(revision 5330)
@@ -0,0 +1,197 @@
+##
+## 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_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/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/util.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/util.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/jambtables/util.py	(revision 5330)
@@ -0,0 +1,44 @@
+##
+## 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.
+"""
+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 not in site['applications'].keys():
+        return None
+    applicant_data = site['applications'][reg_no]
+    entries = site['accesscodes'].search(ac, 'pin')
+    if len(entries) != 1:
+        # XXX: If entries > 1 then we have a problem!
+        return None
+    # XXX: Maybe we have to mark reg_no as used here?
+    return (applicant_data, entries[0])
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.py	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.py	(revision 5330)
@@ -1,4 +1,5 @@
 """Converters for zope.schema-based datatypes.
 """
+import datetime
 import grok
 from zope.component import getMultiAdapter
@@ -8,5 +9,5 @@
 except ImportError:
     from zope.browser.interfaces import ITerms
-from zope.schema.interfaces import IBool, IText, IInt, IChoice
+from zope.schema.interfaces import IBool, IText, IInt, IChoice, IDate
 from waeup.sirp.interfaces import ISchemaTypeConverter
 
@@ -52,4 +53,6 @@
 
     def toString(self, value, strict=True):
+        """Convert given `value` to string according to assigned field type.
+        """
         if strict:
             self.context.validate(value)
@@ -131,8 +134,22 @@
     def _convertValueFromString(self, string):
         if self.tokens is not None:
-            return self.tokens[string]
+            result = None
+            try:
+                result = self.tokens[string]
+            except KeyError:
+                # Be gentle...
+                try:
+                    result = self.tokens[string.lower()]
+                    return result
+                except KeyError:
+                    tokenlist = (','.join(self.tokens[:2]))
+                    raise ValueError(
+                        'The token %s is not valid. Use one of %s, ...' % (
+                            string, tokenlist))
+            return result
         if self.terms is not None:
             return self.terms.getValue(string)
-        return self.context.source.getTermByToken(string).value
+        result = self.context.source.getTermByToken(string).value
+        return result
 
     def _convertValueToString(self, value):
@@ -164,2 +181,59 @@
         return super(ChoiceConverter, self).toString(value=value,
                                                      strict=strict)
+
+class DateConverter(Converter):
+    """A converter for zope.schema.IDate fields.
+
+    Converts date to string and vice versa. Stringified dates are
+    expected in format ``YYYY-MM-DD``.
+
+    To support at least some other formats, we accept for conversion
+    from string also the following formats:
+
+    * ``YYYY/MM/DD``
+    
+    * ``DD/MM/YYYY``
+    
+    * ``D/M/YYYY``
+
+    * ``DD.MM.YYYY``
+
+    * ``D.M.YYYY``
+
+    When converting to strings, always 'YYYY-MM-DD' is returned.
+
+    For convenience, when converting from strings also string data
+    separated by space is stripped before processing. Therefore
+    strings like '1990-04-01 12:12:01 GMT +1' will also work.
+    """
+    grok.context(IDate)
+    grok.provides(ISchemaTypeConverter)
+
+    #: List of supported date formats in `strftime()` notation.
+    formats = ['%Y-%m-%d', '%Y/%m/%d' , '%d/%m/%Y', '%D/%M/%Y',
+               '%d.%m.%Y', '%D.%M.%Y']
+    
+    def _convertValueFromString(self, string):
+        orig_string = string
+        if string is NONE_STRING_VALUE:
+            string = None
+        if string is None:
+            return None
+        value = None
+        if ' ' in string:
+            string = string.split(' ')[0]
+        for format in self.formats:
+            try:
+                value = datetime.datetime.strptime(string, format)
+                break
+            except ValueError:
+                pass
+        if value is None:
+            raise ValueError(
+                'Cannot convert to date: %s. Use YYYY-MM-DD.' %
+                orig_string)
+        value = value.date()
+        return value
+
+    def _convertValueToString(self, value):
+        return datetime.date.strftime(value, '%Y-%m-%d')
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.txt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.txt	(revision 5329)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/utils/converters.txt	(revision 5330)
@@ -267,4 +267,12 @@
        validation checks are disabled with this converter. This speeds
        up things as expensive lookups are avoided.
+
+
+:class:`DateConverter`
+----------------------
+
+.. autoclass:: waeup.sirp.utils.converters.DateConverter
+   :members:
+   :inherited-members:
 
 
@@ -682,5 +690,5 @@
 
 A more complex (but still realistic example) for conversion of
-:mode:`zope.schema.Choice` fields follows. Here we have a special kind
+:mod:`zope.schema.Choice` fields follows. Here we have a special kind
 of object, the `Cave` class, and get their `name` attribute as token.
 
@@ -739,2 +747,115 @@
     >>> converter.toString(obj2)
     'Fred'
+
+Date field converters
+---------------------
+
+We create a field that also implements ``IDate``, a regular
+``Date`` field:
+
+    >>> from zope.schema import Date
+    >>> from datetime import date
+    >>> field = Date(title=u'Some date',
+    ...             default=date(2010, 4, 1),
+    ...             required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`IntConverter` instance:
+
+    >>> from waeup.sirp.utils.converters import DateConverter
+    >>> converter = DateConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.utils.converters.DateConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('2007-04-23')
+    datetime.date(2007, 4, 23)
+
+And back:
+
+    >>> converter.toString(date(2007,4,23))
+    '2007-04-23'
+
+As string input we also accept 'DD/MM/YYYY':
+
+    >>> converter.fromString('13/05/1945')
+    datetime.date(1945, 5, 13)
+
+and some more bizarre formats:
+
+    >>> converter.fromString('1945/05/13')
+    datetime.date(1945, 5, 13)
+
+    >>> converter.fromString('1945/05/13 12:12:34 GMT+1')
+    datetime.date(1945, 5, 13)
+
+    >>> converter.fromString('1945/5/6')
+    datetime.date(1945, 5, 6)
+
+    >>> converter.fromString('06.05.1945')
+    datetime.date(1945, 5, 6)
+
+    >>> converter.fromString('6.5.1945')
+    datetime.date(1945, 5, 6)
+
+while other date formats will mean trouble:
+
+    >>> converter.fromString('first of July 1960')
+    Traceback (most recent call last):
+    ...
+    ValueError: Cannot convert to date: first of July 1960. Use YYYY-MM-DD.
+
+Okay, not very surprising. But the field definitions can help also
+deliver values, if the given value is missing:
+
+    >>> converter.fromString(None)
+    datetime.date(2010, 4, 1)
+
+``None`` is not an acceptable value for fields which are required but
+provide no default:
+
+    >>> field = Date(title=u'Some Title',
+    ...             required=True)
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If we want to avoid this type of exception (and risk non-applicable
+data to be stored), we can use the ``strict`` parameter of
+``fromString()``:
+
+    >>> converter.fromString(None, strict=False) is None
+    True
+
+The same for the inverse operation:
+
+    >>> converter.toString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+    >>> converter.toString(None, strict=False) is None
+    True
+
+
+If a field is not required, we will get the ``missing_value`` type in
+same case:
+
+    >>> field = Date(title=u'Some Title',
+    ...             required=False)
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None) is None
+    True
+
+    >>> converter.toString(None) is None
+    True
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.pt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.pt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.pt	(revision 5330)
@@ -0,0 +1,38 @@
+<table border="0" class="multisequencewidget"
+  i18n:domain="zope">
+  <div tal:repeat="widget view/widgets"
+       tal:omit-tag="" >
+    <tr tal:condition="repeat/widget/start">
+      <th></th>
+      <th tal:repeat="subwidget widget/subwidgets">
+	<span tal:replace="subwidget/label">Column Label</span>
+      </th>
+    </tr>
+    <tr>
+      <td>
+	<input class="editcheck" type="checkbox"
+	       tal:attributes="
+                   name string:${view/name}.remove_${repeat/widget/index}"
+	       tal:condition="view/need_delete" />
+      </td>
+      <td tal:repeat="subwidget widget/subwidgets">
+	<span tal:define="error subwidget/error"
+	      tal:replace="structure error" tal:condition="error" />
+	<input tal:replace="structure subwidget" />
+      </td>
+    </tr>
+  </div>
+  <tr>
+    <td colspan="2">
+      <input type="submit" value="Remove selected items"
+	     tal:condition="view/need_delete"
+	     tal:attributes="name string:${view/name}.remove"
+	     i18n:attributes="value remove-selected-items" />
+      <input type="submit" value="Add foo"
+	     tal:condition="view/need_add"
+	     tal:attributes="name string:${view/name}.add;
+			     value view/addButtonLabel" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/multilistwidget.py	(revision 5330)
@@ -0,0 +1,75 @@
+##
+## multilistwidget.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jul 20 02:53: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
+##
+"""A widget that renders multiple contained items horizontally.
+"""
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.sequencewidget import (
+    ListSequenceWidget, SequenceDisplayWidget)
+from zope.formlib.widget import renderElement
+from zope.i18n import translate
+
+
+class MultiListWidget(ListSequenceWidget):
+    template = ViewPageTemplateFile('multilistwidget.pt')
+
+class MultiListDisplayWidget(SequenceDisplayWidget):
+    cssClass = "multiSequenceWidget"
+
+    def __call__(self):
+        # get the data to display:
+        if self._renderedValueSet():
+            data = self._data
+        else:
+            data = self.context.get(self.context.context)
+
+        # deal with special cases:
+        if data == self.context.missing_value:
+            return translate(self._missingValueMessage, self.request)
+        data = list(data)
+        if not data:
+            return translate(self._emptySequenceMessage, self.request)
+
+        parts = ['<table>']
+        for i, item in enumerate(data):
+            widget = self._getWidget(i)
+            if i == 0:
+                parts.append('<thead><tr>')
+                for x in widget.subwidgets():
+                    parts.append('<th>%s</th>' % x.label)
+                parts.append('</tr></thead><tbody>')
+            widget.setRenderedValue(item)
+            s = widget()
+            #print s
+            if self.itemTag:
+                #s = "<%s>%s</%s>" % (self.itemTag, s, self.itemTag)
+                evenodd = i%2 and 'odd' or 'even'
+                s = '<tr class="%s">%s</tr>' % (evenodd, s)
+            parts.append(s)
+        parts.append('</tbody></table>')
+        contents = "\n".join(parts)
+        if self.tag:
+            contents = "\n%s\n" % contents
+            contents = renderElement(self.tag,
+                                     cssClass=self.cssClass,
+                                     extra=self.extra,
+                                     contents=contents)
+        return contents
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.pt
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.pt	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.pt	(revision 5330)
@@ -0,0 +1,5 @@
+  <td class="row" tal:repeat="widget context/subwidgets">
+    <span tal:define="error widget/error"
+	  tal:replace="structure error" tal:condition="error" />
+    <span tal:replace="structure widget" />
+  </td>
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/objectwidget.py	(revision 5330)
@@ -0,0 +1,77 @@
+##
+## resultswidget.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jul 20 02:04:30 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
+##
+"""A widget to display IObject instances in forms.
+"""
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.interfaces import IDisplayWidget
+from zope.formlib.objectwidget import ObjectWidget, ObjectWidgetView
+from zope.formlib.utility import setUpWidgets
+from zope.interface import implements, implementsOnly
+from zope.schema import getFieldNamesInOrder
+
+class WAeUPObjectWidgetView(ObjectWidgetView):
+    template = ViewPageTemplateFile('objectwidget.pt')
+
+class WAeUPObjectWidget(ObjectWidget):
+
+    def __init__(self, context, request, factory, **kw):
+        #super(ResultsEntryWidget, self).__init__(context, request)
+        super(ObjectWidget, self).__init__(context, request)
+
+        # define view that renders the widget
+        self.view = self._getView(request)
+
+        # factory used to create content that this widget (field)
+        # represents
+        self.factory = factory
+
+        # handle foo_widget specs being passed in
+        self.names = getFieldNamesInOrder(self.context.schema)
+        for k, v in kw.items():
+            if k.endswith('_widget'):
+                setattr(self, k, v)
+
+        # set up my subwidgets
+        self._setUpWidgets()
+
+    def subwidgets(self):
+        result = [self.getSubWidget(name) for name in self.names]
+        return result
+
+    def _setUpWidgets(self):
+        return self._setUpEditWidgets()
+
+    def _getView(self, request):
+        return WAeUPObjectWidgetView(self, request)
+
+class WAeUPObjectDisplayWidget(WAeUPObjectWidget):
+
+    implementsOnly(IDisplayWidget)
+
+    def _setUpDisplayWidgets(self):
+        # subwidgets need a new name
+        setUpWidgets(self, self.context.schema, IDisplayWidget,
+                         prefix=self.name, names=self.names,
+                         context=self.context)
+
+    def _setUpWidgets(self):
+        return self._setUpDisplayWidgets()
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/__init__.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/__init__.py	(revision 5330)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/test_objectwidget.py
===================================================================
--- /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 5330)
+++ /main/waeup.sirp/branches/henrik-experimental-layout/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 5330)
@@ -0,0 +1,219 @@
+"""Tests for objectwidget.
+"""
+import doctest
+import unittest
+import sys
+from zope.component import testing
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyClass
+from zope.publisher.browser import TestRequest
+from zope.schema import Object, TextLine
+from zope.schema.interfaces import ITextLine
+from zope.traversing.adapters import DefaultTraversable
+from zope.traversing.interfaces import ITraversable
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.component import provideAdapter
+
+from zope.formlib.interfaces import (
+    IInputWidget, IDisplayWidget,MissingInputError)
+from zope.formlib.widgets import TextWidget, DisplayWidget
+from zope.formlib.tests.test_browserwidget import BrowserWidgetTest
+from zope.formlib.interfaces import IWidgetInputErrorView
+
+from waeup.sirp.widgets.objectwidget import WAeUPObjectWidget as ObjectWidget
+from waeup.sirp.widgets.objectwidget import (
+    WAeUPObjectDisplayWidget as ObjectDisplayWidget)
+
+class ITestContact(Interface):
+    name = TextLine()
+    email = TextLine()
+    
+class TestContact(object):
+    implements(ITestContact)
+
+class ITestContent(Interface):
+    foo = Object(
+        ITestContact, 
+        title = u'Foo Title',
+        description = u'',
+        )
+    
+class ObjectWidgetInputErrorView(object):
+    implements(IWidgetInputErrorView)
+
+    def __init__(self, error, request):
+        self.error = error
+        self.request = request
+
+    def snippet(self):
+        return repr(self.error)
+
+class ObjectWidgetTest(BrowserWidgetTest):
+    """Documents and tests the object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IInputWidget, ObjectWidget)
+        True
+    """
+
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+
+    def test_applyChanges(self):
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+
+        self.assertEqual(widget.applyChanges(self.content), True)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_error(self):
+        provideAdapter(
+            ObjectWidgetInputErrorView,
+            (MissingInputError, TestRequest),
+            IWidgetInputErrorView)
+
+        widget = self._WidgetFactory(self.field, self.request)
+        self.assertRaises(MissingInputError, widget.getInputValue)
+        error_html = widget.error()
+        if sys.version_info < (2, 5):
+            self.failUnless("email: <zope.formlib.interfaces.Mis" 
+                             in error_html)
+            self.failUnless("name: <zope.formlib.interfaces.Miss"
+                             in error_html)
+        else:
+            self.failUnless(
+                "email: MissingInputError(u'field.foo.email', u'', None)"
+                in error_html)
+            self.failUnless(
+                "name: MissingInputError(u'field.foo.name', u'', None)"
+                in error_html)
+
+    def test_applyChangesNoChange(self):
+        self.content.foo = TestContact()
+        self.content.foo.name = u'Foo Name'
+        self.content.foo.email = u'foo@foo.test'
+
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+        widget.setRenderedValue(self.content.foo)
+
+        self.assertEqual(widget.applyChanges(self.content), False)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_respect_custom_widgets(self):
+        # We can use our own subwidgets when creating ObjectWidgets
+        class CustomTextWidget(TextWidget):
+            pass
+
+        # We create a custom widget for the `name` field of ITestContact
+        name_widget = CustomTextWidget(ITestContact['name'], self.request)
+
+        # Custom widgets are passed by <fieldname>_widget keyword:
+        widget = self._WidgetFactory(self.field, self.request,
+                                     name_widget=name_widget)
+        widget1, widget2 = widget.subwidgets()
+        self.assertTrue(widget1 is name_widget)
+        
+        
+class ObjectDisplayWidgetTest(BrowserWidgetTest):
+    """Documents and tests the display variant of object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IDisplayWidget, ObjectDisplayWidget)
+        True
+
+   """
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectDisplayWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget, (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+        # The widget must be traversable. We register the default
+        # adapter that can turn nearly any object into an ITraversable.
+        provideAdapter(DefaultTraversable, (None,), ITraversable)
+        
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.content.name = u'Foo Name'
+        self.content.email = u'foo@foo.test'
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectDisplayWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+
+        
+    def test_interfaces(self):
+        from zope.interface.verify import DoesNotImplement
+        self.assertTrue(IDisplayWidget.providedBy(self._widget))
+        self.assertFalse(IInputWidget.providedBy(self._widget))
+        self.assertTrue(verifyClass(IDisplayWidget, ObjectDisplayWidget))
+
+    def test_render(self):
+        widget = ObjectDisplayWidget(
+            self.field, self.request, TestContact)
+        widget.setRenderedValue(self.content)
+
+        check_list = [
+            '<td', 'class=', 'row',
+            'Foo Name',
+            '</td>', '<td', 'class=', 'row',
+            'foo@foo.test',
+            '</td>']
+        self.verifyResult(widget(), check_list, inorder=True)    
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(ObjectWidgetTest),
+        unittest.makeSuite(ObjectDisplayWidgetTest),
+        doctest.DocTestSuite(),
+        ))
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
