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

Last change on this file since 7647 was 7647, checked in by uli, 13 years ago

Move ReindexPage? to general browser module as it is not student specific.

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