Index: main/waeup.kofa/trunk/src/waeup/kofa/browser/viewlets.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/browser/viewlets.py	(revision 13934)
+++ main/waeup.kofa/trunk/src/waeup/kofa/browser/viewlets.py	(revision 13935)
@@ -18,48 +18,55 @@
 import grok
 from urllib import urlencode
+from zope.component import getMultiAdapter, queryAdapter, getUtility
 from zope.i18n import translate
-from zope.component import getMultiAdapter, queryAdapter, getUtility
 from zope.interface import Interface
 from zope.location.interfaces import ISite
 from zope.traversing.browser import absoluteURL
+from waeup.kofa.browser.interfaces import (
+    IFacultiesContainer, IFaculty, IDepartment, ICourse, ICertificate,
+    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUsersContainer)
+from waeup.kofa.browser.layout import KofaPage, default_primary_nav_template
 from waeup.kofa.browser.pages import (
     UniversityPage, FacultiesContainerPage, DatacenterPage, FacultyPage,
     DepartmentPage, CoursePage, CertificatePage, CertificateCoursePage,
     UsersContainerPage, UserManageFormPage, DatacenterImportStep1)
-from waeup.kofa.students.browser import EditScoresPage
-from waeup.kofa.browser.interfaces import (
-    IFacultiesContainer, IFaculty, IDepartment, ICourse, ICertificate,
-    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUsersContainer)
+from waeup.kofa.interfaces import MessageFactory as _
 from waeup.kofa.interfaces import (
     IKofaUtils, IKofaObject,
     IDataCenter, IUserAccount)
-from waeup.kofa.browser.layout import KofaPage, default_primary_nav_template
 from waeup.kofa.utils.helpers import get_user_account
 
-from waeup.kofa.interfaces import MessageFactory as _
 
 grok.templatedir('templates')
 grok.context(IKofaObject) # Make IKofaObject the default context
 
+
 class ManageSidebar(grok.ViewletManager):
     grok.name('left_manage')
 
+
 class BreadCrumbManager(grok.ViewletManager):
     grok.name('breadcrumbs')
 
+
 class LanguageManager(grok.ViewletManager):
     grok.name('languages')
 
+
 class ActionBar(grok.ViewletManager):
     grok.name('actionbar')
 
+
 class AdministrationTasks(grok.ViewletManager):
     grok.name('admintasks')
 
+
 class WidgetsSlot(grok.ViewletManager):
     grok.name('widgets')
 
+
 class SourcesLeft(grok.ViewletManager):
     grok.name('sources_left')
+
 
 class SourcesRight(grok.ViewletManager):
@@ -555,27 +562,4 @@
     grok.order(4)
 
-class DownloadCSVFileActionButton(ManageActionButton):
-    """ 'Download csv file' button for courses.
-    """
-    grok.context(ICourse)
-    grok.view(EditScoresPage)
-    grok.name('downloadcsv')
-    grok.require('waeup.editScores')
-    icon = 'actionicon_down.png'
-    text = _('Download editable tickets')
-    target = 'download_scores'
-    grok.order(1)
-
-class DownloadTicketOverviewActionButton(ManageActionButton):
-    """ 'Download ticket overview' button for courses.
-    """
-    grok.context(ICourse)
-    grok.view(EditScoresPage)
-    grok.name('coursetickets')
-    grok.require('waeup.editScores')
-    icon = 'actionicon_pdf.png'
-    text = _('Download pdf file')
-    target = 'coursetickets.pdf'
-    grok.order(2)
 
 class ManageCertificateActionButton(ManageActionButton):
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 13934)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 13935)
@@ -18,70 +18,70 @@
 """UI components for students and related components.
 """
-import sys
+import csv
 import grok
 import pytz
+import sys
+from cStringIO import StringIO
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
 from urllib import urlencode
-from datetime import datetime
-from zope.event import notify
-from zope.i18n import translate
 from zope.catalog.interfaces import ICatalog
 from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.i18n import translate
 from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
-from zope.formlib.textwidgets import BytesDisplayWidget
 from zope.security import checkPermission
-from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
-from waeup.kofa.accesscodes import (
-    invalidate_accesscode, get_access_code)
+from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
 from waeup.kofa.accesscodes.workflow import USED
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.interfaces import ICaptchaManager
 from waeup.kofa.browser.layout import (
     KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
     NullValidator, jsaction, action, UtilityView)
-from waeup.kofa.browser.breadcrumbs import Breadcrumb
 from waeup.kofa.browser.pages import (
     ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
     LocalRoleAssignmentUtilityView)
-from waeup.kofa.browser.interfaces import ICaptchaManager
 from waeup.kofa.hostels.hostel import NOT_OCCUPIED
 from waeup.kofa.interfaces import (
     IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
-    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
-    academic_sessions_vocab, IJobManager, IDataCenter, DOCLINK)
+    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
+    academic_sessions_vocab, IDataCenter, DOCLINK)
 from waeup.kofa.interfaces import MessageFactory as _
-from waeup.kofa.widgets.datewidget import (
-    FriendlyDateWidget, FriendlyDateDisplayWidget,
-    FriendlyDatetimeDisplayWidget)
 from waeup.kofa.mandates.mandate import PasswordMandate
 from waeup.kofa.university.interfaces import (
     IDepartment, ICertificate, ICourse)
+from waeup.kofa.university.certificate import (
+    VirtualCertificateExportJobContainer)
+from waeup.kofa.university.department import (
+    VirtualDepartmentExportJobContainer)
 from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
