Index: /main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- /main/waeup.kofa/trunk/CHANGES.txt	(revision 16011)
+++ /main/waeup.kofa/trunk/CHANGES.txt	(revision 16012)
@@ -4,4 +4,8 @@
 1.6.1.dev0 (unreleased)
 =======================
+
+* Implement `BatchProcessor.checkCreateRequirements` and
+  `BatchProcessor.checkUpdateRequirements` methods mainly
+  to protect course result lists of graduated student.
 
 * Do not allow course validation if no score has been entered.
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 16011)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 16012)
@@ -621,4 +621,11 @@
         return
 
+    def checkCreateRequirements(self, parent, row, site):
+        """
+        """
+        if parent.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
     def checkUpdateRequirements(self, obj, row, site):
         """
@@ -626,4 +633,11 @@
         if obj.student.studycourse_locked:
             return 'Studylevel is locked.'
+        return None
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
         return None
 
@@ -736,4 +750,11 @@
         return
 
+    def checkCreateRequirements(self, parent, row, site):
+        """
+        """
+        if parent.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
     def checkUpdateRequirements(self, obj, row, site):
         """
@@ -744,4 +765,11 @@
             'unlock_score',None):
             return 'Score attribute is locked.'
+        return None
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
         return None
 
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_batching.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_batching.py	(revision 16011)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_batching.py	(revision 16012)
@@ -496,8 +496,8 @@
         num, num_warns, fin_file, fail_file = self.processor.doImport(
             self.csv_file, STUDENT_HEADER_FIELDS)
-        shutil.rmtree(os.path.dirname(fin_file))
         self.assertRaises(
             FatalCSVError, self.processor.doImport, self.csv_file_update4,
             STUDENT_HEADER_FIELDS_UPDATE4, 'update')
+        shutil.rmtree(os.path.dirname(fin_file))
 
     def test_import_remove(self):
@@ -891,5 +891,4 @@
             dict(reg_number='1', level='100'), self.app) is False
         self.assertEqual(num_warns,3)
-
         shutil.rmtree(os.path.dirname(fin_file))
 
@@ -1009,5 +1008,4 @@
         num, num_warns, fin_file, fail_file = self.processor.doImport(
             self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
-        shutil.rmtree(os.path.dirname(fin_file))
         num, num_warns, fin_file, fail_file = self.processor.doImport(
             self.csv_file, COURSETICKET_HEADER_FIELDS,'update')
@@ -1065,4 +1063,59 @@
             '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
             '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_create_locked(self):
+        # In state 'transcript validated' course tickets can't be created
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        fail_file = open(fail_file).read()
+        self.assertEqual(num_warns,7)
+        self.assertEqual(fail_file,
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,True,100,<IGNORE>,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,True,200,2008,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Not all parents do exist yet.\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        self.assertFalse(self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'),
+            self.app))
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove_locked(self):
+        # We perform the same import twice,
+        # the second time in remove mode. 
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        # In state 'transcript validated' course tickets can't be removed
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,7)
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        logcontent = open(self.logfile).read()
+        self.assertFalse(
+            'INFO - system - X666666 - Course ticket in 100 removed: COURSE1'
+            in logcontent)
+        fail_file = open(fail_file).read()
+        self.assertEqual(fail_file,
+            'reg_number,code,matric_number,level,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,100,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,200,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,Cannot remove: no such entry\r\n'
+            '1,NONSENSE,<IGNORE>,100,Cannot remove: no such entry\r\n'
+            '1,COURSE1,<IGNORE>,200,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,300,Cannot remove: no such entry\r\n'
+            '1,COURSE1,<IGNORE>,300,Cannot remove: no such entry\r\n'
+            )
         shutil.rmtree(os.path.dirname(fin_file))
 
@@ -1374,5 +1427,4 @@
             '... INFO - system - X666666 - Returned...',
             logcontent)
-
         shutil.rmtree(os.path.dirname(fin_file))
 
Index: /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py	(revision 16011)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py	(revision 16012)
@@ -196,8 +196,30 @@
         raise NotImplementedError('method not implemented')
 
+    def checkCreateRequirements(self, parent, row, site):
+        """Checks requirements the parent object must fulfill when
+        a new subobject is being created.
+
+        This method is not used in case of updating or removing objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
     def checkUpdateRequirements(self, obj, row, site):
         """Checks requirements the object must fulfill when being updated.
 
         This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being removed.
+
+        This method is not used in case of updating or adding objects.
 
         Returns error messages as strings in case of requirement
@@ -329,4 +351,11 @@
            record is stored in the pending data file.
 
+           The `BatchProcessor.checkCreateRequirements` method checks additional
+           requirements the parent object must fulfill before a new sububject
+           is being added. These requirements are not imposed by the data
+           type but the context of the object. For example, the course results
+           of graduated students must not changed by import, neither by 
+           creating nor updating or removing course tickets.
+
            Now `doImport` tries to add the new object with the data
            from the conversion dictionary. In some cases this
@@ -359,4 +388,11 @@
            a ``no such entry`` warning message is raised and a record is
            stored in the pending data file.
+
+           The `BatchProcessor.checkRemoveRequirements` method checks additional
+           requirements the object must fulfill before being removed.
+           These requirements are not imposed by the data type but the context
+           of the object. For example, the course results of graduated students
+           must not changed by import, neither by creating nor updating or
+           removing course tickets.
 
            Finally, `doImport` removes the existing object.
@@ -425,4 +461,11 @@
                         "This object already exists.")
                     continue
+                parent = self.getParent(row, site)
+                create_errors = self.checkCreateRequirements(parent, row, site)
+                if create_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, create_errors)
+                    continue
                 obj = self.callFactory()
                 # Override all values in row, also
@@ -453,4 +496,11 @@
                         failed_writer, string_row,
                         "Cannot remove: no such entry")
+                    continue
+                obj = self.getEntry(row, site)
+                remove_errors = self.checkRemoveRequirements(obj, row, site)
+                if remove_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, remove_errors)
                     continue
                 self.delEntry(row, site)
