Index: main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 8231)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 8232)
@@ -91,9 +91,9 @@
 
     def getLocator(self, row):
-        if row.get('student_id',None):
+        if row.get('student_id',None) not in (None, IGNORE_MARKER):
             return 'student_id'
-        elif row.get('reg_number',None):
+        elif row.get('reg_number',None) not in (None, IGNORE_MARKER):
             return 'reg_number'
-        elif row.get('matric_number',None):
+        elif row.get('matric_number',None) not in (None, IGNORE_MARKER):
             return 'matric_number'
         else:
@@ -155,15 +155,16 @@
 
         # Remove student_id from row if empty
-        if row.has_key('student_id') and row['student_id'] is None:
+        if row.has_key('student_id') and row['student_id'] in (
+            None, IGNORE_MARKER):
             row.pop('student_id')
 
         # Update password
-        passwd = row.get('password', '')
-        if passwd:
+        passwd = row.get('password', IGNORE_MARKER)
+        if passwd not in ('', IGNORE_MARKER):
             IUserAccount(obj).setPassword(passwd)
             row.pop('password')
 
         # Update registration state
-        if 'reg_state' in row.keys():
+        if 'reg_state' in row.keys(): # and row['reg_state'] not in (IGNORE_MARKER, ''):
             value = row['reg_state']
             IWorkflowState(obj).setState(value)
@@ -225,23 +226,23 @@
         return errs, inv_errs, conv_dict
 
-class StudentStudyCourseProcessor(BatchProcessor):
-    """A batch processor for IStudentStudyCourse objects.
-    """
-    grok.implements(IBatchProcessor)
-    grok.provides(IBatchProcessor)
-    grok.context(Interface)
-    util_name = 'studycourseupdater'
-    grok.name(util_name)
-
-    name = u'StudentStudyCourse Processor (update only)'
-    iface = IStudentStudyCourse
-    factory_name = 'waeup.StudentStudyCourse'
-
-    location_fields = []
+
+class StudentProcessorBase(BatchProcessor):
+    """A base for student subitem processor.
+
+    Helps reducing redundancy.
+    """
+    grok.baseclass()
+
+    #: required fields beside 'student_id', 'reg_number' and 'matric_number'
+    additional_fields = []
+
+    #: header fields additional required
+    additional_headers = []
 
     @property
     def available_fields(self):