-from waeup.kofa.university.department import VirtualDepartmentExportJobContainer
 from waeup.kofa.university.facultiescontainer import (
-    VirtualFacultiesExportJobContainer, FacultiesContainer)
-from waeup.kofa.university.certificate import (
-    VirtualCertificateExportJobContainer,)
+    VirtualFacultiesExportJobContainer)
 from waeup.kofa.university.course import (
     VirtualCourseExportJobContainer,)
 from waeup.kofa.university.vocabularies import course_levels
 from waeup.kofa.utils.batching import VirtualExportJobContainer
-from waeup.kofa.utils.helpers import get_current_principal, to_timezone, now
+from waeup.kofa.utils.helpers import get_current_principal, now
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
 from waeup.kofa.students.interfaces import (
-    IStudentsContainer, IStudent,
-    IUGStudentClearance,IPGStudentClearance,
+    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
     IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
     IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
-    IStudentAccommodation, IStudentStudyLevel,
-    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
-    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
-    IBedTicket, IStudentsUtils, IStudentRequestPW, IStudentTranscript
+    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
+    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
+    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
+    IStudentTranscript
     )
 from waeup.kofa.students.catalog import search, StudentQueryResultItem
-from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
 from waeup.kofa.students.vocabularies import StudyLevelSource
-from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
-    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
-    GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS)
-
-
-grok.context(IKofaObject) # Make IKofaObject the default context
+from waeup.kofa.students.workflow import (
+    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
+    VALIDATED, GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS
+    )
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+
 
 # Save function used for save methods in pages
@@ -90,5 +90,5 @@
     # Turn list of lists into single list
     if changed_fields:
-        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        changed_fields = reduce(lambda x, y: x+y, changed_fields.values())
     # Inform catalog if certificate has changed
     # (applyData does this only for the context)
@@ -1199,5 +1199,4 @@
     def label(self):
         # Here we know that the cookie has been set
-        lang = self.request.cookies.get('kofa.language')
         return _('${a}: Transcript Data', mapping = {
             'a':self.context.student.display_fullname})
@@ -1397,5 +1396,4 @@
     def render(self):
         portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
-        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
         Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
         Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
@@ -3104,7 +3102,24 @@
         return list(coursetickets)
 
+    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`` and ``score`` (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 or not 'score' in row:
+                continue
+            result[row['student_id']] = row['score']
+        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
@@ -3124,37 +3139,43 @@
             self.redirect(self.url(self.context))
             return
-        if 'UPDATE' in form:
-            error = ''
-            if not editable_tickets:
-                return
-            scores = form['scores']
-            sids = form['sids']
-            if isinstance(scores, basestring):
-                scores = [scores]
-            if isinstance(sids, basestring):
-                sids = [sids]
-            formvals = dict([(sids[i], scores[i]) for i in range(len(sids))])
-            for ticket in editable_tickets:
-                score = ticket.score
-                sid = ticket.student.student_id
-                if formvals[sid] == '':
-                    score = None
-                else:
-                    try:
-                        score = int(formvals[sid])
-                    except ValueError:
-                        error += '%s, ' % ticket.student.display_fullname
-                if 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))
-                    #notify(grok.ObjectModifiedEvent(ticket))
-            if error:
-                self.flash(_('Error: Score(s) of %s have not be updated. '
-                  'Only integers are allowed.' % error.strip(', ')),
-                  type="danger")
-        return
+        if not 'UPDATE' in form:
+            return
+        error = ''
+        if not editable_tickets:
+            return
+        formvals = dict(zip(form['sids'], form['scores']))
+        if form['uploadfile']:
+            try:
+                formvals = self._extract_uploadfile(form['uploadfile'])
+            except:
+                self.flash(
+                    _('Uploaded file contains illegal data. Ignored'),
+                    type="danger")
+        for ticket in editable_tickets:
+            score = ticket.score
+            sid = ticket.student.student_id
+            if sid not in formvals:
+                continue
+            if formvals[sid] == '':
+                score = None
+            else:
+                try:
+                    score = int(formvals[sid])
+                except ValueError:
+                    error += '%s, ' % ticket.student.display_fullname
+            if 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 error:
+            self.flash(
+                _('Error: Score(s) of following students have not been '
+                    'updated (only integers are allowed): %s.' % error.strip(', ')),
+                type="danger")
+        return
+
 
 class DownloadScoresView(UtilityView, grok.View):
@@ -3166,5 +3187,4 @@
 
     def update(self):
-        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
         self.current_academic_session = grok.getSite()[
             'configuration'].current_academic_session
@@ -3350,6 +3370,6 @@
             date_format = '%d/%m/%Y'
             try:
-                dummy = datetime.strptime(payments_start, date_format)
-                dummy = datetime.strptime(payments_end, date_format)
+                datetime.strptime(payments_start, date_format)
+                datetime.strptime(payments_end, date_format)
             except ValueError:
                 self.flash(_('Payment dates do not match format d/m/Y.'),
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/editscorespage.pt
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 13934)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 13935)
@@ -1,3 +1,3 @@
-<form  i18n:domain="waeup.kofa"  method="POST">
+<form  i18n:domain="waeup.kofa"  method="POST" enctype="multipart/form-data">
   <br />
 
@@ -15,5 +15,5 @@
     </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/student_id">STUDENT ID</td>
