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

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

Use different msave method in students and in university.

Simplify logging. We don't need the logger_info method.

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