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

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

Start internationalization of students package (work in progress).

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