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

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

Add viewlets for reports.py

Add permission to roles.

Set label attribbute of views.

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