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

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

Add timestamp to personal data. This timestamp will be checked when students log in. Students sould be remembered to frequently update their personal data.

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