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

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

Protect StudyLevelEditFormPage? and CourseTicketAddFormPage2. Students are not allowed to edit study levels which are not current.

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