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

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

Implement max_credits limit for course registration.

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