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

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

Do not override the objects dict values.

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