@@ -24,7 +24,7 @@
 
   	  <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
-          <input type="text" name="scores" class="form-control"
+          <input type="text" name="scores:list" class="form-control"
   	             tal:attributes="value ticket/score" />
-          <input type="hidden" name="sids"
+          <input type="hidden" name="sids:list"
                  tal:attributes="value ticket/student/student_id" />
       </td>
@@ -32,4 +32,22 @@
           tal:content="ticket/score">SCORE</td>
   	</tr>
+
+      <tr>
+        <td i18n:translate="">
+          Set scores from CSV file:
+        </td>
+        <td>
+          <div class="input-group half">
+            <div class="input-group-btn">
+              <div class="btn btn-default btn-file">
+                Select&hellip;
+                <input type="file" name="uploadfile:file" />
+              </div>
+            </div>
+            <input type="text" class="form-control" readonly />
+          </div>
+        </td>
+      </tr>
+
     </tbody>
   </table>
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 13934)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 13935)
@@ -1,2 +1,3 @@
+# -*- coding: utf-8 -*-
 ## $Id$
 ##
@@ -45,5 +46,5 @@
 from waeup.kofa.university.faculty import Faculty
 from waeup.kofa.university.department import Department
-from waeup.kofa.interfaces import IUserAccount, IJobManager
+from waeup.kofa.interfaces import IUserAccount, IJobManager, VALIDATED, CREATED
 from waeup.kofa.authentication import LocalRoleSetEvent
 from waeup.kofa.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
@@ -55,4 +56,5 @@
 SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
 SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+URL_LECTURER_LANDING = 'http://localhost/app/my_courses'
 
 
@@ -1807,122 +1809,4 @@
         self.assertTrue(self.student_id in self.browser.contents)
 
-    def test_handle_courses_by_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()
-        self.assertMatches('...You logged in...', self.browser.contents)
-        # Lecturer is on landing page
-        self.assertTrue('(<span>COURSE1</span>)' in self.browser.contents)
-        # Lecturer can click the 'My Roles' link.
-        self.browser.getLink("My Roles").click()
-        self.assertMatches(
-            '...<div>Academics Officer (view only)</div>...',
-            self.browser.contents)
-        self.assertMatches(
-            '...<a href="http://localhost/app/'
-            'faculties/fac1/dep1/courses/COURSE1">...',
-            self.browser.contents)
-        self.assertFalse('(<span>COURSE1</span>)' in self.browser.contents)
-        # Lecturer can click the 'My Courses' link ...
-        self.browser.getLink("My Courses").click()
-        # ... and is is back on landing page
-        self.assertTrue('(<span>COURSE1</span>)' in self.browser.contents)
-        # The lecturer can go to her course ...
-        self.browser.getLink("COURSE1").click()
-        # Lecturers can neither filter students ...
-        self.assertRaises(
-            Unauthorized, self.browser.open,
-            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/students")
-        # ... nor access the student ...
-        self.assertRaises(
-            Unauthorized, self.browser.open, self.student_path)
-        # ... nor the respective course ticket since
-        # editing course tickets by lecturers is not feasible.
-        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
-        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
-        self.assertRaises(
-            Unauthorized, self.browser.open, course_ticket_path)
-        # Course results can be batch edited via the edit_courses view.
-        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
-        self.browser.open(
-            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1")
-        self.browser.getLink("Update scores").click()
-        self.assertTrue('Score editing disabled' in self.browser.contents)
-        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
-        self.browser.getLink("Update scores").click()
-        self.assertTrue(
-            'Current academic session not set' in self.browser.contents)
-        self.app['configuration'].current_academic_session = 2004
-        self.browser.getLink("Update scores").click()
-        self.assertFalse(
-            '<input type="text" name="scores" class="span1" />'
-            in self.browser.contents)
-        IWorkflowState(self.student).setState('courses validated')
-        # Student must be in state 'courses validated'.
-        self.browser.open(
-            '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("Update scores").click()
-        # New score has been set.
-        self.assertEqual(
-            self.student['studycourse']['100']['COURSE1'].score, 55)
-        # Score editing has been logged.
-        logfile = os.path.join(
-            self.app['datacenter'].storage, 'logs', 'students.log')
-        logcontent = open(logfile).read()
-        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
-            'K1000000 100/COURSE1 score updated (55)' in logcontent)
-        # Non-integer scores won't be accepted.
-        self.browser.open(
-            '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("Update scores").click()
-        self.assertTrue('Error: Score(s) of Anna Tester have not be updated'
-            in self.browser.contents)
-        # Scores can be removed.
-        self.browser.open(
-            'http://localhost/app/faculties/fac1/'
-            'dep1/courses/COURSE1/edit_scores')
-        self.browser.getControl(name="scores", index=0).value = ''
-        self.browser.getControl("Update scores").click()
-        self.assertEqual(
-            self.student['studycourse']['100']['COURSE1'].score, None)
-        logcontent = open(logfile).read()
-        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
-            'K1000000 100/COURSE1 score updated (None)' in logcontent)
-        # A course ticket slip can be downloaded
-        self.browser.open(
-            'http://localhost/app/faculties/fac1/'
-            'dep1/courses/COURSE1/coursetickets.pdf')
-        self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'],
-                         'application/pdf')
-        path = os.path.join(samples_dir(), 'coursetickets.pdf')
-        open(path, 'wb').write(self.browser.contents)
-        print "Sample PDF coursetickets.pdf written to %s" % path
-
     def test_change_current_mode(self):
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
@@ -3898,39 +3782,4 @@
             )
 
