Index: /main/waeup.sirp/trunk/src/waeup/sirp/students/batching.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/students/batching.py	(revision 7535)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/students/batching.py	(revision 7536)
@@ -37,5 +37,6 @@
 from waeup.sirp.students.interfaces import (
     IStudent, IStudentStudyCourse,
-    IStudentUpdateByRegNo, IStudentUpdateByMatricNo)
+    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
+    IStudentStudyLevel)
 from waeup.sirp.students.workflow import  IMPORTABLE_STATES
 from waeup.sirp.utils.batching import BatchProcessor
@@ -268,5 +269,4 @@
         return self.getParent(row, site) is not None
 
-
     def entryExists(self, row, site):
         return self.getEntry(row, site) is not None
@@ -274,5 +274,5 @@
     def getEntry(self, row, site):
         student = self.getParent(row, site)
-        if not student:
+        if student is None:
             return None
         return student.get('studycourse')
@@ -306,2 +306,99 @@
         return errs, inv_errs, conv_dict
 
+class StudentStudyLevelProcessor(BatchProcessor):
+    """A batch processor for IStudentStudyLevel objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studylevelimporter'
+    grok.name(util_name)
+
+    name = u'StudentStudyLevel Importer'
+    iface = IStudentStudyLevel
+    factory_name = 'waeup.StudentStudyLevel'
+
+    location_fields = []
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number','level'] + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        if not 'reg_number' in headerfields and not 'student_id' \
+            in headerfields and not 'matric_number' in headerfields:
+            raise FatalCSVError(
+                "Need at least columns student_id " +
+                "or reg_number or matric_number for import!")
+        if not 'level' in headerfields:
+            raise FatalCSVError(
+                "Need level for import!")
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def getParent(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if 'student_id' in row.keys() and row['student_id']:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student['studycourse']
+        elif 'reg_number' in row.keys() and row['reg_number']:
+            reg_number = row['reg_number']
+            #import pdb; pdb.set_trace()
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]['studycourse']
+        elif 'matric_number' in row.keys() and row['matric_number']:
+            matric_number = row['matric_number']
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(matric_number=(matric_number, matric_number)))
+            if results:
+                return results[0]['studycourse']
+        return None
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def getEntry(self, row, site):
+        studycourse = self.getParent(row, site)
+        if studycourse is None:
+            return None
+        return studycourse.get(row['level'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        obj.level = int(row['level'])
+        parent[row['level']] = obj
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name)
+        # We have to check if level is a valid integer.
+        # This is not by the converter.
+        try:
+            level = int(row['level'])
+            if level not in range(0,600,10):
+                errs.append(('level','no valid integer'))
+        except ValueError:
+            errs.append(('level','no integer'))
+        return errs, inv_errs, conv_dict
Index: /main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py	(revision 7535)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/students/studycourse.py	(revision 7536)
@@ -72,5 +72,5 @@
 
 class StudentStudyCourseFactory(grok.GlobalUtility):
-    """A factory for students.
+    """A factory for student study courses.
     """
     grok.implements(IFactory)
