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

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

Add suspended students search.

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