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

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

Use workflow state constants instead of strings in students components.

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