Index: main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py	(revision 13909)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py	(revision 13937)
@@ -17,4 +17,6 @@
 ##
 import grok
+import csv
+from cStringIO import StringIO
 from zope.i18n import translate
 from zope.component import getUtility, queryUtility
@@ -539,7 +541,25 @@
     grok.template('editscorespage')
 
+
+    def _extract_uploadfile(self, uploadfile):
+        """Get a mapping of student-ids to scores.
+
+        The mapping is constructed by reading contents from `uploadfile`.
+
+        We expect uploadfile to be a regular CSV file with columns
+        ``student_id``, ``score`` and ``ca`` (other cols are ignored).
+        """
+        result = dict()
+        data = StringIO(uploadfile.read())  # ensure we have something seekable
+        reader = csv.DictReader(data)
+        for row in reader:
+            if not ('student_id' in row and 'score' in row and 'ca' in row):
+                continue
+            result[row['student_id']] = (row['score'], row['ca'])
+        return result
+
     def update(self,  *args, **kw):
         form = self.request.form
-        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
         self.current_academic_session = grok.getSite()[
             'configuration'].current_academic_session
@@ -559,52 +579,58 @@
             self.redirect(self.url(self.context))
             return
-        if 'UPDATE' in form:
-            error = ''
-            if not editable_tickets:
+        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
+            return
+
+        if not editable_tickets:
+            return
+        if 'UPDATE_FILE' in form:
+            if form['uploadfile']:
+                try:
+                    formvals = self._extract_uploadfile(form['uploadfile'])
+                except:
+                    self.flash(
+                        _('Uploaded file contains illegal data. Ignored'),
+                        type="danger")
+                    return
+            else:
+                self.flash(
+                    _('No file provided.'), type="danger")
                 return
-            scores = form['scores']
-            cas = form['cas']
-            sids = form['sids']
-            if isinstance(scores, basestring):
-                scores = [scores]
-            if isinstance(cas, basestring):
-                cas = [cas]
-            if isinstance(sids, basestring):
-                sids = [sids]
-            formvals = dict([(sids[i], (scores[i], cas[i]))
-                             for i in range(len(sids))])
-            for ticket in editable_tickets:
-                ticket_error = False
-                score = ticket.score
-                ca = ticket.ca
-                sid = ticket.student.student_id
-                if formvals[sid][0] == '':
-                    score = None
-                if formvals[sid][1] == '':
-                    ca = None
-                try:
-                    if formvals[sid][0]:
-                        score = int(formvals[sid][0])
-                    if formvals[sid][1]:
-                        ca = int(formvals[sid][1])
-                except ValueError:
-                    error += '%s, ' % ticket.student.display_fullname
-                    ticket_error = True
-                if not ticket_error and ticket.score != score:
-                    ticket.score = score
-                    ticket.student.__parent__.logger.info(
-                        '%s - %s %s/%s score updated (%s)' %
-                        (ob_class, ticket.student.student_id,
-                         ticket.level, ticket.code, score))
-                if not ticket_error and ticket.ca != ca:
-                    ticket.ca = ca
-                    ticket.student.__parent__.logger.info(
-                        '%s - %s %s/%s ca updated (%s)' %
-                        (ob_class, ticket.student.student_id,
-                         ticket.level, ticket.code, ca))
-            if error:
-                self.flash(_('Error: Score(s) and CA(s) of %s have not be updated. '
-                  'Only integers are allowed.' % error.strip(', ')),
-                  type="danger")
+        else:
+            formvals = dict(zip(form['sids'], zip(form['scores'], form['cas'])))
+        error = ''
+        for ticket in editable_tickets:
+            ticket_error = False
+            score = ticket.score
+            ca = ticket.ca
+            sid = ticket.student.student_id
+            if formvals[sid][0] == '':
+                score = None
+            if formvals[sid][1] == '':
+                ca = None
+            try:
+                if formvals[sid][0]:
+                    score = int(formvals[sid][0])
+                if formvals[sid][1]:
+                    ca = int(formvals[sid][1])
+            except ValueError:
+                error += '%s, ' % ticket.student.display_fullname
+                ticket_error = True
+            if not ticket_error and ticket.score != score:
+                ticket.score = score
+                ticket.student.__parent__.logger.info(
+                    '%s - %s %s/%s score updated (%s)' %
+                    (ob_class, ticket.student.student_id,
+                     ticket.level, ticket.code, score))
+            if not ticket_error and ticket.ca != ca:
+                ticket.ca = ca
+                ticket.student.__parent__.logger.info(
+                    '%s - %s %s/%s ca updated (%s)' %
+                    (ob_class, ticket.student.student_id,
+                     ticket.level, ticket.code, ca))
+        if error:
+            self.flash(_('Error: Score(s) and CA(s) of %s have not be updated. '
+              'Only integers are allowed.' % error.strip(', ')),
+              type="danger")
         return
 
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/editscorespage.pt
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/editscorespage.pt	(revision 13909)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/editscorespage.pt	(revision 13937)
@@ -1,8 +1,56 @@
-<form  i18n:domain="waeup.kofa"  method="POST">
+<p  i18n:domain="waeup.kofa" i18n:translate="edit_scores_info">
+  This page helps you to update your students' course results.
+  You can either update scores and CAs by importing a csv file
+  (press 'Help' for further information) or by
+  changing score or CA values in the table below. Only scores and CAs
+  of students in state 'courses validated' and current academic session
+  can be modified.
+</p>
+
+<form  i18n:domain="waeup.kofa"  method="POST" enctype="multipart/form-data">
   <br />