-    def test_course_download_lecturer(self):
-        # We add study level 100 to the student's studycourse
-        studylevel = StudentStudyLevel()
-        studylevel.level = 100
-        studylevel.level_session = 2004
-        IWorkflowState(self.student).setState('courses validated')
-        self.student['studycourse'].addStudentStudyLevel(
-            self.certificate,studylevel)
-        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
-        # Create lecturer
-        self.app['users'].addUser('mrlecturer', 'mrlecturersecret')
-        self.app['users']['mrlecturer'].email = 'mrlecturer@foo.ng'
-        self.app['users']['mrlecturer'].title = 'Carlo Intelligent'
-        prmglobal = IPrincipalRoleManager(self.course)
-        prmglobal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrlecturer')
-        # Login as lecturer
-        self.browser.open(self.login_path)
-        self.browser.getControl(name="form.login").value = 'mrlecturer'
-        self.browser.getControl(name="form.password").value = 'mrlecturersecret'
-        self.browser.getControl("Login").click()
-        self.assertMatches('...You logged in...', self.browser.contents)
-        self.browser.open(course1_path)
-        self.assertFalse('Export' in self.browser.contents)
-        self.browser.getLink("Update scores").click()
-        self.assertTrue('Current academic session not set' in self.browser.contents)
-        self.app['configuration'].current_academic_session = 2004
-        self.browser.getLink("Update scores").click()
-        self.browser.getLink("Download editable tickets").click()
-        self.assertEqual(self.browser.headers['Status'], '200 Ok')
-        self.assertEqual(self.browser.headers['Content-Type'],
-                         'text/csv; charset=UTF-8')
-        self.assertEqual(self.browser.contents, 'matric_number,student_id,'
-            'display_fullname,level,code,level_session,score\r\n234,'
-            'K1000000,Anna Tester,100,COURSE1,2004,\r\n')
-
     def test_export_departmet_officers(self):
         # Create department officer
@@ -4004,2 +3853,274 @@
         self.browser.getControl("Discard").click()
         self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+