-        return sorted(list(set(
-            ['student_id','reg_number','matric_number'] + getFields(
+        fields = ['student_id','reg_number','matric_number'
+                  ] + self.additional_fields
+        return sorted(list(set(fields + getFields(
                 self.iface).keys())))
 
@@ -252,4 +253,9 @@
                 "Need at least columns student_id " +
                 "or reg_number or matric_number for import!")
+        for name in self.additional_headers:
+            if not name in headerfields:
+                raise FatalCSVError(
+                    "Need %s for import!" % name)
+
         # Check for fields to be ignored...
         not_ignored_fields = [x for x in headerfields
@@ -260,5 +266,5 @@
         return True
 
-    def getParent(self, row, site):
+    def _getStudent(self, row, site):
         NON_VALUES = ['', IGNORE_MARKER]
         if not 'students' in site.keys():
@@ -285,8 +291,37 @@
 
     def parentsExist(self, row, site):
+        result = self.getParent(row, site) is not None
         return self.getParent(row, site) is not None
 
     def entryExists(self, row, site):
         return self.getEntry(row, site) is not None
+
+    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, mode=mode)
+        return errs, inv_errs, conv_dict
+
+
+class StudentStudyCourseProcessor(StudentProcessorBase):
+    """A batch processor for IStudentStudyCourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studycourseupdater'
+    grok.name(util_name)
+
+    name = u'StudentStudyCourse Processor (update only)'
+    iface = IStudentStudyCourse
+    factory_name = 'waeup.StudentStudyCourse'
+
+    location_fields = []
+    additional_fields = []
+
+    def getParent(self, row, site):
+        return self._getStudent(row, site)
 
     def getEntry(self, row, site):
@@ -312,7 +347,6 @@
         """Validates all values in row.
         """
-        converter = IObjectConverter(self.iface)
-        errs, inv_errs, conv_dict =  converter.fromStringDict(
-            row, self.factory_name, mode=mode)
+        errs, inv_errs, conv_dict = super(
+            StudentStudyCourseProcessor, self).checkConversion(row, mode=mode)
         # We have to check if current_level is in range of certificate.
         if conv_dict.has_key('certificate'):
@@ -323,5 +357,5 @@
         return errs, inv_errs, conv_dict
 
-class StudentStudyLevelProcessor(BatchProcessor):
+class StudentStudyLevelProcessor(StudentProcessorBase):
     """A batch processor for IStudentStudyLevel objects.
     """
@@ -337,56 +371,12 @@
 
     location_fields = []
-
-    @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
+    additional_fields = ['level']
+    additional_headers = ['level']
 
     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']
-            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
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['studycourse']
 
     def getEntry(self, row, site):
@@ -405,7 +395,7 @@
         """Validates all values in row.
         """
-        converter = IObjectConverter(self.iface)
-        errs, inv_errs, conv_dict =  converter.fromStringDict(
-            row, self.factory_name, mode=mode)
+        errs, inv_errs, conv_dict = super(
+            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
+
         # We have to check if level is a valid integer.
         # This is not done by the converter.
@@ -418,5 +408,5 @@
         return errs, inv_errs, conv_dict
 
-class CourseTicketProcessor(BatchProcessor):
+class CourseTicketProcessor(StudentProcessorBase):
     """A batch processor for ICourseTicket objects.
     """
@@ -432,59 +422,12 @@
 
     location_fields = []
-
-    @property
-    def available_fields(self):
-        return sorted(list(set(
-            ['student_id','reg_number','matric_number','level',
-             'code'] + 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!")
-        if not 'code' in headerfields:
-            raise FatalCSVError(
-                "Need code 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
+    additional_fields = ['level', 'code']
+    additional_headers = ['level', 'code']
 
     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'].get(row['level'])
-        elif 'reg_number' in row.keys() and row['reg_number']:
-            reg_number = row['reg_number']
-            cat = queryUtility(ICatalog, name='students_catalog')
-            results = list(
-                cat.searchResults(reg_number=(reg_number, reg_number)))
-            if results:
-                return results[0]['studycourse'].get(row['level'])
-        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'].get(row['level'])
-        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
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['studycourse'].get(row['level'])
 
     def getEntry(self, row, site):
@@ -510,7 +453,7 @@
         """Validates all values in row.
         """
-        converter = IObjectConverter(self.iface)
-        errs, inv_errs, conv_dict =  converter.fromStringDict(
-            row, self.factory_name, mode=mode)
+        errs, inv_errs, conv_dict = super(
+            CourseTicketProcessor, self).checkConversion(row, mode=mode)
+
         # We have to check if course really exists.
         # This is not done by the converter.
@@ -522,5 +465,5 @@
         return errs, inv_errs, conv_dict
 
-class StudentOnlinePaymentProcessor(BatchProcessor):
+class StudentOnlinePaymentProcessor(StudentProcessorBase):
     """A batch processor for IStudentOnlinePayment objects.
     """
@@ -536,56 +479,16 @@
 
     location_fields = []
-
-    @property
-    def available_fields(self):
-        return sorted(list(set(
-            ['student_id','reg_number','matric_number','p_id'] + 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 'p_id' in headerfields:
-            raise FatalCSVError(
-                "Need p_id 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
+    additional_fields = ['p_id']
+    additional_headers = ['p_id']
+
+    def parentsExist(self, row, site):
+        result = self.getParent(row, site) is not None
+        return self.getParent(row, site) is not None
 
     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['payments']
-        elif 'reg_number' in row.keys() and row['reg_number']:
-            reg_number = row['reg_number']
-            cat = queryUtility(ICatalog, name='students_catalog')
-            results = list(
-                cat.searchResults(reg_number=(reg_number, reg_number)))
-            if results:
-                return results[0]['payments']
-        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]['payments']
-        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
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['payments']
 
     def getEntry(self, row, site):
@@ -618,7 +521,7 @@
         """Validates all values in row.
         """
-        converter = IObjectConverter(self.iface)
-        errs, inv_errs, conv_dict =  converter.fromStringDict(
-            row, self.factory_name, mode=mode)
+        errs, inv_errs, conv_dict = super(
+            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
+
         # We have to check p_id.
         p_id = row['p_id'].strip('#')
@@ -646,10 +549,5 @@
     factory_name = 'waeup.StudentStudyCourse'
 
-    @property
-    def available_fields(self):
-        return sorted(list(set(
-            ['student_id','reg_number','matric_number',
-                'current_session', 'current_level'] + getFields(
-                self.iface).keys())))
+    additional_fields = ['current_session', 'current_level']
 
     def checkUpdateRequirements(self, obj, row, site):
@@ -685,10 +583,2 @@
         notify(grok.ObjectModifiedEvent(obj.__parent__))
         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, mode=mode)
-        return errs, inv_errs, conv_dict
