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

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

Payment methods do now all return a success flag, a flash message string and a log string. All these adjustments are necessary for waeup.uniben.

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