+
+UPLOAD_CSV_TEMPLATE = (
+    'matric_number,student_id,display_fullname,level,code,level_session,'
+    'score\r\n'
+    '234,K1000000,Anna Tester,100,COURSE1,2004,%s\r\n')
+
+class LecturerUITests(StudentsFullSetup):
+    # Tests for UI actions when acting as lecturer.
+
+    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)
+
+    @property
+    def stud_log_path(self):
+        return os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+
+    def test_lecturer_lands_on_landing_page(self):
+        # lecturers can login and will be led to landing page.
+        self.login_as_lecturer()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
+
+    def test_my_roles_link_works(self):
+        # lecturers can see their roles
+        self.login_as_lecturer()
+        self.browser.getLink("My Roles").click()
+        self.assertTrue(
+            "<div>Academics Officer (view only)</div>"
+            in self.browser.contents)
+        self.assertTrue(
+            '<a href="%s">' % self.course_url in self.browser.contents)
+
+    def test_my_roles_page_contains_backlink(self):
+        # we can get back from 'My Roles' view to landing page
+        self.login_as_lecturer()
+        self.browser.getLink("My Roles").click()
+        self.browser.getLink("My Courses").click()
+        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
+
+    def test_lecturers_can_reach_their_courses(self):
+        # lecturers get links to their courses on the landing page
+        self.login_as_lecturer()
+        self.browser.getLink("COURSE1").click()
+        self.assertEqual(self.browser.url, self.course_url)
+
+    def test_lecturers_student_access_is_restricted(self):
+        # lecturers are not able to change other student data
+        self.login_as_lecturer()
+        # Lecturers can neither filter students ...
+        self.assertRaises(
+            Unauthorized, self.browser.open, '%s/students' % self.course_url)
+        # ... nor access the student ...
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.student_path)
+        # ... nor the respective course ticket since editing course
+        # tickets by lecturers is not feasible.
+        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
+        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
+        self.assertRaises(
+            Unauthorized, self.browser.open, course_ticket_path)
+
+    def test_score_editing_requires_department_permit(self):
+        # we get a warning if we try to update score while we are not allowed
+        self.login_as_lecturer()
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
+        self.browser.open(self.course_url)
+        self.browser.getLink("Update scores").click()
+        self.assertTrue('Score editing disabled' in self.browser.contents)
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        self.browser.open(self.course_url)
+        self.browser.getLink("Update scores").click()
+        self.assertFalse('Score editing disabled' in self.browser.contents)
+
+    def test_score_editing_requires_validated_students(self):
+        # we can edit only scores of students whose courses have been
+        # validated.
+        self.login_as_lecturer()
+        # set invalid student state
+        IWorkflowState(self.student).setState(CREATED)
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # set valid student state
+        IWorkflowState(self.student).setState(VALIDATED)
+        self.browser.open(self.edit_scores_url)
+        self.assertTrue(
+            self.browser.getControl(name="scores:list") is not None)
+
+    def test_score_editing_offers_only_current_scores(self):
+        # only scores from current academic session can be edited
+        self.login_as_lecturer()
+        IWorkflowState(self.student).setState('courses validated')
+        # with no academic session set
+        self.app['configuration'].current_academic_session = None
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # with wrong academic session set
+        self.app['configuration'].current_academic_session = 1999
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # with right academic session set
+        self.app['configuration'].current_academic_session = 2004
+        self.browser.reload()
+        self.assertTrue(
+            self.browser.getControl(name="scores:list") is not None)
+
+    def test_score_editing_can_change_scores(self):
+        # we can really change scores via edit_scores view
+        self.login_as_lecturer()
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, None)
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = '55'
+        self.browser.getControl("Update scores").click()
+        # the new value is stored in data
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 55)
+        # the new value is displayed on page/prefilled in form
+        self.assertEqual(
+            self.browser.getControl(name="scores:list", index=0).value, '55')
+        # The change has been logged
+        with open(self.stud_log_path, 'r') as fd:
+            self.assertTrue(
+                'mrslecturer - students.browser.EditScoresPage - '
+                'K1000000 100/COURSE1 score updated (55)' in fd.read())
+
+    def test_scores_editing_scores_must_be_integers(self):
+        # Non-integer scores won't be accepted.
+        self.login_as_lecturer()
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = 'abc'
+        self.browser.getControl("Update scores").click()
+        self.assertTrue(
+            'Error: Score(s) of following students have not been updated '
+            '(only integers are allowed): Anna Tester.'
+            in self.browser.contents)
+
+    def test_scores_editing_allows_score_removal(self):
+        # we can remove scores, once they were set
+        self.login_as_lecturer()
+        # without a prior value, we cannot remove
+        self.student['studycourse']['100']['COURSE1'].score = None
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        logcontent = open(self.stud_log_path, 'r').read()
+        self.assertFalse('COURSE1 score updated (None)' in logcontent)
+        # now retry with some value set
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.getControl(name="scores:list", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        logcontent = open(self.stud_log_path, 'r').read()
+        self.assertTrue('COURSE1 score updated (None)' in logcontent)
+
+    def test_lecturers_can_download_course_tickets(self):
+        # A course ticket slip can be downloaded
+        self.login_as_lecturer()
+        pdf_url = '%s/coursetickets.pdf' % self.course_url
+        self.browser.open(pdf_url)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'coursetickets.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF coursetickets.pdf written to %s" % path
+
+    def test_lecturers_can_download_scores_as_csv(self):
+        # Lecturers can download course scores as CSV.
+        self.login_as_lecturer()
+        self.browser.open(self.edit_scores_url)
+        self.browser.getLink("Download editable tickets").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.contents, 'matric_number,student_id,'
+            'display_fullname,level,code,level_session,score\r\n234,'
+            'K1000000,Anna Tester,100,COURSE1,2004,\r\n')
+
+    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')
+        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
+        self.browser.getControl("Update scores").click()
+        # value changed
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 65)
+
+    def test_scores_csv_upload_ignored(self):
+        # for many type of file contents we simply ignore uploaded data
+        self.login_as_lecturer()
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        for content, mimetype, name in (
+                # empty file
+                ('', 'text/foo', 'my.foo'),
+                # plain ASCII text, w/o comma
+                ('abcdef' * 200, 'text/plain', 'my.txt'),
+                # plain UTF-8 text, with umlauts
+                ('umlauts: äöü', 'text/plain', 'my.txt'),
+                # csv file with only a header row
+                ('student_id,score', 'text/csv', 'my.csv'),
+                # csv with student_id column missing
+                ('foo,score\r\nbar,66\r\n', 'text/csv', 'my.csv'),
+                # csv with score column missing
+                ('student_id,foo\r\nK1000000,bar\r\n', 'text/csv', 'my.csv'),
+                # csv with non number as score value
+                (UPLOAD_CSV_TEMPLATE % 'not-a-number', 'text/csv', 'my.csv'),
+                ):
+            upload_ctrl = self.browser.getControl(name='uploadfile:file')
+            upload_ctrl.add_file(StringIO(content), mimetype, name)
+            self.browser.getControl("Update scores").click()
+            self.assertEqual(
+                self.student['studycourse']['100']['COURSE1'].score, 55)
+            self.assertFalse(
+                'Uploaded file contains illegal data' in self.browser.contents)
+
+    def test_scores_csv_upload_warn_illegal_chars(self):
+        # for some types of files we issue a warning if upload data
+        # contains illegal chars (and ignore the data)
+        self.login_as_lecturer()
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        for content, mimetype, name in (
+                # plain ASCII text, commas, control chars
+                ('abv,qwe\n\r\r\t\b\n' * 20, 'text/plain', 'my.txt'),
+                # image data (like a JPEG image)
+                (open(SAMPLE_IMAGE, 'rb').read(), 'image/jpg', 'my.jpg'),
+                ):
+            upload_ctrl = self.browser.getControl(name='uploadfile:file')
+            upload_ctrl.add_file(StringIO(content), mimetype, name)
+            self.browser.getControl("Update scores").click()
+            self.assertEqual(
+                self.student['studycourse']['100']['COURSE1'].score, 55)
+            self.assertTrue(
+                'Uploaded file contains illegal data' in self.browser.contents)
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py	(revision 13934)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py	(revision 13935)
@@ -18,30 +18,31 @@
 import grok
 from zope.component import getUtility
+from zope.i18n import translate
 from zope.interface import Interface
-from zope.i18n import translate
-from waeup.kofa.interfaces import IExtFileStore, IKofaObject
-from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.layout import default_primary_nav_template
 from waeup.kofa.browser.viewlets import (
     PrimaryNavTab, ManageActionButton, AddActionButton)
