source: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser.py @ 10009

Last change on this file since 10009 was 9208, checked in by uli, 12 years ago

Merge changes from trunk r9171:9207.

  • Property svn:keywords set to Id
File size: 80.3 KB
Line 
1## $Id: browser.py 9208 2012-09-20 08:22:52Z uli $
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        try:
810            self.context.addStudentStudyLevel(
811                self.context.certificate,studylevel)
812            self.flash(_('Study level has been added.'))
813        except KeyError:
814            self.flash(_('This level exists.'))
815        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
816        return
817
818    @jsaction(_('Remove selected levels'))
819    def delStudyLevels(self, **data):
820        form = self.request.form
821        if form.has_key('val_id'):
822            child_id = form['val_id']
823        else:
824            self.flash(_('No study level selected.'))
825            self.redirect(self.url(self.context, '@@manage')+'?tab2')
826            return
827        if not isinstance(child_id, list):
828            child_id = [child_id]
829        deleted = []
830        for id in child_id:
831            del self.context[id]
832            deleted.append(id)
833        if len(deleted):
834            self.flash(_('Successfully removed: ${a}',
835                mapping = {'a':', '.join(deleted)}))
836        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
837        return
838
839class StudentTransferFormPage(KofaAddFormPage):
840    """Page to transfer the student.
841    """
842    grok.context(IStudent)
843    grok.name('transfer')
844    grok.require('waeup.manageStudent')
845    label = _('Transfer student')
846    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
847        'entry_mode', 'entry_session')
848    pnav = 4
849
850    def update(self):
851        super(StudentTransferFormPage, self).update()
852        warning.need()
853        return
854
855    @jsaction(_('Transfer'))
856    def transferStudent(self, **data):
857        error = self.context.transfer(**data)
858        if error == -1:
859            self.flash(_('Current level does not match certificate levels.'))
860        elif error == -2:
861            self.flash(_('Former study course record incomplete.'))
862        elif error == -3:
863            self.flash(_('Maximum number of transfers exceeded.'))
864        else:
865            self.flash(_('Successfully transferred.'))
866        return
867
868class StudyLevelDisplayFormPage(KofaDisplayFormPage):
869    """ Page to display student study levels
870    """
871    grok.context(IStudentStudyLevel)
872    grok.name('index')
873    grok.require('waeup.viewStudent')
874    form_fields = grok.AutoFields(IStudentStudyLevel)
875    form_fields[
876        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
877    grok.template('studylevelpage')
878    pnav = 4
879
880    def update(self):
881        super(StudyLevelDisplayFormPage, self).update()
882        datatable.need()
883        return
884
885    @property
886    def translated_values(self):
887        return translated_values(self)
888
889    @property
890    def label(self):
891        # Here we know that the cookie has been set
892        lang = self.request.cookies.get('kofa.language')
893        level_title = translate(self.context.level_title, 'waeup.kofa',
894            target_language=lang)
895        return _('${a}: Study Level ${b}', mapping = {
896            'a':self.context.student.display_fullname,
897            'b':level_title})
898
899    @property
900    def total_credits(self):
901        total_credits = 0
902        for key, val in self.context.items():
903            total_credits += val.credits
904        return total_credits
905
906class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
907    """Deliver a PDF slip of the context.
908    """
909    grok.context(IStudentStudyLevel)
910    grok.name('course_registration.pdf')
911    grok.require('waeup.viewStudent')
912    form_fields = grok.AutoFields(IStudentStudyLevel)
913    prefix = 'form'
914
915    @property
916    def title(self):
917        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
918        return translate(_('Level Data'), 'waeup.kofa',
919            target_language=portal_language)
920
921    @property
922    def content_title(self):
923        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
924        return translate(_('Course List'), 'waeup.kofa',
925            target_language=portal_language)
926
927    @property
928    def label(self):
929        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
930        lang = self.request.cookies.get('kofa.language', portal_language)
931        level_title = translate(self.context.level_title, 'waeup.kofa',
932            target_language=lang)
933        return translate(_('Course Registration Slip'),
934            'waeup.kofa', target_language=portal_language) \
935            + ' %s' % level_title
936
937    def render(self):
938        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
939        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
940        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
941        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
942        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
943        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
944        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
945        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
946        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
947        studentview = StudentBasePDFFormPage(self.context.student,
948            self.request)
949        students_utils = getUtility(IStudentsUtils)
950        tabledata = sorted(self.context.values(),
951            key=lambda value: str(value.semester) + value.code)
952        return students_utils.renderPDF(
953            self, 'course_registration.pdf',
954            self.context.student, studentview,
955            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
956                         (Title,'title', 5),
957                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
958                         (Cred, 'credits', 1.5),
959                         (Mand, 'mandatory', 1.5),
960                         (Score, 'score', 1.5),
961                         #('Auto', 'automatic', 1.5)
962                         ],
963            tabledata=tabledata)
964
965class StudyLevelManageFormPage(KofaEditFormPage):
966    """ Page to edit the student study level data
967    """
968    grok.context(IStudentStudyLevel)
969    grok.name('manage')
970    grok.require('waeup.manageStudent')
971    grok.template('studylevelmanagepage')
972    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
973        'validation_date', 'validated_by')
974    pnav = 4
975    taboneactions = [_('Save'),_('Cancel')]
976    tabtwoactions = [_('Add course ticket'),
977        _('Remove selected tickets'),_('Cancel')]
978
979    def update(self):
980        if not self.context.__parent__.is_current:
981            emit_lock_message(self)
982            return
983        super(StudyLevelManageFormPage, self).update()
984        tabs.need()
985        self.tab1 = self.tab2 = ''
986        qs = self.request.get('QUERY_STRING', '')
987        if not qs:
988            qs = 'tab1'
989        setattr(self, qs, 'active')
990        warning.need()
991        datatable.need()
992        return
993
994    @property
995    def translated_values(self):
996        return translated_values(self)
997
998    @property
999    def label(self):
1000        # Here we know that the cookie has been set
1001        lang = self.request.cookies.get('kofa.language')
1002        level_title = translate(self.context.level_title, 'waeup.kofa',
1003            target_language=lang)
1004        return _('Manage study level ${a}',
1005            mapping = {'a':level_title})
1006
1007    @action(_('Save'), style='primary')
1008    def save(self, **data):
1009        msave(self, **data)
1010        return
1011
1012    @action(_('Add course ticket'))
1013    def addCourseTicket(self, **data):
1014        self.redirect(self.url(self.context, '@@add'))
1015
1016    @jsaction(_('Remove selected tickets'))
1017    def delCourseTicket(self, **data):
1018        form = self.request.form
1019        if form.has_key('val_id'):
1020            child_id = form['val_id']
1021        else:
1022            self.flash(_('No ticket selected.'))
1023            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1024            return
1025        if not isinstance(child_id, list):
1026            child_id = [child_id]
1027        deleted = []
1028        for id in child_id:
1029            del self.context[id]
1030            deleted.append(id)
1031        if len(deleted):
1032            self.flash(_('Successfully removed: ${a}',
1033                mapping = {'a':', '.join(deleted)}))
1034        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1035        return
1036
1037class ValidateCoursesPage(UtilityView, grok.View):
1038    """ Validate course list by course adviser
1039    """
1040    grok.context(IStudentStudyLevel)
1041    grok.name('validate_courses')
1042    grok.require('waeup.validateStudent')
1043
1044    def update(self):
1045        if not self.context.__parent__.is_current:
1046            emit_lock_message(self)
1047            return
1048        if str(self.context.__parent__.current_level) != self.context.__name__:
1049            self.flash(_('This level does not correspond current level.'))
1050        elif self.context.student.state == REGISTERED:
1051            IWorkflowInfo(self.context.student).fireTransition(
1052                'validate_courses')
1053            self.flash(_('Course list has been validated.'))
1054        else:
1055            self.flash(_('Student is in the wrong state.'))
1056        self.redirect(self.url(self.context))
1057        return
1058
1059    def render(self):
1060        return
1061
1062class RejectCoursesPage(UtilityView, grok.View):
1063    """ Reject course list by course adviser
1064    """
1065    grok.context(IStudentStudyLevel)
1066    grok.name('reject_courses')
1067    grok.require('waeup.validateStudent')
1068
1069    def update(self):
1070        if not self.context.__parent__.is_current:
1071            emit_lock_message(self)
1072            return
1073        if str(self.context.__parent__.current_level) != self.context.__name__:
1074            self.flash(_('This level does not correspond current level.'))
1075            self.redirect(self.url(self.context))
1076            return
1077        elif self.context.student.state == VALIDATED:
1078            IWorkflowInfo(self.context.student).fireTransition('reset8')
1079            message = _('Course list request has been annulled.')
1080            self.flash(message)
1081        elif self.context.student.state == REGISTERED:
1082            IWorkflowInfo(self.context.student).fireTransition('reset7')
1083            message = _('Course list request has been rejected:')
1084            self.flash(message)
1085        else:
1086            self.flash(_('Student is in the wrong state.'))
1087            self.redirect(self.url(self.context))
1088            return
1089        args = {'subject':message}
1090        self.redirect(self.url(self.context.student) +
1091            '/contactstudent?%s' % urlencode(args))
1092        return
1093
1094    def render(self):
1095        return
1096
1097class CourseTicketAddFormPage(KofaAddFormPage):
1098    """Add a course ticket.
1099    """
1100    grok.context(IStudentStudyLevel)
1101    grok.name('add')
1102    grok.require('waeup.manageStudent')
1103    label = _('Add course ticket')
1104    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1105        'score', 'automatic', 'carry_over')
1106    pnav = 4
1107
1108    def update(self):
1109        if not self.context.__parent__.is_current:
1110            emit_lock_message(self)
1111            return
1112        super(CourseTicketAddFormPage, self).update()
1113        return
1114
1115    @action(_('Add course ticket'))
1116    def addCourseTicket(self, **data):
1117        ticket = createObject(u'waeup.CourseTicket')
1118        course = data['course']
1119        ticket.automatic = False
1120        ticket.carry_over = False
1121        try:
1122            self.context.addCourseTicket(ticket, course)
1123        except KeyError:
1124            self.flash(_('The ticket exists.'))
1125            return
1126        self.flash(_('Successfully added ${a}.',
1127            mapping = {'a':ticket.code}))
1128        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1129        return
1130
1131    @action(_('Cancel'), validator=NullValidator)
1132    def cancel(self, **data):
1133        self.redirect(self.url(self.context))
1134
1135class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1136    """ Page to display course tickets
1137    """
1138    grok.context(ICourseTicket)
1139    grok.name('index')
1140    grok.require('waeup.viewStudent')
1141    form_fields = grok.AutoFields(ICourseTicket)
1142    grok.template('courseticketpage')
1143    pnav = 4
1144
1145    @property
1146    def label(self):
1147        return _('${a}: Course Ticket ${b}', mapping = {
1148            'a':self.context.student.display_fullname,
1149            'b':self.context.code})
1150
1151class CourseTicketManageFormPage(KofaEditFormPage):
1152    """ Page to manage course tickets
1153    """
1154    grok.context(ICourseTicket)
1155    grok.name('manage')
1156    grok.require('waeup.manageStudent')
1157    form_fields = grok.AutoFields(ICourseTicket)
1158    grok.template('courseticketmanagepage')
1159    pnav = 4
1160
1161    @property
1162    def label(self):
1163        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1164
1165    @action('Save', style='primary')
1166    def save(self, **data):
1167        msave(self, **data)
1168        return
1169
1170class PaymentsManageFormPage(KofaEditFormPage):
1171    """ Page to manage the student payments
1172
1173    This manage form page is for both students and students officers.
1174    """
1175    grok.context(IStudentPaymentsContainer)
1176    grok.name('index')
1177    grok.require('waeup.payStudent')
1178    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1179    grok.template('paymentsmanagepage')
1180    pnav = 4
1181
1182    def unremovable(self, ticket):
1183        usertype = getattr(self.request.principal, 'user_type', None)
1184        if not usertype:
1185            return False
1186        return (self.request.principal.user_type == 'student' and ticket.r_code)
1187
1188    @property
1189    def label(self):
1190        return _('${a}: Payments',
1191            mapping = {'a':self.context.__parent__.display_fullname})
1192
1193    def update(self):
1194        super(PaymentsManageFormPage, self).update()
1195        datatable.need()
1196        warning.need()
1197        return
1198
1199    @jsaction(_('Remove selected tickets'))
1200    def delPaymentTicket(self, **data):
1201        form = self.request.form
1202        if form.has_key('val_id'):
1203            child_id = form['val_id']
1204        else:
1205            self.flash(_('No payment selected.'))
1206            self.redirect(self.url(self.context))
1207            return
1208        if not isinstance(child_id, list):
1209            child_id = [child_id]
1210        deleted = []
1211        for id in child_id:
1212            # Students are not allowed to remove used payment tickets
1213            if not self.unremovable(self.context[id]):
1214                del self.context[id]
1215                deleted.append(id)
1216        if len(deleted):
1217            self.flash(_('Successfully removed: ${a}',
1218                mapping = {'a': ', '.join(deleted)}))
1219            self.context.writeLogMessage(
1220                self,'removed: %s' % ', '.join(deleted))
1221        self.redirect(self.url(self.context))
1222        return
1223
1224    @action(_('Add online payment ticket'))
1225    def addPaymentTicket(self, **data):
1226        self.redirect(self.url(self.context, '@@addop'))
1227
1228class OnlinePaymentAddFormPage(KofaAddFormPage):
1229    """ Page to add an online payment ticket
1230    """
1231    grok.context(IStudentPaymentsContainer)
1232    grok.name('addop')
1233    grok.require('waeup.payStudent')
1234    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1235        'p_category')
1236    label = _('Add online payment')
1237    pnav = 4
1238
1239    @action(_('Create ticket'), style='primary')
1240    def createTicket(self, **data):
1241        p_category = data['p_category']
1242        previous_session = data.get('p_session', None)
1243        previous_level = data.get('p_level', None)
1244        student = self.context.__parent__
1245        if p_category == 'bed_allocation' and student[
1246            'studycourse'].current_session != grok.getSite()[
1247            'hostels'].accommodation_session:
1248                self.flash(
1249                    _('Your current session does not match ' + \
1250                    'accommodation session.'))
1251                self.redirect(self.url(self.context))
1252                return
1253        students_utils = getUtility(IStudentsUtils)
1254        error, payment = students_utils.setPaymentDetails(
1255            p_category, student, previous_session, previous_level)
1256        if error is not None:
1257            self.flash(error)
1258            if 'previous session' in error:
1259                self.redirect(self.url(self.context) + '/@@addpp')
1260                return
1261            self.redirect(self.url(self.context))
1262            return
1263        self.context[payment.p_id] = payment
1264        self.flash(_('Payment ticket created.'))
1265        self.redirect(self.url(self.context))
1266        return
1267
1268class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1269    """ Page to add an online payment ticket for previous sessions
1270    """
1271    grok.context(IStudentPaymentsContainer)
1272    grok.name('addpp')
1273    grok.require('waeup.payStudent')
1274    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1275        'p_category', 'p_session', 'p_level')
1276    label = _('Add previous session online payment')
1277    pnav = 4
1278
1279class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1280    """ Page to view an online payment ticket
1281    """
1282    grok.context(IStudentOnlinePayment)
1283    grok.name('index')
1284    grok.require('waeup.viewStudent')
1285    form_fields = grok.AutoFields(IStudentOnlinePayment)
1286    form_fields[
1287        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1288    form_fields[
1289        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1290    pnav = 4
1291
1292    @property
1293    def label(self):
1294        return _('${a}: Online Payment Ticket ${b}', mapping = {
1295            'a':self.context.student.display_fullname,
1296            'b':self.context.p_id})
1297
1298class OnlinePaymentApprovePage(UtilityView, grok.View):
1299    """ Callback view
1300    """
1301    grok.context(IStudentOnlinePayment)
1302    grok.name('approve')
1303    grok.require('waeup.managePortal')
1304
1305    def update(self):
1306        success, msg, log = self.context.approveStudentPayment()
1307        if log is not None:
1308            self.context.writeLogMessage(self,log)
1309        self.flash(msg)
1310        return
1311
1312    def render(self):
1313        self.redirect(self.url(self.context, '@@index'))
1314        return
1315
1316class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1317    """ Approval view for students.
1318
1319    This view is used for browser tests only and
1320    must be neutralized in custom pages!
1321    """
1322
1323    grok.name('fake_approve')
1324    grok.require('waeup.payStudent')
1325
1326class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1327    """Deliver a PDF slip of the context.
1328    """
1329    grok.context(IStudentOnlinePayment)
1330    grok.name('payment_slip.pdf')
1331    grok.require('waeup.viewStudent')
1332    form_fields = grok.AutoFields(IStudentOnlinePayment)
1333    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1334    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1335    prefix = 'form'
1336    note = None
1337
1338    @property
1339    def title(self):
1340        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1341        return translate(_('Payment Data'), 'waeup.kofa',
1342            target_language=portal_language)
1343
1344    @property
1345    def label(self):
1346        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1347        return translate(_('Online Payment Slip'),
1348            'waeup.kofa', target_language=portal_language) \
1349            + ' %s' % self.context.p_id
1350
1351    def render(self):
1352        #if self.context.p_state != 'paid':
1353        #    self.flash('Ticket not yet paid.')
1354        #    self.redirect(self.url(self.context))
1355        #    return
1356        studentview = StudentBasePDFFormPage(self.context.student,
1357            self.request)
1358        students_utils = getUtility(IStudentsUtils)
1359        return students_utils.renderPDF(self, 'payment_slip.pdf',
1360            self.context.student, studentview, note=self.note)
1361
1362
1363class AccommodationManageFormPage(KofaEditFormPage):
1364    """ Page to manage bed tickets.
1365
1366    This manage form page is for both students and students officers.
1367    """
1368    grok.context(IStudentAccommodation)
1369    grok.name('index')
1370    grok.require('waeup.handleAccommodation')
1371    form_fields = grok.AutoFields(IStudentAccommodation)
1372    grok.template('accommodationmanagepage')
1373    pnav = 4
1374    officers_only_actions = [_('Remove selected')]
1375
1376    @property
1377    def label(self):
1378        return _('${a}: Accommodation',
1379            mapping = {'a':self.context.__parent__.display_fullname})
1380
1381    def update(self):
1382        super(AccommodationManageFormPage, self).update()
1383        datatable.need()
1384        warning.need()
1385        return
1386
1387    @jsaction(_('Remove selected'))
1388    def delBedTickets(self, **data):
1389        if getattr(self.request.principal, 'user_type', None) == 'student':
1390            self.flash(_('You are not allowed to remove bed tickets.'))
1391            self.redirect(self.url(self.context))
1392            return
1393        form = self.request.form
1394        if form.has_key('val_id'):
1395            child_id = form['val_id']
1396        else:
1397            self.flash(_('No bed ticket selected.'))
1398            self.redirect(self.url(self.context))
1399            return
1400        if not isinstance(child_id, list):
1401            child_id = [child_id]
1402        deleted = []
1403        for id in child_id:
1404            del self.context[id]
1405            deleted.append(id)
1406        if len(deleted):
1407            self.flash(_('Successfully removed: ${a}',
1408                mapping = {'a':', '.join(deleted)}))
1409            self.context.writeLogMessage(
1410                self,'removed: % s' % ', '.join(deleted))
1411        self.redirect(self.url(self.context))
1412        return
1413
1414    @property
1415    def selected_actions(self):
1416        if getattr(self.request.principal, 'user_type', None) == 'student':
1417            return [action for action in self.actions
1418                    if not action.label in self.officers_only_actions]
1419        return self.actions
1420
1421class BedTicketAddPage(KofaPage):
1422    """ Page to add an online payment ticket
1423    """
1424    grok.context(IStudentAccommodation)
1425    grok.name('add')
1426    grok.require('waeup.handleAccommodation')
1427    grok.template('enterpin')
1428    ac_prefix = 'HOS'
1429    label = _('Add bed ticket')
1430    pnav = 4
1431    buttonname = _('Create bed ticket')
1432    notice = ''
1433    with_ac = True
1434
1435    def update(self, SUBMIT=None):
1436        student = self.context.student
1437        students_utils = getUtility(IStudentsUtils)
1438        acc_details  = students_utils.getAccommodationDetails(student)
1439        if acc_details.get('expired', False):
1440            startdate = acc_details.get('startdate')
1441            enddate = acc_details.get('enddate')
1442            if startdate and enddate:
1443                tz = getUtility(IKofaUtils).tzinfo
1444                startdate = to_timezone(
1445                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1446                enddate = to_timezone(
1447                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1448                self.flash(_("Outside booking period: ${a} - ${b}",
1449                    mapping = {'a': startdate, 'b': enddate}))
1450            else:
1451                self.flash(_("Outside booking period."))
1452            self.redirect(self.url(self.context))
1453            return
1454        if not acc_details:
1455            self.flash(_("Your data are incomplete."))
1456            self.redirect(self.url(self.context))
1457            return
1458        if not student.state in acc_details['allowed_states']:
1459            self.flash(_("You are in the wrong registration state."))
1460            self.redirect(self.url(self.context))
1461            return
1462        if student['studycourse'].current_session != acc_details[
1463            'booking_session']:
1464            self.flash(
1465                _('Your current session does not match accommodation session.'))
1466            self.redirect(self.url(self.context))
1467            return
1468        if str(acc_details['booking_session']) in self.context.keys():
1469            self.flash(
1470                _('You already booked a bed space in current ' \
1471                    + 'accommodation session.'))
1472            self.redirect(self.url(self.context))
1473            return
1474        if self.with_ac:
1475            self.ac_series = self.request.form.get('ac_series', None)
1476            self.ac_number = self.request.form.get('ac_number', None)
1477        if SUBMIT is None:
1478            return
1479        if self.with_ac:
1480            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1481            code = get_access_code(pin)
1482            if not code:
1483                self.flash(_('Activation code is invalid.'))
1484                return
1485        # Search and book bed
1486        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1487        entries = cat.searchResults(
1488            owner=(student.student_id,student.student_id))
1489        if len(entries):
1490            # If bed space has been manually allocated use this bed
1491            bed = [entry for entry in entries][0]
1492        else:
1493            # else search for other available beds
1494            entries = cat.searchResults(
1495                bed_type=(acc_details['bt'],acc_details['bt']))
1496            available_beds = [
1497                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1498            if available_beds:
1499                students_utils = getUtility(IStudentsUtils)
1500                bed = students_utils.selectBed(available_beds)
1501                bed.bookBed(student.student_id)
1502            else:
1503                self.flash(_('There is no free bed in your category ${a}.',
1504                    mapping = {'a':acc_details['bt']}))
1505                return
1506        if self.with_ac:
1507            # Mark pin as used (this also fires a pin related transition)
1508            if code.state == USED:
1509                self.flash(_('Activation code has already been used.'))
1510                return
1511            else:
1512                comment = _(u'invalidated')
1513                # Here we know that the ac is in state initialized so we do not
1514                # expect an exception, but the owner might be different
1515                if not invalidate_accesscode(
1516                    pin,comment,self.context.student.student_id):
1517                    self.flash(_('You are not the owner of this access code.'))
1518                    return
1519        # Create bed ticket
1520        bedticket = createObject(u'waeup.BedTicket')
1521        if self.with_ac:
1522            bedticket.booking_code = pin
1523        bedticket.booking_session = acc_details['booking_session']
1524        bedticket.bed_type = acc_details['bt']
1525        bedticket.bed = bed
1526        hall_title = bed.__parent__.hostel_name
1527        coordinates = bed.coordinates[1:]
1528        block, room_nr, bed_nr = coordinates
1529        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1530            'a':hall_title, 'b':block,
1531            'c':room_nr, 'd':bed_nr,
1532            'e':bed.bed_type})
1533        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1534        bedticket.bed_coordinates = translate(
1535            bc, 'waeup.kofa',target_language=portal_language)
1536        key = str(acc_details['booking_session'])
1537        self.context[key] = bedticket
1538        self.flash(_('Bed ticket created and bed booked: ${a}',
1539            mapping = {'a':bedticket.bed_coordinates}))
1540        self.redirect(self.url(self.context))
1541        return
1542
1543class BedTicketDisplayFormPage(KofaDisplayFormPage):
1544    """ Page to display bed tickets
1545    """
1546    grok.context(IBedTicket)
1547    grok.name('index')
1548    grok.require('waeup.handleAccommodation')
1549    form_fields = grok.AutoFields(IBedTicket)
1550    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1551    pnav = 4
1552
1553    @property
1554    def label(self):
1555        return _('Bed Ticket for Session ${a}',
1556            mapping = {'a':self.context.getSessionString()})
1557
1558class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1559    """Deliver a PDF slip of the context.
1560    """
1561    grok.context(IBedTicket)
1562    grok.name('bed_allocation.pdf')
1563    grok.require('waeup.handleAccommodation')
1564    form_fields = grok.AutoFields(IBedTicket)
1565    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1566    prefix = 'form'
1567
1568    @property
1569    def title(self):
1570        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1571        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1572            target_language=portal_language)
1573
1574    @property
1575    def label(self):
1576        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1577        #return translate(_('Bed Allocation: '),
1578        #    'waeup.kofa', target_language=portal_language) \
1579        #    + ' %s' % self.context.bed_coordinates
1580        return translate(_('Bed Allocation Slip'),
1581            'waeup.kofa', target_language=portal_language) \
1582            + ' %s' % self.context.getSessionString()
1583
1584    def render(self):
1585        studentview = StudentBasePDFFormPage(self.context.student,
1586            self.request)
1587        students_utils = getUtility(IStudentsUtils)
1588        return students_utils.renderPDF(
1589            self, 'bed_allocation.pdf',
1590            self.context.student, studentview)
1591
1592class BedTicketRelocationPage(UtilityView, grok.View):
1593    """ Callback view
1594    """
1595    grok.context(IBedTicket)
1596    grok.name('relocate')
1597    grok.require('waeup.manageHostels')
1598
1599    # Relocate student if student parameters have changed or the bed_type
1600    # of the bed has changed
1601    def update(self):
1602        student = self.context.student
1603        students_utils = getUtility(IStudentsUtils)
1604        acc_details  = students_utils.getAccommodationDetails(student)
1605        if self.context.bed != None and \
1606              'reserved' in self.context.bed.bed_type:
1607            self.flash(_("Students in reserved beds can't be relocated."))
1608            self.redirect(self.url(self.context))
1609            return
1610        if acc_details['bt'] == self.context.bed_type and \
1611                self.context.bed != None and \
1612                self.context.bed.bed_type == self.context.bed_type:
1613            self.flash(_("Student can't be relocated."))
1614            self.redirect(self.url(self.context))
1615            return
1616        # Search a bed
1617        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1618        entries = cat.searchResults(
1619            owner=(student.student_id,student.student_id))
1620        if len(entries) and self.context.bed == None:
1621            # If booking has been cancelled but other bed space has been
1622            # manually allocated after cancellation use this bed
1623            new_bed = [entry for entry in entries][0]
1624        else:
1625            # Search for other available beds
1626            entries = cat.searchResults(
1627                bed_type=(acc_details['bt'],acc_details['bt']))
1628            available_beds = [
1629                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1630            if available_beds:
1631                students_utils = getUtility(IStudentsUtils)
1632                new_bed = students_utils.selectBed(available_beds)
1633                new_bed.bookBed(student.student_id)
1634            else:
1635                self.flash(_('There is no free bed in your category ${a}.',
1636                    mapping = {'a':acc_details['bt']}))
1637                self.redirect(self.url(self.context))
1638                return
1639        # Release old bed if exists
1640        if self.context.bed != None:
1641            self.context.bed.owner = NOT_OCCUPIED
1642            notify(grok.ObjectModifiedEvent(self.context.bed))
1643        # Alocate new bed
1644        self.context.bed_type = acc_details['bt']
1645        self.context.bed = new_bed
1646        hall_title = new_bed.__parent__.hostel_name
1647        coordinates = new_bed.coordinates[1:]
1648        block, room_nr, bed_nr = coordinates
1649        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1650            'a':hall_title, 'b':block,
1651            'c':room_nr, 'd':bed_nr,
1652            'e':new_bed.bed_type})
1653        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1654        self.context.bed_coordinates = translate(
1655            bc, 'waeup.kofa',target_language=portal_language)
1656        self.flash(_('Student relocated: ${a}',
1657            mapping = {'a':self.context.bed_coordinates}))
1658        self.redirect(self.url(self.context))
1659        return
1660
1661    def render(self):
1662        return
1663
1664class StudentHistoryPage(KofaPage):
1665    """ Page to display student clearance data
1666    """
1667    grok.context(IStudent)
1668    grok.name('history')
1669    grok.require('waeup.viewStudent')
1670    grok.template('studenthistory')
1671    pnav = 4
1672
1673    @property
1674    def label(self):
1675        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1676
1677# Pages for students only
1678
1679class StudentBaseEditFormPage(KofaEditFormPage):
1680    """ View to edit student base data
1681    """
1682    grok.context(IStudent)
1683    grok.name('edit_base')
1684    grok.require('waeup.handleStudent')
1685    form_fields = grok.AutoFields(IStudentBase).select(
1686        'email', 'phone')
1687    label = _('Edit base data')
1688    pnav = 4
1689
1690    @action(_('Save'), style='primary')
1691    def save(self, **data):
1692        msave(self, **data)
1693        return
1694
1695class StudentChangePasswordPage(KofaEditFormPage):
1696    """ View to manage student base data
1697    """
1698    grok.context(IStudent)
1699    grok.name('change_password')
1700    grok.require('waeup.handleStudent')
1701    grok.template('change_password')
1702    label = _('Change password')
1703    pnav = 4
1704
1705    @action(_('Save'), style='primary')
1706    def save(self, **data):
1707        form = self.request.form
1708        password = form.get('change_password', None)
1709        password_ctl = form.get('change_password_repeat', None)
1710        if password:
1711            validator = getUtility(IPasswordValidator)
1712            errors = validator.validate_password(password, password_ctl)
1713            if not errors:
1714                IUserAccount(self.context).setPassword(password)
1715                self.context.writeLogMessage(self, 'saved: password')
1716                self.flash(_('Password changed.'))
1717            else:
1718                self.flash( ' '.join(errors))
1719        return
1720
1721class StudentFilesUploadPage(KofaPage):
1722    """ View to upload files by student
1723    """
1724    grok.context(IStudent)
1725    grok.name('change_portrait')
1726    grok.require('waeup.uploadStudentFile')
1727    grok.template('filesuploadpage')
1728    label = _('Upload portrait')
1729    pnav = 4
1730
1731    def update(self):
1732        if self.context.student.state != ADMITTED:
1733            emit_lock_message(self)
1734            return
1735        super(StudentFilesUploadPage, self).update()
1736        return
1737
1738class StartClearancePage(KofaPage):
1739    grok.context(IStudent)
1740    grok.name('start_clearance')
1741    grok.require('waeup.handleStudent')
1742    grok.template('enterpin')
1743    label = _('Start clearance')
1744    ac_prefix = 'CLR'
1745    notice = ''
1746    pnav = 4
1747    buttonname = _('Start clearance now')
1748
1749    @property
1750    def all_required_fields_filled(self):
1751        if self.context.email and self.context.phone:
1752            return True
1753        return False
1754
1755    @property
1756    def portrait_uploaded(self):
1757        store = getUtility(IExtFileStore)
1758        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1759            return True
1760        return False
1761
1762    def update(self, SUBMIT=None):
1763        if not self.context.state == ADMITTED:
1764            self.flash(_("Wrong state"))
1765            self.redirect(self.url(self.context))
1766            return
1767        if not self.portrait_uploaded:
1768            self.flash(_("No portrait uploaded."))
1769            self.redirect(self.url(self.context, 'change_portrait'))
1770            return
1771        if not self.all_required_fields_filled:
1772            self.flash(_("Not all required fields filled."))
1773            self.redirect(self.url(self.context, 'edit_base'))
1774            return
1775        self.ac_series = self.request.form.get('ac_series', None)
1776        self.ac_number = self.request.form.get('ac_number', None)
1777
1778        if SUBMIT is None:
1779            return
1780        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1781        code = get_access_code(pin)
1782        if not code:
1783            self.flash(_('Activation code is invalid.'))
1784            return
1785        if code.state == USED:
1786            self.flash(_('Activation code has already been used.'))
1787            return
1788        # Mark pin as used (this also fires a pin related transition)
1789        # and fire transition start_clearance
1790        comment = _(u"invalidated")
1791        # Here we know that the ac is in state initialized so we do not
1792        # expect an exception, but the owner might be different
1793        if not invalidate_accesscode(pin, comment, self.context.student_id):
1794            self.flash(_('You are not the owner of this access code.'))
1795            return
1796        self.context.clr_code = pin
1797        IWorkflowInfo(self.context).fireTransition('start_clearance')
1798        self.flash(_('Clearance process has been started.'))
1799        self.redirect(self.url(self.context,'cedit'))
1800        return
1801
1802class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1803    """ View to edit student clearance data by student
1804    """
1805    grok.context(IStudent)
1806    grok.name('cedit')
1807    grok.require('waeup.handleStudent')
1808    label = _('Edit clearance data')
1809
1810    @property
1811    def form_fields(self):
1812        if self.context.is_postgrad:
1813            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1814                'clearance_locked', 'clr_code')
1815        else:
1816            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1817                'clearance_locked', 'clr_code')
1818        return form_fields
1819
1820    def update(self):
1821        if self.context.clearance_locked:
1822            emit_lock_message(self)
1823            return
1824        return super(StudentClearanceEditFormPage, self).update()
1825
1826    @action(_('Save'), style='primary')
1827    def save(self, **data):
1828        self.applyData(self.context, **data)
1829        self.flash(_('Clearance form has been saved.'))
1830        return
1831
1832    def dataNotComplete(self):
1833        """To be implemented in the customization package.
1834        """
1835        return False
1836
1837    @action(_('Save and request clearance'), style='primary')
1838    def requestClearance(self, **data):
1839        self.applyData(self.context, **data)
1840        if self.dataNotComplete():
1841            self.flash(self.dataNotComplete())
1842            return
1843        self.flash(_('Clearance form has been saved.'))
1844        if self.context.clr_code:
1845            self.redirect(self.url(self.context, 'request_clearance'))
1846        else:
1847            # We bypass the request_clearance page if student
1848            # has been imported in state 'clearance started' and
1849            # no clr_code was entered before.
1850            state = IWorkflowState(self.context).getState()
1851            if state != CLEARANCE:
1852                # This shouldn't happen, but the application officer
1853                # might have forgotten to lock the form after changing the state
1854                self.flash(_('This form cannot be submitted. Wrong state!'))
1855                return
1856            IWorkflowInfo(self.context).fireTransition('request_clearance')
1857            self.flash(_('Clearance has been requested.'))
1858            self.redirect(self.url(self.context))
1859        return
1860
1861class RequestClearancePage(KofaPage):
1862    grok.context(IStudent)
1863    grok.name('request_clearance')
1864    grok.require('waeup.handleStudent')
1865    grok.template('enterpin')
1866    label = _('Request clearance')
1867    notice = _('Enter the CLR access code used for starting clearance.')
1868    ac_prefix = 'CLR'
1869    pnav = 4
1870    buttonname = _('Request clearance now')
1871
1872    def update(self, SUBMIT=None):
1873        self.ac_series = self.request.form.get('ac_series', None)
1874        self.ac_number = self.request.form.get('ac_number', None)
1875        if SUBMIT is None:
1876            return
1877        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1878        if self.context.clr_code and self.context.clr_code != pin:
1879            self.flash(_("This isn't your CLR access code."))
1880            return
1881        state = IWorkflowState(self.context).getState()
1882        if state != CLEARANCE:
1883            # This shouldn't happen, but the application officer
1884            # might have forgotten to lock the form after changing the state
1885            self.flash(_('This form cannot be submitted. Wrong state!'))
1886            return
1887        IWorkflowInfo(self.context).fireTransition('request_clearance')
1888        self.flash(_('Clearance has been requested.'))
1889        self.redirect(self.url(self.context))
1890        return
1891
1892class StartSessionPage(KofaPage):
1893    grok.context(IStudentStudyCourse)
1894    grok.name('start_session')
1895    grok.require('waeup.handleStudent')
1896    grok.template('enterpin')
1897    label = _('Start session')
1898    ac_prefix = 'SFE'
1899    notice = ''
1900    pnav = 4
1901    buttonname = _('Start now')
1902
1903    def update(self, SUBMIT=None):
1904        if not self.context.is_current:
1905            emit_lock_message(self)
1906            return
1907        super(StartSessionPage, self).update()
1908        if not self.context.next_session_allowed:
1909            self.flash(_("You are not entitled to start session."))
1910            self.redirect(self.url(self.context))
1911            return
1912        self.ac_series = self.request.form.get('ac_series', None)
1913        self.ac_number = self.request.form.get('ac_number', None)
1914
1915        if SUBMIT is None:
1916            return
1917        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1918        code = get_access_code(pin)
1919        if not code:
1920            self.flash(_('Activation code is invalid.'))
1921            return
1922        # Mark pin as used (this also fires a pin related transition)
1923        if code.state == USED:
1924            self.flash(_('Activation code has already been used.'))
1925            return
1926        else:
1927            comment = _(u"invalidated")
1928            # Here we know that the ac is in state initialized so we do not
1929            # expect an error, but the owner might be different
1930            if not invalidate_accesscode(
1931                pin,comment,self.context.student.student_id):
1932                self.flash(_('You are not the owner of this access code.'))
1933                return
1934        if self.context.student.state == CLEARED:
1935            IWorkflowInfo(self.context.student).fireTransition(
1936                'pay_first_school_fee')
1937        elif self.context.student.state == RETURNING:
1938            IWorkflowInfo(self.context.student).fireTransition(
1939                'pay_school_fee')
1940        elif self.context.student.state == PAID:
1941            IWorkflowInfo(self.context.student).fireTransition(
1942                'pay_pg_fee')
1943        self.flash(_('Session started.'))
1944        self.redirect(self.url(self.context))
1945        return
1946
1947class AddStudyLevelFormPage(KofaEditFormPage):
1948    """ Page for students to add current study levels
1949    """
1950    grok.context(IStudentStudyCourse)
1951    grok.name('add')
1952    grok.require('waeup.handleStudent')
1953    grok.template('studyleveladdpage')
1954    form_fields = grok.AutoFields(IStudentStudyCourse)
1955    pnav = 4
1956
1957    @property
1958    def label(self):
1959        studylevelsource = StudyLevelSource().factory
1960        code = self.context.current_level
1961        title = studylevelsource.getTitle(self.context, code)
1962        return _('Add current level ${a}', mapping = {'a':title})
1963
1964    def update(self):
1965        if not self.context.is_current:
1966            emit_lock_message(self)
1967            return
1968        if self.context.student.state != PAID:
1969            emit_lock_message(self)
1970            return
1971        super(AddStudyLevelFormPage, self).update()
1972        return
1973
1974    @action(_('Create course list now'), style='primary')
1975    def addStudyLevel(self, **data):
1976        studylevel = createObject(u'waeup.StudentStudyLevel')
1977        studylevel.level = self.context.current_level
1978        studylevel.level_session = self.context.current_session
1979        try:
1980            self.context.addStudentStudyLevel(
1981                self.context.certificate,studylevel)
1982        except KeyError:
1983            self.flash(_('This level exists.'))
1984        self.redirect(self.url(self.context))
1985        return
1986
1987class StudyLevelEditFormPage(KofaEditFormPage):
1988    """ Page to edit the student study level data by students
1989    """
1990    grok.context(IStudentStudyLevel)
1991    grok.name('edit')
1992    grok.require('waeup.handleStudent')
1993    grok.template('studyleveleditpage')
1994    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1995        'level_session', 'level_verdict')
1996    pnav = 4
1997    max_credits = 50
1998
1999    def update(self):
2000        if not self.context.__parent__.is_current:
2001            emit_lock_message(self)
2002            return
2003        if self.context.student.state != PAID:
2004            emit_lock_message(self)
2005            return
2006        super(StudyLevelEditFormPage, self).update()
2007        datatable.need()
2008        warning.need()
2009        return
2010
2011    @property
2012    def label(self):
2013        # Here we know that the cookie has been set
2014        lang = self.request.cookies.get('kofa.language')
2015        level_title = translate(self.context.level_title, 'waeup.kofa',
2016            target_language=lang)
2017        return _('Edit course list of ${a}',
2018            mapping = {'a':level_title})
2019
2020    @property
2021    def total_credits(self):
2022        total_credits = 0
2023        for key, val in self.context.items():
2024            total_credits += val.credits
2025        return total_credits
2026
2027    @property
2028    def translated_values(self):
2029        return translated_values(self)
2030
2031    @action(_('Add course ticket'))
2032    def addCourseTicket(self, **data):
2033        self.redirect(self.url(self.context, 'ctadd'))
2034
2035    @jsaction(_('Remove selected tickets'))
2036    def delCourseTicket(self, **data):
2037        form = self.request.form
2038        if form.has_key('val_id'):
2039            child_id = form['val_id']
2040        else:
2041            self.flash(_('No ticket selected.'))
2042            self.redirect(self.url(self.context, '@@edit'))
2043            return
2044        if not isinstance(child_id, list):
2045            child_id = [child_id]
2046        deleted = []
2047        for id in child_id:
2048            # Students are not allowed to remove core tickets
2049            if not self.context[id].mandatory:
2050                del self.context[id]
2051                deleted.append(id)
2052        if len(deleted):
2053            self.flash(_('Successfully removed: ${a}',
2054                mapping = {'a':', '.join(deleted)}))
2055        self.redirect(self.url(self.context, u'@@edit'))
2056        return
2057
2058    @action(_('Register course list'), style='primary')
2059    def registerCourses(self, **data):
2060        if self.total_credits > self.max_credits:
2061            self.flash(_('Maximum credits of ${a} exceeded.',
2062                mapping = {'a':self.max_credits}))
2063            return
2064        IWorkflowInfo(self.context.student).fireTransition(
2065            'register_courses')
2066        self.flash(_('Course list has been registered.'))
2067        self.redirect(self.url(self.context))
2068        return
2069
2070class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2071    """Add a course ticket by student.
2072    """
2073    grok.name('ctadd')
2074    grok.require('waeup.handleStudent')
2075    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2076        'score', 'mandatory', 'automatic', 'carry_over')
2077
2078    def update(self):
2079        if self.context.student.state != PAID:
2080            emit_lock_message(self)
2081            return
2082        super(CourseTicketAddFormPage2, self).update()
2083        return
2084
2085    @action(_('Add course ticket'))
2086    def addCourseTicket(self, **data):
2087        # Safety belt
2088        if self.context.student.state != PAID:
2089            return
2090        ticket = createObject(u'waeup.CourseTicket')
2091        course = data['course']
2092        ticket.automatic = False
2093        ticket.carry_over = False
2094        try:
2095            self.context.addCourseTicket(ticket, course)
2096        except KeyError:
2097            self.flash(_('The ticket exists.'))
2098            return
2099        self.flash(_('Successfully added ${a}.',
2100            mapping = {'a':ticket.code}))
2101        self.redirect(self.url(self.context, u'@@edit'))
2102        return
2103
2104
2105class SetPasswordPage(KofaPage):
2106    grok.context(IKofaObject)
2107    grok.name('setpassword')
2108    grok.require('waeup.Anonymous')
2109    grok.template('setpassword')
2110    label = _('Set password for first-time login')
2111    ac_prefix = 'PWD'
2112    pnav = 0
2113    set_button = _('Set')
2114
2115    def update(self, SUBMIT=None):
2116        self.reg_number = self.request.form.get('reg_number', None)
2117        self.ac_series = self.request.form.get('ac_series', None)
2118        self.ac_number = self.request.form.get('ac_number', None)
2119
2120        if SUBMIT is None:
2121            return
2122        hitlist = search(query=self.reg_number,
2123            searchtype='reg_number', view=self)
2124        if not hitlist:
2125            self.flash(_('No student found.'))
2126            return
2127        if len(hitlist) != 1:   # Cannot happen but anyway
2128            self.flash(_('More than one student found.'))
2129            return
2130        student = hitlist[0].context
2131        self.student_id = student.student_id
2132        student_pw = student.password
2133        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2134        code = get_access_code(pin)
2135        if not code:
2136            self.flash(_('Access code is invalid.'))
2137            return
2138        if student_pw and pin == student.adm_code:
2139            self.flash(_(
2140                'Password has already been set. Your Student Id is ${a}',
2141                mapping = {'a':self.student_id}))
2142            return
2143        elif student_pw:
2144            self.flash(
2145                _('Password has already been set. You are using the ' +
2146                'wrong Access Code.'))
2147            return
2148        # Mark pin as used (this also fires a pin related transition)
2149        # and set student password
2150        if code.state == USED:
2151            self.flash(_('Access code has already been used.'))
2152            return
2153        else:
2154            comment = _(u"invalidated")
2155            # Here we know that the ac is in state initialized so we do not
2156            # expect an exception
2157            invalidate_accesscode(pin,comment)
2158            IUserAccount(student).setPassword(self.ac_number)
2159            student.adm_code = pin
2160        self.flash(_('Password has been set. Your Student Id is ${a}',
2161            mapping = {'a':self.student_id}))
2162        return
2163
2164class StudentRequestPasswordPage(KofaAddFormPage):
2165    """Captcha'd registration page for applicants.
2166    """
2167    grok.name('requestpw')
2168    grok.require('waeup.Anonymous')
2169    grok.template('requestpw')
2170    form_fields = grok.AutoFields(IStudentRequestPW).select(
2171        'firstname','number','email')
2172    label = _('Request password for first-time login')
2173
2174    def update(self):
2175        # Handle captcha
2176        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2177        self.captcha_result = self.captcha.verify(self.request)
2178        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2179        return
2180
2181    def _redirect(self, email, password, student_id):
2182        # Forward only email to landing page in base package.
2183        self.redirect(self.url(self.context, 'requestpw_complete',
2184            data = dict(email=email)))
2185        return
2186
2187    def _pw_used(self):
2188        # XXX: False if password has not been used. We need an extra
2189        #      attribute which remembers if student logged in.
2190        return True
2191
2192    @action(_('Send login credentials to email address'), style='primary')
2193    def get_credentials(self, **data):
2194        if not self.captcha_result.is_valid:
2195            # Captcha will display error messages automatically.
2196            # No need to flash something.
2197            return
2198        number = data.get('number','')
2199        firstname = data.get('firstname','')
2200        cat = getUtility(ICatalog, name='students_catalog')
2201        results = list(
2202            cat.searchResults(reg_number=(number, number)))
2203        if not results:
2204            results = list(
2205                cat.searchResults(matric_number=(number, number)))
2206        if results:
2207            student = results[0]
2208            if getattr(student,'firstname',None) is None:
2209                self.flash(_('An error occurred.'))
2210                return
2211            elif student.firstname.lower() != firstname.lower():
2212                # Don't tell the truth here. Anonymous must not
2213                # know that a record was found and only the firstname
2214                # verification failed.
2215                self.flash(_('No student record found.'))
2216                return
2217            elif student.password is not None and self._pw_used:
2218                self.flash(_('Your password has already been set and used. '
2219                             'Please proceed to the login page.'))
2220                return
2221            # Store email address but nothing else.
2222            student.email = data['email']
2223            notify(grok.ObjectModifiedEvent(student))
2224        else:
2225            # No record found, this is the truth.
2226            self.flash(_('No student record found.'))
2227            return
2228
2229        kofa_utils = getUtility(IKofaUtils)
2230        password = kofa_utils.genPassword()
2231        mandate = PasswordMandate()
2232        mandate.params['password'] = password
2233        mandate.params['user'] = student
2234        site = grok.getSite()
2235        site['mandates'].addMandate(mandate)
2236        # Send email with credentials
2237        args = {'mandate_id':mandate.mandate_id}
2238        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2239        url_info = u'Confirmation link: %s' % mandate_url
2240        msg = _('You have successfully requested a password for the')
2241        if kofa_utils.sendCredentials(IUserAccount(student),
2242            password, url_info, msg):
2243            email_sent = student.email
2244        else:
2245            email_sent = None
2246        self._redirect(email=email_sent, password=password,
2247            student_id=student.student_id)
2248        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2249        self.context.logger.info(
2250            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2251        return
2252
2253class StudentRequestPasswordEmailSent(KofaPage):
2254    """Landing page after successful password request.
2255
2256    """
2257    grok.name('requestpw_complete')
2258    grok.require('waeup.Public')
2259    grok.template('requestpwmailsent')
2260    label = _('Your password request was successful.')
2261
2262    def update(self, email=None, student_id=None, password=None):
2263        self.email = email
2264        self.password = password
2265        self.student_id = student_id
2266        return
Note: See TracBrowser for help on using the repository browser.