source: main/waeup.sirp/branches/ulif-groktoolkit-1.4/src/waeup/sirp/students/browser.py @ 11102

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

Translate transitions and history.

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