-from waeup.kofa.browser.layout import default_primary_nav_template
-from waeup.kofa.students.workflow import (
-    ADMITTED, PAID, REQUESTED, RETURNING, CLEARED, REGISTERED,
-    VALIDATED, GRADUATED, TRANSCRIPT)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import IExtFileStore, IKofaObject
 from waeup.kofa.students.browser import (
-    StudentsContainerPage,
-    StudentsContainerManagePage, StudentBaseDisplayFormPage,
-    StudentClearanceDisplayFormPage, StudentPersonalDisplayFormPage,
-    StudyCourseDisplayFormPage, StudyLevelDisplayFormPage,
-    CourseTicketDisplayFormPage, OnlinePaymentDisplayFormPage,
-    AccommodationManageFormPage, BedTicketDisplayFormPage,
+    StudentsContainerPage, StudentsContainerManagePage,
+    StudentBaseDisplayFormPage, StudentClearanceDisplayFormPage,
+    StudentPersonalDisplayFormPage, StudyCourseDisplayFormPage,
+    StudyLevelDisplayFormPage, CourseTicketDisplayFormPage,
+    OnlinePaymentDisplayFormPage, BedTicketDisplayFormPage,
     StudentClearanceEditFormPage, StudentPersonalEditFormPage,
-    PaymentsManageFormPage, StudyCourseTranscriptPage)
+    PaymentsManageFormPage, StudyCourseTranscriptPage, EditScoresPage)
 from waeup.kofa.students.interfaces import (
-    IStudentsContainer, IStudent, IStudentStudyCourse, IStudentAccommodation,
-    IStudentStudyLevel, ICourseTicket, IStudentOnlinePayment, IBedTicket,
+    IStudentsContainer, IStudent, IStudentStudyCourse, IStudentStudyLevel,
+    ICourseTicket, IStudentOnlinePayment, IBedTicket,
     IStudentPaymentsContainer, IStudentsUtils
     )
-
-grok.context(IKofaObject) # Make IKofaObject the default context
+from waeup.kofa.students.workflow import (
+    ADMITTED, PAID, REQUESTED, CLEARED, REGISTERED, VALIDATED, GRADUATED,
+    TRANSCRIPT)
+from waeup.kofa.university.interfaces import ICourse
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
 grok.templatedir('browser_templates')
 
@@ -49,4 +50,5 @@
 class StudentManageSidebar(grok.ViewletManager):
     grok.name('left_studentmanage')
+
 
 class StudentManageLink(grok.Viewlet):
@@ -69,10 +71,11 @@
         # Here we know that the cookie has been set
         lang = self.request.cookies.get('kofa.language')
-        text = translate(self.text, 'waeup.kofa',
-            target_language=lang)
+        text = translate(
+            self.text, 'waeup.kofa', target_language=lang)
         if not self.link:
             return ''
         return u'<li><a href="%s">%s</a></li>' % (
-                url, text)
+            url, text)
+
 
 class StudentManageApplicationLink(StudentManageLink):
@@ -86,10 +89,11 @@
         if slip:
             lang = self.request.cookies.get('kofa.language')
-            text = translate(self.text, 'waeup.kofa',
-                target_language=lang)
-            url = self.view.url(self.context.student,self.link)
+            text = translate(
+                self.text, 'waeup.kofa', target_language=lang)
+            url = self.view.url(self.context.student, self.link)
             return u'<li><a href="%s">%s</a></li>' % (
-                    url, text)
+                url, text)
         return ''
+
 
 class StudentManageBaseLink(StudentManageLink):
@@ -97,4 +101,5 @@
     link = 'index'
     text = _(u'Base Data')
+
 
 class StudentManageClearanceLink(StudentManageLink):
@@ -104,4 +109,5 @@
     text = _(u'Clearance Data')
 
+
 class StudentManagePersonalLink(StudentManageLink):
     grok.order(4)
@@ -110,8 +116,10 @@
     text = _(u'Personal Data')
 
+
 class StudentManageStudyCourseLink(StudentManageLink):
     grok.order(5)
     link = 'studycourse'
     text = _(u'Study Course')
+
 
 class StudentManagePaymentsLink(StudentManageLink):
@@ -120,4 +128,5 @@
     link = 'payments'
     text = _(u'Payments')
+
 
 class StudentManageAccommodationLink(StudentManageLink):
@@ -128,4 +137,5 @@
     text = _(u'Accommodation')
 
+
 class StudentManageHistoryLink(StudentManageLink):
     grok.order(8)
@@ -141,4 +151,5 @@
     text = _('Manage students section')
 
+
 class StudentsContainerAddActionButton(AddActionButton):
     grok.order(1)
@@ -148,4 +159,5 @@
     text = _('Add student')
     target = 'addstudent'
+
 
 class ContactActionButton(ManageActionButton):
@@ -158,4 +170,5 @@
     target = 'contactstudent'
 
+
 class StudentBaseManageActionButton(ManageActionButton):
     grok.order(1)
@@ -165,4 +178,5 @@
     text = _('Manage')
     target = 'manage_base'
+
 
 class StudentTrigTransActionButton(ManageActionButton):
@@ -175,4 +189,5 @@
     target = 'trigtrans'
 
+
 class StudentLoginAsActionButton(ManageActionButton):
     grok.order(3)
@@ -184,4 +199,5 @@
     target = 'loginasstep1'
 
+
 class AdmissionSlipActionButton(ManageActionButton):
     grok.order(4)
