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

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

Merge changes from trunk, r8786-HEAD

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