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

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

Reenable current_verdict for pg students.

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