source: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py @ 8339

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

Use factory for the creation of CourseTickets?.

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