source: main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py @ 6935

Last change on this file since 6935 was 6930, checked in by Henrik Bettermann, 13 years ago

Implement OnlinePaymentCallbackPage? to simulate a valid callback.

  • Property svn:keywords set to Id
File size: 44.9 KB
RevLine 
[6621]1## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
2## This program is free software; you can redistribute it and/or modify
3## it under the terms of the GNU General Public License as published by
4## the Free Software Foundation; either version 2 of the License, or
5## (at your option) any later version.
6##
7## This program is distributed in the hope that it will be useful,
8## but WITHOUT ANY WARRANTY; without even the implied warranty of
9## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10## GNU General Public License for more details.
11##
12## You should have received a copy of the GNU General Public License
13## along with this program; if not, write to the Free Software
14## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15##
16"""UI components for students and related components.
17"""
18import grok
[6869]19from time import time
20from datetime import date, datetime
[6621]21from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[6760]22from zope.component import createObject
[6621]23from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
24from waeup.sirp.accesscodes.workflow import USED
25from waeup.sirp.browser import (
26    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
27from waeup.sirp.browser.breadcrumbs import Breadcrumb
[6775]28from waeup.sirp.browser.resources import datepicker, datatable, tabs
[6621]29from waeup.sirp.browser.viewlets import (
[6760]30    ManageActionButton, PrimaryNavTab, AddActionButton)
31from waeup.sirp.interfaces import IWAeUPObject, IUserAccount
[6621]32from waeup.sirp.widgets.datewidget import (
[6869]33    FriendlyDateWidget, FriendlyDateDisplayWidget,
34    FriendlyDatetimeDisplayWidget)
[6912]35from waeup.sirp.university.vocabularies import study_modes
[6621]36from waeup.sirp.students.interfaces import (
[6756]37    IStudentsContainer, IStudent, IStudentClearance, IStudentPasswordSetting,
[6859]38    IStudentPersonal, IStudentBase, IStudentStudyCourse,
[6774]39    IStudentAccommodation, IStudentClearanceEdit, IStudentStudyLevel,
[6877]40    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
41    IStudentOnlinePayment
[6621]42    )
[6626]43from waeup.sirp.students.catalog import search
[6722]44from waeup.sirp.students.workflow import CLEARANCE
[6795]45from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
[6775]46from waeup.sirp.students.vocabularies import StudyLevelSource
[6876]47from waeup.sirp.students.utils import getPaymentDetails
[6820]48from waeup.sirp.browser.resources import toggleall
[6621]49
[6762]50# Save function used for save methods in manager pages
51def msave(view, **data):
52    form = view.request.form
53    ob_class = view.__implemented__.__name__.replace('waeup.sirp.','')
54    changed_fields = view.applyData(view.context, **data)
[6771]55    # Turn list of lists into single list
56    if changed_fields:
57        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
58    fields_string = ' + '.join(changed_fields)
[6762]59    view.context._p_changed = True
60    view.flash('Form has been saved.')
61    if fields_string:
[6794]62        view.context.getStudent().loggerInfo(ob_class, 'saved: % s' % fields_string)
[6762]63    return
64
[6621]65class StudentsTab(PrimaryNavTab):
66    """Students tab in primary navigation.
67    """
68
69    grok.context(IWAeUPObject)
70    grok.order(3)
[6660]71    grok.require('waeup.viewStudent')
[6621]72    grok.template('primarynavtab')
73
74    pnav = 4
75    tab_title = u'Students'
76
77    @property
78    def link_target(self):
79        return self.view.application_url('students')
80
[6629]81class StudentsBreadcrumb(Breadcrumb):
82    """A breadcrumb for the students container.
83    """
84    grok.context(IStudentsContainer)
85    title = u'Students'
86
[6818]87class StudentBreadcrumb(Breadcrumb):
88    """A breadcrumb for the student container.
89    """
90    grok.context(IStudent)
91
92    def title(self):
93        return self.context.fullname
94
[6635]95class SudyCourseBreadcrumb(Breadcrumb):
96    """A breadcrumb for the student study course.
97    """
98    grok.context(IStudentStudyCourse)
99    title = u'Study Course'
100
101class PaymentsBreadcrumb(Breadcrumb):
102    """A breadcrumb for the student payments folder.
103    """
[6859]104    grok.context(IStudentPaymentsContainer)
[6635]105    title = u'Payments'
106
[6870]107class OnlinePaymentBreadcrumb(Breadcrumb):
108    """A breadcrumb for course lists.
109    """
[6877]110    grok.context(IStudentOnlinePayment)
[6870]111
112    @property
113    def title(self):
114        return self.context.p_id
115
[6635]116class AccommodationBreadcrumb(Breadcrumb):
117    """A breadcrumb for the student accommodation folder.
118    """
119    grok.context(IStudentAccommodation)
120    title = u'Accommodation'
121
[6776]122class StudyLevelBreadcrumb(Breadcrumb):
123    """A breadcrumb for course lists.
124    """
125    grok.context(IStudentStudyLevel)
126
127    @property
128    def title(self):
129        return self.context.level_title
130
[6626]131class StudentsContainerPage(WAeUPPage):
132    """The standard view for student containers.
[6621]133    """
134    grok.context(IStudentsContainer)
135    grok.name('index')
[6660]136    grok.require('waeup.viewStudent')
[6695]137    grok.template('containerpage')
[6654]138    label = 'Student Section'
139    title = 'Students'
[6642]140    pnav = 4
[6621]141
[6626]142    def update(self, *args, **kw):
143        datatable.need()
144        form = self.request.form
145        self.hitlist = []
146        if 'searchterm' in form and form['searchterm']:
147            self.searchterm = form['searchterm']
148            self.searchtype = form['searchtype']
149        elif 'old_searchterm' in form:
150            self.searchterm = form['old_searchterm']
151            self.searchtype = form['old_searchtype']
152        else:
153            if 'search' in form:
154                self.flash('Empty search string.')
155            return
156        self.hitlist = search(query=self.searchterm,
157            searchtype=self.searchtype, view=self)
158        if not self.hitlist:
159            self.flash('No student found.')
160        return
161
[6773]162class SetPasswordPage(WAeUPPage):
[6699]163    grok.context(IWAeUPObject)
164    grok.name('setpassword')
165    grok.require('waeup.Public')
[6774]166    grok.template('setpassword')
[6699]167    title = ''
168    label = 'Set password for first-time login'
[6758]169    ac_prefix = 'PWD'
[6715]170    pnav = 0
[6699]171
172    def update(self, SUBMIT=None):
[6758]173        self.reg_number = self.request.form.get('reg_number', None)
174        self.ac_series = self.request.form.get('ac_series', None)
175        self.ac_number = self.request.form.get('ac_number', None)
[6756]176
[6699]177        if SUBMIT is None:
178            return
179        hitlist = search(query=self.reg_number,
180            searchtype='reg_number', view=self)
181        if not hitlist:
182            self.flash('No student found.')
183            return
184        if len(hitlist) != 1:   # Cannot happen but anyway
185            self.flash('More than one student found.')
186            return
[6704]187        student = hitlist[0].context
188        self.student_id = student.student_id
189        student_pw = student.password
[6758]190        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
[6699]191        code = get_access_code(pin)
192        if not code:
193            self.flash('Access code is invalid.')
194            return
[6704]195        if student_pw and pin == student.adm_code:
196            self.flash('Password has already been set. Your Student Id is %s'
197                % self.student_id)
198            return
199        elif student_pw:
[6800]200            self.flash('Password has already been set. You are using the wrong Access Code.')
[6704]201            return
[6699]202        # Mark pin as used (this also fires a pin related transition)
203        # and set student password
204        if code.state == USED:
205            self.flash('Access code has already been used.')
206            return
207        else:
[6704]208            comment = u"AC invalidated for %s" % self.student_id
[6699]209            # Here we know that the ac is in state initialized so we do not
210            # expect an exception
211            #import pdb; pdb.set_trace()
212            invalidate_accesscode(pin,comment)
[6758]213            IUserAccount(student).setPassword(self.ac_number)
[6769]214            student.adm_code = pin
[6704]215        self.flash('Password has been set. Your Student Id is %s'
216            % self.student_id)
[6699]217        return
218
[6626]219class StudentsContainerManageActionButton(ManageActionButton):
[6622]220    grok.order(1)
221    grok.context(IStudentsContainer)
222    grok.view(StudentsContainerPage)
223    grok.require('waeup.manageStudents')
[6647]224    text = 'Manage student section'
[6622]225
[6626]226
227class StudentsContainerManagePage(WAeUPPage):
228    """The manage page for student containers.
[6622]229    """
230    grok.context(IStudentsContainer)
231    grok.name('manage')
232    grok.require('waeup.manageStudents')
[6695]233    grok.template('containermanagepage')
[6642]234    pnav = 4
[6647]235    title = 'Manage student section'
[6622]236
237    @property
238    def label(self):
239        return self.title
240
[6626]241    def update(self, *args, **kw):
242        datatable.need()
[6820]243        toggleall.need()
[6626]244        form = self.request.form
245        self.hitlist = []
246        if 'searchterm' in form and form['searchterm']:
247            self.searchterm = form['searchterm']
248            self.searchtype = form['searchtype']
249        elif 'old_searchterm' in form:
250            self.searchterm = form['old_searchterm']
251            self.searchtype = form['old_searchtype']
252        else:
253            if 'search' in form:
254                self.flash('Empty search string.')
255            return
256        if not 'entries' in form:
257            self.hitlist = search(query=self.searchterm,
258                searchtype=self.searchtype, view=self)
259            if not self.hitlist:
260                self.flash('No student found.')
261            return
262        entries = form['entries']
263        if isinstance(entries, basestring):
264            entries = [entries]
265        deleted = []
266        for entry in entries:
267            if 'remove' in form:
268                del self.context[entry]
269                deleted.append(entry)
270        self.hitlist = search(query=self.searchterm,
271            searchtype=self.searchtype, view=self)
272        if len(deleted):
273            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6622]274        return
275
[6626]276class StudentsContainerAddActionButton(AddActionButton):
277    grok.order(1)
278    grok.context(IStudentsContainer)
279    grok.view(StudentsContainerManagePage)
280    grok.require('waeup.manageStudents')
281    text = 'Add student'
282    target = 'addstudent'
283
[6622]284class StudentAddFormPage(WAeUPAddFormPage):
285    """Add-form to add a student.
286    """
287    grok.context(IStudentsContainer)
288    grok.require('waeup.manageStudents')
289    grok.name('addstudent')
290    grok.template('studentaddpage')
291    form_fields = grok.AutoFields(IStudent)
292    title = 'Students'
293    label = 'Add student'
[6642]294    pnav = 4
[6622]295
296    @grok.action('Create student record')
297    def addStudent(self, **data):
298        student = createObject(u'waeup.Student')
299        self.applyData(student, **data)
[6652]300        self.context.addStudent(student)
[6626]301        self.flash('Student record created.')
[6651]302        self.redirect(self.url(self.context[student.student_id], 'index'))
[6622]303        return
304
[6631]305class StudentBaseDisplayFormPage(WAeUPDisplayFormPage):
306    """ Page to display student base data
307    """
[6622]308    grok.context(IStudent)
309    grok.name('index')
[6660]310    grok.require('waeup.viewStudent')
[6695]311    grok.template('basepage')
[6756]312    form_fields = grok.AutoFields(IStudentBase).omit('password')
[6642]313    pnav = 4
314    title = 'Base Data'
[6622]315
316    @property
317    def label(self):
[6818]318        return '%s: Base Data' % self.context.fullname
[6631]319
[6699]320    @property
321    def hasPassword(self):
322        if self.context.password:
323            return 'set'
324        return 'unset'
325
[6631]326class StudentBaseManageActionButton(ManageActionButton):
327    grok.order(1)
328    grok.context(IStudent)
329    grok.view(StudentBaseDisplayFormPage)
330    grok.require('waeup.manageStudents')
[6695]331    text = 'Manage'
[6631]332    target = 'edit_base'
333
334class StudentBaseManageFormPage(WAeUPEditFormPage):
335    """ View to edit student base data
336    """
337    grok.context(IStudent)
338    grok.name('edit_base')
339    grok.require('waeup.manageStudents')
340    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
[6695]341    grok.template('basemanagepage')
342    label = 'Manage base data'
[6642]343    title = 'Base Data'
344    pnav = 4
[6631]345
[6638]346    def update(self):
347        datepicker.need() # Enable jQuery datepicker in date fields.
348        super(StudentBaseManageFormPage, self).update()
349        self.wf_info = IWorkflowInfo(self.context)
350        return
351
352    def getTransitions(self):
353        """Return a list of dicts of allowed transition ids and titles.
354
355        Each list entry provides keys ``name`` and ``title`` for
356        internal name and (human readable) title of a single
357        transition.
358        """
359        allowed_transitions = self.wf_info.getManualTransitions()
360        return [dict(name='', title='No transition')] +[
361            dict(name=x, title=y) for x, y in allowed_transitions]
362
363    @grok.action('Save')
364    def save(self, **data):
[6701]365        form = self.request.form
366        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[6790]367        password = form.get('password', None)
368        password_ctl = form.get('control_password', None)
369        if password:
370            if (password != password_ctl):
[6701]371                self.flash('Passwords do not match.')
[6790]372            else:
373                # XXX: This is too early. PW should only be saved if there
374                #      are no (other) errors left in form.
375                IUserAccount(self.context).setPassword(password)
376                self.context.loggerInfo(ob_class, 'password changed')
377
[6789]378        #self.reg_number = form.get('form.reg_number', None)
379        #if self.reg_number:
380        #    hitlist = search(query=self.reg_number,searchtype='reg_number', view=self)
381        #    if hitlist and hitlist[0].student_id != self.context.student_id:
382        #        self.flash('Registration number exists.')
383        #        return
384        #self.matric_number = form.get('form.matric_number', None)
385        #if self.matric_number:
386        #    hitlist = search(query=self.matric_number,
387        #        searchtype='matric_number', view=self)
388        #    if hitlist and hitlist[0].student_id != self.context.student_id:
389        #        self.flash('Matriculation number exists.')
390        #        return
391
[6771]392        # Turn list of lists into single list
[6638]393        changed_fields = self.applyData(self.context, **data)
[6771]394        if changed_fields:
395            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
396        fields_string = ' + '.join(changed_fields)
[6638]397        self.context._p_changed = True
398        if form.has_key('transition') and form['transition']:
399            transition_id = form['transition']
400            self.wf_info.fireTransition(transition_id)
401        self.flash('Form has been saved.')
[6644]402        if fields_string:
403            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6638]404        return
405
[6631]406class StudentClearanceDisplayFormPage(WAeUPDisplayFormPage):
407    """ Page to display student clearance data
408    """
409    grok.context(IStudent)
410    grok.name('view_clearance')
[6660]411    grok.require('waeup.viewStudent')
[6695]412    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
[6650]413    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[6642]414    title = 'Clearance Data'
415    pnav = 4
[6631]416
417    @property
418    def label(self):
[6818]419        return '%s: Clearance Data' % self.context.fullname
[6631]420
421class StudentClearanceManageActionButton(ManageActionButton):
422    grok.order(1)
423    grok.context(IStudent)
424    grok.view(StudentClearanceDisplayFormPage)
425    grok.require('waeup.manageStudents')
[6695]426    text = 'Manage'
[6631]427    target = 'edit_clearance'
428
429class StudentClearanceManageFormPage(WAeUPEditFormPage):
430    """ Page to edit student clearance data
431    """
432    grok.context(IStudent)
433    grok.name('edit_clearance')
[6649]434    grok.require('waeup.manageStudents')
[6631]435    form_fields = grok.AutoFields(IStudentClearance)
[6695]436    label = 'Manage clearance data'
[6642]437    title = 'Clearance Data'
438    pnav = 4
[6631]439
[6650]440    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
441
442    def update(self):
443        datepicker.need() # Enable jQuery datepicker in date fields.
444        return super(StudentClearanceManageFormPage, self).update()
445
[6695]446    @grok.action('Save')
447    def save(self, **data):
[6762]448        msave(self, **data)
[6695]449        return
450
[6631]451class StudentPersonalDisplayFormPage(WAeUPDisplayFormPage):
452    """ Page to display student personal data
453    """
454    grok.context(IStudent)
455    grok.name('view_personal')
[6660]456    grok.require('waeup.viewStudent')
[6631]457    form_fields = grok.AutoFields(IStudentPersonal)
[6642]458    title = 'Personal Data'
459    pnav = 4
[6631]460
461    @property
462    def label(self):
[6818]463        return '%s: Personal Data' % self.context.fullname
[6631]464
465class StudentPersonalManageActionButton(ManageActionButton):
466    grok.order(1)
467    grok.context(IStudent)
468    grok.view(StudentPersonalDisplayFormPage)
469    grok.require('waeup.manageStudents')
[6695]470    text = 'Manage'
[6631]471    target = 'edit_personal'
472
473class StudentPersonalManageFormPage(WAeUPEditFormPage):
474    """ Page to edit student clearance data
475    """
476    grok.context(IStudent)
477    grok.name('edit_personal')
[6660]478    grok.require('waeup.viewStudent')
[6631]479    form_fields = grok.AutoFields(IStudentPersonal)
[6695]480    label = 'Manage personal data'
[6642]481    title = 'Personal Data'
482    pnav = 4
[6631]483
[6762]484    @grok.action('Save')
485    def save(self, **data):
486        msave(self, **data)
487        return
488
[6635]489class StudyCourseDisplayFormPage(WAeUPDisplayFormPage):
490    """ Page to display the student study course data
491    """
492    grok.context(IStudentStudyCourse)
493    grok.name('index')
[6660]494    grok.require('waeup.viewStudent')
[6635]495    form_fields = grok.AutoFields(IStudentStudyCourse)
[6775]496    grok.template('studycoursepage')
[6642]497    title = 'Study Course'
498    pnav = 4
[6635]499
500    @property
501    def label(self):
[6818]502        return '%s: Study Course' % self.context.__parent__.fullname
[6635]503
[6912]504    @property
505    def current_mode(self):
506        return study_modes.getTermByToken(
507            self.context.certificate.study_mode).title
508
[6649]509class StudyCourseManageActionButton(ManageActionButton):
510    grok.order(1)
511    grok.context(IStudentStudyCourse)
512    grok.view(StudyCourseDisplayFormPage)
513    grok.require('waeup.manageStudents')
[6695]514    text = 'Manage'
[6775]515    target = 'manage'
[6649]516
517class StudyCourseManageFormPage(WAeUPEditFormPage):
518    """ Page to edit the student study course data
519    """
520    grok.context(IStudentStudyCourse)
[6775]521    grok.name('manage')
[6649]522    grok.require('waeup.manageStudents')
[6775]523    grok.template('studycoursemanagepage')
[6649]524    form_fields = grok.AutoFields(IStudentStudyCourse)
525    title = 'Study Course'
[6695]526    label = 'Manage study course'
[6649]527    pnav = 4
[6775]528    taboneactions = ['Save','Cancel']
529    tabtwoactions = ['Remove selected levels','Cancel']
530    tabthreeactions = ['Add study level']
[6649]531
[6775]532    def update(self):
533        super(StudyCourseManageFormPage, self).update()
534        tabs.need()
535        datatable.need()
536        return
537
[6761]538    @grok.action('Save')
539    def save(self, **data):
[6762]540        msave(self, **data)
[6761]541        return
542
[6775]543    @property
544    def level_dict(self):
545        studylevelsource = StudyLevelSource().factory
546        for code in studylevelsource.getValues(self.context):
547            title = studylevelsource.getTitle(self.context, code)
548            yield(dict(code=code, title=title))
549
550    @grok.action('Add study level')
[6774]551    def addStudyLevel(self, **data):
[6775]552        level_code = self.request.form.get('addlevel', None)
[6774]553        studylevel = StudentStudyLevel()
[6775]554        studylevel.level = int(level_code)
555        try:
[6782]556            self.context.addStudentStudyLevel(
557                self.context.certificate,studylevel)
[6775]558        except KeyError:
559            self.flash('This level exists.')
560        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
[6774]561        return
562
[6775]563    @grok.action('Remove selected levels')
564    def delStudyLevels(self, **data):
565        form = self.request.form
566        if form.has_key('val_id'):
567            child_id = form['val_id']
568        else:
569            self.flash('No study level selected.')
570            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
571            return
572        if not isinstance(child_id, list):
573            child_id = [child_id]
574        deleted = []
575        for id in child_id:
576            try:
577                del self.context[id]
578                deleted.append(id)
579            except:
580                self.flash('Could not delete %s: %s: %s' % (
581                        id, sys.exc_info()[0], sys.exc_info()[1]))
582        if len(deleted):
583            self.flash('Successfully removed: %s' % ', '.join(deleted))
584        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
585        return
[6774]586
587class StudyLevelDisplayFormPage(WAeUPDisplayFormPage):
588    """ Page to display student study levels
589    """
590    grok.context(IStudentStudyLevel)
591    grok.name('index')
592    grok.require('waeup.viewStudent')
[6775]593    form_fields = grok.AutoFields(IStudentStudyLevel)
[6783]594    grok.template('studylevelpage')
[6774]595    pnav = 4
596
597    @property
[6792]598    def title(self):
599        return 'Study Level %s' % self.context.level_title
600
601    @property
[6774]602    def label(self):
[6776]603        return '%s: Study Level %s' % (
[6818]604            self.context.getStudent().fullname,self.context.level_title)
[6774]605
[6803]606    @property
607    def total_credits(self):
608        total_credits = 0
609        for key, val in self.context.items():
610            total_credits += val.credits
611        return total_credits
612
[6792]613class StudyLevelManageActionButton(ManageActionButton):
614    grok.order(1)
615    grok.context(IStudentStudyLevel)
616    grok.view(StudyLevelDisplayFormPage)
617    grok.require('waeup.manageStudents')
618    text = 'Manage'
619    target = 'manage'
620
621class StudyLevelManageFormPage(WAeUPEditFormPage):
622    """ Page to edit the student study level data
623    """
624    grok.context(IStudentStudyLevel)
625    grok.name('manage')
626    grok.require('waeup.manageStudents')
627    grok.template('studylevelmanagepage')
628    form_fields = grok.AutoFields(IStudentStudyLevel)
629    pnav = 4
630    taboneactions = ['Save','Cancel']
[6795]631    tabtwoactions = ['Add course ticket','Remove selected tickets','Cancel']
[6792]632
633    def update(self):
634        super(StudyLevelManageFormPage, self).update()
635        tabs.need()
636        datatable.need()
637        return
638
639    @property
640    def title(self):
641        return 'Study Level %s' % self.context.level_title
642
643    @property
644    def label(self):
645        return 'Manage study level %s' % self.context.level_title
646
647    @grok.action('Save')
648    def save(self, **data):
649        msave(self, **data)
650        return
651
652    @grok.action('Add course ticket')
[6795]653    def addCourseTicket(self, **data):
654        self.redirect(self.url(self.context, '@@add'))
[6792]655
656    @grok.action('Remove selected tickets')
657    def delCourseTicket(self, **data):
658        form = self.request.form
659        if form.has_key('val_id'):
660            child_id = form['val_id']
661        else:
662            self.flash('No ticket selected.')
663            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
664            return
665        if not isinstance(child_id, list):
666            child_id = [child_id]
667        deleted = []
668        for id in child_id:
669            try:
670                del self.context[id]
671                deleted.append(id)
672            except:
673                self.flash('Could not delete %s: %s: %s' % (
674                        id, sys.exc_info()[0], sys.exc_info()[1]))
675        if len(deleted):
676            self.flash('Successfully removed: %s' % ', '.join(deleted))
677        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
678        return
679
[6795]680class CourseTicketAddFormPage(WAeUPAddFormPage):
[6808]681    """Add a course ticket.
[6795]682    """
683    grok.context(IStudentStudyLevel)
684    grok.name('add')
685    grok.require('waeup.manageStudents')
686    label = 'Add course ticket'
[6808]687    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
688        'grade', 'score', 'automatic')
[6795]689    pnav = 4
690
691    @property
692    def title(self):
693        return 'Study Level %s' % self.context.level_title
694
695    @grok.action('Add course ticket')
696    def addCourseTicket(self, **data):
697        ticket = CourseTicket()
698        course = data['course']
699        ticket.core_or_elective = data['core_or_elective']
[6802]700        ticket.automatic = False
[6795]701        ticket.code = course.code
702        ticket.title = course.title
703        ticket.faculty = course.__parent__.__parent__.__parent__.title
704        ticket.department = course.__parent__.__parent__.title
705        ticket.credits = course.credits
706        ticket.passmark = course.passmark
707        ticket.semester = course.semester
708        try:
709            self.context.addCourseTicket(ticket)
710        except KeyError:
711            self.flash('The ticket exists.')
712            return
[6799]713        self.flash('Successfully added %s.' % ticket.code)
[6795]714        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
715        return
716
717    @grok.action('Cancel')
718    def cancel(self, **data):
719        self.redirect(self.url(self.context))
720
[6796]721class CourseTicketDisplayFormPage(WAeUPDisplayFormPage):
722    """ Page to display course tickets
723    """
724    grok.context(ICourseTicket)
725    grok.name('index')
726    grok.require('waeup.viewStudent')
727    form_fields = grok.AutoFields(ICourseTicket)
728    grok.template('courseticketpage')
729    pnav = 4
730
731    @property
732    def title(self):
733        return 'Course Ticket %s' % self.context.code
734
735    @property
736    def label(self):
737        return '%s: Course Ticket %s' % (
[6818]738            self.context.getStudent().fullname,self.context.code)
[6796]739
740class CourseTicketManageActionButton(ManageActionButton):
741    grok.order(1)
742    grok.context(ICourseTicket)
743    grok.view(CourseTicketDisplayFormPage)
744    grok.require('waeup.manageStudents')
745    text = 'Manage'
746    target = 'manage'
747
748class CourseTicketManageFormPage(WAeUPEditFormPage):
749    """ Page to manage course tickets
750    """
751    grok.context(ICourseTicket)
752    grok.name('manage')
753    grok.require('waeup.manageStudents')
754    form_fields = grok.AutoFields(ICourseTicket)
755    grok.template('courseticketmanagepage')
756    pnav = 4
757
758    @property
759    def title(self):
760        return 'Course Ticket %s' % self.context.code
761
762    @property
763    def label(self):
764        return 'Manage course ticket %s' % self.context.code
765
766    @grok.action('Save')
767    def save(self, **data):
768        msave(self, **data)
769        return
770
[6635]771class PaymentsDisplayFormPage(WAeUPDisplayFormPage):
772    """ Page to display the student payments
773    """
[6859]774    grok.context(IStudentPaymentsContainer)
[6635]775    grok.name('index')
[6660]776    grok.require('waeup.viewStudent')
[6859]777    form_fields = grok.AutoFields(IStudentPaymentsContainer)
[6869]778    grok.template('paymentspage')
[6642]779    title = 'Payments'
780    pnav = 4
[6635]781
[6869]782    def formatDatetime(self,datetimeobj):
783        if isinstance(datetimeobj, datetime):
784            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
785        else:
786            return None
787
[6635]788    @property
789    def label(self):
[6818]790        return '%s: Payments' % self.context.__parent__.fullname
[6635]791
[6869]792    def update(self):
793        super(PaymentsDisplayFormPage, self).update()
794        datatable.need()
795        return
796
797class PaymentsManageFormPage(WAeUPEditFormPage):
798    """ Page to manage the student payments
799    """
800    grok.context(IStudentPaymentsContainer)
801    grok.name('manage')
802    grok.require('waeup.manageStudents')
803    form_fields = grok.AutoFields(IStudentPaymentsContainer)
804    grok.template('paymentsmanagepage')
805    title = 'Payments'
806    pnav = 4
807
808    def formatDatetime(self,datetimeobj):
809        if isinstance(datetimeobj, datetime):
810            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
811        else:
812            return None
813
814    @property
815    def label(self):
816        return '%s: Payments' % self.context.__parent__.fullname
817
818    def update(self):
819        super(PaymentsManageFormPage, self).update()
820        datatable.need()
821        return
822
823    @grok.action('Remove selected tickets')
824    def delPaymentTicket(self, **data):
825        form = self.request.form
826        if form.has_key('val_id'):
827            child_id = form['val_id']
828        else:
829            self.flash('No payment selected.')
830            self.redirect(self.url(self.context, '@@manage'))
831            return
832        if not isinstance(child_id, list):
833            child_id = [child_id]
834        deleted = []
835        for id in child_id:
836            try:
837                del self.context[id]
838                deleted.append(id)
839            except:
840                self.flash('Could not delete %s: %s: %s' % (
841                        id, sys.exc_info()[0], sys.exc_info()[1]))
842        if len(deleted):
843            self.flash('Successfully removed: %s' % ', '.join(deleted))
844        self.redirect(self.url(self.context, u'@@manage'))
845        return
846
847    @grok.action('Add online payment ticket')
848    def addPaymentTicket(self, **data):
849        self.redirect(self.url(self.context, '@@addop'))
850
851class OnlinePaymentManageActionButton(ManageActionButton):
852    grok.order(1)
853    grok.context(IStudentPaymentsContainer)
854    grok.view(PaymentsDisplayFormPage)
855    grok.require('waeup.manageStudents')
856    text = 'Manage payments'
857    target = 'manage'
858
859class OnlinePaymentAddFormPage(WAeUPAddFormPage):
860    """ Page to add an online payment ticket
861    """
862    grok.context(IStudentPaymentsContainer)
863    grok.name('addop')
864    grok.require('waeup.handleStudent')
[6877]865    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]866        'p_category')
[6906]867    #zzgrok.template('addpaymentpage')
[6869]868    label = 'Add online payment'
869    title = 'Payments'
870    pnav = 4
871
872    @grok.action('Create ticket')
873    def createTicket(self, **data):
[6876]874        payment = createObject(u'waeup.StudentOnlinePayment')
[6869]875        self.applyData(payment, **data)
876        timestamp = "%d" % int(time()*1000)
877        #order_id = "%s%s" % (student_id[1:],timestamp)
878        payment.p_id = "p%s" % timestamp
[6876]879        (payment.amount_auth,
[6877]880            payment.p_item, payment.p_session,
[6876]881            payment.surcharge_1,
882            payment.surcharge_2,
[6920]883            payment.surcharge_3,
884            error)  = getPaymentDetails(
[6876]885            data['p_category'],self.context.__parent__)
[6920]886        if error:
887            self.flash(error)
888            self.redirect(self.url(self.context, u'@@manage'))
889            return
[6869]890        self.context[payment.p_id] = payment
891        self.flash('Payment ticket created.')
892        self.redirect(self.url(self.context, u'@@manage'))
893        return
894
895class OnlinePaymentDisplayFormPage(WAeUPDisplayFormPage):
896    """ Page to view an online payment ticket
897    """
[6877]898    grok.context(IStudentOnlinePayment)
[6869]899    grok.name('index')
900    grok.require('waeup.viewStudent')
[6877]901    form_fields = grok.AutoFields(IStudentOnlinePayment)
[6869]902    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
903    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
904    pnav = 4
905
906    @property
907    def title(self):
908        return 'Online Payment Ticket %s' % self.context.p_id
909
910    @property
911    def label(self):
912        return '%s: Online Payment Ticket %s' % (
913            self.context.__parent__.__parent__.fullname,self.context.p_id)
914
[6930]915class OnlinePaymentCallbackPage(grok.View):
916    """ Callback view
917    """
918    grok.context(IStudentOnlinePayment)
919    grok.name('callback')
920    grok.require('waeup.payStudent')
921
922    # This update method simulates a valid callback und must be
923    # specified in the customization package. The parameters must be taken
924    # from the incoming request.
925    def update(self):
926        self.context.r_amount_approved = self.context.amount_auth
927        self.context.r_card_num = u'0000'
928        self.context.r_code = u'00'
929        self.context.p_state = 'paid'
930        self.context.payment_date = datetime.now()
931        if self.context.p_category == 'clearance':
932            # Find valid CLR access code
933            pass
934        elif self.context.p_category == 'schoolfee':
935            # Find valid SFE access code
936            pass
937        return
938
939    def render(self):
940        self.redirect(self.url(self.context))
941        return
942
[6635]943class AccommodationDisplayFormPage(WAeUPDisplayFormPage):
944    """ Page to display the student accommodation data
945    """
946    grok.context(IStudentAccommodation)
947    grok.name('index')
[6660]948    grok.require('waeup.viewStudent')
[6635]949    form_fields = grok.AutoFields(IStudentAccommodation)
950    #grok.template('accommodationpage')
[6642]951    title = 'Accommodation'
952    pnav = 4
[6635]953
954    @property
955    def label(self):
[6818]956        return '%s: Accommodation Data' % self.context.__parent__.fullname
[6637]957
958class StudentHistoryPage(WAeUPPage):
959    """ Page to display student clearance data
960    """
961    grok.context(IStudent)
962    grok.name('history')
[6660]963    grok.require('waeup.viewStudent')
[6637]964    grok.template('studenthistory')
[6642]965    title = 'History'
966    pnav = 4
[6637]967
968    @property
969    def label(self):
[6818]970        return '%s: History' % self.context.fullname
[6694]971
972# Pages for students only
973
974class StudentBaseEditActionButton(ManageActionButton):
975    grok.order(1)
976    grok.context(IStudent)
977    grok.view(StudentBaseDisplayFormPage)
978    grok.require('waeup.handleStudent')
979    text = 'Change password'
980    target = 'bedit'
981
[6756]982class StudentPasswordSetting(grok.Adapter):
983    """Adapt IStudent to data needed for password settings.
984
985    We provide password getters/setters for the attached context (an
986    IStudent object) that cooperate seamless with the usual
987    formlib/form techniques.
988    """
989    grok.context(IStudent)
990    grok.provides(IStudentPasswordSetting)
991
992    def __init__(self, context):
[6818]993        self.name = context.fullname
[6756]994        self.password_repeat = context.password
995        self.context = context
996        return
997
998    def getPassword(self):
999        return self.context.password
1000
1001    def setPassword(self, password):
1002        IUserAccount(self.context).setPassword(password)
1003        return
1004
1005    password = property(getPassword, setPassword)
1006
[6694]1007class StudentBaseEditFormPage(WAeUPEditFormPage):
1008    """ View to edit student base data by student
1009    """
1010    grok.context(IStudent)
1011    grok.name('bedit')
1012    grok.require('waeup.handleStudent')
[6756]1013    #form_fields = grok.AutoFields(IStudentBaseEdit).omit(
1014    #    'student_id', 'reg_number', 'matric_number')
1015    form_fields = grok.AutoFields(IStudentPasswordSetting)
[6695]1016    grok.template('baseeditpage')
[6694]1017    label = 'Change password'
1018    title = 'Base Data'
1019    pnav = 4
1020
1021    def update(self):
1022        super(StudentBaseEditFormPage, self).update()
1023        self.wf_info = IWorkflowInfo(self.context)
1024        return
1025
[6756]1026    def onFailure(self, action, data, errors):
1027        new_status = []
1028        other_errors = False
1029        for error in errors:
1030            msg = getattr(error, 'message', '')
1031            if isinstance(msg, basestring) and msg != '':
1032                new_status.append(msg)
1033            else:
1034                other_errors = True
1035        if other_errors:
1036            if new_status:
1037                new_status.append('see below for further errors')
1038            else:
1039                new_status.append('See below for details.')
1040        if new_status:
1041            self.status = u'There were errors: %s' % ', '.join(new_status)
1042        return
1043
1044    @grok.action('Save', failure=onFailure)
[6694]1045    def save(self, **data):
[6771]1046        self.applyData(self.context, **data)
1047        self.flash('Form has been saved.')
[6694]1048        return
[6695]1049
[6719]1050class StudentClearanceStartActionButton(ManageActionButton):
1051    grok.order(1)
1052    grok.context(IStudent)
1053    grok.view(StudentClearanceDisplayFormPage)
1054    grok.require('waeup.handleStudent')
1055    icon = 'actionicon_start.png'
1056    text = 'Start clearance'
1057    target = 'start_clearance'
1058
1059    @property
1060    def target_url(self):
1061        if self.context.state != 'admitted':
1062            return ''
1063        return self.view.url(self.view.context, self.target)
1064
[6773]1065class StartClearancePage(WAeUPPage):
[6770]1066    grok.context(IStudent)
1067    grok.name('start_clearance')
1068    grok.require('waeup.handleStudent')
1069    grok.template('enterpin')
1070    title = 'Start clearance'
1071    label = 'Start clearance'
1072    ac_prefix = 'CLR'
1073    notice = ''
1074    pnav = 4
1075    buttonname = 'Start clearance now'
1076
1077    def update(self, SUBMIT=None):
1078        self.ac_series = self.request.form.get('ac_series', None)
1079        self.ac_number = self.request.form.get('ac_number', None)
1080
1081        if SUBMIT is None:
1082            return
1083        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1084        code = get_access_code(pin)
1085        if not code:
1086            self.flash('Access code is invalid.')
1087            return
1088        # Mark pin as used (this also fires a pin related transition)
1089        # and fire transition start_clearance
1090        if code.state == USED:
1091            self.flash('Access code has already been used.')
1092            return
1093        else:
1094            comment = u"AC invalidated for %s" % self.context.student_id
1095            # Here we know that the ac is in state initialized so we do not
[6927]1096            # expect an exception, but the owner might be different
1097            if not invalidate_accesscode(pin,comment,self.context.student_id):
1098                self.flash('You are not the owner of this access code.')
1099                return
[6770]1100            self.context.clr_code = pin
1101        IWorkflowInfo(self.context).fireTransition('start_clearance')
[6771]1102        self.flash('Clearance process has been started.')
[6770]1103        self.redirect(self.url(self.context,'cedit'))
1104        return
1105
[6695]1106class StudentClearanceEditActionButton(ManageActionButton):
1107    grok.order(1)
1108    grok.context(IStudent)
1109    grok.view(StudentClearanceDisplayFormPage)
1110    grok.require('waeup.handleStudent')
[6722]1111    text = 'Edit'
[6695]1112    target = 'cedit'
1113
[6717]1114    @property
1115    def target_url(self):
1116        if self.context.clearance_locked:
1117            return ''
1118        return self.view.url(self.view.context, self.target)
1119
[6695]1120class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1121    """ View to edit student clearance data by student
1122    """
1123    grok.context(IStudent)
1124    grok.name('cedit')
1125    grok.require('waeup.handleStudent')
[6756]1126    form_fields = grok.AutoFields(
1127        IStudentClearanceEdit).omit('clearance_locked')
[6722]1128    label = 'Edit clearance data'
[6695]1129    title = 'Clearance Data'
1130    pnav = 4
1131    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[6718]1132
1133    def emitLockMessage(self):
1134        self.flash('The requested form is locked (read-only).')
1135        self.redirect(self.url(self.context))
1136        return
1137
1138    def update(self):
1139        if self.context.clearance_locked:
1140            self.emitLockMessage()
1141            return
1142        datepicker.need()
1143        return super(StudentClearanceEditFormPage, self).update()
[6719]1144
[6722]1145    @grok.action('Save')
1146    def save(self, **data):
1147        self.applyData(self.context, **data)
[6771]1148        self.flash('Clearance form has been saved.')
[6722]1149        return
1150
1151    @grok.action('Save and request clearance')
1152    def requestclearance(self, **data):
1153        self.applyData(self.context, **data)
1154        self.context._p_changed = True
1155        #if self.dataNotComplete():
1156        #    self.flash(self.dataNotComplete())
1157        #    return
[6771]1158        self.flash('Clearance form has been saved.')
[6769]1159        self.redirect(self.url(self.context,'request_clearance'))
[6722]1160        return
1161
[6773]1162class RequestClearancePage(WAeUPPage):
[6769]1163    grok.context(IStudent)
1164    grok.name('request_clearance')
1165    grok.require('waeup.handleStudent')
1166    grok.template('enterpin')
1167    title = 'Request clearance'
1168    label = 'Request clearance'
1169    notice = 'Enter the CLR access code used for starting clearance.'
1170    ac_prefix = 'CLR'
1171    pnav = 4
1172    buttonname = 'Request clearance now'
1173
1174    def update(self, SUBMIT=None):
1175        self.ac_series = self.request.form.get('ac_series', None)
1176        self.ac_number = self.request.form.get('ac_number', None)
1177        if SUBMIT is None:
1178            return
1179        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1180        if self.context.clr_code != pin:
1181            self.flash("This isn't your CLR access code.")
1182            return
1183        state = IWorkflowState(self.context).getState()
1184        # This shouldn't happen, but the application officer
1185        # might have forgotten to lock the form after changing the state
1186        if state != CLEARANCE:
1187            self.flash('This form cannot be submitted. Wrong state!')
1188            return
1189        IWorkflowInfo(self.context).fireTransition('request_clearance')
1190        self.flash('Clearance has been requested.')
1191        self.redirect(self.url(self.context))
[6789]1192        return
[6806]1193
[6808]1194class AddStudyLevelActionButton(AddActionButton):
1195    grok.order(1)
1196    grok.context(IStudentStudyCourse)
1197    grok.view(StudyCourseDisplayFormPage)
1198    grok.require('waeup.handleStudent')
1199    text = 'Add course list'
1200    target = 'add'
1201
1202    @property
1203    def target_url(self):
1204        student = self.view.context.getStudent()
1205        condition1 = student.state != 'school fee paid'
1206        condition2 = str(student['studycourse'].current_level) in \
1207            self.view.context.keys()
1208        if condition1 or condition2:
1209            return ''
1210        return self.view.url(self.view.context, self.target)
1211
[6806]1212class AddStudyLevelFormPage(WAeUPEditFormPage):
1213    """ Page for students to add current study levels
1214    """
1215    grok.context(IStudentStudyCourse)
1216    grok.name('add')
1217    grok.require('waeup.handleStudent')
1218    grok.template('studyleveladdpage')
1219    form_fields = grok.AutoFields(IStudentStudyCourse)
1220    title = 'Study Course'
1221    pnav = 4
1222
1223    @property
1224    def label(self):
1225        studylevelsource = StudyLevelSource().factory
1226        code = self.context.current_level
1227        title = studylevelsource.getTitle(self.context, code)
1228        return 'Add current level %s' % title
1229
1230    def emitLockMessage(self):
1231        self.flash('The requested form is locked (read-only).')
1232        self.redirect(self.url(self.context))
1233        return
1234
1235    def update(self):
1236        if self.context.getStudent().state != 'school fee paid':
1237            self.emitLockMessage()
1238            return
1239        super(AddStudyLevelFormPage, self).update()
1240        return
1241
1242    @grok.action('Create course list now')
1243    def addStudyLevel(self, **data):
1244        studylevel = StudentStudyLevel()
1245        studylevel.level = self.context.current_level
1246        studylevel.level_session = self.context.current_session
1247        try:
1248            self.context.addStudentStudyLevel(
1249                self.context.certificate,studylevel)
1250        except KeyError:
1251            self.flash('This level exists.')
1252        self.redirect(self.url(self.context))
1253        return
[6808]1254
1255class StudyLevelEditActionButton(ManageActionButton):
1256    grok.order(1)
1257    grok.context(IStudentStudyLevel)
1258    grok.view(StudyLevelDisplayFormPage)
1259    grok.require('waeup.handleStudent')
1260    text = 'Add and remove courses'
1261    target = 'edit'
1262
1263    @property
1264    def target_url(self):
1265        student = self.view.context.getStudent()
1266        condition1 = student.state != 'school fee paid'
1267        condition2 = student[
1268            'studycourse'].current_level != self.view.context.level
1269        if condition1 or condition2:
1270            return ''
1271        return self.view.url(self.view.context, self.target)
1272
1273class StudyLevelEditFormPage(WAeUPEditFormPage):
1274    """ Page to edit the student study level data by students
1275    """
1276    grok.context(IStudentStudyLevel)
1277    grok.name('edit')
1278    grok.require('waeup.handleStudent')
1279    grok.template('studyleveleditpage')
1280    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1281        'level_session', 'level_verdict')
1282    pnav = 4
1283
1284    def update(self):
1285        super(StudyLevelEditFormPage, self).update()
1286    #    tabs.need()
1287        datatable.need()
1288        return
1289
1290    @property
1291    def title(self):
1292        return 'Study Level %s' % self.context.level_title
1293
1294    @property
1295    def label(self):
1296        return 'Add and remove course tickets of study level %s' % self.context.level_title
1297
1298    @property
1299    def total_credits(self):
1300        total_credits = 0
1301        for key, val in self.context.items():
1302            total_credits += val.credits
1303        return total_credits
1304
1305    @grok.action('Add course ticket')
1306    def addCourseTicket(self, **data):
1307        self.redirect(self.url(self.context, 'ctadd'))
1308
1309    @grok.action('Remove selected tickets')
1310    def delCourseTicket(self, **data):
1311        form = self.request.form
1312        if form.has_key('val_id'):
1313            child_id = form['val_id']
1314        else:
1315            self.flash('No ticket selected.')
1316            self.redirect(self.url(self.context, '@@edit'))
1317            return
1318        if not isinstance(child_id, list):
1319            child_id = [child_id]
1320        deleted = []
1321        for id in child_id:
1322            # Student are not allowed to remove core tickets
1323            if not self.context[id].core_or_elective:
1324                try:
1325                    del self.context[id]
1326                    deleted.append(id)
1327                except:
1328                    self.flash('Could not delete %s: %s: %s' % (
1329                            id, sys.exc_info()[0], sys.exc_info()[1]))
1330        if len(deleted):
1331            self.flash('Successfully removed: %s' % ', '.join(deleted))
1332        self.redirect(self.url(self.context, u'@@edit'))
1333        return
1334
[6810]1335    @grok.action('Register course list')
1336    def register_courses(self, **data):
1337        state = IWorkflowState(self.context.getStudent()).getState()
1338        IWorkflowInfo(self.context.getStudent()).fireTransition('register_courses')
1339        self.flash('Course list has been registered.')
1340        self.redirect(self.url(self.context))
1341        return
1342
[6808]1343class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1344    """Add a course ticket by student.
1345    """
1346    grok.name('ctadd')
1347    grok.require('waeup.handleStudent')
1348    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1349        'grade', 'score', 'core_or_elective', 'automatic')
1350
1351    @grok.action('Add course ticket')
1352    def addCourseTicket(self, **data):
1353        ticket = CourseTicket()
1354        course = data['course']
1355        ticket.automatic = False
1356        ticket.code = course.code
1357        ticket.title = course.title
1358        ticket.faculty = course.__parent__.__parent__.__parent__.title
1359        ticket.department = course.__parent__.__parent__.title
1360        ticket.credits = course.credits
1361        ticket.passmark = course.passmark
1362        ticket.semester = course.semester
1363        try:
1364            self.context.addCourseTicket(ticket)
1365        except KeyError:
1366            self.flash('The ticket exists.')
1367            return
1368        self.flash('Successfully added %s.' % ticket.code)
1369        self.redirect(self.url(self.context, u'@@edit'))
1370        return
Note: See TracBrowser for help on using the repository browser.