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

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

Limit the total number of credits per level.

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