Index: main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- main/waeup.kofa/trunk/CHANGES.txt	(revision 15999)
+++ main/waeup.kofa/trunk/CHANGES.txt	(revision 16000)
@@ -4,4 +4,6 @@
 1.6.1.dev0 (unreleased)
 =======================
+
+* Add `unlock_score` field to `CourseTicketProcessor`.
 
 * Add `UpdateCourseTicketsView`.
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 15999)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/batching.py	(revision 16000)
@@ -637,5 +637,5 @@
     Like all other student data importers, this processor also requires
     either `student_id`, `reg_number` or `matric_number` to find the student.
-    Then  it needs `level` and `code` to localize the course ticket.
+    Then it needs `level` and `code` to localize the course ticket.
 
     `checkConversion` first searches the courses catalog for the imported
@@ -650,9 +650,14 @@
     nor by import. Other values can be overwritten by import.
 
-    `ticket_session` is an additional attribute which can be used to store the 
+    `ticket_session` is an additional field which can be used to store the
     session of the course when it was taken. Usually this information is 
     redundant because the parent study level object already contains this
     information, except for the study level zero container which can be used to
     store 'orphaned' course tickets.
+
+    `checkUpdateRequirements` ensures that the `score` attribute can't
+    be accidentally overwritten by import in update mode. The `score`
+    attribute can be unlocked by setting the boolean field
+    `unlock_score` = 1.
     """
     grok.implements(IBatchProcessor)
@@ -675,5 +680,6 @@
             'mandatory', 'score', 'carry_over', 'automatic',
             'outstanding', 'course_category', 'level_session',
-            'title', 'credits', 'passmark', 'semester', 'ticket_session'
+            'title', 'credits', 'passmark', 'semester', 'ticket_session',
+            'unlock_score'
             ] + self.additional_fields
         return sorted(fields)
@@ -735,4 +741,7 @@
         if obj.student.studycourse_locked:
             return 'Studycourse is locked.'
+        if row.get('score',None) and obj.score and not row.get(
+            'unlock_score',None):
+            return 'Score attribute is locked.'
         return None
 
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/sample_courseticket_data.csv
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 15999)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 16000)
@@ -1,10 +1,10 @@
-reg_number,matric_number,level,code,score,mandatory,level_session,ticket_session
-1,,100,COURSE1,1,True,,
-2,,100,COURSE1,2,False,,
-,100002,100,COURSE1,3,False,,
-1,,200,COURSE1,1,True,2008,
-1,,nonsense,COURSE1,5,,,
-1,,100,NONSENSE,5,,,
-1,,200,COURSE1,6,,2004,
-1,,300,COURSE1,6,,2008,
-1,,300,COURSE1,6,,2008X,
+reg_number,matric_number,level,code,score,mandatory,level_session,ticket_session,unlock_score
+1,,100,COURSE1,1,True,,,
+2,,100,COURSE1,2,False,,,
+,100002,100,COURSE1,3,False,,,
+1,,200,COURSE1,1,True,2008,,
+1,,nonsense,COURSE1,5,,,,
+1,,100,NONSENSE,5,,,,
+1,,200,COURSE1,6,,2004,,
+1,,300,COURSE1,6,,2008,,
+1,,300,COURSE1,6,,2008X,,
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 15999)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_batching.py	(revision 16000)
@@ -962,10 +962,10 @@
         self.assertEqual(num_warns,5)
         self.assertEqual(fail_file,
-            'reg_number,code,mandatory,level,level_session,ticket_session,score,matric_number,--ERRORS--\r\n'
-            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,Not all parents do exist yet.\r\n'
-            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,code: non-existent\r\n'
-            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,level_session: does not match 2008\r\n'
-            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,level object: does not exist\r\n'
-            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,level_session: Invalid value\r\n')
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\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')
         assert self.processor.entryExists(
             dict(reg_number='1', level='100', code='COURSE1'),
@@ -1015,10 +1015,10 @@
         self.assertEqual(num_warns,5)
         self.assertEqual(fail_file,
-            'reg_number,code,mandatory,level,level_session,ticket_session,score,matric_number,--ERRORS--\r\n'
-            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,Cannot update: no such entry\r\n'
-            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,code: non-existent\r\n'
-            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,level_session: does not match 2008\r\n'
-            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,level object: does not exist\r\n'
-            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,level_session: Invalid value\r\n')
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Cannot update: no such entry\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')
         shutil.rmtree(os.path.dirname(fin_file))
 
@@ -1057,12 +1057,12 @@
         self.assertEqual(num_warns,7)
         self.assertEqual(fail_file,
-            'reg_number,code,mandatory,level,level_session,ticket_session,score,matric_number,--ERRORS--\r\n'
-            '1,COURSE1,True,100,<IGNORE>,<IGNORE>,1,<IGNORE>,Studycourse is locked.\r\n'
-            '1,COURSE1,True,200,2008,<IGNORE>,1,<IGNORE>,Studycourse is locked.\r\n'
-            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,Cannot update: no such entry\r\n'
-            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,code: non-existent\r\n'
-            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,level_session: does not match 2008\r\n'
-            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,level object: does not exist\r\n'
-            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,level_session: Invalid value\r\n')
+            '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>,Cannot update: no such entry\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')
         shutil.rmtree(os.path.dirname(fin_file))
 
