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

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

We need to reduce selectable payment categories. Instead of defining another interface we render the select box manually.

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