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

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

Add field 'suspended_comment'. This attribute will be used to render flash messages for deactivated students. The field is only displayed on the student manage page.

  • Property svn:keywords set to Id
File size: 89.1 KB
Line 
1## $Id: browser.py 9702 2012-11-21 15:18:31Z 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                'current_verdict', '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                'current_verdict', '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                self.redirect(self.url(self.context))
1410                return
1411        if 'maintenance' in p_category:
1412            current_session = str(student['studycourse'].current_session)
1413            if not current_session in student['accommodation']:
1414                self.flash(_('You have not yet booked accommodation.'))
1415                self.redirect(self.url(self.context))
1416                return
1417        students_utils = getUtility(IStudentsUtils)
1418        error, payment = students_utils.setPaymentDetails(
1419            p_category, student, previous_session, previous_level)
1420        if error is not None:
1421            self.flash(error)
1422            return
1423        self.context[payment.p_id] = payment
1424        self.flash(_('Payment ticket created.'))
1425        self.redirect(self.url(self.context))
1426        return
1427
1428    @action(_('Cancel'), validator=NullValidator)
1429    def cancel(self, **data):
1430        self.redirect(self.url(self.context))
1431
1432class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1433    """ Page to add an online payment ticket for previous sessions
1434    """
1435    grok.context(IStudentPaymentsContainer)
1436    grok.name('addpp')
1437    grok.require('waeup.payStudent')
1438    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1439        'p_category', 'p_session', 'p_level')
1440    label = _('Add previous session online payment')
1441    pnav = 4
1442
1443    def update(self):
1444        if self.context.student.before_payment:
1445            self.flash(_("No previous payment to be made."))
1446            self.redirect(self.url(self.context))
1447        super(PreviousPaymentAddFormPage, self).update()
1448        return
1449
1450class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1451    """ Page to view an online payment ticket
1452    """
1453    grok.context(IStudentOnlinePayment)
1454    grok.name('index')
1455    grok.require('waeup.viewStudent')
1456    form_fields = grok.AutoFields(IStudentOnlinePayment)
1457    form_fields[
1458        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1459    form_fields[
1460        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1461    pnav = 4
1462
1463    @property
1464    def label(self):
1465        return _('${a}: Online Payment Ticket ${b}', mapping = {
1466            'a':self.context.student.display_fullname,
1467            'b':self.context.p_id})
1468
1469class OnlinePaymentApprovePage(UtilityView, grok.View):
1470    """ Callback view
1471    """
1472    grok.context(IStudentOnlinePayment)
1473    grok.name('approve')
1474    grok.require('waeup.managePortal')
1475
1476    def update(self):
1477        success, msg, log = self.context.approveStudentPayment()
1478        if log is not None:
1479            self.context.writeLogMessage(self,log)
1480        self.flash(msg)
1481        return
1482
1483    def render(self):
1484        self.redirect(self.url(self.context, '@@index'))
1485        return
1486
1487class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1488    """ Approval view for students.
1489
1490    This view is used for browser tests only and
1491    must be neutralized in custom pages!
1492    """
1493
1494    grok.name('fake_approve')
1495    grok.require('waeup.payStudent')
1496
1497class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1498    """Deliver a PDF slip of the context.
1499    """
1500    grok.context(IStudentOnlinePayment)
1501    grok.name('payment_slip.pdf')
1502    grok.require('waeup.viewStudent')
1503    form_fields = grok.AutoFields(IStudentOnlinePayment)
1504    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1505    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1506    prefix = 'form'
1507    note = None
1508    omit_fields = (
1509        'password', 'suspended', 'phone',
1510        'adm_code', 'sex', 'suspended_comment')
1511
1512    @property
1513    def title(self):
1514        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1515        return translate(_('Payment Data'), 'waeup.kofa',
1516            target_language=portal_language)
1517
1518    @property
1519    def label(self):
1520        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1521        return translate(_('Online Payment Slip'),
1522            'waeup.kofa', target_language=portal_language) \
1523            + ' %s' % self.context.p_id
1524
1525    def render(self):
1526        #if self.context.p_state != 'paid':
1527        #    self.flash('Ticket not yet paid.')
1528        #    self.redirect(self.url(self.context))
1529        #    return
1530        studentview = StudentBasePDFFormPage(self.context.student,
1531            self.request, self.omit_fields)
1532        students_utils = getUtility(IStudentsUtils)
1533        return students_utils.renderPDF(self, 'payment_slip.pdf',
1534            self.context.student, studentview, note=self.note)
1535
1536
1537class AccommodationManageFormPage(KofaEditFormPage):
1538    """ Page to manage bed tickets.
1539
1540    This manage form page is for both students and students officers.
1541    """
1542    grok.context(IStudentAccommodation)
1543    grok.name('index')
1544    grok.require('waeup.handleAccommodation')
1545    form_fields = grok.AutoFields(IStudentAccommodation)
1546    grok.template('accommodationmanagepage')
1547    pnav = 4
1548    officers_only_actions = [_('Remove selected')]
1549
1550    @property
1551    def label(self):
1552        return _('${a}: Accommodation',
1553            mapping = {'a':self.context.__parent__.display_fullname})
1554
1555    def update(self):
1556        super(AccommodationManageFormPage, self).update()
1557        datatable.need()
1558        warning.need()
1559        return
1560
1561    @jsaction(_('Remove selected'))
1562    def delBedTickets(self, **data):
1563        if getattr(self.request.principal, 'user_type', None) == 'student':
1564            self.flash(_('You are not allowed to remove bed tickets.'))
1565            self.redirect(self.url(self.context))
1566            return
1567        form = self.request.form
1568        if 'val_id' in form:
1569            child_id = form['val_id']
1570        else:
1571            self.flash(_('No bed ticket selected.'))
1572            self.redirect(self.url(self.context))
1573            return
1574        if not isinstance(child_id, list):
1575            child_id = [child_id]
1576        deleted = []
1577        for id in child_id:
1578            del self.context[id]
1579            deleted.append(id)
1580        if len(deleted):
1581            self.flash(_('Successfully removed: ${a}',
1582                mapping = {'a':', '.join(deleted)}))
1583            self.context.writeLogMessage(
1584                self,'removed: % s' % ', '.join(deleted))
1585        self.redirect(self.url(self.context))
1586        return
1587
1588    @property
1589    def selected_actions(self):
1590        if getattr(self.request.principal, 'user_type', None) == 'student':
1591            return [action for action in self.actions
1592                    if not action.label in self.officers_only_actions]
1593        return self.actions
1594
1595class BedTicketAddPage(KofaPage):
1596    """ Page to add an online payment ticket
1597    """
1598    grok.context(IStudentAccommodation)
1599    grok.name('add')
1600    grok.require('waeup.handleAccommodation')
1601    grok.template('enterpin')
1602    ac_prefix = 'HOS'
1603    label = _('Add bed ticket')
1604    pnav = 4
1605    buttonname = _('Create bed ticket')
1606    notice = ''
1607    with_ac = True
1608
1609    def update(self, SUBMIT=None):
1610        student = self.context.student
1611        students_utils = getUtility(IStudentsUtils)
1612        acc_details  = students_utils.getAccommodationDetails(student)
1613        if acc_details.get('expired', False):
1614            startdate = acc_details.get('startdate')
1615            enddate = acc_details.get('enddate')
1616            if startdate and enddate:
1617                tz = getUtility(IKofaUtils).tzinfo
1618                startdate = to_timezone(
1619                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1620                enddate = to_timezone(
1621                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1622                self.flash(_("Outside booking period: ${a} - ${b}",
1623                    mapping = {'a': startdate, 'b': enddate}))
1624            else:
1625                self.flash(_("Outside booking period."))
1626            self.redirect(self.url(self.context))
1627            return
1628        if not acc_details:
1629            self.flash(_("Your data are incomplete."))
1630            self.redirect(self.url(self.context))
1631            return
1632        if not student.state in acc_details['allowed_states']:
1633            self.flash(_("You are in the wrong registration state."))
1634            self.redirect(self.url(self.context))
1635            return
1636        if student['studycourse'].current_session != acc_details[
1637            'booking_session']:
1638            self.flash(
1639                _('Your current session does not match accommodation session.'))
1640            self.redirect(self.url(self.context))
1641            return
1642        if str(acc_details['booking_session']) in self.context.keys():
1643            self.flash(
1644                _('You already booked a bed space in current ' \
1645                    + 'accommodation session.'))
1646            self.redirect(self.url(self.context))
1647            return
1648        if self.with_ac:
1649            self.ac_series = self.request.form.get('ac_series', None)
1650            self.ac_number = self.request.form.get('ac_number', None)
1651        if SUBMIT is None:
1652            return
1653        if self.with_ac:
1654            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1655            code = get_access_code(pin)
1656            if not code:
1657                self.flash(_('Activation code is invalid.'))
1658                return
1659        # Search and book bed
1660        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1661        entries = cat.searchResults(
1662            owner=(student.student_id,student.student_id))
1663        if len(entries):
1664            # If bed space has been manually allocated use this bed
1665            bed = [entry for entry in entries][0]
1666            # Safety belt for paranoids: Does this bed really exist on portal?
1667            # XXX: Can be remove if nobody complains.
1668            if bed.__parent__.__parent__ is None:
1669                self.flash(_('System error: Please contact the adminsitrator.'))
1670                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1671                return
1672        else:
1673            # else search for other available beds
1674            entries = cat.searchResults(
1675                bed_type=(acc_details['bt'],acc_details['bt']))
1676            available_beds = [
1677                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1678            if available_beds:
1679                students_utils = getUtility(IStudentsUtils)
1680                bed = students_utils.selectBed(available_beds)
1681                # Safety belt for paranoids: Does this bed really exist in portal?
1682                # XXX: Can be remove if nobody complains.
1683                if bed.__parent__.__parent__ is None:
1684                    self.flash(_('System error: Please contact the adminsitrator.'))
1685                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1686                    return
1687                bed.bookBed(student.student_id)
1688            else:
1689                self.flash(_('There is no free bed in your category ${a}.',
1690                    mapping = {'a':acc_details['bt']}))
1691                return
1692        if self.with_ac:
1693            # Mark pin as used (this also fires a pin related transition)
1694            if code.state == USED:
1695                self.flash(_('Activation code has already been used.'))
1696                return
1697            else:
1698                comment = _(u'invalidated')
1699                # Here we know that the ac is in state initialized so we do not
1700                # expect an exception, but the owner might be different
1701                if not invalidate_accesscode(
1702                    pin,comment,self.context.student.student_id):
1703                    self.flash(_('You are not the owner of this access code.'))
1704                    return
1705        # Create bed ticket
1706        bedticket = createObject(u'waeup.BedTicket')
1707        if self.with_ac:
1708            bedticket.booking_code = pin
1709        bedticket.booking_session = acc_details['booking_session']
1710        bedticket.bed_type = acc_details['bt']
1711        bedticket.bed = bed
1712        hall_title = bed.__parent__.hostel_name
1713        coordinates = bed.coordinates[1:]
1714        block, room_nr, bed_nr = coordinates
1715        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1716            'a':hall_title, 'b':block,
1717            'c':room_nr, 'd':bed_nr,
1718            'e':bed.bed_type})
1719        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1720        bedticket.bed_coordinates = translate(
1721            bc, 'waeup.kofa',target_language=portal_language)
1722        self.context.addBedTicket(bedticket)
1723        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1724        self.flash(_('Bed ticket created and bed booked: ${a}',
1725            mapping = {'a':bedticket.bed_coordinates}))
1726        self.redirect(self.url(self.context))
1727        return
1728
1729class BedTicketDisplayFormPage(KofaDisplayFormPage):
1730    """ Page to display bed tickets
1731    """
1732    grok.context(IBedTicket)
1733    grok.name('index')
1734    grok.require('waeup.handleAccommodation')
1735    form_fields = grok.AutoFields(IBedTicket)
1736    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1737    pnav = 4
1738
1739    @property
1740    def label(self):
1741        return _('Bed Ticket for Session ${a}',
1742            mapping = {'a':self.context.getSessionString()})
1743
1744class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1745    """Deliver a PDF slip of the context.
1746    """
1747    grok.context(IBedTicket)
1748    grok.name('bed_allocation_slip.pdf')
1749    grok.require('waeup.handleAccommodation')
1750    form_fields = grok.AutoFields(IBedTicket)
1751    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1752    prefix = 'form'
1753    omit_fields = (
1754        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
1755
1756    @property
1757    def title(self):
1758        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1759        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1760            target_language=portal_language)
1761
1762    @property
1763    def label(self):
1764        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1765        #return translate(_('Bed Allocation: '),
1766        #    'waeup.kofa', target_language=portal_language) \
1767        #    + ' %s' % self.context.bed_coordinates
1768        return translate(_('Bed Allocation Slip'),
1769            'waeup.kofa', target_language=portal_language) \
1770            + ' %s' % self.context.getSessionString()
1771
1772    def render(self):
1773        studentview = StudentBasePDFFormPage(self.context.student,
1774            self.request, self.omit_fields)
1775        students_utils = getUtility(IStudentsUtils)
1776        return students_utils.renderPDF(
1777            self, 'bed_allocation_slip.pdf',
1778            self.context.student, studentview)
1779
1780class BedTicketRelocationPage(UtilityView, grok.View):
1781    """ Callback view
1782    """
1783    grok.context(IBedTicket)
1784    grok.name('relocate')
1785    grok.require('waeup.manageHostels')
1786
1787    # Relocate student if student parameters have changed or the bed_type
1788    # of the bed has changed
1789    def update(self):
1790        student = self.context.student
1791        students_utils = getUtility(IStudentsUtils)
1792        acc_details  = students_utils.getAccommodationDetails(student)
1793        if self.context.bed != None and \
1794              'reserved' in self.context.bed.bed_type:
1795            self.flash(_("Students in reserved beds can't be relocated."))
1796            self.redirect(self.url(self.context))
1797            return
1798        if acc_details['bt'] == self.context.bed_type and \
1799                self.context.bed != None and \
1800                self.context.bed.bed_type == self.context.bed_type:
1801            self.flash(_("Student can't be relocated."))
1802            self.redirect(self.url(self.context))
1803            return
1804        # Search a bed
1805        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1806        entries = cat.searchResults(
1807            owner=(student.student_id,student.student_id))
1808        if len(entries) and self.context.bed == None:
1809            # If booking has been cancelled but other bed space has been
1810            # manually allocated after cancellation use this bed
1811            new_bed = [entry for entry in entries][0]
1812        else:
1813            # Search for other available beds
1814            entries = cat.searchResults(
1815                bed_type=(acc_details['bt'],acc_details['bt']))
1816            available_beds = [
1817                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1818            if available_beds:
1819                students_utils = getUtility(IStudentsUtils)
1820                new_bed = students_utils.selectBed(available_beds)
1821                new_bed.bookBed(student.student_id)
1822            else:
1823                self.flash(_('There is no free bed in your category ${a}.',
1824                    mapping = {'a':acc_details['bt']}))
1825                self.redirect(self.url(self.context))
1826                return
1827        # Release old bed if exists
1828        if self.context.bed != None:
1829            self.context.bed.owner = NOT_OCCUPIED
1830            notify(grok.ObjectModifiedEvent(self.context.bed))
1831        # Alocate new bed
1832        self.context.bed_type = acc_details['bt']
1833        self.context.bed = new_bed
1834        hall_title = new_bed.__parent__.hostel_name
1835        coordinates = new_bed.coordinates[1:]
1836        block, room_nr, bed_nr = coordinates
1837        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1838            'a':hall_title, 'b':block,
1839            'c':room_nr, 'd':bed_nr,
1840            'e':new_bed.bed_type})
1841        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1842        self.context.bed_coordinates = translate(
1843            bc, 'waeup.kofa',target_language=portal_language)
1844        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
1845        self.flash(_('Student relocated: ${a}',
1846            mapping = {'a':self.context.bed_coordinates}))
1847        self.redirect(self.url(self.context))
1848        return
1849
1850    def render(self):
1851        return
1852
1853class StudentHistoryPage(KofaPage):
1854    """ Page to display student clearance data
1855    """
1856    grok.context(IStudent)
1857    grok.name('history')
1858    grok.require('waeup.viewStudent')
1859    grok.template('studenthistory')
1860    pnav = 4
1861
1862    @property
1863    def label(self):
1864        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1865
1866# Pages for students only
1867
1868class StudentBaseEditFormPage(KofaEditFormPage):
1869    """ View to edit student base data
1870    """
1871    grok.context(IStudent)
1872    grok.name('edit_base')
1873    grok.require('waeup.handleStudent')
1874    form_fields = grok.AutoFields(IStudentBase).select(
1875        'email', 'phone')
1876    label = _('Edit base data')
1877    pnav = 4
1878
1879    @action(_('Save'), style='primary')
1880    def save(self, **data):
1881        msave(self, **data)
1882        return
1883
1884class StudentChangePasswordPage(KofaEditFormPage):
1885    """ View to manage student base data
1886    """
1887    grok.context(IStudent)
1888    grok.name('change_password')
1889    grok.require('waeup.handleStudent')
1890    grok.template('change_password')
1891    label = _('Change password')
1892    pnav = 4
1893
1894    @action(_('Save'), style='primary')
1895    def save(self, **data):
1896        form = self.request.form
1897        password = form.get('change_password', None)
1898        password_ctl = form.get('change_password_repeat', None)
1899        if password:
1900            validator = getUtility(IPasswordValidator)
1901            errors = validator.validate_password(password, password_ctl)
1902            if not errors:
1903                IUserAccount(self.context).setPassword(password)
1904                self.context.writeLogMessage(self, 'saved: password')
1905                self.flash(_('Password changed.'))
1906            else:
1907                self.flash( ' '.join(errors))
1908        return
1909
1910class StudentFilesUploadPage(KofaPage):
1911    """ View to upload files by student
1912    """
1913    grok.context(IStudent)
1914    grok.name('change_portrait')
1915    grok.require('waeup.uploadStudentFile')
1916    grok.template('filesuploadpage')
1917    label = _('Upload portrait')
1918    pnav = 4
1919
1920    def update(self):
1921        if self.context.student.state != ADMITTED:
1922            emit_lock_message(self)
1923            return
1924        super(StudentFilesUploadPage, self).update()
1925        return
1926
1927class StartClearancePage(KofaPage):
1928    grok.context(IStudent)
1929    grok.name('start_clearance')
1930    grok.require('waeup.handleStudent')
1931    grok.template('enterpin')
1932    label = _('Start clearance')
1933    ac_prefix = 'CLR'
1934    notice = ''
1935    pnav = 4
1936    buttonname = _('Start clearance now')
1937
1938    @property
1939    def all_required_fields_filled(self):
1940        if self.context.email and self.context.phone:
1941            return True
1942        return False
1943
1944    @property
1945    def portrait_uploaded(self):
1946        store = getUtility(IExtFileStore)
1947        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1948            return True
1949        return False
1950
1951    def update(self, SUBMIT=None):
1952        if not self.context.state == ADMITTED:
1953            self.flash(_("Wrong state"))
1954            self.redirect(self.url(self.context))
1955            return
1956        if not self.portrait_uploaded:
1957            self.flash(_("No portrait uploaded."))
1958            self.redirect(self.url(self.context, 'change_portrait'))
1959            return
1960        if not self.all_required_fields_filled:
1961            self.flash(_("Not all required fields filled."))
1962            self.redirect(self.url(self.context, 'edit_base'))
1963            return
1964        self.ac_series = self.request.form.get('ac_series', None)
1965        self.ac_number = self.request.form.get('ac_number', None)
1966
1967        if SUBMIT is None:
1968            return
1969        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1970        code = get_access_code(pin)
1971        if not code:
1972            self.flash(_('Activation code is invalid.'))
1973            return
1974        if code.state == USED:
1975            self.flash(_('Activation code has already been used.'))
1976            return
1977        # Mark pin as used (this also fires a pin related transition)
1978        # and fire transition start_clearance
1979        comment = _(u"invalidated")
1980        # Here we know that the ac is in state initialized so we do not
1981        # expect an exception, but the owner might be different
1982        if not invalidate_accesscode(pin, comment, self.context.student_id):
1983            self.flash(_('You are not the owner of this access code.'))
1984            return
1985        self.context.clr_code = pin
1986        IWorkflowInfo(self.context).fireTransition('start_clearance')
1987        self.flash(_('Clearance process has been started.'))
1988        self.redirect(self.url(self.context,'cedit'))
1989        return
1990
1991class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1992    """ View to edit student clearance data by student
1993    """
1994    grok.context(IStudent)
1995    grok.name('cedit')
1996    grok.require('waeup.handleStudent')
1997    label = _('Edit clearance data')
1998
1999    @property
2000    def form_fields(self):
2001        if self.context.is_postgrad:
2002            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2003                'clearance_locked', 'clr_code', 'officer_comment')
2004        else:
2005            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2006                'clearance_locked', 'clr_code', 'officer_comment')
2007        return form_fields
2008
2009    def update(self):
2010        if self.context.clearance_locked:
2011            emit_lock_message(self)
2012            return
2013        return super(StudentClearanceEditFormPage, self).update()
2014
2015    @action(_('Save'), style='primary')
2016    def save(self, **data):
2017        self.applyData(self.context, **data)
2018        self.flash(_('Clearance form has been saved.'))
2019        return
2020
2021    def dataNotComplete(self):
2022        """To be implemented in the customization package.
2023        """
2024        return False
2025
2026    @action(_('Save and request clearance'), style='primary')
2027    def requestClearance(self, **data):
2028        self.applyData(self.context, **data)
2029        if self.dataNotComplete():
2030            self.flash(self.dataNotComplete())
2031            return
2032        self.flash(_('Clearance form has been saved.'))
2033        if self.context.clr_code:
2034            self.redirect(self.url(self.context, 'request_clearance'))
2035        else:
2036            # We bypass the request_clearance page if student
2037            # has been imported in state 'clearance started' and
2038            # no clr_code was entered before.
2039            state = IWorkflowState(self.context).getState()
2040            if state != CLEARANCE:
2041                # This shouldn't happen, but the application officer
2042                # might have forgotten to lock the form after changing the state
2043                self.flash(_('This form cannot be submitted. Wrong state!'))
2044                return
2045            IWorkflowInfo(self.context).fireTransition('request_clearance')
2046            self.flash(_('Clearance has been requested.'))
2047            self.redirect(self.url(self.context))
2048        return
2049
2050class RequestClearancePage(KofaPage):
2051    grok.context(IStudent)
2052    grok.name('request_clearance')
2053    grok.require('waeup.handleStudent')
2054    grok.template('enterpin')
2055    label = _('Request clearance')
2056    notice = _('Enter the CLR access code used for starting clearance.')
2057    ac_prefix = 'CLR'
2058    pnav = 4
2059    buttonname = _('Request clearance now')
2060
2061    def update(self, SUBMIT=None):
2062        self.ac_series = self.request.form.get('ac_series', None)
2063        self.ac_number = self.request.form.get('ac_number', None)
2064        if SUBMIT is None:
2065            return
2066        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2067        if self.context.clr_code and self.context.clr_code != pin:
2068            self.flash(_("This isn't your CLR access code."))
2069            return
2070        state = IWorkflowState(self.context).getState()
2071        if state != CLEARANCE:
2072            # This shouldn't happen, but the application officer
2073            # might have forgotten to lock the form after changing the state
2074            self.flash(_('This form cannot be submitted. Wrong state!'))
2075            return
2076        IWorkflowInfo(self.context).fireTransition('request_clearance')
2077        self.flash(_('Clearance has been requested.'))
2078        self.redirect(self.url(self.context))
2079        return
2080
2081class StartSessionPage(KofaPage):
2082    grok.context(IStudentStudyCourse)
2083    grok.name('start_session')
2084    grok.require('waeup.handleStudent')
2085    grok.template('enterpin')
2086    label = _('Start session')
2087    ac_prefix = 'SFE'
2088    notice = ''
2089    pnav = 4
2090    buttonname = _('Start now')
2091
2092    def update(self, SUBMIT=None):
2093        if not self.context.is_current:
2094            emit_lock_message(self)
2095            return
2096        super(StartSessionPage, self).update()
2097        if not self.context.next_session_allowed:
2098            self.flash(_("You are not entitled to start session."))
2099            self.redirect(self.url(self.context))
2100            return
2101        self.ac_series = self.request.form.get('ac_series', None)
2102        self.ac_number = self.request.form.get('ac_number', None)
2103
2104        if SUBMIT is None:
2105            return
2106        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2107        code = get_access_code(pin)
2108        if not code:
2109            self.flash(_('Activation code is invalid.'))
2110            return
2111        # Mark pin as used (this also fires a pin related transition)
2112        if code.state == USED:
2113            self.flash(_('Activation code has already been used.'))
2114            return
2115        else:
2116            comment = _(u"invalidated")
2117            # Here we know that the ac is in state initialized so we do not
2118            # expect an error, but the owner might be different
2119            if not invalidate_accesscode(
2120                pin,comment,self.context.student.student_id):
2121                self.flash(_('You are not the owner of this access code.'))
2122                return
2123        try:
2124            if self.context.student.state == CLEARED:
2125                IWorkflowInfo(self.context.student).fireTransition(
2126                    'pay_first_school_fee')
2127            elif self.context.student.state == RETURNING:
2128                IWorkflowInfo(self.context.student).fireTransition(
2129                    'pay_school_fee')
2130            elif self.context.student.state == PAID:
2131                IWorkflowInfo(self.context.student).fireTransition(
2132                    'pay_pg_fee')
2133        except ConstraintNotSatisfied:
2134            self.flash(_('An error occurred, please contact the system administrator.'))
2135            return
2136        self.flash(_('Session started.'))
2137        self.redirect(self.url(self.context))
2138        return
2139
2140class AddStudyLevelFormPage(KofaEditFormPage):
2141    """ Page for students to add current study levels
2142    """
2143    grok.context(IStudentStudyCourse)
2144    grok.name('add')
2145    grok.require('waeup.handleStudent')
2146    grok.template('studyleveladdpage')
2147    form_fields = grok.AutoFields(IStudentStudyCourse)
2148    pnav = 4
2149
2150    @property
2151    def label(self):
2152        studylevelsource = StudyLevelSource().factory
2153        code = self.context.current_level
2154        title = studylevelsource.getTitle(self.context, code)
2155        return _('Add current level ${a}', mapping = {'a':title})
2156
2157    def update(self):
2158        if not self.context.is_current:
2159            emit_lock_message(self)
2160            return
2161        if self.context.student.state != PAID:
2162            emit_lock_message(self)
2163            return
2164        super(AddStudyLevelFormPage, self).update()
2165        return
2166
2167    @action(_('Create course list now'), style='primary')
2168    def addStudyLevel(self, **data):
2169        studylevel = createObject(u'waeup.StudentStudyLevel')
2170        studylevel.level = self.context.current_level
2171        studylevel.level_session = self.context.current_session
2172        try:
2173            self.context.addStudentStudyLevel(
2174                self.context.certificate,studylevel)
2175        except KeyError:
2176            self.flash(_('This level exists.'))
2177        except RequiredMissing:
2178            self.flash(_('Your data are incomplete'))
2179        self.redirect(self.url(self.context))
2180        return
2181
2182class StudyLevelEditFormPage(KofaEditFormPage):
2183    """ Page to edit the student study level data by students
2184    """
2185    grok.context(IStudentStudyLevel)
2186    grok.name('edit')
2187    grok.require('waeup.handleStudent')
2188    grok.template('studyleveleditpage')
2189    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2190        'level_session', 'level_verdict')
2191    pnav = 4
2192    max_credits = 50
2193
2194    def update(self):
2195        if not self.context.__parent__.is_current:
2196            emit_lock_message(self)
2197            return
2198        if self.context.student.state != PAID or \
2199            not self.context.is_current_level:
2200            emit_lock_message(self)
2201            return
2202        super(StudyLevelEditFormPage, self).update()
2203        datatable.need()
2204        warning.need()
2205        return
2206
2207    @property
2208    def label(self):
2209        # Here we know that the cookie has been set
2210        lang = self.request.cookies.get('kofa.language')
2211        level_title = translate(self.context.level_title, 'waeup.kofa',
2212            target_language=lang)
2213        return _('Edit course list of ${a}',
2214            mapping = {'a':level_title})
2215
2216    @property
2217    def translated_values(self):
2218        return translated_values(self)
2219
2220    @action(_('Add course ticket'))
2221    def addCourseTicket(self, **data):
2222        self.redirect(self.url(self.context, 'ctadd'))
2223
2224    def _delCourseTicket(self, **data):
2225        form = self.request.form
2226        if 'val_id' in form:
2227            child_id = form['val_id']
2228        else:
2229            self.flash(_('No ticket selected.'))
2230            self.redirect(self.url(self.context, '@@edit'))
2231            return
2232        if not isinstance(child_id, list):
2233            child_id = [child_id]
2234        deleted = []
2235        for id in child_id:
2236            # Students are not allowed to remove core tickets
2237            if id in self.context and \
2238                self.context[id].removable_by_student:
2239                del self.context[id]
2240                deleted.append(id)
2241        if len(deleted):
2242            self.flash(_('Successfully removed: ${a}',
2243                mapping = {'a':', '.join(deleted)}))
2244            self.context.writeLogMessage(
2245                self,'removed: %s' % ', '.join(deleted))
2246        self.redirect(self.url(self.context, u'@@edit'))
2247        return
2248
2249    @jsaction(_('Remove selected tickets'))
2250    def delCourseTicket(self, **data):
2251        self._delCourseTicket(**data)
2252        return
2253
2254    def _registerCourses(self, **data):
2255        if self.context.student.is_postgrad:
2256            self.flash(_(
2257                "You are a postgraduate student, "
2258                "your course list can't bee registered."))
2259            self.redirect(self.url(self.context))
2260            return
2261        if self.context.total_credits > self.max_credits:
2262            self.flash(_('Maximum credits of ${a} exceeded.',
2263                mapping = {'a':self.max_credits}))
2264            return
2265        IWorkflowInfo(self.context.student).fireTransition(
2266            'register_courses')
2267        self.flash(_('Course list has been registered.'))
2268        self.redirect(self.url(self.context))
2269        return
2270
2271    @action(_('Register course list'), style='primary')
2272    def registerCourses(self, **data):
2273        self._registerCourses(**data)
2274        return
2275
2276
2277class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2278    """Add a course ticket by student.
2279    """
2280    grok.name('ctadd')
2281    grok.require('waeup.handleStudent')
2282    form_fields = grok.AutoFields(ICourseTicketAdd)
2283
2284    def update(self):
2285        if self.context.student.state != PAID or \
2286            not self.context.is_current_level:
2287            emit_lock_message(self)
2288            return
2289        super(CourseTicketAddFormPage2, self).update()
2290        return
2291
2292    @action(_('Add course ticket'))
2293    def addCourseTicket(self, **data):
2294        students_utils = getUtility(IStudentsUtils)
2295        # Safety belt
2296        if self.context.student.state != PAID:
2297            return
2298        ticket = createObject(u'waeup.CourseTicket')
2299        course = data['course']
2300        ticket.automatic = False
2301        ticket.carry_over = False
2302        max_credits = students_utils.maxCreditsExceeded(self.context, course)
2303        if max_credits:
2304            self.flash(_(
2305                'Your total credits exceed ${a}.',
2306                mapping = {'a': max_credits}))
2307            return
2308        try:
2309            self.context.addCourseTicket(ticket, course)
2310        except KeyError:
2311            self.flash(_('The ticket exists.'))
2312            return
2313        self.flash(_('Successfully added ${a}.',
2314            mapping = {'a':ticket.code}))
2315        self.redirect(self.url(self.context, u'@@edit'))
2316        return
2317
2318
2319class SetPasswordPage(KofaPage):
2320    grok.context(IKofaObject)
2321    grok.name('setpassword')
2322    grok.require('waeup.Anonymous')
2323    grok.template('setpassword')
2324    label = _('Set password for first-time login')
2325    ac_prefix = 'PWD'
2326    pnav = 0
2327    set_button = _('Set')
2328
2329    def update(self, SUBMIT=None):
2330        self.reg_number = self.request.form.get('reg_number', None)
2331        self.ac_series = self.request.form.get('ac_series', None)
2332        self.ac_number = self.request.form.get('ac_number', None)
2333
2334        if SUBMIT is None:
2335            return
2336        hitlist = search(query=self.reg_number,
2337            searchtype='reg_number', view=self)
2338        if not hitlist:
2339            self.flash(_('No student found.'))
2340            return
2341        if len(hitlist) != 1:   # Cannot happen but anyway
2342            self.flash(_('More than one student found.'))
2343            return
2344        student = hitlist[0].context
2345        self.student_id = student.student_id
2346        student_pw = student.password
2347        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2348        code = get_access_code(pin)
2349        if not code:
2350            self.flash(_('Access code is invalid.'))
2351            return
2352        if student_pw and pin == student.adm_code:
2353            self.flash(_(
2354                'Password has already been set. Your Student Id is ${a}',
2355                mapping = {'a':self.student_id}))
2356            return
2357        elif student_pw:
2358            self.flash(
2359                _('Password has already been set. You are using the ' +
2360                'wrong Access Code.'))
2361            return
2362        # Mark pin as used (this also fires a pin related transition)
2363        # and set student password
2364        if code.state == USED:
2365            self.flash(_('Access code has already been used.'))
2366            return
2367        else:
2368            comment = _(u"invalidated")
2369            # Here we know that the ac is in state initialized so we do not
2370            # expect an exception
2371            invalidate_accesscode(pin,comment)
2372            IUserAccount(student).setPassword(self.ac_number)
2373            student.adm_code = pin
2374        self.flash(_('Password has been set. Your Student Id is ${a}',
2375            mapping = {'a':self.student_id}))
2376        return
2377
2378class StudentRequestPasswordPage(KofaAddFormPage):
2379    """Captcha'd registration page for applicants.
2380    """
2381    grok.name('requestpw')
2382    grok.require('waeup.Anonymous')
2383    grok.template('requestpw')
2384    form_fields = grok.AutoFields(IStudentRequestPW).select(
2385        'firstname','number','email')
2386    label = _('Request password for first-time login')
2387
2388    def update(self):
2389        # Handle captcha
2390        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2391        self.captcha_result = self.captcha.verify(self.request)
2392        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2393        return
2394
2395    def _redirect(self, email, password, student_id):
2396        # Forward only email to landing page in base package.
2397        self.redirect(self.url(self.context, 'requestpw_complete',
2398            data = dict(email=email)))
2399        return
2400
2401    def _pw_used(self):
2402        # XXX: False if password has not been used. We need an extra
2403        #      attribute which remembers if student logged in.
2404        return True
2405
2406    @action(_('Send login credentials to email address'), style='primary')
2407    def get_credentials(self, **data):
2408        if not self.captcha_result.is_valid:
2409            # Captcha will display error messages automatically.
2410            # No need to flash something.
2411            return
2412        number = data.get('number','')
2413        firstname = data.get('firstname','')
2414        cat = getUtility(ICatalog, name='students_catalog')
2415        results = list(
2416            cat.searchResults(reg_number=(number, number)))
2417        if not results:
2418            results = list(
2419                cat.searchResults(matric_number=(number, number)))
2420        if results:
2421            student = results[0]
2422            if getattr(student,'firstname',None) is None:
2423                self.flash(_('An error occurred.'))
2424                return
2425            elif student.firstname.lower() != firstname.lower():
2426                # Don't tell the truth here. Anonymous must not
2427                # know that a record was found and only the firstname
2428                # verification failed.
2429                self.flash(_('No student record found.'))
2430                return
2431            elif student.password is not None and self._pw_used:
2432                self.flash(_('Your password has already been set and used. '
2433                             'Please proceed to the login page.'))
2434                return
2435            # Store email address but nothing else.
2436            student.email = data['email']
2437            notify(grok.ObjectModifiedEvent(student))
2438        else:
2439            # No record found, this is the truth.
2440            self.flash(_('No student record found.'))
2441            return
2442
2443        kofa_utils = getUtility(IKofaUtils)
2444        password = kofa_utils.genPassword()
2445        mandate = PasswordMandate()
2446        mandate.params['password'] = password
2447        mandate.params['user'] = student
2448        site = grok.getSite()
2449        site['mandates'].addMandate(mandate)
2450        # Send email with credentials
2451        args = {'mandate_id':mandate.mandate_id}
2452        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2453        url_info = u'Confirmation link: %s' % mandate_url
2454        msg = _('You have successfully requested a password for the')
2455        if kofa_utils.sendCredentials(IUserAccount(student),
2456            password, url_info, msg):
2457            email_sent = student.email
2458        else:
2459            email_sent = None
2460        self._redirect(email=email_sent, password=password,
2461            student_id=student.student_id)
2462        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2463        self.context.logger.info(
2464            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2465        return
2466
2467class StudentRequestPasswordEmailSent(KofaPage):
2468    """Landing page after successful password request.
2469
2470    """
2471    grok.name('requestpw_complete')
2472    grok.require('waeup.Public')
2473    grok.template('requestpwmailsent')
2474    label = _('Your password request was successful.')
2475
2476    def update(self, email=None, student_id=None, password=None):
2477        self.email = email
2478        self.password = password
2479        self.student_id = student_id
2480        return
Note: See TracBrowser for help on using the repository browser.