Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/batching.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/batching.py	(revision 7262)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/batching.py	(revision 7263)
@@ -66,5 +66,5 @@
         return
 
-class ApplicantProcessor(BatchProcessor):
+class ApplicantImporter(BatchProcessor):
     """A batch processor for IApplicant objects.
     """
@@ -118,5 +118,5 @@
     def delEntry(self, row, site):
         applicant = self.entryExists(row, site)
-        if applicant:
+        if applicant is not None:
             parent = self.getParent(row, site)
             del parent[row['application_number']]
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py	(revision 7262)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py	(revision 7263)
@@ -931,5 +931,5 @@
     form_fields = grok.AutoFields(IApplicantEdit).omit(
         'locked', 'course_admitted', 'student_id',
-        'screening_score', 'applicant_id'
+        'screening_score', 'applicant_id', 'reg_no'
         )
     form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7262)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py	(revision 7263)
@@ -22,7 +22,10 @@
 
 from zope import schema
-from zope.interface import Interface, Attribute
-from zope.component import getUtilitiesFor
+from zope.interface import Interface, Attribute, implements, directlyProvides
+from zope.component import getUtilitiesFor, queryUtility
+from zope.catalog.interfaces import ICatalog
+from zope.schema.interfaces import ValidationError, ISource, IContextSourceBinder
 from zc.sourcefactory.basic import BasicSourceFactory
+from waeup.sirp.schema import TextLineChoice
 from waeup.sirp.interfaces import (
     IWAeUPObject, year_range, validate_email, academic_sessions_vocab)
@@ -40,4 +43,40 @@
 MAX_UPLOAD_SIZE = 1024 * 20
 
+class RegNumInSource(ValidationError):
+    """Registration number exists already
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class RegNumberSource(object):
+    implements(ISource)
+    cat_name = 'applicants_catalog'
+    field_name = 'reg_no'
+    validation_error = RegNumInSource
+    def __init__(self, context):
+        self.context = context
+        return
+
+    def __contains__(self, value):
+        cat = queryUtility(ICatalog, self.cat_name)
+        if cat is None:
+            return True
+        kw = {self.field_name: (value, value)}
+        results = cat.searchResults(**kw)
+        for entry in results:
+            if entry.applicant_id != self.context.applicant_id:
+                # XXX: sources should simply return False.
+                #      But then we get some stupid error message in forms
+                #      when validation fails.
+                raise self.validation_error(value)
+                #return False
+        return True
+
+def contextual_reg_num_source(context):
+    source = RegNumberSource(context)
+    return source
+directlyProvides(contextual_reg_num_source, IContextSourceBinder)
+
 class ApplicantContainerProviderSource(BasicSourceFactory):
     """A source offering all available applicants container types.
@@ -259,8 +298,10 @@
         readonly = False,
         )
-    reg_no = schema.TextLine(
+    reg_no = TextLineChoice(
         title = u'JAMB Registration Number',
-        readonly = True,
-        required = False,
+        readonly = False,
+        required = True,
+        default = None,
+        source = contextual_reg_num_source,
         )
     access_code = schema.TextLine(
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data.csv
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data.csv	(revision 7263)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data.csv	(revision 7263)
@@ -0,0 +1,4 @@
+container_code,reg_no,firstname,lastname,date_of_birth,email,course1
+dp2011,1001,Aaren,Pieri,1990-01-02,xx@yy.zz,CERT1
+dp2011,1002,Aaren,Finau,1990-01-03,xx@yy.zz,CERT1
+dp2011,1003,Aaren,Berson,1990-01-04,xx@yy.zz,CERT1
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv	(revision 7263)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv	(revision 7263)
@@ -0,0 +1,4 @@
+container_code,reg_no,firstname
+dp2011,1001,Aaren
+dp2011,1002,Alfons
+dp2011,1003,Abraham
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_batching.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_batching.py	(revision 7262)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_batching.py	(revision 7263)
@@ -23,9 +23,14 @@
 import unittest
 from zope.component.hooks import setSite, clearSite
+from zope.component import createObject
 from zope.interface.verify import verifyClass, verifyObject
 
 from waeup.sirp.app import University
-from waeup.sirp.applicants.batching import ApplicantsContainerImporter
+from waeup.sirp.applicants.batching import (
+    ApplicantsContainerImporter, ApplicantImporter)
 from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.applicant import Applicant
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
 from waeup.sirp.interfaces import IBatchProcessor
@@ -39,4 +44,19 @@
 # The header fields of the above CSV snippet
 APPS_CONTAINER_HEADER_FIELDS = APPS_CONTAINER_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+# The same for students
+APPLICANT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_applicant_data.csv'),
+    'rb').read()
+
+APPLICANT_HEADER_FIELDS = APPLICANT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+APPLICANT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_applicant_data_update.csv'),
+    'rb').read()
+
+APPLICANT_HEADER_FIELDS_UPDATE = APPLICANT_SAMPLE_DATA_UPDATE.split(
     '\n')[0].split(',')
 
@@ -119,12 +139,117 @@
         shutil.rmtree(os.path.dirname(fin_file))
 
-def test_suite():
-    suite = unittest.TestSuite()
-    for testcase in [
-        ApplicantsContainerImporterTest,
-        ]:
-        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
-                testcase
-                )
-        )
-    return suite
+class ApplicantImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantImporterTest, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Add an applicants container
+        self.container = ApplicantsContainer()
+        self.container.code = u'dp2011'
+        self.app['applicants']['dp2011'] = self.container
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.start_level = 100
+        self.certificate.end_level = 500
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+
+        # Add applicant with subobjects
+        applicant = Applicant()
+        applicant.firstname = u'Anna'
+        applicant.firstname = u'Tester'
+        self.app['applicants']['dp2011'].addApplicant(applicant)
+        self.application_number = applicant.application_number
+        self.applicant = self.app['applicants']['dp2011'][
+            self.application_number]
+        self.importer = ApplicantImporter()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sample_applicant_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_applicant_data_update.csv')
+        open(self.csv_file, 'wb').write(APPLICANT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(APPLICANT_SAMPLE_DATA_UPDATE)
+
+    def tearDown(self):
+        super(ApplicantImporterTest, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.importer) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantImporter) is True
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+                dict(container_code='dp2011', application_number='999'),
+                self.app) is None
+
+    def test_getEntry(self):
+        applicant = self.importer.getEntry(
+                dict(container_code='dp2011',
+                     application_number=self.application_number), self.app)
+        self.assertEqual(applicant.applicant_id, self.applicant.applicant_id)
+
+    def test_addEntry(self):
+        new_applicant = Applicant()
+        self.importer.addEntry(
+            new_applicant, dict(container_code='dp2011'), self.app)
+        assert len(self.app['applicants']['dp2011'].keys()) == 2
+
+    def test_delEntry(self):
+        assert self.application_number in self.app['applicants']['dp2011'].keys()
+        self.importer.delEntry(
+            dict(container_code='dp2011',
+                application_number=self.application_number), self.app)
+        assert self.application_number not in self.app['applicants']['dp2011'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        assert len(self.app['applicants']['dp2011'].keys()) == 4
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, 'remove')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+
Index: main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7262)
+++ main/waeup.sirp/trunk/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7263)
@@ -118,4 +118,7 @@
         # Add an applicant
         self.applicant = Applicant()
+        # reg_no is the only field which has to be preset here
+        # because managers are allowed to edit this required field
+        self.applicant.reg_no = u'1234'
         app['applicants']['app2009'].addApplicant(self.applicant)
         IUserAccount(
