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

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

Maybe this prevents pre-filling of contact form body field with values previously forwarded to ContactStudentForm?.

To Uli: Es gab ein sehr seltsames aber bei mir lokal nicht reproduzierbares Phänomen, dass beliebige Contact-Forms vorausgefüllt wurden mit dem letzten Mail-Body, den ein Clearance Officer versendet hat. Hier ist Kärungsbedarf.

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