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

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

Some minor changes on. The po files are now released for editing.

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