+  <div class="input-group">
+    <div class="input-group-btn">
+      <div class="btn btn-default btn-file" i18n:translate="">
+        Select file&hellip;
+        <input type="file" name="uploadfile:file" />
+      </div>
+    </div>
+    <input type="text" class="form-control" readonly>
+    <div class="input-group-btn">
+      <input type="submit" name="UPDATE_FILE" i18n:translate=""
+             value="Update editable scores from csv file"
+             class="btn btn-primary" />
+      <button class="btn btn-warning" data-toggle="modal"
+              data-target="#myModal" i18n:translate="">
+        Help
+      </button>
+    </div>
+  </div>
+
+  <!-- Modal -->
+  <div class="modal fade" id="myModal" tabindex="-1" role="dialog"
+       aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+      <div class="modal-content">
+        <div class="modal-header">
+          <button type="button" class="close" data-dismiss="modal"
+                  aria-hidden="true">&times;</button>
+          <h4 class="modal-title" id="myModalLabel" i18n:translate="">
+            Instructions for File Upload
+          </h4>
+        </div>
+        <div class="modal-body" i18n:translate="lecturer_help">
+          ...
+        </div>
+      </div><!-- /.modal-content -->
+    </div><!-- /.modal-dialog -->
+  </div><!-- /.modal -->
+
+  <br /><br />
 
   <table class="kofa-data-table dataTable">
     <thead>
-  	<tr>
+    <tr>
       <th i18n:translate="">Matric No.</th>
       <th i18n:translate="">Fullname</th>
@@ -12,22 +60,22 @@
       <th i18n:translate="">Score</th>
       <th i18n:translate="">CA</th>
-  	</tr>
+    </tr>
     </thead>
     <tbody>
-  	<tr tal:repeat="ticket view/tickets">
+    <tr tal:repeat="ticket view/tickets">
       <td tal:content="ticket/student/matric_number">MATRIC_NUMBER</td>
-  	  <td tal:content="ticket/student/display_fullname">FULLNAME</td>
+      <td tal:content="ticket/student/display_fullname">FULLNAME</td>
       <td tal:content="ticket/student/translated_state">STATE</td>
       <td tal:content="ticket/student/certcode">CERTCODE</td>
       <td tal:content="ticket/level">LEVEL</td>
-  	  <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
-          <input type="text" name="scores" class="form-control"
-  	             tal:attributes="value ticket/score" />
-          <input type="hidden" name="sids"
+      <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
+          <input type="text" name="scores:list" class="form-control"
+                 tal:attributes="value ticket/score" />
+          <input type="hidden" name="sids:list"
                  tal:attributes="value ticket/student/student_id" />
       </td>
-  	  <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
-          <input type="text" name="cas" class="form-control"
-  	             tal:attributes="value ticket/ca" />
+      <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
+          <input type="text" name="cas:list" class="form-control"
+                 tal:attributes="value ticket/ca" />
       </td>
       <td tal:condition="not: ticket/editable_by_lecturer"
@@ -35,10 +83,10 @@
       <td tal:condition="not: ticket/editable_by_lecturer"
           tal:content="ticket/ca">CA</td>
-  	</tr>
+    </tr>
     </tbody>
   </table>
 
