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

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

Implement a ChangePasswordRequestPage? for all portal users (more tests will follow).

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