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

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

Use new date and datetime widgets everywhere in base package.

Use standard datetime widgets for imports. I don't know if this is really necessary.

  • Property svn:keywords set to Id
File size: 71.6 KB
Line 
1## $Id: browser.py 8160 2012-04-15 06:40:01Z 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 FriendlyDateDisplayWidget
48from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
49from waeup.kofa.widgets.phonewidget import PhoneWidget
50from waeup.kofa.students.interfaces import (
51    IStudentsContainer, IStudent,
52    IUGStudentClearance,IPGStudentClearance,
53    IStudentPersonal, IStudentBase, IStudentStudyCourse,
54    IStudentAccommodation, IStudentStudyLevel,
55    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
56    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentChangePassword
57    )
58from waeup.kofa.students.catalog import search
59from waeup.kofa.students.workflow import (ADMITTED, PAID,
60    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
61from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
62from waeup.kofa.students.vocabularies import StudyLevelSource
63from waeup.kofa.browser.resources import toggleall
64from waeup.kofa.hostels.hostel import NOT_OCCUPIED
65from waeup.kofa.utils.helpers import get_current_principal
66
67def write_log_message(view, message):
68    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
69    view.context.getStudent().loggerInfo(ob_class, message)
70    return
71
72# Save function used for save methods in pages
73def msave(view, **data):
74    changed_fields = view.applyData(view.context, **data)
75    # Turn list of lists into single list
76    if changed_fields:
77        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
78    # Inform catalog if certificate has changed
79    # (applyData does this only for the context)
80    if 'certificate' in changed_fields:
81        notify(grok.ObjectModifiedEvent(view.context.getStudent()))
82    fields_string = ' + '.join(changed_fields)
83    view.flash(_('Form has been saved.'))
84    if fields_string:
85        write_log_message(view, 'saved: %s' % fields_string)
86    return
87
88def emit_lock_message(view):
89    """Flash a lock message.
90    """
91    view.flash(_('The requested form is locked (read-only).'))
92    view.redirect(view.url(view.context))
93    return
94
95class StudentsBreadcrumb(Breadcrumb):
96    """A breadcrumb for the students container.
97    """
98    grok.context(IStudentsContainer)
99    title = _('Students')
100
101    @property
102    def target(self):
103        user = get_current_principal()
104        if getattr(user, 'user_type', None) == 'student':
105            return None
106        return self.viewname
107
108class StudentBreadcrumb(Breadcrumb):
109    """A breadcrumb for the student container.
110    """
111    grok.context(IStudent)
112
113    def title(self):
114        return self.context.display_fullname
115
116class SudyCourseBreadcrumb(Breadcrumb):
117    """A breadcrumb for the student study course.
118    """
119    grok.context(IStudentStudyCourse)
120    title = _('Study Course')
121
122class PaymentsBreadcrumb(Breadcrumb):
123    """A breadcrumb for the student payments folder.
124    """
125    grok.context(IStudentPaymentsContainer)
126    title = _('Payments')
127
128class OnlinePaymentBreadcrumb(Breadcrumb):
129    """A breadcrumb for payments.
130    """
131    grok.context(IStudentOnlinePayment)
132
133    @property
134    def title(self):
135        return self.context.p_id
136
137class AccommodationBreadcrumb(Breadcrumb):
138    """A breadcrumb for the student accommodation folder.
139    """
140    grok.context(IStudentAccommodation)
141    title = _('Accommodation')
142
143class BedTicketBreadcrumb(Breadcrumb):
144    """A breadcrumb for bed tickets.
145    """
146    grok.context(IBedTicket)
147
148    @property
149    def title(self):
150        return _('Bed Ticket ${a}',
151            mapping = {'a':self.context.getSessionString()})
152
153class StudyLevelBreadcrumb(Breadcrumb):
154    """A breadcrumb for course lists.
155    """
156    grok.context(IStudentStudyLevel)
157
158    @property
159    def title(self):
160        return self.context.level_title
161
162class StudentsContainerPage(KofaPage):
163    """The standard view for student containers.
164    """
165    grok.context(IStudentsContainer)
166    grok.name('index')
167    grok.require('waeup.viewStudentsContainer')
168    grok.template('containerpage')
169    label = _('Student Section')
170    search_button = _('Search')
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            try:
189                self.searchterm = int(self.searchterm)
190            except ValueError:
191                self.flash('Only year dates allowed (e.g. 2011).')
192                return
193        self.hitlist = search(query=self.searchterm,
194            searchtype=self.searchtype, view=self)
195        if not self.hitlist:
196            self.flash('No student found.')
197        return
198
199class StudentsContainerManagePage(KofaPage):
200    """The manage page for student containers.
201    """
202    grok.context(IStudentsContainer)
203    grok.name('manage')
204    grok.require('waeup.manageStudent')
205    grok.template('containermanagepage')
206    pnav = 4
207    label = _('Manage student section')
208    search_button = _('Search')
209    remove_button = _('Remove selected')
210
211    def update(self, *args, **kw):
212        datatable.need()
213        toggleall.need()
214        warning.need()
215        form = self.request.form
216        self.hitlist = []
217        if 'searchterm' in form and form['searchterm']:
218            self.searchterm = form['searchterm']
219            self.searchtype = form['searchtype']
220        elif 'old_searchterm' in form:
221            self.searchterm = form['old_searchterm']
222            self.searchtype = form['old_searchtype']
223        else:
224            if 'search' in form:
225                self.flash(_('Empty search string'))
226            return
227        if self.searchtype == 'current_session':
228            try:
229                self.searchterm = int(self.searchterm)
230            except ValueError:
231                self.flash('Only year dates allowed (e.g. 2011).')
232                return
233        if not 'entries' in form:
234            self.hitlist = search(query=self.searchterm,
235                searchtype=self.searchtype, view=self)
236            if not self.hitlist:
237                self.flash(_('No student found.'))
238            if 'remove' in form:
239                self.flash(_('No item selected.'))
240            return
241        entries = form['entries']
242        if isinstance(entries, basestring):
243            entries = [entries]
244        deleted = []
245        for entry in entries:
246            if 'remove' in form:
247                del self.context[entry]
248                deleted.append(entry)
249        self.hitlist = search(query=self.searchterm,
250            searchtype=self.searchtype, view=self)
251        if len(deleted):
252            self.flash(_('Successfully removed: ${a}',
253                mapping = {'a':', '.join(deleted)}))
254        return
255
256class StudentAddFormPage(KofaAddFormPage):
257    """Add-form to add a student.
258    """
259    grok.context(IStudentsContainer)
260    grok.require('waeup.manageStudent')
261    grok.name('addstudent')
262    form_fields = grok.AutoFields(IStudent).select(
263        'firstname', 'middlename', 'lastname', 'reg_number')
264    label = _('Add student')
265    pnav = 4
266
267    @action(_('Create student record'), style='primary')
268    def addStudent(self, **data):
269        student = createObject(u'waeup.Student')
270        self.applyData(student, **data)
271        self.context.addStudent(student)
272        self.flash(_('Student record created.'))
273        self.redirect(self.url(self.context[student.student_id], 'index'))
274        return
275
276class StudentBaseDisplayFormPage(KofaDisplayFormPage):
277    """ Page to display student base data
278    """
279    grok.context(IStudent)
280    grok.name('index')
281    grok.require('waeup.viewStudent')
282    grok.template('basepage')
283    form_fields = grok.AutoFields(IStudentBase).omit('password')
284    pnav = 4
285
286    @property
287    def label(self):
288        return _('${a}: Base Data',
289            mapping = {'a':self.context.display_fullname})
290
291    @property
292    def hasPassword(self):
293        if self.context.password:
294            return _('set')
295        return _('unset')
296
297class ContactStudentForm(ContactAdminForm):
298    grok.context(IStudent)
299    grok.name('contactstudent')
300    grok.require('waeup.viewStudent')
301    pnav = 4
302    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
303
304    def update(self, subject=u''):
305        self.form_fields.get('subject').field.default = subject
306        self.subject = subject
307        return
308
309    def label(self):
310        return _(u'Send message to ${a}',
311            mapping = {'a':self.context.display_fullname})
312
313    @action('Send message now', style='primary')
314    def send(self, *args, **data):
315        try:
316            email = self.request.principal.email
317        except AttributeError:
318            email = self.config.email_admin
319        usertype = getattr(self.request.principal,
320                           'user_type', 'system').title()
321        kofa_utils = getUtility(IKofaUtils)
322        success = kofa_utils.sendContactForm(
323                self.request.principal.title,email,
324                self.context.display_fullname,self.context.email,
325                self.request.principal.id,usertype,
326                self.config.name,
327                data['body'],data['subject'])
328        if success:
329            self.flash(_('Your message has been sent.'))
330        else:
331            self.flash(_('An smtp server error occurred.'))
332        return
333
334class StudentBaseManageFormPage(KofaEditFormPage):
335    """ View to manage student base data
336    """
337    grok.context(IStudent)
338    grok.name('manage_base')
339    grok.require('waeup.manageStudent')
340    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
341    form_fields['phone'].custom_widget = PhoneWidget
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 = 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 = 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
1057    @action(_('Create ticket'), style='primary')
1058    def createTicket(self, **data):
1059        p_category = data['p_category']
1060        student = self.context.__parent__
1061        if p_category == 'bed_allocation' and student[
1062            'studycourse'].current_session != grok.getSite()[
1063            'configuration'].accommodation_session:
1064                self.flash(
1065                    _('Your current session does not match ' + \
1066                    'accommodation session.'))
1067                self.redirect(self.url(self.context))
1068                return
1069        students_utils = getUtility(IStudentsUtils)
1070        try:
1071            pay_details  = students_utils.getPaymentDetails(
1072                p_category,student)
1073        except AttributeError:
1074            self.flash(
1075                _('Study course data are incomplete.'))
1076            self.redirect(self.url(self.context))
1077            return
1078        if pay_details['error']:
1079            self.flash(pay_details['error'])
1080            self.redirect(self.url(self.context))
1081            return
1082        p_item = pay_details['p_item']
1083        p_session = pay_details['p_session']
1084        for key in self.context.keys():
1085            ticket = self.context[key]
1086            if ticket.p_state == 'paid' and\
1087               ticket.p_category == p_category and \
1088               ticket.p_item == p_item and \
1089               ticket.p_session == p_session:
1090                  self.flash(
1091                      _('This type of payment has already been made.'))
1092                  self.redirect(self.url(self.context))
1093                  return
1094        payment = createObject(u'waeup.StudentOnlinePayment')
1095        self.applyData(payment, **data)
1096        timestamp = "%d" % int(time()*1000)
1097        payment.p_id = "p%s" % timestamp
1098        payment.p_item = p_item
1099        payment.p_session = p_session
1100        payment.amount_auth = pay_details['amount']
1101        payment.surcharge_1 = pay_details['surcharge_1']
1102        payment.surcharge_2 = pay_details['surcharge_2']
1103        payment.surcharge_3 = pay_details['surcharge_3']
1104        self.context[payment.p_id] = payment
1105        self.flash(_('Payment ticket created.'))
1106        self.redirect(self.url(self.context))
1107        return
1108
1109class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1110    """ Page to view an online payment ticket
1111    """
1112    grok.context(IStudentOnlinePayment)
1113    grok.name('index')
1114    grok.require('waeup.viewStudent')
1115    form_fields = grok.AutoFields(IStudentOnlinePayment)
1116    pnav = 4
1117
1118    @property
1119    def label(self):
1120        return _('${a}: Online Payment Ticket ${b}', mapping = {
1121            'a':self.context.getStudent().display_fullname,
1122            'b':self.context.p_id})
1123
1124class OnlinePaymentCallbackPage(UtilityView, grok.View):
1125    """ Callback view
1126    """
1127    grok.context(IStudentOnlinePayment)
1128    grok.name('simulate_callback')
1129    grok.require('waeup.payStudent')
1130
1131    # This update method simulates a valid callback und must be
1132    # neutralized in the customization package.
1133    def update(self):
1134        if self.context.p_state == 'paid':
1135            self.flash(_('This ticket has already been paid.'))
1136            return
1137        student = self.context.getStudent()
1138        write_log_message(self,'valid callback: %s' % self.context.p_id)
1139        self.context.r_amount_approved = self.context.amount_auth
1140        self.context.r_card_num = u'0000'
1141        self.context.r_code = u'00'
1142        self.context.p_state = 'paid'
1143        self.context.payment_date = datetime.now()
1144        if self.context.p_category == 'clearance':
1145            # Create CLR access code
1146            pin, error = create_accesscode('CLR',0,student.student_id)
1147            if error:
1148                self.flash(_('Valid callback received. ${a}',
1149                    mapping = {'a':error}))
1150                return
1151            self.context.ac = pin
1152        elif self.context.p_category == 'schoolfee':
1153            # Create SFE access code
1154            pin, error = create_accesscode('SFE',0,student.student_id)
1155            if error:
1156                self.flash(_('Valid callback received. ${a}',
1157                    mapping = {'a':error}))
1158                return
1159            self.context.ac = pin
1160        elif self.context.p_category == 'bed_allocation':
1161            # Create HOS access code
1162            pin, error = create_accesscode('HOS',0,student.student_id)
1163            if error:
1164                self.flash(_('Valid callback received. ${a}',
1165                    mapping = {'a':error}))
1166                return
1167            self.context.ac = pin
1168        self.flash(_('Valid callback received.'))
1169        return
1170
1171    def render(self):
1172        self.redirect(self.url(self.context, '@@index'))
1173        return
1174
1175class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1176    """Deliver a PDF slip of the context.
1177    """
1178    grok.context(IStudentOnlinePayment)
1179    grok.name('payment_receipt.pdf')
1180    grok.require('waeup.viewStudent')
1181    form_fields = grok.AutoFields(IStudentOnlinePayment)
1182    #form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1183    #form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1184    prefix = 'form'
1185    title = 'Payment Data'
1186
1187    @property
1188    def label(self):
1189        return 'Online Payment Receipt %s' % self.context.p_id
1190
1191    def render(self):
1192        if self.context.p_state != 'paid':
1193            self.flash('Ticket not yet paid.')
1194            self.redirect(self.url(self.context))
1195            return
1196        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1197            self.request)
1198        students_utils = getUtility(IStudentsUtils)
1199        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1200            self.context.getStudent(), studentview)
1201
1202
1203class AccommodationManageFormPage(KofaEditFormPage):
1204    """ Page to manage bed tickets.
1205
1206    This manage form page is for both students and students officers.
1207    """
1208    grok.context(IStudentAccommodation)
1209    grok.name('index')
1210    grok.require('waeup.handleAccommodation')
1211    form_fields = grok.AutoFields(IStudentAccommodation)
1212    grok.template('accommodationmanagepage')
1213    pnav = 4
1214    officers_only_actions = [_('Remove selected')]
1215
1216    @property
1217    def label(self):
1218        return _('${a}: Accommodation',
1219            mapping = {'a':self.context.__parent__.display_fullname})
1220
1221    def update(self):
1222        super(AccommodationManageFormPage, self).update()
1223        datatable.need()
1224        warning.need()
1225        return
1226
1227    @jsaction(_('Remove selected'))
1228    def delBedTickets(self, **data):
1229        if getattr(self.request.principal, 'user_type', None) == 'student':
1230            self.flash(_('You are not allowed to remove bed tickets.'))
1231            self.redirect(self.url(self.context))
1232            return
1233        form = self.request.form
1234        if form.has_key('val_id'):
1235            child_id = form['val_id']
1236        else:
1237            self.flash(_('No bed ticket selected.'))
1238            self.redirect(self.url(self.context))
1239            return
1240        if not isinstance(child_id, list):
1241            child_id = [child_id]
1242        deleted = []
1243        for id in child_id:
1244            del self.context[id]
1245            deleted.append(id)
1246        if len(deleted):
1247            self.flash(_('Successfully removed: ${a}',
1248                mapping = {'a':', '.join(deleted)}))
1249            write_log_message(self,'removed: % s' % ', '.join(deleted))
1250        self.redirect(self.url(self.context))
1251        return
1252
1253    @property
1254    def selected_actions(self):
1255        if getattr(self.request.principal, 'user_type', None) == 'student':
1256            return [action for action in self.actions
1257                    if not action.label in self.officers_only_actions]
1258        return self.actions
1259
1260class BedTicketAddPage(KofaPage):
1261    """ Page to add an online payment ticket
1262    """
1263    grok.context(IStudentAccommodation)
1264    grok.name('add')
1265    grok.require('waeup.handleAccommodation')
1266    grok.template('enterpin')
1267    ac_prefix = 'HOS'
1268    label = _('Add bed ticket')
1269    pnav = 4
1270    buttonname = _('Create bed ticket')
1271    notice = ''
1272
1273    def update(self, SUBMIT=None):
1274        student = self.context.getStudent()
1275        students_utils = getUtility(IStudentsUtils)
1276        acc_details  = students_utils.getAccommodationDetails(student)
1277        if not acc_details:
1278            self.flash(_("Your data are incomplete."))
1279            self.redirect(self.url(self.context))
1280            return
1281        if not student.state in acc_details['allowed_states']:
1282            self.flash(_("You are in the wrong registration state."))
1283            self.redirect(self.url(self.context))
1284            return
1285        if student['studycourse'].current_session != acc_details[
1286            'booking_session']:
1287            self.flash(
1288                _('Your current session does not match accommodation session.'))
1289            self.redirect(self.url(self.context))
1290            return
1291        if str(acc_details['booking_session']) in self.context.keys():
1292            self.flash(
1293                _('You already booked a bed space in current ' \
1294                    + 'accommodation session.'))
1295            self.redirect(self.url(self.context))
1296            return
1297        self.ac_series = self.request.form.get('ac_series', None)
1298        self.ac_number = self.request.form.get('ac_number', None)
1299        if SUBMIT is None:
1300            return
1301        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1302        code = get_access_code(pin)
1303        if not code:
1304            self.flash(_('Activation code is invalid.'))
1305            return
1306        # Search and book bed
1307        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1308        entries = cat.searchResults(
1309            owner=(student.student_id,student.student_id))
1310        if len(entries):
1311            # If bed space has bee manually allocated use this bed
1312            bed = [entry for entry in entries][0]
1313        else:
1314            # else search for other available beds
1315            entries = cat.searchResults(
1316                bed_type=(acc_details['bt'],acc_details['bt']))
1317            available_beds = [
1318                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1319            if available_beds:
1320                students_utils = getUtility(IStudentsUtils)
1321                bed = students_utils.selectBed(available_beds)
1322                bed.bookBed(student.student_id)
1323            else:
1324                self.flash(_('There is no free bed in your category ${a}.',
1325                    mapping = {'a':acc_details['bt']}))
1326                return
1327        # Mark pin as used (this also fires a pin related transition)
1328        if code.state == USED:
1329            self.flash(_('Activation code has already been used.'))
1330            return
1331        else:
1332            comment = _(u'invalidated')
1333            # Here we know that the ac is in state initialized so we do not
1334            # expect an exception, but the owner might be different
1335            if not invalidate_accesscode(
1336                pin,comment,self.context.getStudent().student_id):
1337                self.flash(_('You are not the owner of this access code.'))
1338                return
1339        # Create bed ticket
1340        bedticket = createObject(u'waeup.BedTicket')
1341        bedticket.booking_code = pin
1342        bedticket.booking_session = acc_details['booking_session']
1343        bedticket.bed_type = acc_details['bt']
1344        bedticket.bed = bed
1345        hall_title = bed.__parent__.hostel_name
1346        coordinates = bed.getBedCoordinates()[1:]
1347        block, room_nr, bed_nr = coordinates
1348        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1349            'a':hall_title, 'b':block,
1350            'c':room_nr, 'd':bed_nr,
1351            'e':bed.bed_type})
1352        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1353        bedticket.bed_coordinates = translate(
1354            bc, 'waeup.kofa',target_language=portal_language)
1355        key = str(acc_details['booking_session'])
1356        self.context[key] = bedticket
1357        self.flash(_('Bed ticket created and bed booked: ${a}',
1358            mapping = {'a':bedticket.bed_coordinates}))
1359        self.redirect(self.url(self.context))
1360        return
1361
1362class BedTicketDisplayFormPage(KofaDisplayFormPage):
1363    """ Page to display bed tickets
1364    """
1365    grok.context(IBedTicket)
1366    grok.name('index')
1367    grok.require('waeup.handleAccommodation')
1368    form_fields = grok.AutoFields(IBedTicket)
1369    pnav = 4
1370
1371    @property
1372    def label(self):
1373        return _('Bed Ticket for Session ${a}',
1374            mapping = {'a':self.context.getSessionString()})
1375
1376class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1377    """Deliver a PDF slip of the context.
1378    """
1379    grok.context(IBedTicket)
1380    grok.name('bed_allocation.pdf')
1381    grok.require('waeup.handleAccommodation')
1382    form_fields = grok.AutoFields(IBedTicket)
1383    #form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1384    prefix = 'form'
1385
1386    @property
1387    def title(self):
1388        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1389        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1390            target_language=portal_language)
1391
1392    @property
1393    def label(self):
1394        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1395        return translate(_('Bed Allocation: '),
1396            'waeup.kofa', target_language=portal_language) \
1397            + ' %s' % self.context.bed_coordinates
1398
1399    def render(self):
1400        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1401            self.request)
1402        students_utils = getUtility(IStudentsUtils)
1403        return students_utils.renderPDF(
1404            self, 'bed_allocation.pdf',
1405            self.context.getStudent(), studentview)
1406
1407class BedTicketRelocationPage(UtilityView, grok.View):
1408    """ Callback view
1409    """
1410    grok.context(IBedTicket)
1411    grok.name('relocate')
1412    grok.require('waeup.manageHostels')
1413
1414    # Relocate student if student parameters have changed or the bed_type
1415    # of the bed has changed
1416    def update(self):
1417        student = self.context.getStudent()
1418        students_utils = getUtility(IStudentsUtils)
1419        acc_details  = students_utils.getAccommodationDetails(student)
1420        if self.context.bed != None and \
1421              'reserved' in self.context.bed.bed_type:
1422            self.flash(_("Students in reserved beds can't be relocated."))
1423            self.redirect(self.url(self.context))
1424            return
1425        if acc_details['bt'] == self.context.bed_type and \
1426                self.context.bed != None and \
1427                self.context.bed.bed_type == self.context.bed_type:
1428            self.flash(_("Student can't be relocated."))
1429            self.redirect(self.url(self.context))
1430            return
1431        # Search a bed
1432        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1433        entries = cat.searchResults(
1434            owner=(student.student_id,student.student_id))
1435        if len(entries) and self.context.bed == None:
1436            # If booking has been cancelled but other bed space has been
1437            # manually allocated after cancellation use this bed
1438            new_bed = [entry for entry in entries][0]
1439        else:
1440            # Search for other available beds
1441            entries = cat.searchResults(
1442                bed_type=(acc_details['bt'],acc_details['bt']))
1443            available_beds = [
1444                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1445            if available_beds:
1446                students_utils = getUtility(IStudentsUtils)
1447                new_bed = students_utils.selectBed(available_beds)
1448                new_bed.bookBed(student.student_id)
1449            else:
1450                self.flash(_('There is no free bed in your category ${a}.',
1451                    mapping = {'a':acc_details['bt']}))
1452                self.redirect(self.url(self.context))
1453                return
1454        # Release old bed if exists
1455        if self.context.bed != None:
1456            self.context.bed.owner = NOT_OCCUPIED
1457            notify(grok.ObjectModifiedEvent(self.context.bed))
1458        # Alocate new bed
1459        self.context.bed_type = acc_details['bt']
1460        self.context.bed = new_bed
1461        hall_title = new_bed.__parent__.hostel_name
1462        coordinates = new_bed.getBedCoordinates()[1:]
1463        block, room_nr, bed_nr = coordinates
1464        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1465            'a':hall_title, 'b':block,
1466            'c':room_nr, 'd':bed_nr,
1467            'e':new_bed.bed_type})
1468        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1469        self.context.bed_coordinates = translate(
1470            bc, 'waeup.kofa',target_language=portal_language)
1471        self.flash(_('Student relocated: ${a}',
1472            mapping = {'a':self.context.bed_coordinates}))
1473        self.redirect(self.url(self.context))
1474        return
1475
1476    def render(self):
1477        return
1478
1479class StudentHistoryPage(KofaPage):
1480    """ Page to display student clearance data
1481    """
1482    grok.context(IStudent)
1483    grok.name('history')
1484    grok.require('waeup.viewStudent')
1485    grok.template('studenthistory')
1486    pnav = 4
1487
1488    @property
1489    def label(self):
1490        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1491
1492# Pages for students only
1493
1494class StudentBaseEditFormPage(KofaEditFormPage):
1495    """ View to edit student base data
1496    """
1497    grok.context(IStudent)
1498    grok.name('edit_base')
1499    grok.require('waeup.handleStudent')
1500    form_fields = grok.AutoFields(IStudentBase).select(
1501        'email', 'phone')
1502    form_fields['phone'].custom_widget = PhoneWidget
1503    label = _('Edit base data')
1504    pnav = 4
1505
1506    @action(_('Save'), style='primary')
1507    def save(self, **data):
1508        msave(self, **data)
1509        return
1510
1511class StudentChangePasswordPage(KofaEditFormPage):
1512    """ View to manage student base data
1513    """
1514    grok.context(IStudent)
1515    grok.name('change_password')
1516    grok.require('waeup.handleStudent')
1517    grok.template('change_password')
1518    label = _('Change password')
1519    pnav = 4
1520
1521    @action(_('Save'), style='primary')
1522    def save(self, **data):
1523        form = self.request.form
1524        password = form.get('change_password', None)
1525        password_ctl = form.get('change_password_repeat', None)
1526        if password:
1527            validator = getUtility(IPasswordValidator)
1528            errors = validator.validate_password(password, password_ctl)
1529            if not errors:
1530                IUserAccount(self.context).setPassword(password)
1531                write_log_message(self, 'saved: password')
1532                self.flash(_('Password changed.'))
1533            else:
1534                self.flash( ' '.join(errors))
1535        return
1536
1537class StudentFilesUploadPage(KofaPage):
1538    """ View to upload files by student
1539    """
1540    grok.context(IStudent)
1541    grok.name('change_portrait')
1542    grok.require('waeup.uploadStudentFile')
1543    grok.template('filesuploadpage')
1544    label = _('Upload portrait')
1545    pnav = 4
1546
1547    def update(self):
1548        if self.context.getStudent().state != ADMITTED:
1549            emit_lock_message(self)
1550            return
1551        super(StudentFilesUploadPage, self).update()
1552        return
1553
1554class StartClearancePage(KofaPage):
1555    grok.context(IStudent)
1556    grok.name('start_clearance')
1557    grok.require('waeup.handleStudent')
1558    grok.template('enterpin')
1559    label = _('Start clearance')
1560    ac_prefix = 'CLR'
1561    notice = ''
1562    pnav = 4
1563    buttonname = _('Start clearance now')
1564
1565    @property
1566    def all_required_fields_filled(self):
1567        if self.context.email and self.context.phone:
1568            return True
1569        return False
1570
1571    @property
1572    def portrait_uploaded(self):
1573        store = getUtility(IExtFileStore)
1574        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1575            return True
1576        return False
1577
1578    def update(self, SUBMIT=None):
1579        if not self.context.state == ADMITTED:
1580            self.flash(_("Wrong state"))
1581            self.redirect(self.url(self.context))
1582            return
1583        if not self.portrait_uploaded:
1584            self.flash(_("No portrait uploaded."))
1585            self.redirect(self.url(self.context, 'change_portrait'))
1586            return
1587        if not self.all_required_fields_filled:
1588            self.flash(_("Not all required fields filled."))
1589            self.redirect(self.url(self.context, 'edit_base'))
1590            return
1591        self.ac_series = self.request.form.get('ac_series', None)
1592        self.ac_number = self.request.form.get('ac_number', None)
1593
1594        if SUBMIT is None:
1595            return
1596        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1597        code = get_access_code(pin)
1598        if not code:
1599            self.flash(_('Activation code is invalid.'))
1600            return
1601        # Mark pin as used (this also fires a pin related transition)
1602        # and fire transition start_clearance
1603        if code.state == USED:
1604            self.flash(_('Activation code has already been used.'))
1605            return
1606        else:
1607            comment = _(u"invalidated")
1608            # Here we know that the ac is in state initialized so we do not
1609            # expect an exception, but the owner might be different
1610            if not invalidate_accesscode(pin,comment,self.context.student_id):
1611                self.flash(_('You are not the owner of this access code.'))
1612                return
1613            self.context.clr_code = pin
1614        IWorkflowInfo(self.context).fireTransition('start_clearance')
1615        self.flash(_('Clearance process has been started.'))
1616        self.redirect(self.url(self.context,'cedit'))
1617        return
1618
1619class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1620    """ View to edit student clearance data by student
1621    """
1622    grok.context(IStudent)
1623    grok.name('cedit')
1624    grok.require('waeup.handleStudent')
1625    label = _('Edit clearance data')
1626
1627    @property
1628    def form_fields(self):
1629        cm = getattr(self.context,'current_mode', None)
1630        if cm is not None and cm.startswith('pg'):
1631            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
1632        else:
1633            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
1634        return form_fields
1635
1636    def update(self):
1637        if self.context.clearance_locked:
1638            emit_lock_message(self)
1639            return
1640        return super(StudentClearanceEditFormPage, self).update()
1641
1642    @action(_('Save'), style='primary')
1643    def save(self, **data):
1644        self.applyData(self.context, **data)
1645        self.flash(_('Clearance form has been saved.'))
1646        return
1647
1648    def dataNotComplete(self):
1649        """To be implemented in the customization package.
1650        """
1651        return False
1652
1653    @action(_('Save and request clearance'), style='primary')
1654    def requestClearance(self, **data):
1655        self.applyData(self.context, **data)
1656        if self.dataNotComplete():
1657            self.flash(self.dataNotComplete())
1658            return
1659        self.flash(_('Clearance form has been saved.'))
1660        self.redirect(self.url(self.context,'request_clearance'))
1661        return
1662
1663class RequestClearancePage(KofaPage):
1664    grok.context(IStudent)
1665    grok.name('request_clearance')
1666    grok.require('waeup.handleStudent')
1667    grok.template('enterpin')
1668    label = _('Request clearance')
1669    notice = _('Enter the CLR access code used for starting clearance.')
1670    ac_prefix = 'CLR'
1671    pnav = 4
1672    buttonname = _('Request clearance now')
1673
1674    def update(self, SUBMIT=None):
1675        self.ac_series = self.request.form.get('ac_series', None)
1676        self.ac_number = self.request.form.get('ac_number', None)
1677        if SUBMIT is None:
1678            return
1679        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1680        if self.context.clr_code != pin:
1681            self.flash(_("This isn't your CLR access code."))
1682            return
1683        state = IWorkflowState(self.context).getState()
1684        # This shouldn't happen, but the application officer
1685        # might have forgotten to lock the form after changing the state
1686        if state != CLEARANCE:
1687            self.flash(_('This form cannot be submitted. Wrong state!'))
1688            return
1689        IWorkflowInfo(self.context).fireTransition('request_clearance')
1690        self.flash(_('Clearance has been requested.'))
1691        self.redirect(self.url(self.context))
1692        return
1693
1694class StartCourseRegistrationPage(KofaPage):
1695    grok.context(IStudentStudyCourse)
1696    grok.name('start_course_registration')
1697    grok.require('waeup.handleStudent')
1698    grok.template('enterpin')
1699    label = _('Start course registration')
1700    ac_prefix = 'SFE'
1701    notice = ''
1702    pnav = 4
1703    buttonname = _('Start course registration now')
1704
1705    def update(self, SUBMIT=None):
1706        if not self.context.getStudent().state in (CLEARED,RETURNING):
1707            self.flash(_("Wrong state"))
1708            self.redirect(self.url(self.context))
1709            return
1710        self.ac_series = self.request.form.get('ac_series', None)
1711        self.ac_number = self.request.form.get('ac_number', None)
1712
1713        if SUBMIT is None:
1714            return
1715        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1716        code = get_access_code(pin)
1717        if not code:
1718            self.flash(_('Activation code is invalid.'))
1719            return
1720        # Mark pin as used (this also fires a pin related transition)
1721        # and fire transition start_clearance
1722        if code.state == USED:
1723            self.flash(_('Activation code has already been used.'))
1724            return
1725        else:
1726            comment = _(u"invalidated")
1727            # Here we know that the ac is in state initialized so we do not
1728            # expect an exception, but the owner might be different
1729            if not invalidate_accesscode(
1730                pin,comment,self.context.getStudent().student_id):
1731                self.flash(_('You are not the owner of this access code.'))
1732                return
1733        if self.context.getStudent().state == CLEARED:
1734            IWorkflowInfo(self.context.getStudent()).fireTransition(
1735                'pay_first_school_fee')
1736        elif self.context.getStudent().state == RETURNING:
1737            IWorkflowInfo(self.context.getStudent()).fireTransition(
1738                'pay_school_fee')
1739        self.flash(_('Course registration has been started.'))
1740        self.redirect(self.url(self.context))
1741        return
1742
1743class AddStudyLevelFormPage(KofaEditFormPage):
1744    """ Page for students to add current study levels
1745    """
1746    grok.context(IStudentStudyCourse)
1747    grok.name('add')
1748    grok.require('waeup.handleStudent')
1749    grok.template('studyleveladdpage')
1750    form_fields = grok.AutoFields(IStudentStudyCourse)
1751    pnav = 4
1752
1753    @property
1754    def label(self):
1755        studylevelsource = StudyLevelSource().factory
1756        code = self.context.current_level
1757        title = studylevelsource.getTitle(self.context, code)
1758        return _('Add current level ${a}', mapping = {'a':title})
1759
1760    def update(self):
1761        if self.context.getStudent().state != PAID:
1762            emit_lock_message(self)
1763            return
1764        super(AddStudyLevelFormPage, self).update()
1765        return
1766
1767    @action(_('Create course list now'), style='primary')
1768    def addStudyLevel(self, **data):
1769        studylevel = StudentStudyLevel()
1770        studylevel.level = self.context.current_level
1771        studylevel.level_session = self.context.current_session
1772        try:
1773            self.context.addStudentStudyLevel(
1774                self.context.certificate,studylevel)
1775        except KeyError:
1776            self.flash(_('This level exists.'))
1777        self.redirect(self.url(self.context))
1778        return
1779
1780class StudyLevelEditFormPage(KofaEditFormPage):
1781    """ Page to edit the student study level data by students
1782    """
1783    grok.context(IStudentStudyLevel)
1784    grok.name('edit')
1785    grok.require('waeup.handleStudent')
1786    grok.template('studyleveleditpage')
1787    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1788        'level_session', 'level_verdict')
1789    pnav = 4
1790
1791    def update(self):
1792        if self.context.getStudent().state != PAID:
1793            emit_lock_message(self)
1794            return
1795        super(StudyLevelEditFormPage, self).update()
1796        datatable.need()
1797        warning.need()
1798        return
1799
1800    @property
1801    def label(self):
1802        # Here we know that the cookie has been set
1803        lang = self.request.cookies.get('kofa.language')
1804        level_title = translate(self.context.level_title, 'waeup.kofa',
1805            target_language=lang)
1806        return _('Add and remove course tickets of study level ${a}',
1807            mapping = {'a':level_title})
1808
1809    @property
1810    def total_credits(self):
1811        total_credits = 0
1812        for key, val in self.context.items():
1813            total_credits += val.credits
1814        return total_credits
1815
1816    @action(_('Add course ticket'))
1817    def addCourseTicket(self, **data):
1818        self.redirect(self.url(self.context, 'ctadd'))
1819
1820    @jsaction(_('Remove selected tickets'))
1821    def delCourseTicket(self, **data):
1822        form = self.request.form
1823        if form.has_key('val_id'):
1824            child_id = form['val_id']
1825        else:
1826            self.flash(_('No ticket selected.'))
1827            self.redirect(self.url(self.context, '@@edit'))
1828            return
1829        if not isinstance(child_id, list):
1830            child_id = [child_id]
1831        deleted = []
1832        for id in child_id:
1833            # Students are not allowed to remove core tickets
1834            if not self.context[id].mandatory:
1835                del self.context[id]
1836                deleted.append(id)
1837        if len(deleted):
1838            self.flash(_('Successfully removed: ${a}',
1839                mapping = {'a':', '.join(deleted)}))
1840        self.redirect(self.url(self.context, u'@@edit'))
1841        return
1842
1843    @action(_('Register course list'), style='primary')
1844    def RegisterCourses(self, **data):
1845        IWorkflowInfo(self.context.getStudent()).fireTransition(
1846            'register_courses')
1847        self.flash(_('Course list has been registered.'))
1848        self.redirect(self.url(self.context))
1849        return
1850
1851class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1852    """Add a course ticket by student.
1853    """
1854    grok.name('ctadd')
1855    grok.require('waeup.handleStudent')
1856    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1857        'score', 'mandatory', 'automatic', 'carry_over')
1858
1859    def update(self):
1860        if self.context.getStudent().state != PAID:
1861            emit_lock_message(self)
1862            return
1863        super(CourseTicketAddFormPage2, self).update()
1864        return
1865
1866    @action(_('Add course ticket'))
1867    def addCourseTicket(self, **data):
1868        # Safety belt
1869        if self.context.getStudent().state != PAID:
1870            return
1871        ticket = CourseTicket()
1872        course = data['course']
1873        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1874            setattr(ticket, name, getattr(course, name))
1875        ticket.automatic = False
1876        try:
1877            self.context.addCourseTicket(ticket)
1878        except KeyError:
1879            self.flash(_('The ticket exists.'))
1880            return
1881        self.flash(_('Successfully added ${a}.',
1882            mapping = {'a':ticket.code}))
1883        self.redirect(self.url(self.context, u'@@edit'))
1884        return
1885
1886class ChangePasswordRequestPage(KofaForm):
1887    """Captcha'd page for students to request a password change.
1888    """
1889    grok.context(IUniversity)
1890    grok.name('sendpw')
1891    grok.require('waeup.Anonymous')
1892    grok.template('sendpassword')
1893    label = _('Send me a new password')
1894    form_fields = grok.AutoFields(IStudentChangePassword)
1895
1896    def update(self):
1897        # Handle captcha
1898        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1899        self.captcha_result = self.captcha.verify(self.request)
1900        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1901        return
1902
1903    @action(_('Get new login credentials'), style='primary')
1904    def request(self, **data):
1905        if not self.captcha_result.is_valid:
1906            # Captcha will display error messages automatically.
1907            # No need to flash something.
1908            return
1909        # Search student or applicant
1910        reg_number = data['reg_number']
1911        email = data['email']
1912        cat = queryUtility(ICatalog, name='students_catalog')
1913        results = cat.searchResults(
1914            reg_number=(reg_number, reg_number),
1915            email=(email,email))
1916        if len(results) == 0:
1917            # Try also the applicants_catalog if no student record was found.
1918            cat = queryUtility(ICatalog, name='applicants_catalog')
1919            if cat is None:
1920                self.flash(_('Application package not installed.'))
1921                return
1922            results = cat.searchResults(
1923                reg_number=(reg_number, reg_number),
1924                email=(email,email))
1925        if len(results) == 0:
1926            self.flash(_('No record found.'))
1927            return
1928        student = list(results)[0]
1929        # Change password
1930        kofa_utils = getUtility(IKofaUtils)
1931        pwd = kofa_utils.genPassword()
1932        IUserAccount(student).setPassword(pwd)
1933        # Send email with new redentials
1934        msg = _('You have successfully changed your password for the')
1935        login_url = self.url(grok.getSite(), 'login')
1936        success = kofa_utils.sendCredentials(
1937            IUserAccount(student),pwd,login_url,msg)
1938        if success:
1939            self.flash(_('An email with your user name and password ' +
1940                'has been sent to ${a}.', mapping = {'a':email}))
1941        else:
1942            self.flash(_('An smtp server error occurred.'))
1943        return
1944
1945class SetPasswordPage(KofaPage):
1946    grok.context(IKofaObject)
1947    grok.name('setpassword')
1948    grok.require('waeup.Anonymous')
1949    grok.template('setpassword')
1950    label = _('Set password for first-time login')
1951    ac_prefix = 'PWD'
1952    pnav = 0
1953    set_button = _('Set')
1954
1955    def update(self, SUBMIT=None):
1956        self.reg_number = self.request.form.get('reg_number', None)
1957        self.ac_series = self.request.form.get('ac_series', None)
1958        self.ac_number = self.request.form.get('ac_number', None)
1959
1960        if SUBMIT is None:
1961            return
1962        hitlist = search(query=self.reg_number,
1963            searchtype='reg_number', view=self)
1964        if not hitlist:
1965            self.flash(_('No student found.'))
1966            return
1967        if len(hitlist) != 1:   # Cannot happen but anyway
1968            self.flash(_('More than one student found.'))
1969            return
1970        student = hitlist[0].context
1971        self.student_id = student.student_id
1972        student_pw = student.password
1973        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1974        code = get_access_code(pin)
1975        if not code:
1976            self.flash(_('Access code is invalid.'))
1977            return
1978        if student_pw and pin == student.adm_code:
1979            self.flash(_(
1980                'Password has already been set. Your Student Id is ${a}',
1981                mapping = {'a':self.student_id}))
1982            return
1983        elif student_pw:
1984            self.flash(
1985                _('Password has already been set. You are using the ' +
1986                'wrong Access Code.'))
1987            return
1988        # Mark pin as used (this also fires a pin related transition)
1989        # and set student password
1990        if code.state == USED:
1991            self.flash(_('Access code has already been used.'))
1992            return
1993        else:
1994            comment = _(u"invalidated")
1995            # Here we know that the ac is in state initialized so we do not
1996            # expect an exception
1997            invalidate_accesscode(pin,comment)
1998            IUserAccount(student).setPassword(self.ac_number)
1999            student.adm_code = pin
2000        self.flash(_('Password has been set. Your Student Id is ${a}',
2001            mapping = {'a':self.student_id}))
2002        return
Note: See TracBrowser for help on using the repository browser.