Index: /main/waeup.sirp/trunk/src/waeup/sirp/students/studylevel.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/students/studylevel.py	(revision 7535)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/students/studylevel.py	(revision 7536)
@@ -21,4 +21,5 @@
 """
 import grok
+from zope.component.interfaces import IFactory
 from waeup.sirp.students.interfaces import (
     IStudentStudyLevel, IStudentNavigation, ICourseTicket)
@@ -58,4 +59,18 @@
 StudentStudyLevel = attrs_to_fields(StudentStudyLevel)
 
+class StudentStudyLevelFactory(grok.GlobalUtility):
+    """A factory for student study levels.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.StudentStudyLevel')
+    title = u"Create a new student study level.",
+    description = u"This factory instantiates new student study level instances."
+
+    def __call__(self, *args, **kw):
+        return StudentStudyLevel()
+
+    def getInterfaces(self):
+        return implementedBy(StudentStudyLevel)
+
 class CourseTicket(grok.Model):
     """This is a course ticket which allows the
Index: /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/sample_studylevel_data.csv
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/sample_studylevel_data.csv	(revision 7536)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/sample_studylevel_data.csv	(revision 7536)
@@ -0,0 +1,6 @@
+reg_number,matric_number,level_session,level_verdict,level
+1,,2008,A,100
+2,,2008,B,100
+,100002,2009,C,100
+1,,2008,A,111
+1,,2008,A,nonsense
Index: /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_batching.py
===================================================================
--- /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_batching.py	(revision 7535)
+++ /main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_batching.py	(revision 7536)
@@ -31,5 +31,6 @@
 from waeup.sirp.university.department import Department
 from waeup.sirp.students.batching import (
-    StudentProcessor, StudentStudyCourseProcessor)
+    StudentProcessor, StudentStudyCourseProcessor,
+    StudentStudyLevelProcessor)
 from waeup.sirp.students.student import Student
 from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
@@ -69,4 +70,11 @@
 
 STUDENT_HEADER_FIELDS_MIGRATION = STUDENT_SAMPLE_DATA_MIGRATION.split(
+    '\n')[0].split(',')
+
+STUDYLEVEL_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_studylevel_data.csv'),
+    'rb').read()
+
+STUDYLEVEL_HEADER_FIELDS = STUDYLEVEL_SAMPLE_DATA.split(
     '\n')[0].split(',')
 
@@ -212,4 +220,5 @@
         shutil.rmtree(os.path.dirname(fin_file))
 
+
 class StudentStudyCourseImporterTest(FunctionalTestCase):
 
@@ -218,17 +227,11 @@
     def setUp(self):
         super(StudentStudyCourseImporterTest, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
         app = University()
-        self.dc_root = tempfile.mkdtemp()
         app['datacenter'].setStoragePath(self.dc_root)
-
         self.getRootFolder()['app'] = app
         self.app = self.getRootFolder()['app']
         setSite(app)
-
-        self.workdir = tempfile.mkdtemp()
-        self.importer = StudentStudyCourseProcessor()
-        self.csv_file = os.path.join(
-            self.workdir, 'sample_studycourse_data.csv')
-        open(self.csv_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
 
         # Import students with subobjects
@@ -249,4 +252,9 @@
         self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
             self.certificate)
+
+        self.importer = StudentStudyCourseProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(self.csv_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
         return
 
@@ -283,9 +291,83 @@
         shutil.rmtree(os.path.dirname(fin_file))
 
+class StudentStudyLevelImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyLevelImporterTest, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.start_level = 200
+        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)
+
+        # Update study courses
+        studycourse_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(studycourse_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        importer = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = importer.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.importer = StudentStudyLevelProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studylevel_data.csv')
+        open(self.csv_file, 'wb').write(STUDYLEVEL_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(StudentStudyLevelImporterTest, 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, StudentStudyLevelProcessor) is True
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,2)
+        assert self.importer.entryExists(
+            dict(reg_number='1', level='100'), self.app) is True
+        studylevel = self.importer.getEntry(
+            dict(reg_number='1', level='100'), self.app)
+        self.assertEqual(studylevel.__parent__.certificate.code, u'CERT1')
+        self.assertEqual(studylevel.level_session, 2008)
+        self.assertEqual(studylevel.level_verdict, 'A')
+        self.assertEqual(studylevel.level, 100)
+        shutil.rmtree(os.path.dirname(fin_file))
+        
+
 def test_suite():
     suite = unittest.TestSuite()
     for testcase in [
         StudentImporterTest,StudentStudyCourseImporterTest,
-        ]:
+        StudentStudyLevelImporterTest,]:
         suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
                 testcase
@@ -293,2 +375,4 @@
         )
     return suite
+
+
