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

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

Move SetPasswordPage? and ChangePasswordRequestPage? to the end of the module. They are closely related.

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