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

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

Enable previous session payments. Do not create activation code after successful previous session payment. Show previous session payment form only if current session ticket creation failed.

Attention: setPaymentDetails method has changed. Two additional parameters expected.

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