@@ -193,4 +209,5 @@
     target = 'admission_slip.pdf'
 
+
 class StudentTransferButton(ManageActionButton):
     grok.order(6)
@@ -202,4 +219,5 @@
     icon = 'actionicon_redo.png'
 
+
 class StudentDeactivateActionButton(ManageActionButton):
     grok.order(7)
@@ -222,4 +240,5 @@
             "'A history message will be added. Are you sure?'")
 
+
 class StudentActivateActionButton(ManageActionButton):
     grok.order(7)
@@ -242,4 +261,5 @@
             "'A history message will be added. Are you sure?'")
 
+
 class StudentClearanceManageActionButton(ManageActionButton):
     grok.order(1)
@@ -249,4 +269,5 @@
     text = _('Manage')
     target = 'manage_clearance'
+
 
 class StudentClearActionButton(ManageActionButton):
@@ -261,5 +282,6 @@
     @property
     def target_url(self):
-        cdm = getUtility(IStudentsUtils).clearance_disabled_message(self.context)
+        cdm = getUtility(
+            IStudentsUtils).clearance_disabled_message(self.context)
         if cdm:
             return ''
@@ -267,4 +289,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class StudentRejectClearanceActionButton(ManageActionButton):
@@ -279,5 +302,6 @@
     @property
     def target_url(self):
-        cdm = getUtility(IStudentsUtils).clearance_disabled_message(self.context)
+        cdm = getUtility(
+            IStudentsUtils).clearance_disabled_message(self.context)
         if cdm:
             return ''
@@ -285,4 +309,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class ClearanceSlipActionButton(ManageActionButton):
@@ -295,4 +320,5 @@
     target = 'clearance_slip.pdf'
 
+
 class ClearanceViewActionButton(ManageActionButton):
     grok.order(1)
@@ -304,4 +330,5 @@
     target = 'view_clearance'
 
+
 class PersonalViewActionButton(ManageActionButton):
     grok.order(1)
@@ -313,4 +340,5 @@
     target = 'view_personal'
 
+
 class StudentPersonalManageActionButton(ManageActionButton):
     grok.order(1)
@@ -321,4 +349,5 @@
     target = 'manage_personal'
 
+
 class StudentPersonalEditActionButton(ManageActionButton):
     grok.order(2)
@@ -328,4 +357,5 @@
     text = _('Edit')
     target = 'edit_personal'
+
 
 class StudyCourseManageActionButton(ManageActionButton):
@@ -342,4 +372,5 @@
             return self.view.url(self.view.context, self.target)
         return False
+
 
 class StudyCourseTranscriptActionButton(ManageActionButton):
@@ -358,4 +389,5 @@
         return False
 
+
 class TranscriptSlipActionButton(ManageActionButton):
     grok.order(1)
@@ -373,4 +405,5 @@
         return False
 
+
 class RevertTransferActionButton(ManageActionButton):
     grok.order(1)
@@ -388,4 +421,5 @@
         return False
 
+
 class StudyLevelManageActionButton(ManageActionButton):
     grok.order(1)
@@ -402,4 +436,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class StudentValidateCoursesActionButton(ManageActionButton):
@@ -414,10 +449,12 @@
     @property
     def target_url(self):
-        is_current = self.context.__parent__.is_current
-        if self.context.student.state != REGISTERED or \
-            str(self.context.__parent__.current_level) != self.context.__name__ or\
-            not is_current:
-            return ''
-        return self.view.url(self.view.context, self.target)
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state != REGISTERED:
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
 
 class StudentRejectCoursesActionButton(ManageActionButton):
@@ -432,10 +469,12 @@
     @property
     def target_url(self):
-        is_current = self.context.__parent__.is_current
-        if self.context.student.state not in (VALIDATED, REGISTERED) or \
-            str(self.context.__parent__.current_level) != self.context.__name__ or\
-            not is_current:
-            return ''
-        return self.view.url(self.view.context, self.target)
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state not in (VALIDATED, REGISTERED):
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
 
 class StudentUnregisterCoursesActionButton(ManageActionButton):
@@ -450,10 +489,12 @@
     @property
     def target_url(self):
-        is_current = self.context.__parent__.is_current
-        if self.context.student.state != REGISTERED or \
-            str(self.context.__parent__.current_level) != self.context.__name__ or\
-            not is_current:
-            return ''
-        return self.view.url(self.view.context, self.target)
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state != REGISTERED:
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
 
 class CourseRegistrationSlipActionButton(ManageActionButton):
@@ -473,4 +514,5 @@
         return self.view.url(self.view.context, self.target)
 
+
 class CourseTicketManageActionButton(ManageActionButton):
     grok.order(1)
@@ -481,14 +523,7 @@
     target = 'manage'
 
-#class OnlinePaymentManageActionButton(ManageActionButton):
-#    grok.order(1)
-#    grok.context(IStudentPaymentsContainer)
-#    grok.view(PaymentsDisplayFormPage)
-#    grok.require('waeup.manageStudent')
-#    text = 'Manage payments'
-#    target = 'manage'
 
 class PaymentReceiptActionButton(ManageActionButton):
-    grok.order(9) # This button should always be the last one.
+    grok.order(9)  # This button should always be the last one.
     grok.context(IStudentOnlinePayment)
     grok.view(OnlinePaymentDisplayFormPage)
@@ -503,4 +538,5 @@
         #    return ''
         return self.view.url(self.view.context, self.target)
