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

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

Add views for temporarily login as student.

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