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

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

In contrast to the comment of the last revision: We need to store validation_date and validated_by also in Kofa. They have to be set by the workflow transition events. They must not be editable (but importable and exportable).

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