+
 
 class ApprovePaymentActionButton(ManageActionButton):
@@ -519,4 +555,5 @@
         return self.view.url(self.view.context, self.target)
 
+
 class BedTicketSlipActionButton(ManageActionButton):
     grok.order(1)
@@ -528,4 +565,5 @@
     target = 'bed_allocation_slip.pdf'
 
+
 class RelocateStudentActionButton(ManageActionButton):
     grok.order(2)
@@ -537,4 +575,5 @@
     target = 'relocate'
 
+
 class StudentBaseActionButton(ManageActionButton):
     grok.order(1)
@@ -544,4 +583,5 @@
     text = _('Edit')
     target = 'edit_base'
+
 
 class StudentPasswordActionButton(ManageActionButton):
@@ -554,4 +594,5 @@
     target = 'change_password'
 
+
 class StudentPassportActionButton(ManageActionButton):
     grok.order(3)
@@ -565,8 +606,10 @@
     @property
     def target_url(self):
-        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
+        PORTRAIT_CHANGE_STATES = getUtility(
+            IStudentsUtils).PORTRAIT_CHANGE_STATES
         if self.context.state not in PORTRAIT_CHANGE_STATES:
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class StudentClearanceStartActionButton(ManageActionButton):
@@ -585,4 +628,5 @@
         return self.view.url(self.view.context, self.target)
 
+
 class StudentClearanceEditActionButton(ManageActionButton):
     grok.order(1)
@@ -598,4 +642,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class StartSessionActionButton(ManageActionButton):
@@ -613,4 +658,5 @@
             return self.view.url(self.view.context, self.target)
         return False
+
 
 class AddStudyLevelActionButton(AddActionButton):
@@ -632,4 +678,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class StudyLevelEditActionButton(ManageActionButton):
@@ -651,4 +698,5 @@
         return ''
 
+
 class AddPaymentActionButton(AddActionButton):
     grok.order(1)
@@ -658,4 +706,5 @@
     text = _('Add current session payment ticket')
     target = 'addop'
+
 
 class AddPreviousPaymentActionButton(AddActionButton):
@@ -675,4 +724,5 @@
         return self.view.url(self.view.context, self.target)
 
+
 class AddBalancePaymentActionButton(AddActionButton):
     grok.order(3)
@@ -690,4 +740,5 @@
         return self.view.url(self.view.context, self.target)
 
+
 class RequestTranscriptActionButton(ManageActionButton):
     grok.order(8)
@@ -704,4 +755,5 @@
             return ''
         return self.view.url(self.view.context, self.target)
+
 
 class ProcessTranscriptRequestActionButton(ManageActionButton):
@@ -720,8 +772,8 @@
         return self.view.url(self.view.context, self.target)
 
+
 class StudentsTab(PrimaryNavTab):
     """Students tab in primary navigation.
     """
-
     grok.context(IKofaObject)
     grok.order(4)
@@ -736,8 +788,10 @@
         return self.view.application_url('students')
 
+
 class PrimaryStudentNavManager(grok.ViewletManager):
     """Viewlet manager for the primary navigation tab.
     """
     grok.name('primary_nav_student')
+
 
 class PrimaryStudentNavTab(grok.Viewlet):
@@ -763,4 +817,5 @@
             return 'active'
         return ''
+
 
 class MyStudentDataTab(PrimaryStudentNavTab):
@@ -789,16 +844,43 @@
         targets = []
         if app_slip:
-            targets = [{'url':student_url + '/application_slip',
-                        'title':_('Application Slip')},]
+            targets = [{'url': student_url + '/application_slip',
+                        'title': _('Application Slip')}, ]
         targets += [
-            {'url':student_url, 'title':'Base Data'},
-            {'url':student_url + '/view_clearance',
-             'title':_('Clearance Data')},
-            {'url':student_url + '/view_personal', 'title':_('Personal Data')},
-            {'url':student_url + '/studycourse', 'title':_('Study Course')},
-            {'url':student_url + '/payments', 'title':_('Payments')},
-            {'url':student_url + '/accommodation',
-             'title':_('Accommodation Data')},
-            {'url':student_url + '/history', 'title':_('History')},
+            {'url': student_url, 'title': 'Base Data'},
+            {'url': student_url + '/view_clearance',
+             'title': _('Clearance Data')},
+            {'url': student_url + '/view_personal',
+             'title': _('Personal Data')},
+            {'url': student_url + '/studycourse', 'title': _('Study Course')},
+            {'url': student_url + '/payments', 'title': _('Payments')},
+            {'url': student_url + '/accommodation',
+             'title': _('Accommodation Data')},
+            {'url': student_url + '/history', 'title': _('History')},
             ]
         return targets
+
+
+class DownloadCSVFileActionButton(ManageActionButton):
+    """ 'Download csv file' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(EditScoresPage)
+    grok.name('downloadcsv')
+    grok.require('waeup.editScores')
+    icon = 'actionicon_down.png'
+    text = _('Download editable tickets')
+    target = 'download_scores'
+    grok.order(1)
+
+
+class DownloadTicketOverviewActionButton(ManageActionButton):
+    """ 'Download ticket overview' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(EditScoresPage)
+    grok.name('coursetickets')
+    grok.require('waeup.editScores')
+    icon = 'actionicon_pdf.png'
+    text = _('Download pdf file')
+    target = 'coursetickets.pdf'
+    grok.order(2)
