Index: main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py	(revision 13768)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py	(revision 13770)
@@ -36,4 +36,7 @@
     StudentFilesUploadPage,
     PaymentsManageFormPage,
+    CourseTicketDisplayFormPage,
+    CourseTicketManageFormPage,
+    EditScoresPage
     )
 from kofacustom.nigeria.students.browser import (
@@ -55,5 +58,6 @@
     ICustomStudentPersonal,
     ICustomStudentPersonalEdit,
-    ICustomUGStudentClearance)
+    ICustomUGStudentClearance,
+    ICustomCourseTicket)
 from waeup.aaue.interswitch.browser import gateway_net_amt
 from waeup.aaue.interfaces import MessageFactory as _
@@ -444,2 +448,92 @@
     requires that no application slip exists.
     """
+
+class CustomCourseTicketDisplayFormPage(CourseTicketDisplayFormPage):
+    """ Page to display course tickets
+    """
+    form_fields = grok.AutoFields(ICustomCourseTicket)
+
+class CustomCourseTicketManageFormPage(CourseTicketManageFormPage):
+    """ Page to manage course tickets
+    """
+    form_fields = grok.AutoFields(ICustomCourseTicket)
+    form_fields['title'].for_display = True
+    form_fields['fcode'].for_display = True
+    form_fields['dcode'].for_display = True
+    form_fields['semester'].for_display = True
+    form_fields['passmark'].for_display = True
+    form_fields['credits'].for_display = True
+    form_fields['mandatory'].for_display = False
+    form_fields['automatic'].for_display = True
+    form_fields['carry_over'].for_display = True
+
+class CustomEditScoresPage(EditScoresPage):
+    """Page that filters and lists students.
+    """
+    grok.template('editscorespage')
+
+    def update(self,  *args, **kw):
+        form = self.request.form
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if self.context.__parent__.__parent__.score_editing_disabled:
+            self.flash(_('Score editing disabled.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        self.tickets = self._searchCatalog(self.current_academic_session)
+        editable_tickets = [
+            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
+        if not self.tickets:
+            self.flash(_('No student found.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if 'UPDATE' in form:
+            tno = 0
+            error = ''
+            if not editable_tickets:
+                return
+            scores = form['scores']
+            cas = form['cas']
+            if isinstance(scores, basestring):
+                scores = [scores]
+            if isinstance(cas, basestring):
+                cas = [cas]
+            for ticket in editable_tickets:
+                ticket_error = False
+                score = ticket.score
+                ca = ticket.ca
+                if scores[tno] == '':
+                    score = None
+                if cas[tno] == '':
+                    ca = None
+                try:
+                    if scores[tno]:
+                        score = int(scores[tno])
+                    if cas[tno]:
+                        ca = int(cas[tno])
+                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))
+                tno += 1
+            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 13770)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/browser_templates/editscorespage.pt	(revision 13770)
@@ -0,0 +1,46 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <br />
+
+  <table class="kofa-data-table dataTable">
+    <thead>
+  	<tr>
+      <th i18n:translate="">Matric No.</th>
+      <th i18n:translate="">Score</th>
+      <th i18n:translate="">CA</th>
+      <th i18n:translate="">Fullname</th>
+      <th i18n:translate="">Status</th>
+      <th i18n:translate="">Certificate</th>
+      <th i18n:translate="">Current Session</th>
+      <th i18n:translate="">Current Level</th>
+      <th i18n:translate="">Course Level</th>
+  	</tr>
+    </thead>
+    <tbody>
+  	<tr tal:repeat="ticket view/tickets">
+      <td tal:content="ticket/student/matric_number">MATRIC_NUMBER</td>
+  	  <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
+          <input type="text" name="scores" class="form-control"
+  	             tal:attributes="value ticket/score" />
+      </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>
+      <td tal:condition="not: ticket/editable_by_lecturer"
+          tal:content="ticket/score">SCORE</td>
+      <td tal:condition="not: ticket/editable_by_lecturer"
+          tal:content="ticket/ca">CA</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/student/current_session">SESSION</td>
+      <td tal:content="ticket/student/current_level">LEVEL</td>
+      <td tal:content="ticket/level">LEVEL</td>
+  	</tr>
+    </tbody>
+  </table>
+
+  <input type="submit" name="UPDATE"
+         value="Update scores" class="btn btn-primary"
+         />
+</form>
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/export.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/export.py	(revision 13768)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/export.py	(revision 13770)
@@ -89,4 +89,4 @@
 
     fields = ('matric_number', 'reg_number', 'student_id',
-              'level', 'code', 'level_session', 'score')
+              'level', 'code', 'level_session', 'score', 'ca')
 
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/interfaces.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/interfaces.py	(revision 13768)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/interfaces.py	(revision 13770)
@@ -281,4 +281,13 @@
     """
 
+    ca = schema.Int(
+        title = _(u'Continuous Assessment'),
+        default = None,
+        required = False,
+        missing_value = None,
+        )
+
+ICustomCourseTicket['ca'].order = ICustomCourseTicket['mandatory'].order
+
 class ICustomStudentUpdateByRegNo(INigeriaStudentUpdateByRegNo):
     """Representation of a student. Skip regular reg_number validation.
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 13768)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_browser.py	(revision 13770)
@@ -22,4 +22,5 @@
 import grok
 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
 from datetime import datetime, timedelta, date
 from mechanize import LinkNotFoundError
@@ -33,4 +34,5 @@
 from waeup.kofa.students.accommodation import BedTicket
 from waeup.kofa.testing import FunctionalTestCase
+from waeup.kofa.authentication import LocalRoleSetEvent
 from waeup.kofa.browser.tests.test_pdf import samples_dir
 from waeup.aaue.testing import FunctionalLayer
@@ -634,2 +636,70 @@
             in self.browser.contents)
         return
+
+    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.browser.getLink("My Roles").click()
+        self.browser.getLink(
+            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1").click()
+        # Course results can be batch edited via the edit_courses view.
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        self.app['configuration'].current_academic_session = 2004
+        IWorkflowState(self.student).setState('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(name="cas", index=0).value = '66'
+        self.browser.getControl("Update scores").click()
+        # New score and ca has been set.
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 55)
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].ca, 66)
+        # Score editing has been logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('mrslecturer - waeup.aaue.students.browser.CustomEditScoresPage - '
+                        'E1000000 100/COURSE1 score updated (55)' in logcontent)
+        self.assertTrue('mrslecturer - waeup.aaue.students.browser.CustomEditScoresPage - '
+                        'E1000000 100/COURSE1 ca updated (66)' 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) and CA(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 - waeup.aaue.students.browser.CustomEditScoresPage - '
+                        'E1000000 100/COURSE1 score updated (None)' in logcontent)
Index: main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_export.py
===================================================================
--- main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_export.py	(revision 13768)
+++ main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_export.py	(revision 13770)
@@ -162,8 +162,8 @@
         self.assertEqual(
             result,
-            'automatic,carry_over,code,credits,dcode,fcode,level,level_session,'
+            'automatic,ca,carry_over,code,credits,dcode,fcode,level,level_session,'
             'mandatory,passmark,score,semester,title,student_id,certcode,'
             'display_fullname,matric_number\r\n'
-            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1,'
+            '1,,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1,'
             'Anna M. Tester,234\r\n'
             )
