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

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

Since decorated methods can't be easily customized, this is a workaround for customization. Maybe there is a better solution?!

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