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

Last change on this file since 8481 was 8481, checked in by Henrik Bettermann, 12 years ago

Clean up code.

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