Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 15421)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 15422)
@@ -33,4 +33,5 @@
 from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
 from zope.security import checkPermission
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
 from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
 from waeup.kofa.accesscodes.workflow import USED
@@ -3379,5 +3380,4 @@
         return
 
-
 class EditScoresPage(KofaPage):
     """Page that allows to edit batches of scores.
@@ -3464,4 +3464,30 @@
         return True
 
+    def _validate_results(self, form):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
+        user = get_current_principal()
+        if user is None:
+            usertitle = 'system'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+        self.context.results_validated_by = usertitle
+        self.context.results_validation_date = datetime.utcnow()
+        self.context.results_validation_session = self.current_academic_session
+        return
+
+    def _results_editable(self, results_validation_session,
+                         current_academic_session):
+        user = get_current_principal()
+        prm = IPrincipalRoleManager(self.context)
+        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
+        if 'waeup.local.LocalStudentsManager' in roles:
+            return True
+        if results_validation_session \
+            and results_validation_session >= current_academic_session:
+            return False
+        return True
+
     def update(self,  *args, **kw):
         form = self.request.form
@@ -3476,4 +3502,12 @@
             self.redirect(self.url(self.context))
             return
+        vs = self.context.results_validation_session
+        if not self._results_editable(vs, self.current_academic_session):
+            self.flash(
+                _('Course results have already been '
+                  'validated and can no longer be changed.'),
+                type="danger")
+            self.redirect(self.url(self.context))
+            return
         self.session_title = academic_sessions_vocab.getTerm(
             self.current_academic_session).title
@@ -3485,5 +3519,16 @@
         self.editable_tickets = [
             ticket for ticket in self.tickets if ticket.editable_by_lecturer]
-        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
+        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
+            and not 'VALIDATE_RESULTS' in form:
+            return
+        if 'VALIDATE_RESULTS' in form:
+            if vs and vs >= self.current_academic_session:
+                self.flash(
+                    _('Course results have already been validated.'),
+                    type="danger")
+                return
+            self._validate_results(form)
+            self.flash(_('You successfully validated the course results.'))
+            self.redirect(self.url(self.context))
             return
         if not self.editable_tickets:
@@ -3494,5 +3539,4 @@
         return
 
-
 class DownloadScoresView(UtilityView, grok.View):
     """View that exports scores.
@@ -3501,4 +3545,16 @@
     grok.require('waeup.editScores')
     grok.name('download_scores')
+
+    def _results_editable(self, results_validation_session,
+                         current_academic_session):
+        user = get_current_principal()
+        prm = IPrincipalRoleManager(self.context)
+        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
+        if 'waeup.local.LocalStudentsManager' in roles:
+            return True
+        if results_validation_session \
+            and results_validation_session >= current_academic_session:
+            return False
+        return True
 
     def update(self):
@@ -3511,4 +3567,12 @@
         if not self.current_academic_session:
             self.flash(_('Current academic session not set.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        vs = self.context.results_validation_session
+        if not self._results_editable(vs, self.current_academic_session):
+            self.flash(
+                _('Course results have already been '
+                  'validated and can no longer be changed.'),
+                type="danger")
             self.redirect(self.url(self.context))
             return
@@ -3537,5 +3601,5 @@
     grok.context(ICourse)
     grok.name('coursetickets.pdf')
-    grok.require('waeup.editScores')
+    grok.require('waeup.showStudents')
 
     @property
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 15421)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 15422)
@@ -108,3 +108,7 @@
          value="Update scores from table" class="btn btn-primary"
          />
+
+  <input type="submit" name="VALIDATE_RESULTS" i18n:translate=""
+         value="Validate course results" class="btn btn-primary"
+         />
 </form>
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 15421)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 15422)
@@ -4458,4 +4458,49 @@
         self.assertTrue('COURSE1 score updated (None)' in logcontent)
 
+    def test_lecturer_can_validate_courses(self):
+        # the form is locked after validation
+        self.login_as_lecturer()
+        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()
+        self.browser.getControl("Validate").click()
+        self.assertTrue(
+            'You successfully validated the course results'
+            in self.browser.contents)
+        self.assertEqual(self.course.results_validation_session, 2004)
+        self.assertEqual(self.course.results_validated_by, 'Mercedes Benz')
+        self.assertEqual(self.browser.url, self.course_url)
+        # Lecturer can't open edit_scores again
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertEqual(self.browser.url, self.course_url)
+        self.assertTrue(
+            'Course results have already been validated'
+            ' and can no longer be changed.'
+            in self.browser.contents)
+        # Also DownloadScoresView is blocked
+        self.browser.open(self.browser.url + '/download_scores')
+        self.assertEqual(self.browser.url, self.course_url)
+        self.assertTrue(
+            'Course results have already been validated'
+            ' and can no longer be changed.'
+            in self.browser.contents)
+        # Students Manager can open page ...
+        prmlocal = IPrincipalRoleManager(self.course)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.LocalStudentsManager', 'mrslecturer')
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertEqual(self.browser.url, self.edit_scores_url)
+        self.browser.getLink("Download csv file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        # ... but can't validate courses a second time
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl("Validate").click()
+        self.assertTrue(
+            'Course results have already been validated.'
+            in self.browser.contents)
+
     def test_lecturers_can_download_course_tickets(self):
         # A course ticket slip can be downloaded
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py	(revision 15421)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/viewlets.py	(revision 15422)
@@ -32,5 +32,5 @@
     OnlinePaymentDisplayFormPage, BedTicketDisplayFormPage,
     StudentClearanceEditFormPage, StudentPersonalEditFormPage,
-    PaymentsManageFormPage, StudyCourseTranscriptPage, EditScoresPage)
+    PaymentsManageFormPage, StudyCourseTranscriptPage)
 from waeup.kofa.students.interfaces import (
     IStudentsContainer, IStudent, IStudentStudyCourse, IStudentStudyLevel,
@@ -41,5 +41,4 @@
     ADMITTED, PAID, REQUESTED, CLEARED, REGISTERED, VALIDATED, GRADUATED,
     TRANSREQ, TRANSVAL, TRANSREL)
-from waeup.kofa.university.interfaces import ICourse
 
 
@@ -922,28 +921,2 @@
             ]
         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 csv file (editable scores only)')
-    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)
