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

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

Before submission of clearance forms it must be checked if required scans are missing. This has to be specified in the customization packages.

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