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

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

Remove all payment attributes which are not really necessary in the base package. All these parameters are only subject to customization.

  • Property svn:keywords set to Id
File size: 71.7 KB
Line 
1## $Id: browser.py 8244 2012-04-22 10:12:23Z 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 = 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    form_fields[
1117        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1118    form_fields[
1119        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1120    pnav = 4
1121
1122    @property
1123    def label(self):
1124        return _('${a}: Online Payment Ticket ${b}', mapping = {
1125            'a':self.context.getStudent().display_fullname,
1126            'b':self.context.p_id})
1127
1128class OnlinePaymentCallbackPage(UtilityView, grok.View):
1129    """ Callback view
1130    """
1131    grok.context(IStudentOnlinePayment)
1132    grok.name('simulate_callback')
1133    grok.require('waeup.payStudent')
1134
1135    # This update method simulates a valid callback und must be
1136    # neutralized in the customization package.
1137    def update(self):
1138        if self.context.p_state == 'paid':
1139            self.flash(_('This ticket has already been paid.'))
1140            return
1141        student = self.context.getStudent()
1142        write_log_message(self,'valid callback: %s' % self.context.p_id)
1143        self.context.r_amount_approved = self.context.amount_auth
1144        self.context.r_code = u'00'
1145        self.context.p_state = 'paid'
1146        self.context.payment_date = datetime.utcnow()
1147        if self.context.p_category == 'clearance':
1148            # Create CLR access code
1149            pin, error = create_accesscode('CLR',0,student.student_id)
1150            if error:
1151                self.flash(_('Valid callback received. ${a}',
1152                    mapping = {'a':error}))
1153                return
1154            self.context.ac = pin
1155        elif self.context.p_category == 'schoolfee':
1156            # Create SFE access code
1157            pin, error = create_accesscode('SFE',0,student.student_id)
1158            if error:
1159                self.flash(_('Valid callback received. ${a}',
1160                    mapping = {'a':error}))
1161                return
1162            self.context.ac = pin
1163        elif self.context.p_category == 'bed_allocation':
1164            # Create HOS access code
1165            pin, error = create_accesscode('HOS',0,student.student_id)
1166            if error:
1167                self.flash(_('Valid callback received. ${a}',
1168                    mapping = {'a':error}))
1169                return
1170            self.context.ac = pin
1171        self.flash(_('Valid callback received.'))
1172        return
1173
1174    def render(self):
1175        self.redirect(self.url(self.context, '@@index'))
1176        return
1177
1178class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1179    """Deliver a PDF slip of the context.
1180    """
1181    grok.context(IStudentOnlinePayment)
1182    grok.name('payment_receipt.pdf')
1183    grok.require('waeup.viewStudent')
1184    form_fields = grok.AutoFields(IStudentOnlinePayment)
1185    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1186    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1187    prefix = 'form'
1188    title = 'Payment Data'
1189
1190    @property
1191    def label(self):
1192        return 'Online Payment Receipt %s' % self.context.p_id
1193
1194    def render(self):
1195        if self.context.p_state != 'paid':
1196            self.flash('Ticket not yet paid.')
1197            self.redirect(self.url(self.context))
1198            return
1199        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1200            self.request)
1201        students_utils = getUtility(IStudentsUtils)
1202        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1203            self.context.getStudent(), studentview)
1204
1205
1206class AccommodationManageFormPage(KofaEditFormPage):
1207    """ Page to manage bed tickets.
1208
1209    This manage form page is for both students and students officers.
1210    """
1211    grok.context(IStudentAccommodation)
1212    grok.name('index')
1213    grok.require('waeup.handleAccommodation')
1214    form_fields = grok.AutoFields(IStudentAccommodation)
1215    grok.template('accommodationmanagepage')
1216    pnav = 4
1217    officers_only_actions = [_('Remove selected')]
1218
1219    @property
1220    def label(self):
1221        return _('${a}: Accommodation',
1222            mapping = {'a':self.context.__parent__.display_fullname})
1223
1224    def update(self):
1225        super(AccommodationManageFormPage, self).update()
1226        datatable.need()
1227        warning.need()
1228        return
1229
1230    @jsaction(_('Remove selected'))
1231    def delBedTickets(self, **data):
1232        if getattr(self.request.principal, 'user_type', None) == 'student':
1233            self.flash(_('You are not allowed to remove bed tickets.'))
1234            self.redirect(self.url(self.context))
1235            return
1236        form = self.request.form
1237        if form.has_key('val_id'):
1238            child_id = form['val_id']
1239        else:
1240            self.flash(_('No bed ticket selected.'))
1241            self.redirect(self.url(self.context))
1242            return
1243        if not isinstance(child_id, list):
1244            child_id = [child_id]
1245        deleted = []
1246        for id in child_id:
1247            del self.context[id]
1248            deleted.append(id)
1249        if len(deleted):
1250            self.flash(_('Successfully removed: ${a}',
1251                mapping = {'a':', '.join(deleted)}))
1252            write_log_message(self,'removed: % s' % ', '.join(deleted))
1253        self.redirect(self.url(self.context))
1254        return
1255
1256    @property
1257    def selected_actions(self):
1258        if getattr(self.request.principal, 'user_type', None) == 'student':
1259            return [action for action in self.actions
1260                    if not action.label in self.officers_only_actions]
1261        return self.actions
1262
1263class BedTicketAddPage(KofaPage):
1264    """ Page to add an online payment ticket
1265    """
1266    grok.context(IStudentAccommodation)
1267    grok.name('add')
1268    grok.require('waeup.handleAccommodation')
1269    grok.template('enterpin')
1270    ac_prefix = 'HOS'
1271    label = _('Add bed ticket')
1272    pnav = 4
1273    buttonname = _('Create bed ticket')
1274    notice = ''
1275
1276    def update(self, SUBMIT=None):
1277        student = self.context.getStudent()
1278        students_utils = getUtility(IStudentsUtils)
1279        acc_details  = students_utils.getAccommodationDetails(student)
1280        if not acc_details:
1281            self.flash(_("Your data are incomplete."))
1282            self.redirect(self.url(self.context))
1283            return
1284        if not student.state in acc_details['allowed_states']:
1285            self.flash(_("You are in the wrong registration state."))
1286            self.redirect(self.url(self.context))
1287            return
1288        if student['studycourse'].current_session != acc_details[
1289            'booking_session']:
1290            self.flash(
1291                _('Your current session does not match accommodation session.'))
1292            self.redirect(self.url(self.context))
1293            return
1294        if str(acc_details['booking_session']) in self.context.keys():
1295            self.flash(
1296                _('You already booked a bed space in current ' \
1297                    + 'accommodation session.'))
1298            self.redirect(self.url(self.context))
1299            return
1300        self.ac_series = self.request.form.get('ac_series', None)
1301        self.ac_number = self.request.form.get('ac_number', None)
1302        if SUBMIT is None:
1303            return
1304        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1305        code = get_access_code(pin)
1306        if not code:
1307            self.flash(_('Activation code is invalid.'))
1308            return
1309        # Search and book bed
1310        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1311        entries = cat.searchResults(
1312            owner=(student.student_id,student.student_id))
1313        if len(entries):
1314            # If bed space has bee manually allocated use this bed
1315            bed = [entry for entry in entries][0]
1316        else:
1317            # else search for other available beds
1318            entries = cat.searchResults(
1319                bed_type=(acc_details['bt'],acc_details['bt']))
1320            available_beds = [
1321                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1322            if available_beds:
1323                students_utils = getUtility(IStudentsUtils)
1324                bed = students_utils.selectBed(available_beds)
1325                bed.bookBed(student.student_id)
1326            else:
1327                self.flash(_('There is no free bed in your category ${a}.',
1328                    mapping = {'a':acc_details['bt']}))
1329                return
1330        # Mark pin as used (this also fires a pin related transition)
1331        if code.state == USED:
1332            self.flash(_('Activation code has already been used.'))
1333            return
1334        else:
1335            comment = _(u'invalidated')
1336            # Here we know that the ac is in state initialized so we do not
1337            # expect an exception, but the owner might be different
1338            if not invalidate_accesscode(
1339                pin,comment,self.context.getStudent().student_id):
1340                self.flash(_('You are not the owner of this access code.'))
1341                return
1342        # Create bed ticket
1343        bedticket = createObject(u'waeup.BedTicket')
1344        bedticket.booking_code = pin
1345        bedticket.booking_session = acc_details['booking_session']
1346        bedticket.bed_type = acc_details['bt']
1347        bedticket.bed = bed
1348        hall_title = bed.__parent__.hostel_name
1349        coordinates = bed.getBedCoordinates()[1:]
1350        block, room_nr, bed_nr = coordinates
1351        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1352            'a':hall_title, 'b':block,
1353            'c':room_nr, 'd':bed_nr,
1354            'e':bed.bed_type})
1355        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1356        bedticket.bed_coordinates = translate(
1357            bc, 'waeup.kofa',target_language=portal_language)
1358        key = str(acc_details['booking_session'])
1359        self.context[key] = bedticket
1360        self.flash(_('Bed ticket created and bed booked: ${a}',
1361            mapping = {'a':bedticket.bed_coordinates}))
1362        self.redirect(self.url(self.context))
1363        return
1364
1365class BedTicketDisplayFormPage(KofaDisplayFormPage):
1366    """ Page to display bed tickets
1367    """
1368    grok.context(IBedTicket)
1369    grok.name('index')
1370    grok.require('waeup.handleAccommodation')
1371    form_fields = grok.AutoFields(IBedTicket)
1372    pnav = 4
1373
1374    @property
1375    def label(self):
1376        return _('Bed Ticket for Session ${a}',
1377            mapping = {'a':self.context.getSessionString()})
1378
1379class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1380    """Deliver a PDF slip of the context.
1381    """
1382    grok.context(IBedTicket)
1383    grok.name('bed_allocation.pdf')
1384    grok.require('waeup.handleAccommodation')
1385    form_fields = grok.AutoFields(IBedTicket)
1386    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1387    prefix = 'form'
1388
1389    @property
1390    def title(self):
1391        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1392        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1393            target_language=portal_language)
1394
1395    @property
1396    def label(self):
1397        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1398        return translate(_('Bed Allocation: '),
1399            'waeup.kofa', target_language=portal_language) \
1400            + ' %s' % self.context.bed_coordinates
1401
1402    def render(self):
1403        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1404            self.request)
1405        students_utils = getUtility(IStudentsUtils)
1406        return students_utils.renderPDF(
1407            self, 'bed_allocation.pdf',
1408            self.context.getStudent(), studentview)
1409
1410class BedTicketRelocationPage(UtilityView, grok.View):
1411    """ Callback view
1412    """
1413    grok.context(IBedTicket)
1414    grok.name('relocate')
1415    grok.require('waeup.manageHostels')
1416
1417    # Relocate student if student parameters have changed or the bed_type
1418    # of the bed has changed
1419    def update(self):
1420        student = self.context.getStudent()
1421        students_utils = getUtility(IStudentsUtils)
1422        acc_details  = students_utils.getAccommodationDetails(student)
1423        if self.context.bed != None and \
1424              'reserved' in self.context.bed.bed_type:
1425            self.flash(_("Students in reserved beds can't be relocated."))
1426            self.redirect(self.url(self.context))
1427            return
1428        if acc_details['bt'] == self.context.bed_type and \
1429                self.context.bed != None and \
1430                self.context.bed.bed_type == self.context.bed_type:
1431            self.flash(_("Student can't be relocated."))
1432            self.redirect(self.url(self.context))
1433            return
1434        # Search a bed
1435        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1436        entries = cat.searchResults(
1437            owner=(student.student_id,student.student_id))
1438        if len(entries) and self.context.bed == None:
1439            # If booking has been cancelled but other bed space has been
1440            # manually allocated after cancellation use this bed
1441            new_bed = [entry for entry in entries][0]
1442        else:
1443            # Search for other available beds
1444            entries = cat.searchResults(
1445                bed_type=(acc_details['bt'],acc_details['bt']))
1446            available_beds = [
1447                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1448            if available_beds:
1449                students_utils = getUtility(IStudentsUtils)
1450                new_bed = students_utils.selectBed(available_beds)
1451                new_bed.bookBed(student.student_id)
1452            else:
1453                self.flash(_('There is no free bed in your category ${a}.',
1454                    mapping = {'a':acc_details['bt']}))
1455                self.redirect(self.url(self.context))
1456                return
1457        # Release old bed if exists
1458        if self.context.bed != None:
1459            self.context.bed.owner = NOT_OCCUPIED
1460            notify(grok.ObjectModifiedEvent(self.context.bed))
1461        # Alocate new bed
1462        self.context.bed_type = acc_details['bt']
1463        self.context.bed = new_bed
1464        hall_title = new_bed.__parent__.hostel_name
1465        coordinates = new_bed.getBedCoordinates()[1:]
1466        block, room_nr, bed_nr = coordinates
1467        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1468            'a':hall_title, 'b':block,
1469            'c':room_nr, 'd':bed_nr,
1470            'e':new_bed.bed_type})
1471        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1472        self.context.bed_coordinates = translate(
1473            bc, 'waeup.kofa',target_language=portal_language)
1474        self.flash(_('Student relocated: ${a}',
1475            mapping = {'a':self.context.bed_coordinates}))
1476        self.redirect(self.url(self.context))
1477        return
1478
1479    def render(self):
1480        return
1481
1482class StudentHistoryPage(KofaPage):
1483    """ Page to display student clearance data
1484    """
1485    grok.context(IStudent)
1486    grok.name('history')
1487    grok.require('waeup.viewStudent')
1488    grok.template('studenthistory')
1489    pnav = 4
1490
1491    @property
1492    def label(self):
1493        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1494
1495# Pages for students only
1496
1497class StudentBaseEditFormPage(KofaEditFormPage):
1498    """ View to edit student base data
1499    """
1500    grok.context(IStudent)
1501    grok.name('edit_base')
1502    grok.require('waeup.handleStudent')
1503    form_fields = grok.AutoFields(IStudentBase).select(
1504        'email', 'phone')
1505    label = _('Edit base data')
1506    pnav = 4
1507
1508    @action(_('Save'), style='primary')
1509    def save(self, **data):
1510        msave(self, **data)
1511        return
1512
1513class StudentChangePasswordPage(KofaEditFormPage):
1514    """ View to manage student base data
1515    """
1516    grok.context(IStudent)
1517    grok.name('change_password')
1518    grok.require('waeup.handleStudent')
1519    grok.template('change_password')
1520    label = _('Change password')
1521    pnav = 4
1522
1523    @action(_('Save'), style='primary')
1524    def save(self, **data):
1525        form = self.request.form
1526        password = form.get('change_password', None)
1527        password_ctl = form.get('change_password_repeat', None)
1528        if password:
1529            validator = getUtility(IPasswordValidator)
1530            errors = validator.validate_password(password, password_ctl)
1531            if not errors:
1532                IUserAccount(self.context).setPassword(password)
1533                write_log_message(self, 'saved: password')
1534                self.flash(_('Password changed.'))
1535            else:
1536                self.flash( ' '.join(errors))
1537        return
1538
1539class StudentFilesUploadPage(KofaPage):
1540    """ View to upload files by student
1541    """
1542    grok.context(IStudent)
1543    grok.name('change_portrait')
1544    grok.require('waeup.uploadStudentFile')
1545    grok.template('filesuploadpage')
1546    label = _('Upload portrait')
1547    pnav = 4
1548
1549    def update(self):
1550        if self.context.getStudent().state != ADMITTED:
1551            emit_lock_message(self)
1552            return
1553        super(StudentFilesUploadPage, self).update()
1554        return
1555
1556class StartClearancePage(KofaPage):
1557    grok.context(IStudent)
1558    grok.name('start_clearance')
1559    grok.require('waeup.handleStudent')
1560    grok.template('enterpin')
1561    label = _('Start clearance')
1562    ac_prefix = 'CLR'
1563    notice = ''
1564    pnav = 4
1565    buttonname = _('Start clearance now')
1566
1567    @property
1568    def all_required_fields_filled(self):
1569        if self.context.email and self.context.phone:
1570            return True
1571        return False
1572
1573    @property
1574    def portrait_uploaded(self):
1575        store = getUtility(IExtFileStore)
1576        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1577            return True
1578        return False
1579
1580    def update(self, SUBMIT=None):
1581        if not self.context.state == ADMITTED:
1582            self.flash(_("Wrong state"))
1583            self.redirect(self.url(self.context))
1584            return
1585        if not self.portrait_uploaded:
1586            self.flash(_("No portrait uploaded."))
1587            self.redirect(self.url(self.context, 'change_portrait'))
1588            return
1589        if not self.all_required_fields_filled:
1590            self.flash(_("Not all required fields filled."))
1591            self.redirect(self.url(self.context, 'edit_base'))
1592            return
1593        self.ac_series = self.request.form.get('ac_series', None)
1594        self.ac_number = self.request.form.get('ac_number', None)
1595
1596        if SUBMIT is None:
1597            return
1598        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1599        code = get_access_code(pin)
1600        if not code:
1601            self.flash(_('Activation code is invalid.'))
1602            return
1603        # Mark pin as used (this also fires a pin related transition)
1604        # and fire transition start_clearance
1605        if code.state == USED:
1606            self.flash(_('Activation code has already been used.'))
1607            return
1608        else:
1609            comment = _(u"invalidated")
1610            # Here we know that the ac is in state initialized so we do not
1611            # expect an exception, but the owner might be different
1612            if not invalidate_accesscode(pin,comment,self.context.student_id):
1613                self.flash(_('You are not the owner of this access code.'))
1614                return
1615            self.context.clr_code = pin
1616        IWorkflowInfo(self.context).fireTransition('start_clearance')
1617        self.flash(_('Clearance process has been started.'))
1618        self.redirect(self.url(self.context,'cedit'))
1619        return
1620
1621class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1622    """ View to edit student clearance data by student
1623    """
1624    grok.context(IStudent)
1625    grok.name('cedit')
1626    grok.require('waeup.handleStudent')
1627    label = _('Edit clearance data')
1628
1629    @property
1630    def form_fields(self):
1631        cm = getattr(self.context,'current_mode', None)
1632        if cm is not None and cm.startswith('pg'):
1633            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
1634        else:
1635            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
1636        return form_fields
1637
1638    def update(self):
1639        if self.context.clearance_locked:
1640            emit_lock_message(self)
1641            return
1642        return super(StudentClearanceEditFormPage, self).update()
1643
1644    @action(_('Save'), style='primary')
1645    def save(self, **data):
1646        self.applyData(self.context, **data)
1647        self.flash(_('Clearance form has been saved.'))
1648        return
1649
1650    def dataNotComplete(self):
1651        """To be implemented in the customization package.
1652        """
1653        return False
1654
1655    @action(_('Save and request clearance'), style='primary')
1656    def requestClearance(self, **data):
1657        self.applyData(self.context, **data)
1658        if self.dataNotComplete():
1659            self.flash(self.dataNotComplete())
1660            return
1661        self.flash(_('Clearance form has been saved.'))
1662        self.redirect(self.url(self.context,'request_clearance'))
1663        return
1664
1665class RequestClearancePage(KofaPage):
1666    grok.context(IStudent)
1667    grok.name('request_clearance')
1668    grok.require('waeup.handleStudent')
1669    grok.template('enterpin')
1670    label = _('Request clearance')
1671    notice = _('Enter the CLR access code used for starting clearance.')
1672    ac_prefix = 'CLR'
1673    pnav = 4
1674    buttonname = _('Request clearance now')
1675
1676    def update(self, SUBMIT=None):
1677        self.ac_series = self.request.form.get('ac_series', None)
1678        self.ac_number = self.request.form.get('ac_number', None)
1679        if SUBMIT is None:
1680            return
1681        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1682        if self.context.clr_code != pin:
1683            self.flash(_("This isn't your CLR access code."))
1684            return
1685        state = IWorkflowState(self.context).getState()
1686        # This shouldn't happen, but the application officer
1687        # might have forgotten to lock the form after changing the state
1688        if state != CLEARANCE:
1689            self.flash(_('This form cannot be submitted. Wrong state!'))
1690            return
1691        IWorkflowInfo(self.context).fireTransition('request_clearance')
1692        self.flash(_('Clearance has been requested.'))
1693        self.redirect(self.url(self.context))
1694        return
1695
1696class StartCourseRegistrationPage(KofaPage):
1697    grok.context(IStudentStudyCourse)
1698    grok.name('start_course_registration')
1699    grok.require('waeup.handleStudent')
1700    grok.template('enterpin')
1701    label = _('Start course registration')
1702    ac_prefix = 'SFE'
1703    notice = ''
1704    pnav = 4
1705    buttonname = _('Start course registration now')
1706
1707    def update(self, SUBMIT=None):
1708        if not self.context.getStudent().state in (CLEARED,RETURNING):
1709            self.flash(_("Wrong state"))
1710            self.redirect(self.url(self.context))
1711            return
1712        self.ac_series = self.request.form.get('ac_series', None)
1713        self.ac_number = self.request.form.get('ac_number', None)
1714
1715        if SUBMIT is None:
1716            return
1717        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1718        code = get_access_code(pin)
1719        if not code:
1720            self.flash(_('Activation code is invalid.'))
1721            return
1722        # Mark pin as used (this also fires a pin related transition)
1723        # and fire transition start_clearance
1724        if code.state == USED:
1725            self.flash(_('Activation code has already been used.'))
1726            return
1727        else:
1728            comment = _(u"invalidated")
1729            # Here we know that the ac is in state initialized so we do not
1730            # expect an exception, but the owner might be different
1731            if not invalidate_accesscode(
1732                pin,comment,self.context.getStudent().student_id):
1733                self.flash(_('You are not the owner of this access code.'))
1734                return
1735        if self.context.getStudent().state == CLEARED:
1736            IWorkflowInfo(self.context.getStudent()).fireTransition(
1737                'pay_first_school_fee')
1738        elif self.context.getStudent().state == RETURNING:
1739            IWorkflowInfo(self.context.getStudent()).fireTransition(
1740                'pay_school_fee')
1741        self.flash(_('Course registration has been started.'))
1742        self.redirect(self.url(self.context))
1743        return
1744
1745class AddStudyLevelFormPage(KofaEditFormPage):
1746    """ Page for students to add current study levels
1747    """
1748    grok.context(IStudentStudyCourse)
1749    grok.name('add')
1750    grok.require('waeup.handleStudent')
1751    grok.template('studyleveladdpage')
1752    form_fields = grok.AutoFields(IStudentStudyCourse)
1753    pnav = 4
1754
1755    @property
1756    def label(self):
1757        studylevelsource = StudyLevelSource().factory
1758        code = self.context.current_level
1759        title = studylevelsource.getTitle(self.context, code)
1760        return _('Add current level ${a}', mapping = {'a':title})
1761
1762    def update(self):
1763        if self.context.getStudent().state != PAID:
1764            emit_lock_message(self)
1765            return
1766        super(AddStudyLevelFormPage, self).update()
1767        return
1768
1769    @action(_('Create course list now'), style='primary')
1770    def addStudyLevel(self, **data):
1771        studylevel = StudentStudyLevel()
1772        studylevel.level = self.context.current_level
1773        studylevel.level_session = self.context.current_session
1774        try:
1775            self.context.addStudentStudyLevel(
1776                self.context.certificate,studylevel)
1777        except KeyError:
1778            self.flash(_('This level exists.'))
1779        self.redirect(self.url(self.context))
1780        return
1781
1782class StudyLevelEditFormPage(KofaEditFormPage):
1783    """ Page to edit the student study level data by students
1784    """
1785    grok.context(IStudentStudyLevel)
1786    grok.name('edit')
1787    grok.require('waeup.handleStudent')
1788    grok.template('studyleveleditpage')
1789    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1790        'level_session', 'level_verdict')
1791    pnav = 4
1792
1793    def update(self):
1794        if self.context.getStudent().state != PAID:
1795            emit_lock_message(self)
1796            return
1797        super(StudyLevelEditFormPage, self).update()
1798        datatable.need()
1799        warning.need()
1800        return
1801
1802    @property
1803    def label(self):
1804        # Here we know that the cookie has been set
1805        lang = self.request.cookies.get('kofa.language')
1806        level_title = translate(self.context.level_title, 'waeup.kofa',
1807            target_language=lang)
1808        return _('Add and remove course tickets of study level ${a}',
1809            mapping = {'a':level_title})
1810
1811    @property
1812    def total_credits(self):
1813        total_credits = 0
1814        for key, val in self.context.items():
1815            total_credits += val.credits
1816        return total_credits
1817
1818    @action(_('Add course ticket'))
1819    def addCourseTicket(self, **data):
1820        self.redirect(self.url(self.context, 'ctadd'))
1821
1822    @jsaction(_('Remove selected tickets'))
1823    def delCourseTicket(self, **data):
1824        form = self.request.form
1825        if form.has_key('val_id'):
1826            child_id = form['val_id']
1827        else:
1828            self.flash(_('No ticket selected.'))
1829            self.redirect(self.url(self.context, '@@edit'))
1830            return
1831        if not isinstance(child_id, list):
1832            child_id = [child_id]
1833        deleted = []
1834        for id in child_id:
1835            # Students are not allowed to remove core tickets
1836            if not self.context[id].mandatory:
1837                del self.context[id]
1838                deleted.append(id)
1839        if len(deleted):
1840            self.flash(_('Successfully removed: ${a}',
1841                mapping = {'a':', '.join(deleted)}))
1842        self.redirect(self.url(self.context, u'@@edit'))
1843        return
1844
1845    @action(_('Register course list'), style='primary')
1846    def RegisterCourses(self, **data):
1847        IWorkflowInfo(self.context.getStudent()).fireTransition(
1848            'register_courses')
1849        self.flash(_('Course list has been registered.'))
1850        self.redirect(self.url(self.context))
1851        return
1852
1853class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1854    """Add a course ticket by student.
1855    """
1856    grok.name('ctadd')
1857    grok.require('waeup.handleStudent')
1858    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1859        'score', 'mandatory', 'automatic', 'carry_over')
1860
1861    def update(self):
1862        if self.context.getStudent().state != PAID:
1863            emit_lock_message(self)
1864            return
1865        super(CourseTicketAddFormPage2, self).update()
1866        return
1867
1868    @action(_('Add course ticket'))
1869    def addCourseTicket(self, **data):
1870        # Safety belt
1871        if self.context.getStudent().state != PAID:
1872            return
1873        ticket = CourseTicket()
1874        course = data['course']
1875        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1876            setattr(ticket, name, getattr(course, name))
1877        ticket.automatic = False
1878        try:
1879            self.context.addCourseTicket(ticket)
1880        except KeyError:
1881            self.flash(_('The ticket exists.'))
1882            return
1883        self.flash(_('Successfully added ${a}.',
1884            mapping = {'a':ticket.code}))
1885        self.redirect(self.url(self.context, u'@@edit'))
1886        return
1887
1888class ChangePasswordRequestPage(KofaForm):
1889    """Captcha'd page for students to request a password change.
1890    """
1891    grok.context(IUniversity)
1892    grok.name('sendpw')
1893    grok.require('waeup.Anonymous')
1894    grok.template('sendpassword')
1895    label = _('Send me a new password')
1896    form_fields = grok.AutoFields(IStudentChangePassword)
1897
1898    def update(self):
1899        # Handle captcha
1900        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1901        self.captcha_result = self.captcha.verify(self.request)
1902        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1903        return
1904
1905    @action(_('Get new login credentials'), style='primary')
1906    def request(self, **data):
1907        if not self.captcha_result.is_valid:
1908            # Captcha will display error messages automatically.
1909            # No need to flash something.
1910            return
1911        # Search student or applicant
1912        reg_number = data['reg_number']
1913        email = data['email']
1914        cat = queryUtility(ICatalog, name='students_catalog')
1915        results = cat.searchResults(
1916            reg_number=(reg_number, reg_number),
1917            email=(email,email))
1918        if len(results) == 0:
1919            # Try also the applicants_catalog if no student record was found.
1920            cat = queryUtility(ICatalog, name='applicants_catalog')
1921            if cat is None:
1922                self.flash(_('Application package not installed.'))
1923                return
1924            results = cat.searchResults(
1925                reg_number=(reg_number, reg_number),
1926                email=(email,email))
1927        if len(results) == 0:
1928            self.flash(_('No record found.'))
1929            return
1930        student = list(results)[0]
1931        # Change password
1932        kofa_utils = getUtility(IKofaUtils)
1933        pwd = kofa_utils.genPassword()
1934        IUserAccount(student).setPassword(pwd)
1935        # Send email with new redentials
1936        msg = _('You have successfully changed your password for the')
1937        login_url = self.url(grok.getSite(), 'login')
1938        success = kofa_utils.sendCredentials(
1939            IUserAccount(student),pwd,login_url,msg)
1940        if success:
1941            self.flash(_('An email with your user name and password ' +
1942                'has been sent to ${a}.', mapping = {'a':email}))
1943        else:
1944            self.flash(_('An smtp server error occurred.'))
1945        return
1946
1947class SetPasswordPage(KofaPage):
1948    grok.context(IKofaObject)
1949    grok.name('setpassword')
1950    grok.require('waeup.Anonymous')
1951    grok.template('setpassword')
1952    label = _('Set password for first-time login')
1953    ac_prefix = 'PWD'
1954    pnav = 0
1955    set_button = _('Set')
1956
1957    def update(self, SUBMIT=None):
1958        self.reg_number = self.request.form.get('reg_number', None)
1959        self.ac_series = self.request.form.get('ac_series', None)
1960        self.ac_number = self.request.form.get('ac_number', None)
1961
1962        if SUBMIT is None:
1963            return
1964        hitlist = search(query=self.reg_number,
1965            searchtype='reg_number', view=self)
1966        if not hitlist:
1967            self.flash(_('No student found.'))
1968            return
1969        if len(hitlist) != 1:   # Cannot happen but anyway
1970            self.flash(_('More than one student found.'))
1971            return
1972        student = hitlist[0].context
1973        self.student_id = student.student_id
1974        student_pw = student.password
1975        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1976        code = get_access_code(pin)
1977        if not code:
1978            self.flash(_('Access code is invalid.'))
1979            return
1980        if student_pw and pin == student.adm_code:
1981            self.flash(_(
1982                'Password has already been set. Your Student Id is ${a}',
1983                mapping = {'a':self.student_id}))
1984            return
1985        elif student_pw:
1986            self.flash(
1987                _('Password has already been set. You are using the ' +
1988                'wrong Access Code.'))
1989            return
1990        # Mark pin as used (this also fires a pin related transition)
1991        # and set student password
1992        if code.state == USED:
1993            self.flash(_('Access code has already been used.'))
1994            return
1995        else:
1996            comment = _(u"invalidated")
1997            # Here we know that the ac is in state initialized so we do not
1998            # expect an exception
1999            invalidate_accesscode(pin,comment)
2000            IUserAccount(student).setPassword(self.ac_number)
2001            student.adm_code = pin
2002        self.flash(_('Password has been set. Your Student Id is ${a}',
2003            mapping = {'a':self.student_id}))
2004        return
Note: See TracBrowser for help on using the repository browser.