-  <input type="submit" name="UPDATE"
-         value="Update scores" class="btn btn-primary"
+  <input type="submit" name="UPDATE_TABLE" i18n:translate=""
+         value="Update scores from table" class="btn btn-primary"
          />
 </form>
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/studyleveleditpage.pt
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/studyleveleditpage.pt	(revision 13909)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/studyleveleditpage.pt	(revision 13937)
@@ -27,5 +27,5 @@
        <td>
         <input type="checkbox" name="val_id"
-  		  tal:attributes="value value/__name__"
+        tal:attributes="value value/__name__"
         tal:condition="value/removable_by_student" />
       </td>
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_browser.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_browser.py	(revision 13909)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_browser.py	(revision 13937)
@@ -21,4 +21,5 @@
 import pytz
 import grok
+from StringIO import StringIO
 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
 from zope.securitypolicy.interfaces import IPrincipalRoleManager
@@ -31,4 +32,5 @@
 from zope.catalog.interfaces import ICatalog
 from waeup.kofa.app import University
+from waeup.kofa.interfaces import VALIDATED
 from waeup.kofa.students.tests.test_browser import StudentsFullSetup
 from waeup.kofa.students.accommodation import BedTicket
@@ -72,8 +74,45 @@
         return
 
+UPLOAD_CSV_TEMPLATE = (
+    'matric_number,student_id,display_fullname,level,code,level_session,'
+    'score,ca\r\n'
+    '234,E1000000,Anna Tester,100,COURSE1,2004,%s,%s\r\n')
+
 class OfficerUITests(StudentsFullSetup):
     # Tests for Student class views and pages
 
     layer = FunctionalLayer
+
+    def login_as_lecturer(self):
+        self.app['users'].addUser('mrslecturer', 'mrslecturersecret')
+        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
+        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
+        # Add course ticket
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2004
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        # Assign local Lecturer role for a certificate.
+        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        prmlocal = IPrincipalRoleManager(course)
+        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
+        notify(LocalRoleSetEvent(
+            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
+        # Login as lecturer.
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrslecturer'
+        self.browser.getControl(
+            name="form.password").value = 'mrslecturersecret'
+        self.browser.getControl("Login").click()
+        # Store reused urls/paths
+        self.course_url = (
+            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1')
+        self.edit_scores_url = '%s/edit_scores' % self.course_url
+        # Set standard parameters
+        self.app['configuration'].current_academic_session = 2004
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        IWorkflowState(self.student).setState(VALIDATED)
+
 
     def test_gpa_calculation(self):
@@ -281,4 +320,20 @@
         return
 
+    def test_scores_csv_upload_available(self):
+        # lecturers can upload a CSV file to set values.
+        self.login_as_lecturer()
+        # set value to change from
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        upload_ctrl = self.browser.getControl(name='uploadfile:file')
+        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % ('65','77'))
+        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
+        self.browser.getControl("Update editable scores from").click()
+        # value changed
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 65)
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].ca, 77)
+
 class StudentUITests(StudentsFullSetup):
     """Tests for customized student class views and pages
@@ -688,9 +743,9 @@
             "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
         self.assertTrue(
-            'input type="text" name="scores"'
-            in self.browser.contents)
-        self.browser.getControl(name="scores", index=0).value = '55'
-        self.browser.getControl(name="cas", index=0).value = '66'
-        self.browser.getControl("Update scores").click()
+            'input type="text" name="scores:list"'
+            in self.browser.contents)
+        self.browser.getControl(name="scores:list", index=0).value = '55'
+        self.browser.getControl(name="cas:list", index=0).value = '66'
+        self.browser.getControl("Update scores from").click()
         # New score and ca has been set.
         self.assertEqual(
@@ -710,5 +765,5 @@
             "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
         self.assertTrue('value="55" />' in self.browser.contents)
-        self.browser.getControl(name="scores", index=0).value = 'abc'
+        self.browser.getControl(name="scores:list", index=0).value = 'abc'
         self.browser.getControl("Update scores").click()
         self.assertTrue('Error: Score(s) and CA(s) of Anna Tester have not be updated.'
@@ -717,5 +772,5 @@
         self.browser.open(
             "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
-        self.browser.getControl(name="scores", index=0).value = ''
+        self.browser.getControl(name="scores:list", index=0).value = ''
         self.browser.getControl("Update scores").click()
         self.assertEqual(
