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

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

We need to import credits and passmark of course tickets in order to repair them. Let's use the form field validation for import.

Both passmark and credits must not be edited via the UI.

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