Index: main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- main/waeup.kofa/trunk/CHANGES.txt	(revision 14683)
+++ main/waeup.kofa/trunk/CHANGES.txt	(revision 14684)
@@ -4,4 +4,6 @@
 1.6.dev0 (unreleased)
 =======================
+
+* Add 'Update all tickets' button to StudyLevelEditFormPage.
 
 * Do not allow to create more than 10 students with a single request to
Index: main/waeup.kofa/trunk/docs/source/userdocs/students/browser.rst
===================================================================
--- main/waeup.kofa/trunk/docs/source/userdocs/students/browser.rst	(revision 14683)
+++ main/waeup.kofa/trunk/docs/source/userdocs/students/browser.rst	(revision 14684)
@@ -260,7 +260,13 @@
 (non-mandatory) course tickets (condition customizable).
 
-The edit form page provides a 'Register course list' button which
-submits the course list to the course adviser for validation. If the
-course registration deadline
+The edit form page provides two additional buttons. 'Update all
+tickets' ignores the select boxes and checks all course ticket at
+that level. It looks up the associated course object for each ticket
+and updates the ticket's course parameters (including course title)
+if needed. Attention: If a course was removed, the associated
+course ticket will be invalidated by adding '(course cancelled)' to
+the title and setting the credits to zero. The 'Register course
+list' button submits the course list to the course adviser for
+validation. If the course registration deadline
 (`ISessionConfiguration.coursereg_deadline`) is set and the
 registration period has expired, a late registration fee
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 14683)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 14684)
@@ -2724,4 +2724,29 @@
         return
 
+    def _updateTickets(self, **data):
+        cat = queryUtility(ICatalog, name='courses_catalog')
+        invalidated = list()
+        for value in self.context.values():
+            result = cat.searchResults(code=(value.code, value.code))
+            if len(result) != 1:
+                course = None
+            else:
+                course = list(result)[0]
+            invalid = self.context.updateCourseTicket(value, course)
+            if invalid:
+                invalidated.append(invalid)
+        if invalidated:
+            invalidated_string = ', '.join(invalidated)
+            self.context.writeLogMessage(
+                self, 'course tickets invalidated: %s' % invalidated_string)
+        self.flash(_('All course tickets updated.'))
+        return
+
+    @action(_('Update all tickets'),
+        tooltip=_('Update all course parameters including course titles.'))
+    def updateTickets(self, **data):
+        self._updateTickets(**data)
+        return
+
     def _registerCourses(self, **data):
         if self.context.student.is_postgrad and \
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/interfaces.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/interfaces.py	(revision 14683)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/interfaces.py	(revision 14684)
@@ -646,4 +646,9 @@
         """
 
+    def updateCourseTicket(ticket, course):
+        """Updates a course ticket object and return code
+        if ticket has been invalidated.
+        """
+
 class ICourseTicket(IKofaObject):
     """A representation of course ticket data.
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/studylevel.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/studylevel.py	(revision 14683)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/studylevel.py	(revision 14684)
@@ -268,4 +268,26 @@
         return
 
+    def updateCourseTicket(self, ticket, course):
+        """Updates a course ticket object and return code
+        if ticket has been invalidated.
+        """
+        if not course:
+            if ticket.title.endswith('cancelled)'):
+                # Skip this tiket
+                return
+            # Invalidate course ticket
+            ticket.title += u' (course cancelled)'
+            ticket.credits = 0
+            ticket.passmark = 0
+            return ticket.code
+        ticket.code = course.code
+        ticket.title = course.title
+        ticket.fcode = course.__parent__.__parent__.__parent__.code
+        ticket.dcode = course.__parent__.__parent__.code
+        ticket.credits = course.credits
+        ticket.passmark = course.passmark
+        ticket.semester = course.semester
+        return
+
 StudentStudyLevel = attrs_to_fields(
     StudentStudyLevel, omit=['total_credits', 'gpa'])
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 14683)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 14684)
@@ -2578,4 +2578,46 @@
         return
 
+    def test_student_ticket_update(self):
+        IWorkflowState(self.student).setState('school fee paid')
+        self.student['studycourse'].current_level = 100
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        # A level with one course ticket was created
+        self.assertEqual(
+            self.student['studycourse']['100'].number_of_tickets, 1)
+        self.browser.getLink("100").click()
+        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
+        self.browser.getLink("Edit course list").click()
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue('All course tickets updated.' in self.browser.contents)
+        # ... nothing has changed
+        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
+        # We change the title of the course
+        self.course.title = u'New Title'
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue('<td>New Title</td>' in self.browser.contents)
+        # We remove the course
+        del self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue(' <td>New Title (course cancelled)</td>'
+            in self.browser.contents)
+        # Course ticket invalidation has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'K1000000 - students.browser.StudyLevelEditFormPage - '
+            'K1000000 - course tickets invalidated: COURSE1'
+            in logcontent)
+        return
+
     def test_student_course_registration_outstanding(self):
         self.course = createObject('waeup.Course')
