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

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

Move student workflow history to the bottom. Reformat pdf slips slightly.

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