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

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

Also protect students section against unintentional deletions with new jsaction.

  • Property svn:keywords set to Id
File size: 71.4 KB
Line 
1## $Id: browser.py 7329 2011-12-11 13:18:40Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import sys
21import grok
22from urllib import urlencode
23from time import time
24from datetime import datetime
25from zope.event import notify
26from zope.catalog.interfaces import ICatalog
27from zope.component import queryUtility, getUtility
28from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
29from zope.component import createObject
30from waeup.sirp.accesscodes import (
31    invalidate_accesscode, get_access_code, create_accesscode)
32from waeup.sirp.accesscodes.workflow import USED
33from waeup.sirp.browser import (
34    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
35    ContactAdminForm)
36from waeup.sirp.browser.breadcrumbs import Breadcrumb
37from waeup.sirp.browser.resources import datepicker, datatable, tabs, warning
38from waeup.sirp.browser.viewlets import (
39    ManageActionButton, AddActionButton)
40from waeup.sirp.browser.layout import jsaction, JSAction
41from waeup.sirp.interfaces import (
42    ISIRPObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm)
43from waeup.sirp.widgets.datewidget import (
44    FriendlyDateWidget, FriendlyDateDisplayWidget,
45    FriendlyDatetimeDisplayWidget)
46from waeup.sirp.university.vocabularies import study_modes
47from waeup.sirp.students.interfaces import (
48    IStudentsContainer, IStudent, IStudentClearance,
49    IStudentPersonal, IStudentBase, IStudentStudyCourse,
50    IStudentAccommodation, IStudentClearanceEdit, IStudentStudyLevel,
51    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
52    IStudentOnlinePayment, IBedTicket, IStudentsUtils
53    )
54from waeup.sirp.students.catalog import search
55from waeup.sirp.students.workflow import (
56    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
57from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
58from waeup.sirp.students.vocabularies import StudyLevelSource
59from waeup.sirp.browser.resources import toggleall
60from waeup.sirp.hostels.hostel import NOT_OCCUPIED
61from waeup.sirp.utils.helpers import send_mail
62
63def write_log_message(view, message):
64    ob_class = view.__implemented__.__name__.replace('waeup.sirp.','')
65    view.context.getStudent().loggerInfo(ob_class, message)
66    return
67
68# Save function used for save methods in pages
69def msave(view, **data):
70    changed_fields = view.applyData(view.context, **data)
71    # Turn list of lists into single list
72    if changed_fields:
73        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
74    # Inform catalog if certificate has changed
75    # (applyData does this only for the context)
76    if 'certificate' in changed_fields:
77        notify(grok.ObjectModifiedEvent(view.context.getStudent()))
78    fields_string = ' + '.join(changed_fields)
79    view.flash('Form has been saved.')
80    if fields_string:
81        write_log_message(view, 'saved: %s' % fields_string)
82    return
83
84def emit_lock_message(view):
85    view.flash('The requested form is locked (read-only).')
86    view.redirect(view.url(view.context))
87    return
88
89class StudentsBreadcrumb(Breadcrumb):
90    """A breadcrumb for the students container.
91    """
92    grok.context(IStudentsContainer)
93    title = 'Students'
94
95class StudentBreadcrumb(Breadcrumb):
96    """A breadcrumb for the student container.
97    """
98    grok.context(IStudent)
99
100    def title(self):
101        return self.context.fullname
102
103class SudyCourseBreadcrumb(Breadcrumb):
104    """A breadcrumb for the student study course.
105    """
106    grok.context(IStudentStudyCourse)
107    title = 'Study Course'
108
109class PaymentsBreadcrumb(Breadcrumb):
110    """A breadcrumb for the student payments folder.
111    """
112    grok.context(IStudentPaymentsContainer)
113    title = 'Payments'
114
115class OnlinePaymentBreadcrumb(Breadcrumb):
116    """A breadcrumb for payments.
117    """
118    grok.context(IStudentOnlinePayment)
119
120    @property
121    def title(self):
122        return self.context.p_id
123
124class AccommodationBreadcrumb(Breadcrumb):
125    """A breadcrumb for the student accommodation folder.
126    """
127    grok.context(IStudentAccommodation)
128    title = 'Accommodation'
129
130    #@property
131    #def target(self):
132    #    prm = get_principal_role_manager()
133    #    principal = get_current_principal()
134    #    roles = [x[0] for x in prm.getRolesForPrincipal(principal.id)]
135    #    if 'waeup.Student' in roles:
136    #        return 'index'
137    #    else:
138    #        return 'manage'
139
140class BedTicketBreadcrumb(Breadcrumb):
141    """A breadcrumb for bed tickets.
142    """
143    grok.context(IBedTicket)
144
145    @property
146    def title(self):
147        return 'Bed Ticket %s' % self.context.getSessionString()
148
149class StudyLevelBreadcrumb(Breadcrumb):
150    """A breadcrumb for course lists.
151    """
152    grok.context(IStudentStudyLevel)
153
154    @property
155    def title(self):
156        return self.context.level_title
157
158class StudentsContainerPage(SIRPPage):
159    """The standard view for student containers.
160    """
161    grok.context(IStudentsContainer)
162    grok.name('index')
163    grok.require('waeup.viewStudentsContainer')
164    grok.template('containerpage')
165    label = 'Student Section'
166    title = 'Students'
167    pnav = 4
168
169    def update(self, *args, **kw):
170        datatable.need()
171        form = self.request.form
172        self.hitlist = []
173        if 'searchterm' in form and form['searchterm']:
174            self.searchterm = form['searchterm']
175            self.searchtype = form['searchtype']
176        elif 'old_searchterm' in form:
177            self.searchterm = form['old_searchterm']
178            self.searchtype = form['old_searchtype']
179        else:
180            if 'search' in form:
181                self.flash('Empty search string.')
182            return
183        if self.searchtype == 'current_session':
184            self.searchterm = int(self.searchterm)
185        self.hitlist = search(query=self.searchterm,
186            searchtype=self.searchtype, view=self)
187        if not self.hitlist:
188            self.flash('No student found.')
189        return
190
191class SetPasswordPage(SIRPPage):
192    grok.context(ISIRPObject)
193    grok.name('setpassword')
194    grok.require('waeup.Public')
195    grok.template('setpassword')
196    title = ''
197    label = 'Set password for first-time login'
198    ac_prefix = 'PWD'
199    pnav = 0
200
201    def update(self, SUBMIT=None):
202        self.reg_number = self.request.form.get('reg_number', None)
203        self.ac_series = self.request.form.get('ac_series', None)
204        self.ac_number = self.request.form.get('ac_number', None)
205
206        if SUBMIT is None:
207            return
208        hitlist = search(query=self.reg_number,
209            searchtype='reg_number', view=self)
210        if not hitlist:
211            self.flash('No student found.')
212            return
213        if len(hitlist) != 1:   # Cannot happen but anyway
214            self.flash('More than one student found.')
215            return
216        student = hitlist[0].context
217        self.student_id = student.student_id
218        student_pw = student.password
219        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
220        code = get_access_code(pin)
221        if not code:
222            self.flash('Access code is invalid.')
223            return
224        if student_pw and pin == student.adm_code:
225            self.flash('Password has already been set. Your Student Id is %s'
226                % self.student_id)
227            return
228        elif student_pw:
229            self.flash('Password has already been set. You are using the wrong Access Code.')
230            return
231        # Mark pin as used (this also fires a pin related transition)
232        # and set student password
233        if code.state == USED:
234            self.flash('Access code has already been used.')
235            return
236        else:
237            comment = u"AC invalidated for %s" % self.student_id
238            # Here we know that the ac is in state initialized so we do not
239            # expect an exception
240            #import pdb; pdb.set_trace()
241            invalidate_accesscode(pin,comment)
242            IUserAccount(student).setPassword(self.ac_number)
243            student.adm_code = pin
244        self.flash('Password has been set. Your Student Id is %s'
245            % self.student_id)
246        return
247
248class StudentsContainerManageActionButton(ManageActionButton):
249    grok.order(1)
250    grok.context(IStudentsContainer)
251    grok.view(StudentsContainerPage)
252    grok.require('waeup.manageStudent')
253    text = 'Manage student section'
254
255
256class StudentsContainerManagePage(SIRPPage):
257    """The manage page for student containers.
258    """
259    grok.context(IStudentsContainer)
260    grok.name('manage')
261    grok.require('waeup.manageStudent')
262    grok.template('containermanagepage')
263    pnav = 4
264    title = 'Manage student section'
265
266    @property
267    def label(self):
268        return self.title
269
270    def update(self, *args, **kw):
271        datatable.need()
272        toggleall.need()
273        warning.need()
274        form = self.request.form
275        self.hitlist = []
276        if 'searchterm' in form and form['searchterm']:
277            self.searchterm = form['searchterm']
278            self.searchtype = form['searchtype']
279        elif 'old_searchterm' in form:
280            self.searchterm = form['old_searchterm']
281            self.searchtype = form['old_searchtype']
282        else:
283            if 'search' in form:
284                self.flash('Empty search string.')
285            return
286        if not 'entries' in form:
287            self.hitlist = search(query=self.searchterm,
288                searchtype=self.searchtype, view=self)
289            if not self.hitlist:
290                self.flash('No student found.')
291            return
292        entries = form['entries']
293        if isinstance(entries, basestring):
294            entries = [entries]
295        deleted = []
296        for entry in entries:
297            if 'remove' in form:
298                del self.context[entry]
299                deleted.append(entry)
300        self.hitlist = search(query=self.searchterm,
301            searchtype=self.searchtype, view=self)
302        if len(deleted):
303            self.flash('Successfully removed: %s' % ', '.join(deleted))
304        return
305
306class StudentsContainerAddActionButton(AddActionButton):
307    grok.order(1)
308    grok.context(IStudentsContainer)
309    grok.view(StudentsContainerManagePage)
310    grok.require('waeup.manageStudent')
311    text = 'Add student'
312    target = 'addstudent'
313
314class StudentAddFormPage(SIRPAddFormPage):
315    """Add-form to add a student.
316    """
317    grok.context(IStudentsContainer)
318    grok.require('waeup.manageStudent')
319    grok.name('addstudent')
320    grok.template('studentaddpage')
321    form_fields = grok.AutoFields(IStudent)
322    title = 'Students'
323    label = 'Add student'
324    pnav = 4
325
326    @grok.action('Create student record')
327    def addStudent(self, **data):
328        student = createObject(u'waeup.Student')
329        self.applyData(student, **data)
330        self.context.addStudent(student)
331        self.flash('Student record created.')
332        self.redirect(self.url(self.context[student.student_id], 'index'))
333        return
334
335class StudentBaseDisplayFormPage(SIRPDisplayFormPage):
336    """ Page to display student base data
337    """
338    grok.context(IStudent)
339    grok.name('index')
340    grok.require('waeup.viewStudent')
341    grok.template('basepage')
342    form_fields = grok.AutoFields(IStudentBase).omit('password')
343    pnav = 4
344    title = 'Base Data'
345
346    @property
347    def label(self):
348        return '%s: Base Data' % self.context.fullname
349
350    @property
351    def hasPassword(self):
352        if self.context.password:
353            return 'set'
354        return 'unset'
355
356class ContactActionButton(ManageActionButton):
357    grok.order(4)
358    grok.context(IStudent)
359    grok.view(StudentBaseDisplayFormPage)
360    grok.require('waeup.manageStudent')
361    icon = 'actionicon_mail.png'
362    text = 'Send email'
363    target = 'contactstudent'
364
365class ContactStudentForm(ContactAdminForm):
366    grok.context(IStudent)
367    grok.name('contactstudent')
368    grok.require('waeup.viewStudent')
369    pnav = 4
370    title = 'Contact'
371    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
372
373    def update(self, subject=u''):
374        self.form_fields.get('subject').field.default = subject
375        self.subject = subject
376        return
377
378    def label(self):
379        return u'Send message to %s' % self.context.fullname
380
381    @grok.action('Send message now')
382    def send(self, *args, **data):
383        fullname = self.request.principal.title
384        try:
385            email_from = self.request.principal.email
386        except AttributeError:
387            email_from = self.config.email_admin
388        username = self.request.principal.id
389        usertype = self.request.principal.user_type.title()
390        body = data['body']
391        #subject = u'Mail from SIRP'
392        subject = data['subject']
393        email_to = self.context.email
394        success = send_mail(fullname,username,usertype,self.config.name,
395                  body,email_from,email_to,subject)
396        if success:
397            self.flash('Your message has been sent.')
398        else:
399            self.flash('An smtp server error occurred.')
400        return
401
402class StudentBaseManageActionButton(ManageActionButton):
403    grok.order(1)
404    grok.context(IStudent)
405    grok.view(StudentBaseDisplayFormPage)
406    grok.require('waeup.manageStudent')
407    text = 'Manage'
408    target = 'manage_base'
409
410class StudentBaseManageFormPage(SIRPEditFormPage):
411    """ View to manage student base data
412    """
413    grok.context(IStudent)
414    grok.name('manage_base')
415    grok.require('waeup.manageStudent')
416    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
417    grok.template('basemanagepage')
418    label = 'Manage base data'
419    title = 'Base Data'
420    pnav = 4
421
422    def update(self):
423        datepicker.need() # Enable jQuery datepicker in date fields.
424        tabs.need()
425        super(StudentBaseManageFormPage, self).update()
426        self.wf_info = IWorkflowInfo(self.context)
427        return
428
429    def getTransitions(self):
430        """Return a list of dicts of allowed transition ids and titles.
431
432        Each list entry provides keys ``name`` and ``title`` for
433        internal name and (human readable) title of a single
434        transition.
435        """
436        allowed_transitions = self.wf_info.getManualTransitions()
437        return [dict(name='', title='No transition')] +[
438            dict(name=x, title=y) for x, y in allowed_transitions]
439
440    @grok.action('Save')
441    def save(self, **data):
442        form = self.request.form
443        password = form.get('password', None)
444        password_ctl = form.get('control_password', None)
445        if password:
446            validator = getUtility(IPasswordValidator)
447            errors = validator.validate_password(password, password_ctl)
448            if errors:
449                self.flash( ' '.join(errors))
450                return
451        changed_fields = self.applyData(self.context, **data)
452        # Turn list of lists into single list
453        if changed_fields:
454            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
455        else:
456            changed_fields = []
457        if password:
458            # Now we know that the form has no errors and can set password ...
459            IUserAccount(self.context).setPassword(password)
460            changed_fields.append('password')
461        # ... and execute transition
462        if form.has_key('transition') and form['transition']:
463            transition_id = form['transition']
464            self.wf_info.fireTransition(transition_id)
465        fields_string = ' + '.join(changed_fields)
466        self.flash('Form has been saved.')
467        if fields_string:
468            write_log_message(self, 'saved: % s' % fields_string)
469        return
470
471class StudentClearanceDisplayFormPage(SIRPDisplayFormPage):
472    """ Page to display student clearance data
473    """
474    grok.context(IStudent)
475    grok.name('view_clearance')
476    grok.require('waeup.viewStudent')
477    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
478    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
479    title = 'Clearance Data'
480    pnav = 4
481
482    @property
483    def label(self):
484        return '%s: Clearance Data' % self.context.fullname
485
486class StudentClearanceManageActionButton(ManageActionButton):
487    grok.order(1)
488    grok.context(IStudent)
489    grok.view(StudentClearanceDisplayFormPage)
490    grok.require('waeup.manageStudent')
491    text = 'Manage'
492    target = 'edit_clearance'
493
494class StudentClearActionButton(ManageActionButton):
495    grok.order(2)
496    grok.context(IStudent)
497    grok.view(StudentClearanceDisplayFormPage)
498    grok.require('waeup.clearStudent')
499    text = 'Clear student'
500    target = 'clear'
501    icon = 'actionicon_accept.png'
502
503    @property
504    def target_url(self):
505        if self.context.state != REQUESTED:
506            return ''
507        return self.view.url(self.view.context, self.target)
508
509class StudentRejectClearanceActionButton(ManageActionButton):
510    grok.order(3)
511    grok.context(IStudent)
512    grok.view(StudentClearanceDisplayFormPage)
513    grok.require('waeup.clearStudent')
514    text = 'Reject clearance'
515    target = 'reject_clearance'
516    icon = 'actionicon_reject.png'
517
518    @property
519    def target_url(self):
520        if self.context.state not in (REQUESTED, CLEARED):
521            return ''
522        return self.view.url(self.view.context, self.target)
523
524class ClearanceSlipActionButton(ManageActionButton):
525    grok.order(4)
526    grok.context(IStudent)
527    grok.view(StudentClearanceDisplayFormPage)
528    grok.require('waeup.viewStudent')
529    icon = 'actionicon_pdf.png'
530    text = 'Download clearance slip'
531    target = 'clearance.pdf'
532
533class ExportPDFClearanceSlipPage(grok.View):
534    """Deliver a PDF slip of the context.
535    """
536    grok.context(IStudent)
537    grok.name('clearance.pdf')
538    grok.require('waeup.viewStudent')
539    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
540    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
541    prefix = 'form'
542    title = 'Clearance Data'
543
544    @property
545    def label(self):
546        return 'Clearance Slip of %s' % self.context.fullname
547
548    def render(self):
549        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
550            self.request)
551        students_utils = getUtility(IStudentsUtils)
552        return students_utils.renderPDF(
553            self, 'clearance.pdf',
554            self.context.getStudent(), studentview)
555
556class StudentClearanceManageFormPage(SIRPEditFormPage):
557    """ Page to edit student clearance data
558    """
559    grok.context(IStudent)
560    grok.name('edit_clearance')
561    grok.require('waeup.manageStudent')
562    grok.template('clearanceeditpage')
563    form_fields = grok.AutoFields(IStudentClearance)
564    label = 'Manage clearance data'
565    title = 'Clearance Data'
566    pnav = 4
567    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
568
569    def update(self):
570        datepicker.need() # Enable jQuery datepicker in date fields.
571        tabs.need()
572        return super(StudentClearanceManageFormPage, self).update()
573
574    @grok.action('Save')
575    def save(self, **data):
576        msave(self, **data)
577        return
578
579class StudentClearPage(grok.View):
580    """ Clear student by clearance officer
581    """
582    grok.context(IStudent)
583    grok.name('clear')
584    grok.require('waeup.clearStudent')
585
586    def update(self):
587        if self.context.state == REQUESTED:
588            IWorkflowInfo(self.context).fireTransition('clear')
589            self.flash('Student has been cleared.')
590        else:
591            self.flash('Student is in the wrong state.')
592        self.redirect(self.url(self.context,'view_clearance'))
593        return
594
595    def render(self):
596        self.redirect(self.url(self.context, 'view_clearance'))
597        return
598
599class StudentRejectClearancePage(grok.View):
600    """ Reject clearance by clearance officers
601    """
602    grok.context(IStudent)
603    grok.name('reject_clearance')
604    grok.require('waeup.clearStudent')
605
606    def update(self):
607        if self.context.state == CLEARED:
608            IWorkflowInfo(self.context).fireTransition('reset4')
609            message = 'Clearance has been annulled'
610            self.flash(message)
611        elif self.context.state == REQUESTED:
612            IWorkflowInfo(self.context).fireTransition('reset3')
613            message = 'Clearance request has been rejected'
614            self.flash(message)
615        else:
616            self.flash('Student is in the wrong state.')
617            return
618        args = {'subject':message}
619        self.redirect(self.url(self.context) +
620            '/contactstudent?%s' % urlencode(args))
621        return
622
623    def render(self):
624        return
625
626class StudentPersonalDisplayFormPage(SIRPDisplayFormPage):
627    """ Page to display student personal data
628    """
629    grok.context(IStudent)
630    grok.name('view_personal')
631    grok.require('waeup.viewStudent')
632    form_fields = grok.AutoFields(IStudentPersonal)
633    title = 'Personal Data'
634    pnav = 4
635
636    @property
637    def label(self):
638        return '%s: Personal Data' % self.context.fullname
639
640class StudentPersonalManageActionButton(ManageActionButton):
641    grok.order(1)
642    grok.context(IStudent)
643    grok.view(StudentPersonalDisplayFormPage)
644    grok.require('waeup.manageStudent')
645    text = 'Manage'
646    target = 'edit_personal'
647
648class StudentPersonalManageFormPage(SIRPEditFormPage):
649    """ Page to edit student clearance data
650    """
651    grok.context(IStudent)
652    grok.name('edit_personal')
653    grok.require('waeup.viewStudent')
654    form_fields = grok.AutoFields(IStudentPersonal)
655    label = 'Manage personal data'
656    title = 'Personal Data'
657    pnav = 4
658
659    @grok.action('Save')
660    def save(self, **data):
661        msave(self, **data)
662        return
663
664class StudyCourseDisplayFormPage(SIRPDisplayFormPage):
665    """ Page to display the student study course data
666    """
667    grok.context(IStudentStudyCourse)
668    grok.name('index')
669    grok.require('waeup.viewStudent')
670    form_fields = grok.AutoFields(IStudentStudyCourse)
671    grok.template('studycoursepage')
672    title = 'Study Course'
673    pnav = 4
674
675    @property
676    def label(self):
677        return '%s: Study Course' % self.context.__parent__.fullname
678
679    @property
680    def current_mode(self):
681        if self.context.certificate:
682            current_mode = study_modes.getTermByToken(
683                self.context.certificate.study_mode).title
684            return current_mode
685        return
686       
687    @property
688    def department(self):
689        if self.context.certificate is not None:
690            return self.context.certificate.__parent__.__parent__
691        return
692
693    @property
694    def faculty(self):
695        if self.context.certificate is not None:
696            return self.context.certificate.__parent__.__parent__.__parent__
697        return
698
699class StudyCourseManageActionButton(ManageActionButton):
700    grok.order(1)
701    grok.context(IStudentStudyCourse)
702    grok.view(StudyCourseDisplayFormPage)
703    grok.require('waeup.manageStudent')
704    text = 'Manage'
705    target = 'manage'
706
707class StudyCourseManageFormPage(SIRPEditFormPage):
708    """ Page to edit the student study course data
709    """
710    grok.context(IStudentStudyCourse)
711    grok.name('manage')
712    grok.require('waeup.manageStudent')
713    grok.template('studycoursemanagepage')
714    form_fields = grok.AutoFields(IStudentStudyCourse)
715    title = 'Study Course'
716    label = 'Manage study course'
717    pnav = 4
718    taboneactions = ['Save','Cancel']
719    tabtwoactions = ['Remove selected levels','Cancel']
720    tabthreeactions = ['Add study level']
721
722    def update(self):
723        super(StudyCourseManageFormPage, self).update()
724        tabs.need()
725        warning.need()
726        datatable.need()
727        return
728
729    @grok.action('Save')
730    def save(self, **data):
731        msave(self, **data)
732        return
733
734    @property
735    def level_dict(self):
736        studylevelsource = StudyLevelSource().factory
737        for code in studylevelsource.getValues(self.context):
738            title = studylevelsource.getTitle(self.context, code)
739            yield(dict(code=code, title=title))
740
741    @grok.action('Add study level')
742    def addStudyLevel(self, **data):
743        level_code = self.request.form.get('addlevel', None)
744        studylevel = StudentStudyLevel()
745        studylevel.level = int(level_code)
746        try:
747            self.context.addStudentStudyLevel(
748                self.context.certificate,studylevel)
749        except KeyError:
750            self.flash('This level exists.')
751        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
752        return
753
754    @jsaction('Remove selected levels')
755    def delStudyLevels(self, **data):
756        form = self.request.form
757        if form.has_key('val_id'):
758            child_id = form['val_id']
759        else:
760            self.flash('No study level selected.')
761            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
762            return
763        if not isinstance(child_id, list):
764            child_id = [child_id]
765        deleted = []
766        for id in child_id:
767            try:
768                del self.context[id]
769                deleted.append(id)
770            except:
771                self.flash('Could not delete %s: %s: %s' % (
772                        id, sys.exc_info()[0], sys.exc_info()[1]))
773        if len(deleted):
774            self.flash('Successfully removed: %s' % ', '.join(deleted))
775        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
776        return
777
778class StudyLevelDisplayFormPage(SIRPDisplayFormPage):
779    """ Page to display student study levels
780    """
781    grok.context(IStudentStudyLevel)
782    grok.name('index')
783    grok.require('waeup.viewStudent')
784    form_fields = grok.AutoFields(IStudentStudyLevel)
785    grok.template('studylevelpage')
786    pnav = 4
787
788    def update(self):
789        super(StudyLevelDisplayFormPage, self).update()
790        datatable.need()
791        return
792
793    @property
794    def title(self):
795        return 'Study Level %s' % self.context.level_title
796
797    @property
798    def label(self):
799        return '%s: Study Level %s' % (
800            self.context.getStudent().fullname,self.context.level_title)
801
802    @property
803    def total_credits(self):
804        total_credits = 0
805        for key, val in self.context.items():
806            total_credits += val.credits
807        return total_credits
808
809class CourseRegistrationSlipActionButton(ManageActionButton):
810    grok.order(1)
811    grok.context(IStudentStudyLevel)
812    grok.view(StudyLevelDisplayFormPage)
813    grok.require('waeup.viewStudent')
814    icon = 'actionicon_pdf.png'
815    text = 'Download course registration slip'
816    target = 'course_registration.pdf'
817
818class ExportPDFCourseRegistrationSlipPage(grok.View):
819    """Deliver a PDF slip of the context.
820    """
821    grok.context(IStudentStudyLevel)
822    grok.name('course_registration.pdf')
823    grok.require('waeup.viewStudent')
824    form_fields = grok.AutoFields(IStudentStudyLevel)
825    prefix = 'form'
826    title = 'Level Data'
827    content_title = 'Course List'
828
829    @property
830    def label(self):
831        return 'Course Registration Slip %s' % self.context.level_title
832
833    def render(self):
834        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
835            self.request)
836        students_utils = getUtility(IStudentsUtils)
837        tabledata = sorted(self.context.values(),
838            key=lambda value: str(value.semester) + value.code)
839        return students_utils.renderPDF(
840            self, 'course_registration.pdf',
841            self.context.getStudent(), studentview,
842            tableheader=[('Sem.','semester', 1.5),('Code','code', 2.5),
843                         ('Title','title', 5),
844                         ('Dept.','dcode', 1.5), ('Faculty','fcode', 1.5),
845                         ('Cred.', 'credits', 1.5),
846                         ('Mand.', 'core_or_elective', 1.5),
847                         ('Score', 'score', 1.5),('Auto', 'automatic', 1.5)
848                         ],
849            tabledata=tabledata)
850
851class StudyLevelManageActionButton(ManageActionButton):
852    grok.order(2)
853    grok.context(IStudentStudyLevel)
854    grok.view(StudyLevelDisplayFormPage)
855    grok.require('waeup.manageStudent')
856    text = 'Manage'
857    target = 'manage'
858
859class StudyLevelManageFormPage(SIRPEditFormPage):
860    """ Page to edit the student study level data
861    """
862    grok.context(IStudentStudyLevel)
863    grok.name('manage')
864    grok.require('waeup.manageStudent')
865    grok.template('studylevelmanagepage')
866    form_fields = grok.AutoFields(IStudentStudyLevel)
867    pnav = 4
868    taboneactions = ['Save','Cancel']
869    tabtwoactions = ['Add course ticket','Remove selected tickets','Cancel']
870
871    def update(self):
872        super(StudyLevelManageFormPage, self).update()
873        tabs.need()
874        warning.need()
875        datatable.need()
876        return
877
878    @property
879    def title(self):
880        return 'Study Level %s' % self.context.level_title
881
882    @property
883    def label(self):
884        return 'Manage study level %s' % self.context.level_title
885
886    @grok.action('Save')
887    def save(self, **data):
888        msave(self, **data)
889        return
890
891    @grok.action('Add course ticket')
892    def addCourseTicket(self, **data):
893        self.redirect(self.url(self.context, '@@add'))
894
895    @jsaction('Remove selected tickets')
896    def delCourseTicket(self, **data):
897        form = self.request.form
898        if form.has_key('val_id'):
899            child_id = form['val_id']
900        else:
901            self.flash('No ticket selected.')
902            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
903            return
904        if not isinstance(child_id, list):
905            child_id = [child_id]
906        deleted = []
907        for id in child_id:
908            try:
909                del self.context[id]
910                deleted.append(id)
911            except:
912                self.flash('Could not delete %s: %s: %s' % (
913                        id, sys.exc_info()[0], sys.exc_info()[1]))
914        if len(deleted):
915            self.flash('Successfully removed: %s' % ', '.join(deleted))
916        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
917        return
918
919class CourseTicketAddFormPage(SIRPAddFormPage):
920    """Add a course ticket.
921    """
922    grok.context(IStudentStudyLevel)
923    grok.name('add')
924    grok.require('waeup.manageStudent')
925    label = 'Add course ticket'
926    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
927        'grade', 'score', 'automatic')
928    pnav = 4
929
930    @property
931    def title(self):
932        return 'Study Level %s' % self.context.level_title
933
934    @grok.action('Add course ticket')
935    def addCourseTicket(self, **data):
936        ticket = CourseTicket()
937        course = data['course']
938        ticket.core_or_elective = data['core_or_elective']
939        ticket.automatic = False
940        ticket.code = course.code
941        ticket.title = course.title
942        ticket.faculty = course.__parent__.__parent__.__parent__.title
943        ticket.department = course.__parent__.__parent__.title
944        ticket.fcode = course.__parent__.__parent__.__parent__.code
945        ticket.dcode = course.__parent__.__parent__.code
946        ticket.credits = course.credits
947        ticket.passmark = course.passmark
948        ticket.semester = course.semester
949        try:
950            self.context.addCourseTicket(ticket)
951        except KeyError:
952            self.flash('The ticket exists.')
953            return
954        self.flash('Successfully added %s.' % ticket.code)
955        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
956        return
957
958    @grok.action('Cancel')
959    def cancel(self, **data):
960        self.redirect(self.url(self.context))
961
962class CourseTicketDisplayFormPage(SIRPDisplayFormPage):
963    """ Page to display course tickets
964    """
965    grok.context(ICourseTicket)
966    grok.name('index')
967    grok.require('waeup.viewStudent')
968    form_fields = grok.AutoFields(ICourseTicket)
969    grok.template('courseticketpage')
970    pnav = 4
971
972    @property
973    def title(self):
974        return 'Course Ticket %s' % self.context.code
975
976    @property
977    def label(self):
978        return '%s: Course Ticket %s' % (
979            self.context.getStudent().fullname,self.context.code)
980
981class CourseTicketManageActionButton(ManageActionButton):
982    grok.order(1)
983    grok.context(ICourseTicket)
984    grok.view(CourseTicketDisplayFormPage)
985    grok.require('waeup.manageStudent')
986    text = 'Manage'
987    target = 'manage'
988
989class CourseTicketManageFormPage(SIRPEditFormPage):
990    """ Page to manage course tickets
991    """
992    grok.context(ICourseTicket)
993    grok.name('manage')
994    grok.require('waeup.manageStudent')
995    form_fields = grok.AutoFields(ICourseTicket)
996    grok.template('courseticketmanagepage')
997    pnav = 4
998
999    @property
1000    def title(self):
1001        return 'Course Ticket %s' % self.context.code
1002
1003    @property
1004    def label(self):
1005        return 'Manage course ticket %s' % self.context.code
1006
1007    @grok.action('Save')
1008    def save(self, **data):
1009        msave(self, **data)
1010        return
1011
1012# We don't need the display form page yet
1013#class PaymentsDisplayFormPage(SIRPDisplayFormPage):
1014#    """ Page to display the student payments
1015#    """
1016#    grok.context(IStudentPaymentsContainer)
1017#    grok.name('view')
1018#    grok.require('waeup.viewStudent')
1019#    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1020#    grok.template('paymentspage')
1021#    title = 'Payments'
1022#    pnav = 4
1023
1024#    @property
1025#    def label(self):
1026#        return '%s: Payments' % self.context.__parent__.fullname
1027
1028#    def update(self):
1029#        super(PaymentsDisplayFormPage, self).update()
1030#        datatable.need()
1031#        return
1032
1033# This manage form page is for both students and students officers.
1034class PaymentsManageFormPage(SIRPEditFormPage):
1035    """ Page to manage the student payments
1036    """
1037    grok.context(IStudentPaymentsContainer)
1038    grok.name('index')
1039    grok.require('waeup.payStudent')
1040    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1041    grok.template('paymentsmanagepage')
1042    title = 'Payments'
1043    pnav = 4
1044
1045    def unremovable(self, ticket):
1046        usertype = getattr(self.request.principal, 'user_type', None)
1047        if not usertype:
1048            return False
1049        return (self.request.principal.user_type == 'student' and ticket.r_code)
1050
1051    @property
1052    def label(self):
1053        return '%s: Payments' % self.context.__parent__.fullname
1054
1055    def update(self):
1056        super(PaymentsManageFormPage, self).update()
1057        datatable.need()
1058        warning.need()
1059        return
1060
1061    @jsaction('Remove selected tickets')
1062    def delPaymentTicket(self, **data):
1063        form = self.request.form
1064        if form.has_key('val_id'):
1065            child_id = form['val_id']
1066        else:
1067            self.flash('No payment selected.')
1068            self.redirect(self.url(self.context))
1069            return
1070        if not isinstance(child_id, list):
1071            child_id = [child_id]
1072        deleted = []
1073        for id in child_id:
1074            # Students are not allowed to remove used payment tickets
1075            if not self.unremovable(self.context[id]):
1076                try:
1077                    del self.context[id]
1078                    deleted.append(id)
1079                except:
1080                    self.flash('Could not delete %s: %s: %s' % (
1081                            id, sys.exc_info()[0], sys.exc_info()[1]))
1082        if len(deleted):
1083            self.flash('Successfully removed: %s' % ', '.join(deleted))
1084            write_log_message(self,'removed: % s' % ', '.join(deleted))
1085        self.redirect(self.url(self.context))
1086        return
1087
1088    @grok.action('Add online payment ticket')
1089    def addPaymentTicket(self, **data):
1090        self.redirect(self.url(self.context, '@@addop'))
1091
1092#class OnlinePaymentManageActionButton(ManageActionButton):
1093#    grok.order(1)
1094#    grok.context(IStudentPaymentsContainer)
1095#    grok.view(PaymentsDisplayFormPage)
1096#    grok.require('waeup.manageStudent')
1097#    text = 'Manage payments'
1098#    target = 'manage'
1099
1100class OnlinePaymentAddFormPage(SIRPAddFormPage):
1101    """ Page to add an online payment ticket
1102    """
1103    grok.context(IStudentPaymentsContainer)
1104    grok.name('addop')
1105    grok.require('waeup.payStudent')
1106    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1107        'p_category')
1108    #zzgrok.template('addpaymentpage')
1109    label = 'Add online payment'
1110    title = 'Payments'
1111    pnav = 4
1112   
1113    @grok.action('Create ticket')
1114    def createTicket(self, **data):
1115        p_category = data['p_category']
1116        student = self.context.__parent__
1117        if p_category == 'bed_allocation' and student[
1118            'studycourse'].current_session != grok.getSite()[
1119            'configuration'].accommodation_session:
1120                self.flash(
1121                    'Your current session does not match accommodation session.')
1122                self.redirect(self.url(self.context))
1123                return
1124        students_utils = getUtility(IStudentsUtils)
1125        pay_details  = students_utils.getPaymentDetails(
1126            p_category,student)
1127        if pay_details['error']:
1128            self.flash(pay_details['error'])
1129            self.redirect(self.url(self.context))
1130            return
1131        p_item = pay_details['p_item']
1132        p_session = pay_details['p_session']
1133        for key in self.context.keys():
1134            ticket = self.context[key]
1135            if ticket.p_state == 'paid' and\
1136               ticket.p_category == p_category and \
1137               ticket.p_item == p_item and \
1138               ticket.p_session == p_session:
1139                  self.flash(
1140                      'This type of payment has already been made.')
1141                  self.redirect(self.url(self.context))
1142                  return
1143        payment = createObject(u'waeup.StudentOnlinePayment')
1144        self.applyData(payment, **data)
1145        timestamp = "%d" % int(time()*1000)
1146        #order_id = "%s%s" % (student_id[1:],timestamp)
1147        payment.p_id = "p%s" % timestamp
1148        payment.p_item = p_item
1149        payment.p_session = p_session
1150        payment.amount_auth = pay_details['amount']
1151        payment.surcharge_1 = pay_details['surcharge_1']
1152        payment.surcharge_2 = pay_details['surcharge_2']
1153        payment.surcharge_3 = pay_details['surcharge_3']
1154        self.context[payment.p_id] = payment
1155        self.flash('Payment ticket created.')
1156        self.redirect(self.url(self.context))
1157        return
1158
1159class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
1160    """ Page to view an online payment ticket
1161    """
1162    grok.context(IStudentOnlinePayment)
1163    grok.name('index')
1164    grok.require('waeup.viewStudent')
1165    form_fields = grok.AutoFields(IStudentOnlinePayment)
1166    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1167    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1168    pnav = 4
1169
1170    @property
1171    def title(self):
1172        return 'Online Payment Ticket %s' % self.context.p_id
1173
1174    @property
1175    def label(self):
1176        return '%s: Online Payment Ticket %s' % (
1177            self.context.getStudent().fullname,self.context.p_id)
1178
1179class PaymentReceiptActionButton(ManageActionButton):
1180    grok.order(1)
1181    grok.context(IStudentOnlinePayment)
1182    grok.view(OnlinePaymentDisplayFormPage)
1183    grok.require('waeup.viewStudent')
1184    icon = 'actionicon_pdf.png'
1185    text = 'Download payment receipt'
1186    target = 'payment_receipt.pdf'
1187
1188    @property
1189    def target_url(self):
1190        if self.context.p_state != 'paid':
1191            return ''
1192        return self.view.url(self.view.context, self.target)
1193
1194class RequestCallbackActionButton(ManageActionButton):
1195    grok.order(2)
1196    grok.context(IStudentOnlinePayment)
1197    grok.view(OnlinePaymentDisplayFormPage)
1198    grok.require('waeup.payStudent')
1199    icon = 'actionicon_call.png'
1200    text = 'Request callback'
1201    target = 'callback'
1202
1203    @property
1204    def target_url(self):
1205        if self.context.p_state != 'unpaid':
1206            return ''
1207        return self.view.url(self.view.context, self.target)
1208
1209class OnlinePaymentCallbackPage(grok.View):
1210    """ Callback view
1211    """
1212    grok.context(IStudentOnlinePayment)
1213    grok.name('callback')
1214    grok.require('waeup.payStudent')
1215
1216    # This update method simulates a valid callback und must be
1217    # specified in the customization package. The parameters must be taken
1218    # from the incoming request.
1219    def update(self):
1220        if self.context.p_state == 'paid':
1221            self.flash('This ticket has already been paid.')
1222            return
1223        student = self.context.getStudent()
1224        write_log_message(self,'valid callback: %s' % self.context.p_id)
1225        self.context.r_amount_approved = self.context.amount_auth
1226        self.context.r_card_num = u'0000'
1227        self.context.r_code = u'00'
1228        self.context.p_state = 'paid'
1229        self.context.payment_date = datetime.now()
1230        if self.context.p_category == 'clearance':
1231            # Create CLR access code
1232            pin, error = create_accesscode('CLR',0,student.student_id)
1233            if error:
1234                self.flash('Valid callback received. ' + error)
1235                return
1236            self.context.ac = pin
1237        elif self.context.p_category == 'schoolfee':
1238            # Create SFE access code
1239            pin, error = create_accesscode('SFE',0,student.student_id)
1240            if error:
1241                self.flash('Valid callback received. ' + error)
1242                return
1243            self.context.ac = pin
1244        elif self.context.p_category == 'bed_allocation':
1245            # Create HOS access code
1246            pin, error = create_accesscode('HOS',0,student.student_id)
1247            if error:
1248                self.flash('Valid callback received. ' + error)
1249                return
1250            self.context.ac = pin
1251        self.flash('Valid callback received.')
1252        return
1253
1254    def render(self):
1255        self.redirect(self.url(self.context, '@@index'))
1256        return
1257
1258class ExportPDFPaymentSlipPage(grok.View):
1259    """Deliver a PDF slip of the context.
1260    """
1261    grok.context(IStudentOnlinePayment)
1262    grok.name('payment_receipt.pdf')
1263    grok.require('waeup.viewStudent')
1264    form_fields = grok.AutoFields(IStudentOnlinePayment)
1265    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1266    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1267    prefix = 'form'
1268    title = 'Payment Data'
1269
1270    @property
1271    def label(self):
1272        return 'Online Payment Receipt %s' % self.context.p_id
1273
1274    def render(self):
1275        if self.context.p_state != 'paid':
1276            self.flash('Ticket not yet paid.')
1277            self.redirect(self.url(self.context))
1278            return
1279        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1280            self.request)
1281        students_utils = getUtility(IStudentsUtils)
1282        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1283            self.context.getStudent(), studentview)
1284
1285# We don't need the display form page yet
1286#class AccommodationDisplayFormPage(SIRPDisplayFormPage):
1287#    """ Page to display the student accommodation data
1288#    """
1289#    grok.context(IStudentAccommodation)
1290#    grok.name('xxx')
1291#    grok.require('waeup.viewStudent')
1292#    form_fields = grok.AutoFields(IStudentAccommodation)
1293#    #grok.template('accommodationpage')
1294#    title = 'Accommodation'
1295#    pnav = 4
1296
1297#    @property
1298#    def label(self):
1299#        return '%s: Accommodation Data' % self.context.__parent__.fullname
1300
1301# This manage form page is for both students and students officers.
1302class AccommodationManageFormPage(SIRPEditFormPage):
1303    """ Page to manage bed tickets.
1304    """
1305    grok.context(IStudentAccommodation)
1306    grok.name('index')
1307    grok.require('waeup.handleAccommodation')
1308    form_fields = grok.AutoFields(IStudentAccommodation)
1309    grok.template('accommodationmanagepage')
1310    title = 'Accommodation'
1311    pnav = 4
1312    officers_only_actions = ['Remove selected']
1313
1314    @property
1315    def label(self):
1316        return '%s: Accommodation' % self.context.__parent__.fullname
1317
1318    def update(self):
1319        super(AccommodationManageFormPage, self).update()
1320        datatable.need()
1321        warning.need()
1322        return
1323
1324    @jsaction('Remove selected')
1325    def delBedTickets(self, **data):
1326        if getattr(self.request.principal, 'user_type', None) == 'student':
1327            self.flash('You are not allowed to remove bed tickets.')
1328            self.redirect(self.url(self.context))
1329            return
1330        form = self.request.form
1331        if form.has_key('val_id'):
1332            child_id = form['val_id']
1333        else:
1334            self.flash('No bed ticket selected.')
1335            self.redirect(self.url(self.context))
1336            return
1337        if not isinstance(child_id, list):
1338            child_id = [child_id]
1339        deleted = []
1340        for id in child_id:
1341            del self.context[id]
1342            deleted.append(id)
1343        if len(deleted):
1344            self.flash('Successfully removed: %s' % ', '.join(deleted))
1345            write_log_message(self,'removed: % s' % ', '.join(deleted))
1346        self.redirect(self.url(self.context))
1347        return
1348
1349    @property
1350    def selected_actions(self):
1351        sa = self.actions
1352        if getattr(self.request.principal, 'user_type', None) == 'student':
1353            sa = [action for action in self.actions
1354                  if not action.label in self.officers_only_actions]
1355        return sa
1356
1357class AddBedTicketActionButton(ManageActionButton):
1358    grok.order(1)
1359    grok.context(IStudentAccommodation)
1360    grok.view(AccommodationManageFormPage)
1361    grok.require('waeup.handleAccommodation')
1362    icon = 'actionicon_home.png'
1363    text = 'Book accommodation'
1364    target = 'add'
1365
1366class BedTicketAddPage(SIRPPage):
1367    """ Page to add an online payment ticket
1368    """
1369    grok.context(IStudentAccommodation)
1370    grok.name('add')
1371    grok.require('waeup.handleAccommodation')
1372    grok.template('enterpin')
1373    ac_prefix = 'HOS'
1374    label = 'Add bed ticket'
1375    title = 'Add bed ticket'
1376    pnav = 4
1377    buttonname = 'Create bed ticket'
1378    notice = ''
1379
1380    def update(self, SUBMIT=None):
1381        student = self.context.getStudent()
1382        students_utils = getUtility(IStudentsUtils)
1383        acc_details  = students_utils.getAccommodationDetails(student)
1384        if not student.state in acc_details['allowed_states']:
1385            self.flash("You are in the wrong registration state.")
1386            self.redirect(self.url(self.context))
1387            return
1388        if student['studycourse'].current_session != acc_details['booking_session']:
1389            self.flash(
1390                'Your current session does not match accommodation session.')
1391            self.redirect(self.url(self.context))
1392            return
1393        if str(acc_details['booking_session']) in self.context.keys():
1394            self.flash('You already booked a bed space in current accommodation session.')
1395            self.redirect(self.url(self.context))
1396            return
1397        self.ac_series = self.request.form.get('ac_series', None)
1398        self.ac_number = self.request.form.get('ac_number', None)
1399        if SUBMIT is None:
1400            return
1401        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1402        code = get_access_code(pin)
1403        if not code:
1404            self.flash('Activation code is invalid.')
1405            return
1406        # Search and book bed
1407        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1408        entries = cat.searchResults(
1409            owner=(student.student_id,student.student_id))
1410        if len(entries):
1411            # If bed space has bee manually allocated use this bed
1412            bed = [entry for entry in entries][0]
1413        else:
1414            # else search for other available beds
1415            entries = cat.searchResults(
1416                bed_type=(acc_details['bt'],acc_details['bt']))
1417            available_beds = [
1418                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1419            if available_beds:
1420                students_utils = getUtility(IStudentsUtils)
1421                bed = students_utils.selectBed(available_beds)
1422                bed.bookBed(student.student_id)
1423            else:
1424                self.flash('There is no free bed in your category %s.'
1425                            % acc_details['bt'])
1426                return
1427        # Mark pin as used (this also fires a pin related transition)
1428        if code.state == USED:
1429            self.flash('Activation code has already been used.')
1430            return
1431        else:
1432            comment = u"AC invalidated for %s" % self.context.getStudent().student_id
1433            # Here we know that the ac is in state initialized so we do not
1434            # expect an exception, but the owner might be different
1435            if not invalidate_accesscode(
1436                pin,comment,self.context.getStudent().student_id):
1437                self.flash('You are not the owner of this access code.')
1438                return
1439        # Create bed ticket
1440        bedticket = createObject(u'waeup.BedTicket')
1441        bedticket.booking_code = pin
1442        bedticket.booking_session = acc_details['booking_session']
1443        bedticket.bed_type = acc_details['bt']
1444        bedticket.bed = bed
1445        hall_title = bed.__parent__.hostel_name
1446        coordinates = bed.getBedCoordinates()[1:]
1447        block, room_nr, bed_nr = coordinates
1448        bedticket.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1449            hall_title, block, room_nr, bed_nr, bed.bed_type)
1450        key = str(acc_details['booking_session'])
1451        self.context[key] = bedticket
1452        self.flash('Bed ticket created and bed booked: %s'
1453            % bedticket.bed_coordinates)
1454        self.redirect(self.url(self.context))
1455        return
1456
1457class BedTicketDisplayFormPage(SIRPDisplayFormPage):
1458    """ Page to display bed tickets
1459    """
1460    grok.context(IBedTicket)
1461    grok.name('index')
1462    grok.require('waeup.handleAccommodation')
1463    form_fields = grok.AutoFields(IBedTicket)
1464    form_fields[
1465        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1466    pnav = 4
1467
1468    @property
1469    def label(self):
1470        return 'Bed Ticket for Session %s' % self.context.getSessionString()
1471
1472    @property
1473    def title(self):
1474        return 'Bed Ticket %s' % self.context.getSessionString()
1475
1476class BedTicketSlipActionButton(ManageActionButton):
1477    grok.order(1)
1478    grok.context(IBedTicket)
1479    grok.view(BedTicketDisplayFormPage)
1480    grok.require('waeup.handleAccommodation')
1481    icon = 'actionicon_pdf.png'
1482    text = 'Download bed allocation slip'
1483    target = 'bed_allocation.pdf'
1484
1485class ExportPDFBedTicketSlipPage(grok.View):
1486    """Deliver a PDF slip of the context.
1487    """
1488    grok.context(IBedTicket)
1489    grok.name('bed_allocation.pdf')
1490    grok.require('waeup.handleAccommodation')
1491    form_fields = grok.AutoFields(IBedTicket)
1492    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1493    prefix = 'form'
1494    title = 'Bed Allocation Data'
1495
1496    @property
1497    def label(self):
1498        return 'Bed Allocation: %s' % self.context.bed_coordinates
1499
1500    def render(self):
1501        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1502            self.request)
1503        students_utils = getUtility(IStudentsUtils)
1504        return students_utils.renderPDF(
1505            self, 'bed_allocation.pdf',
1506            self.context.getStudent(), studentview)
1507
1508class RelocateStudentActionButton(ManageActionButton):
1509    grok.order(2)
1510    grok.context(IBedTicket)
1511    grok.view(BedTicketDisplayFormPage)
1512    grok.require('waeup.manageHostels')
1513    icon = 'actionicon_reload.png'
1514    text = 'Relocate student'
1515    target = 'relocate'
1516
1517class BedTicketRelocationPage(grok.View):
1518    """ Callback view
1519    """
1520    grok.context(IBedTicket)
1521    grok.name('relocate')
1522    grok.require('waeup.manageHostels')
1523
1524    # Relocate student if student parameters have changed or the bed_type
1525    # of the bed has changed
1526    def update(self):
1527        student = self.context.getStudent()
1528        students_utils = getUtility(IStudentsUtils)
1529        acc_details  = students_utils.getAccommodationDetails(student)
1530        if self.context.bed != None and \
1531              'reserved' in self.context.bed.bed_type:
1532            self.flash("Students in reserved beds can't be relocated.")
1533            self.redirect(self.url(self.context))
1534            return
1535        if acc_details['bt'] == self.context.bed_type and \
1536                self.context.bed != None and \
1537                self.context.bed.bed_type == self.context.bed_type:
1538            self.flash("Student can't be relocated.")
1539            self.redirect(self.url(self.context))
1540            return
1541        # Search a bed
1542        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1543        entries = cat.searchResults(
1544            owner=(student.student_id,student.student_id))
1545        if len(entries) and self.context.bed == None:
1546            # If booking has been cancelled but other bed space has been
1547            # manually allocated after cancellation use this bed
1548            new_bed = [entry for entry in entries][0]
1549        else:
1550            # Search for other available beds
1551            entries = cat.searchResults(
1552                bed_type=(acc_details['bt'],acc_details['bt']))
1553            available_beds = [
1554                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1555            if available_beds:
1556                students_utils = getUtility(IStudentsUtils)
1557                new_bed = students_utils.selectBed(available_beds)
1558                new_bed.bookBed(student.student_id)
1559            else:
1560                self.flash('There is no free bed in your category %s.'
1561                            % acc_details['bt'])
1562                self.redirect(self.url(self.context))
1563                return
1564        # Rlease old bed if exists
1565        if self.context.bed != None:
1566            self.context.bed.owner = NOT_OCCUPIED
1567            notify(grok.ObjectModifiedEvent(self.context.bed))
1568        # Alocate new bed
1569        self.context.bed_type = acc_details['bt']
1570        self.context.bed = new_bed
1571        hall_title = new_bed.__parent__.hostel_name
1572        coordinates = new_bed.getBedCoordinates()[1:]
1573        block, room_nr, bed_nr = coordinates
1574        self.context.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1575            hall_title, block, room_nr, bed_nr, new_bed.bed_type)
1576        self.flash('Student relocated: %s' % self.context.bed_coordinates)
1577        self.redirect(self.url(self.context))
1578        return
1579
1580    def render(self):
1581        #self.redirect(self.url(self.context, '@@index'))
1582        return
1583
1584class StudentHistoryPage(SIRPPage):
1585    """ Page to display student clearance data
1586    """
1587    grok.context(IStudent)
1588    grok.name('history')
1589    grok.require('waeup.viewStudent')
1590    grok.template('studenthistory')
1591    title = 'History'
1592    pnav = 4
1593
1594    @property
1595    def label(self):
1596        return '%s: History' % self.context.fullname
1597
1598# Pages for students only
1599
1600class StudentBaseActionButton(ManageActionButton):
1601    grok.order(1)
1602    grok.context(IStudent)
1603    grok.view(StudentBaseDisplayFormPage)
1604    grok.require('waeup.handleStudent')
1605    text = 'Edit base data'
1606    target = 'edit_base'
1607
1608class StudentPasswordActionButton(ManageActionButton):
1609    grok.order(2)
1610    grok.context(IStudent)
1611    grok.view(StudentBaseDisplayFormPage)
1612    grok.require('waeup.handleStudent')
1613    icon = 'actionicon_key.png'
1614    text = 'Change password'
1615    target = 'change_password'
1616
1617class StudentPassportActionButton(ManageActionButton):
1618    grok.order(3)
1619    grok.context(IStudent)
1620    grok.view(StudentBaseDisplayFormPage)
1621    grok.require('waeup.handleStudent')
1622    icon = 'actionicon_portrait.png'
1623    text = 'Change portrait'
1624    target = 'change_portrait'
1625
1626    @property
1627    def target_url(self):
1628        if self.context.state != 'admitted':
1629            return ''
1630        return self.view.url(self.view.context, self.target)
1631
1632class StudentBaseEditFormPage(SIRPEditFormPage):
1633    """ View to edit student base data
1634    """
1635    grok.context(IStudent)
1636    grok.name('edit_base')
1637    grok.require('waeup.handleStudent')
1638    form_fields = grok.AutoFields(IStudentBase).select(
1639        'email', 'phone')
1640    label = 'Edit base data'
1641    title = 'Base Data'
1642    pnav = 4
1643
1644    @grok.action('Save')
1645    def save(self, **data):
1646        msave(self, **data)
1647        return
1648
1649class StudentChangePasswordPage(SIRPEditFormPage):
1650    """ View to manage student base data
1651    """
1652    grok.context(IStudent)
1653    grok.name('change_password')
1654    grok.require('waeup.handleStudent')
1655    grok.template('change_password')
1656    label = 'Change password'
1657    title = 'Base Data'
1658    pnav = 4
1659
1660    @grok.action('Save')
1661    def save(self, **data):
1662        form = self.request.form
1663        password = form.get('change_password', None)
1664        password_ctl = form.get('change_password_repeat', None)
1665        if password:
1666            validator = getUtility(IPasswordValidator)
1667            errors = validator.validate_password(password, password_ctl)
1668            if not errors:
1669                IUserAccount(self.context).setPassword(password)
1670                write_log_message(self, 'saved: password')
1671                self.flash('Password changed.')
1672            else:
1673                self.flash( ' '.join(errors))
1674        return
1675
1676class StudentFilesUploadPage(SIRPPage):
1677    """ View to upload files by student
1678    """
1679    grok.context(IStudent)
1680    grok.name('change_portrait')
1681    grok.require('waeup.uploadStudentFile')
1682    grok.template('filesuploadpage')
1683    label = 'Upload portrait'
1684    title = 'Base Data'
1685    pnav = 4
1686
1687    def update(self):
1688        if self.context.getStudent().state != 'admitted':
1689            emit_lock_message(self)
1690            return
1691        super(StudentFilesUploadPage, self).update()
1692        return
1693
1694class StudentClearanceStartActionButton(ManageActionButton):
1695    grok.order(1)
1696    grok.context(IStudent)
1697    grok.view(StudentClearanceDisplayFormPage)
1698    grok.require('waeup.handleStudent')
1699    icon = 'actionicon_start.gif'
1700    text = 'Start clearance'
1701    target = 'start_clearance'
1702
1703    @property
1704    def target_url(self):
1705        if self.context.state != 'admitted':
1706            return ''
1707        return self.view.url(self.view.context, self.target)
1708
1709class StartClearancePage(SIRPPage):
1710    grok.context(IStudent)
1711    grok.name('start_clearance')
1712    grok.require('waeup.handleStudent')
1713    grok.template('enterpin')
1714    title = 'Start clearance'
1715    label = 'Start clearance'
1716    ac_prefix = 'CLR'
1717    notice = ''
1718    pnav = 4
1719    buttonname = 'Start clearance now'
1720
1721    @property
1722    def all_required_fields_filled(self):
1723        if self.context.email and self.context.phone:
1724            return True
1725        return False
1726
1727    @property
1728    def portrait_uploaded(self):
1729        store = getUtility(IExtFileStore)
1730        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1731            return True
1732        return False
1733
1734    def update(self, SUBMIT=None):
1735        if not self.context.state == 'admitted':
1736            self.flash("Wrong state.")
1737            self.redirect(self.url(self.context))
1738            return
1739        if not self.portrait_uploaded:
1740            self.flash("No portrait uploaded.")
1741            self.redirect(self.url(self.context, 'change_portrait'))
1742            return
1743        if not self.all_required_fields_filled:
1744            self.flash("Not all required fields filled.")
1745            self.redirect(self.url(self.context, 'edit_base'))
1746            return
1747        self.ac_series = self.request.form.get('ac_series', None)
1748        self.ac_number = self.request.form.get('ac_number', None)
1749
1750        if SUBMIT is None:
1751            return
1752        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1753        code = get_access_code(pin)
1754        if not code:
1755            self.flash('Activation code is invalid.')
1756            return
1757        # Mark pin as used (this also fires a pin related transition)
1758        # and fire transition start_clearance
1759        if code.state == USED:
1760            self.flash('Activation code has already been used.')
1761            return
1762        else:
1763            comment = u"AC invalidated for %s" % self.context.student_id
1764            # Here we know that the ac is in state initialized so we do not
1765            # expect an exception, but the owner might be different
1766            if not invalidate_accesscode(pin,comment,self.context.student_id):
1767                self.flash('You are not the owner of this access code.')
1768                return
1769            self.context.clr_code = pin
1770        IWorkflowInfo(self.context).fireTransition('start_clearance')
1771        self.flash('Clearance process has been started.')
1772        self.redirect(self.url(self.context,'cedit'))
1773        return
1774
1775class StudentClearanceEditActionButton(ManageActionButton):
1776    grok.order(1)
1777    grok.context(IStudent)
1778    grok.view(StudentClearanceDisplayFormPage)
1779    grok.require('waeup.handleStudent')
1780    text = 'Edit'
1781    target = 'cedit'
1782
1783    @property
1784    def target_url(self):
1785        if self.context.clearance_locked:
1786            return ''
1787        return self.view.url(self.view.context, self.target)
1788
1789class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1790    """ View to edit student clearance data by student
1791    """
1792    grok.context(IStudent)
1793    grok.name('cedit')
1794    grok.require('waeup.handleStudent')
1795    form_fields = grok.AutoFields(
1796        IStudentClearanceEdit).omit('clearance_locked')
1797    label = 'Edit clearance data'
1798    title = 'Clearance Data'
1799    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
1800
1801    def update(self):
1802        if self.context.clearance_locked:
1803            emit_lock_message(self)
1804            return
1805        return super(StudentClearanceEditFormPage, self).update()
1806
1807    @grok.action('Save')
1808    def save(self, **data):
1809        self.applyData(self.context, **data)
1810        self.flash('Clearance form has been saved.')
1811        return
1812
1813    # To be specified in the customisation package
1814    def dataNotComplete(self):
1815        #store = getUtility(IExtFileStore)
1816        #if not store.getFileByContext(self.context, attr=u'xyz.jpg'):
1817        #    return 'No xyz scan uploaded.'
1818        return False
1819
1820    @grok.action('Save and request clearance')
1821    def requestClearance(self, **data):
1822        self.applyData(self.context, **data)
1823        #self.context._p_changed = True
1824        if self.dataNotComplete():
1825            self.flash(self.dataNotComplete())
1826            return
1827        self.flash('Clearance form has been saved.')
1828        self.redirect(self.url(self.context,'request_clearance'))
1829        return
1830
1831class RequestClearancePage(SIRPPage):
1832    grok.context(IStudent)
1833    grok.name('request_clearance')
1834    grok.require('waeup.handleStudent')
1835    grok.template('enterpin')
1836    title = 'Request clearance'
1837    label = 'Request clearance'
1838    notice = 'Enter the CLR access code used for starting clearance.'
1839    ac_prefix = 'CLR'
1840    pnav = 4
1841    buttonname = 'Request clearance now'
1842
1843    def update(self, SUBMIT=None):
1844        self.ac_series = self.request.form.get('ac_series', None)
1845        self.ac_number = self.request.form.get('ac_number', None)
1846        if SUBMIT is None:
1847            return
1848        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1849        if self.context.clr_code != pin:
1850            self.flash("This isn't your CLR access code.")
1851            return
1852        state = IWorkflowState(self.context).getState()
1853        # This shouldn't happen, but the application officer
1854        # might have forgotten to lock the form after changing the state
1855        if state != CLEARANCE:
1856            self.flash('This form cannot be submitted. Wrong state!')
1857            return
1858        IWorkflowInfo(self.context).fireTransition('request_clearance')
1859        self.flash('Clearance has been requested.')
1860        self.redirect(self.url(self.context))
1861        return
1862
1863class CourseRegistrationStartActionButton(ManageActionButton):
1864    grok.order(1)
1865    grok.context(IStudentStudyCourse)
1866    grok.view(StudyCourseDisplayFormPage)
1867    grok.require('waeup.handleStudent')
1868    icon = 'actionicon_start.gif'
1869    text = 'Start course registration'
1870    target = 'start_course_registration'
1871
1872    @property
1873    def target_url(self):
1874        if not self.context.getStudent().state in (CLEARED,RETURNING):
1875            return ''
1876        return self.view.url(self.view.context, self.target)
1877
1878class StartCourseRegistrationPage(SIRPPage):
1879    grok.context(IStudentStudyCourse)
1880    grok.name('start_course_registration')
1881    grok.require('waeup.handleStudent')
1882    grok.template('enterpin')
1883    title = 'Start course registration'
1884    label = 'Start course registration'
1885    ac_prefix = 'SFE'
1886    notice = ''
1887    pnav = 4
1888    buttonname = 'Start course registration now'
1889
1890    def update(self, SUBMIT=None):
1891        if not self.context.getStudent().state in (CLEARED,RETURNING):
1892            self.flash("Wrong state.")
1893            self.redirect(self.url(self.context))
1894            return
1895        self.ac_series = self.request.form.get('ac_series', None)
1896        self.ac_number = self.request.form.get('ac_number', None)
1897
1898        if SUBMIT is None:
1899            return
1900        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1901        code = get_access_code(pin)
1902        if not code:
1903            self.flash('Activation code is invalid.')
1904            return
1905        # Mark pin as used (this also fires a pin related transition)
1906        # and fire transition start_clearance
1907        if code.state == USED:
1908            self.flash('Activation code has already been used.')
1909            return
1910        else:
1911            comment = u"AC invalidated for %s" % self.context.getStudent().student_id
1912            # Here we know that the ac is in state initialized so we do not
1913            # expect an exception, but the owner might be different
1914            if not invalidate_accesscode(
1915                pin,comment,self.context.getStudent().student_id):
1916                self.flash('You are not the owner of this access code.')
1917                return
1918        if self.context.getStudent().state == CLEARED:
1919            IWorkflowInfo(self.context.getStudent()).fireTransition(
1920                'pay_first_school_fee')
1921        elif self.context.getStudent().state == RETURNING:
1922            IWorkflowInfo(self.context.getStudent()).fireTransition(
1923                'pay_school_fee')
1924        self.flash('Course registration has been started.')
1925        self.redirect(self.url(self.context))
1926        return
1927
1928
1929class AddStudyLevelActionButton(AddActionButton):
1930    grok.order(1)
1931    grok.context(IStudentStudyCourse)
1932    grok.view(StudyCourseDisplayFormPage)
1933    grok.require('waeup.handleStudent')
1934    text = 'Add course list'
1935    target = 'add'
1936
1937    @property
1938    def target_url(self):
1939        student = self.view.context.getStudent()
1940        condition1 = student.state != 'school fee paid'
1941        condition2 = str(student['studycourse'].current_level) in \
1942            self.view.context.keys()
1943        if condition1 or condition2:
1944            return ''
1945        return self.view.url(self.view.context, self.target)
1946
1947class AddStudyLevelFormPage(SIRPEditFormPage):
1948    """ Page for students to add current study levels
1949    """
1950    grok.context(IStudentStudyCourse)
1951    grok.name('add')
1952    grok.require('waeup.handleStudent')
1953    grok.template('studyleveladdpage')
1954    form_fields = grok.AutoFields(IStudentStudyCourse)
1955    title = 'Study Course'
1956    pnav = 4
1957
1958    @property
1959    def label(self):
1960        studylevelsource = StudyLevelSource().factory
1961        code = self.context.current_level
1962        title = studylevelsource.getTitle(self.context, code)
1963        return 'Add current level %s' % title
1964
1965    def update(self):
1966        if self.context.getStudent().state != 'school fee paid':
1967            emit_lock_message(self)
1968            return
1969        super(AddStudyLevelFormPage, self).update()
1970        return
1971
1972    @grok.action('Create course list now')
1973    def addStudyLevel(self, **data):
1974        studylevel = StudentStudyLevel()
1975        studylevel.level = self.context.current_level
1976        studylevel.level_session = self.context.current_session
1977        try:
1978            self.context.addStudentStudyLevel(
1979                self.context.certificate,studylevel)
1980        except KeyError:
1981            self.flash('This level exists.')
1982        self.redirect(self.url(self.context))
1983        return
1984
1985class StudyLevelEditActionButton(ManageActionButton):
1986    grok.order(1)
1987    grok.context(IStudentStudyLevel)
1988    grok.view(StudyLevelDisplayFormPage)
1989    grok.require('waeup.handleStudent')
1990    text = 'Add and remove courses'
1991    target = 'edit'
1992
1993    @property
1994    def target_url(self):
1995        student = self.view.context.getStudent()
1996        condition1 = student.state != 'school fee paid'
1997        condition2 = student[
1998            'studycourse'].current_level != self.view.context.level
1999        if condition1 or condition2:
2000            return ''
2001        return self.view.url(self.view.context, self.target)
2002
2003class StudyLevelEditFormPage(SIRPEditFormPage):
2004    """ Page to edit the student study level data by students
2005    """
2006    grok.context(IStudentStudyLevel)
2007    grok.name('edit')
2008    grok.require('waeup.handleStudent')
2009    grok.template('studyleveleditpage')
2010    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2011        'level_session', 'level_verdict')
2012    pnav = 4
2013
2014    def update(self):
2015        super(StudyLevelEditFormPage, self).update()
2016    #    tabs.need()
2017        datatable.need()
2018        warning.need()
2019        return
2020
2021    @property
2022    def title(self):
2023        return 'Study Level %s' % self.context.level_title
2024
2025    @property
2026    def label(self):
2027        return 'Add and remove course tickets of study level %s' % self.context.level_title
2028
2029    @property
2030    def total_credits(self):
2031        total_credits = 0
2032        for key, val in self.context.items():
2033            total_credits += val.credits
2034        return total_credits
2035
2036    @grok.action('Add course ticket')
2037    def addCourseTicket(self, **data):
2038        self.redirect(self.url(self.context, 'ctadd'))
2039
2040    @jsaction('Remove selected tickets')
2041    def delCourseTicket(self, **data):
2042        form = self.request.form
2043        if form.has_key('val_id'):
2044            child_id = form['val_id']
2045        else:
2046            self.flash('No ticket selected.')
2047            self.redirect(self.url(self.context, '@@edit'))
2048            return
2049        if not isinstance(child_id, list):
2050            child_id = [child_id]
2051        deleted = []
2052        for id in child_id:
2053            # Students are not allowed to remove core tickets
2054            if not self.context[id].core_or_elective:
2055                try:
2056                    del self.context[id]
2057                    deleted.append(id)
2058                except:
2059                    self.flash('Could not delete %s: %s: %s' % (
2060                            id, sys.exc_info()[0], sys.exc_info()[1]))
2061        if len(deleted):
2062            self.flash('Successfully removed: %s' % ', '.join(deleted))
2063        self.redirect(self.url(self.context, u'@@edit'))
2064        return
2065
2066    @grok.action('Register course list')
2067    def RegisterCourses(self, **data):
2068        IWorkflowInfo(self.context.getStudent()).fireTransition('register_courses')
2069        self.flash('Course list has been registered.')
2070        self.redirect(self.url(self.context))
2071        return
2072
2073class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2074    """Add a course ticket by student.
2075    """
2076    grok.name('ctadd')
2077    grok.require('waeup.handleStudent')
2078    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2079        'grade', 'score', 'core_or_elective', 'automatic')
2080
2081    @grok.action('Add course ticket')
2082    def addCourseTicket(self, **data):
2083        ticket = CourseTicket()
2084        course = data['course']
2085        ticket.automatic = False
2086        ticket.code = course.code
2087        ticket.title = course.title
2088        ticket.faculty = course.__parent__.__parent__.__parent__.title
2089        ticket.department = course.__parent__.__parent__.title
2090        ticket.credits = course.credits
2091        ticket.passmark = course.passmark
2092        ticket.semester = course.semester
2093        try:
2094            self.context.addCourseTicket(ticket)
2095        except KeyError:
2096            self.flash('The ticket exists.')
2097            return
2098        self.flash('Successfully added %s.' % ticket.code)
2099        self.redirect(self.url(self.context, u'@@edit'))
2100        return
Note: See TracBrowser for help on using the repository browser.