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

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

Add log message to payments.log after student payment approval.

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