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

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

Workaround to avoid RuntimeErrors?.

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