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

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

We bypass the request_clearance page if student
has been imported in state 'clearance started' and
no clr_code was entered before.

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