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

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

Implement ExportPDFTranscriptPage.

  • Property svn:keywords set to Id
File size: 112.2 KB
RevLine 
[7191]1## $Id: browser.py 10250 2013-05-30 11:30:31Z 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
[7015]24from zope.event import notify
[7723]25from zope.i18n import translate
[6996]26from zope.catalog.interfaces import ICatalog
[7386]27from zope.component import queryUtility, getUtility, createObject
[9467]28from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
[7386]29from zope.formlib.textwidgets import BytesDisplayWidget
[10080]30from zope.security import checkPermission
[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)
[9797]38from waeup.kofa.browser.breadcrumbs import Breadcrumb
[9822]39from waeup.kofa.browser.pages import ContactAdminForm, ExportCSVView, doll_up
[9797]40from waeup.kofa.browser.resources import (
41    datepicker, datatable, tabs, warning, toggleall)
[7811]42from waeup.kofa.browser.layout import jsaction, action, UtilityView
[8779]43from waeup.kofa.browser.interfaces import ICaptchaManager
[9797]44from waeup.kofa.hostels.hostel import NOT_OCCUPIED
[7811]45from waeup.kofa.interfaces import (
[7819]46    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
[9797]47    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
[9833]48    academic_sessions_vocab, IJobManager, IDataCenter)
[7811]49from waeup.kofa.interfaces import MessageFactory as _
[8170]50from waeup.kofa.widgets.datewidget import (
51    FriendlyDateWidget, FriendlyDateDisplayWidget,
52    FriendlyDatetimeDisplayWidget)
[9797]53from waeup.kofa.mandates.mandate import PasswordMandate
[9806]54from waeup.kofa.university.interfaces import (
55    IDepartment, ICertificate, ICourse)
[9797]56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer,)
[10247]58from waeup.kofa.university.facultiescontainer import (
59    VirtualFacultiesExportJobContainer, FacultiesContainer)
[9842]60from waeup.kofa.university.certificate import (
61    VirtualCertificateExportJobContainer,)
[9843]62from waeup.kofa.university.course import (
63    VirtualCourseExportJobContainer,)
[9797]64from waeup.kofa.university.vocabularies import course_levels
[9813]65from waeup.kofa.utils.batching import VirtualExportJobContainer
[9797]66from waeup.kofa.utils.helpers import get_current_principal, to_timezone
[7868]67from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
[7811]68from waeup.kofa.students.interfaces import (
[7993]69    IStudentsContainer, IStudent,
70    IUGStudentClearance,IPGStudentClearance,
[9563]71    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
[10250]72    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
[7538]73    IStudentAccommodation, IStudentStudyLevel,
[6877]74    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
[9864]75    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
[9148]76    IBedTicket, IStudentsUtils, IStudentRequestPW
[6621]77    )
[9806]78from waeup.kofa.students.catalog import search, StudentQueryResultItem
[9804]79from waeup.kofa.students.export import EXPORTER_NAMES
[9797]80from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
81from waeup.kofa.students.vocabularies import StudyLevelSource
[8779]82from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
[9028]83    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
84    FORBIDDEN_POSTGRAD_TRANS)
[6621]85
[9797]86
[8779]87grok.context(IKofaObject) # Make IKofaObject the default context
88
[8737]89# Save function used for save methods in pages
90def msave(view, **data):
91    changed_fields = view.applyData(view.context, **data)
92    # Turn list of lists into single list
93    if changed_fields:
94        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
95    # Inform catalog if certificate has changed
96    # (applyData does this only for the context)
97    if 'certificate' in changed_fields:
98        notify(grok.ObjectModifiedEvent(view.context.student))
99    fields_string = ' + '.join(changed_fields)
100    view.flash(_('Form has been saved.'))
101    if fields_string:
102        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
103    return
104
[7145]105def emit_lock_message(view):
[7642]106    """Flash a lock message.
107    """
[7723]108    view.flash(_('The requested form is locked (read-only).'))
[7133]109    view.redirect(view.url(view.context))
110    return
111
[8921]112def translated_values(view):
[9685]113    """Translate course ticket attribute values to be displayed on
114    studylevel pages.
115    """
[8921]116    lang = view.request.cookies.get('kofa.language')
117    for value in view.context.values():
[9328]118        # We have to unghostify (according to Tres Seaver) the __dict__
119        # by activating the object, otherwise value_dict will be empty
120        # when calling the first time.
[9330]121        value._p_activate()
[8921]122        value_dict = dict([i for i in value.__dict__.items()])
[9698]123        value_dict['removable_by_student'] = value.removable_by_student
[8921]124        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
125            target_language=lang)
126        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
127            target_language=lang)
128        value_dict['automatic'] = translate(str(value.automatic), 'zope',
129            target_language=lang)
[9685]130        value_dict['grade'] = value.grade
131        value_dict['weight'] = value.weight
[8921]132        yield value_dict
133
[9814]134def clearance_disabled_message(student):
135    try:
136        session_config = grok.getSite()[
137            'configuration'][str(student.current_session)]
138    except KeyError:
139        return _('Session configuration object is not available.')
140    if not session_config.clearance_enabled:
141        return _('Clearance is disabled for this session.')
142    return None
143
[9895]144
145def addCourseTicket(view, course=None):
146    students_utils = getUtility(IStudentsUtils)
147    ticket = createObject(u'waeup.CourseTicket')
148    ticket.automatic = False
149    ticket.carry_over = False
150    max_credits = students_utils.maxCreditsExceeded(view.context, course)
151    if max_credits:
152        view.flash(_(
153            'Total credits exceed ${a}.',
154            mapping = {'a': max_credits}))
155        return False
156    try:
157        view.context.addCourseTicket(ticket, course)
158    except KeyError:
159        view.flash(_('The ticket exists.'))
160        return False
161    view.flash(_('Successfully added ${a}.',
162        mapping = {'a':ticket.code}))
[9924]163    view.context.writeLogMessage(
164        view,'added: %s|%s|%s' % (
[9925]165        ticket.code, ticket.level, ticket.level_session))
[9895]166    return True
167
[6629]168class StudentsBreadcrumb(Breadcrumb):
169    """A breadcrumb for the students container.
170    """
171    grok.context(IStudentsContainer)
[7723]172    title = _('Students')
[6629]173
[7459]174    @property
175    def target(self):
176        user = get_current_principal()
177        if getattr(user, 'user_type', None) == 'student':
178            return None
179        return self.viewname
180
[6818]181class StudentBreadcrumb(Breadcrumb):
182    """A breadcrumb for the student container.
183    """
184    grok.context(IStudent)
185
186    def title(self):
[7364]187        return self.context.display_fullname
[6818]188
[6635]189class SudyCourseBreadcrumb(Breadcrumb):
190    """A breadcrumb for the student study course.
191    """
192    grok.context(IStudentStudyCourse)
193
[9140]194    def title(self):
195        if self.context.is_current:
196            return _('Study Course')
197        else:
198            return _('Previous Study Course')
199
[6635]200class PaymentsBreadcrumb(Breadcrumb):
201    """A breadcrumb for the student payments folder.
202    """
[6859]203    grok.context(IStudentPaymentsContainer)
[7723]204    title = _('Payments')
[6635]205
[6870]206class OnlinePaymentBreadcrumb(Breadcrumb):
[7251]207    """A breadcrumb for payments.
[6870]208    """
[6877]209    grok.context(IStudentOnlinePayment)
[6870]210
211    @property
212    def title(self):
213        return self.context.p_id
214
[6635]215class AccommodationBreadcrumb(Breadcrumb):
216    """A breadcrumb for the student accommodation folder.
217    """
218    grok.context(IStudentAccommodation)
[7723]219    title = _('Accommodation')
[6635]220
[6994]221class BedTicketBreadcrumb(Breadcrumb):
222    """A breadcrumb for bed tickets.
223    """
224    grok.context(IBedTicket)
[7009]225
[6994]226    @property
227    def title(self):
[7723]228        return _('Bed Ticket ${a}',
229            mapping = {'a':self.context.getSessionString()})
[6994]230
[6776]231class StudyLevelBreadcrumb(Breadcrumb):
232    """A breadcrumb for course lists.
233    """
234    grok.context(IStudentStudyLevel)
235
236    @property
237    def title(self):
[7834]238        return self.context.level_title
[6776]239
[7819]240class StudentsContainerPage(KofaPage):
[6626]241    """The standard view for student containers.
[6621]242    """
243    grok.context(IStudentsContainer)
244    grok.name('index')
[7240]245    grok.require('waeup.viewStudentsContainer')
[6695]246    grok.template('containerpage')
[7723]247    label = _('Student Section')
[7735]248    search_button = _('Search')
[6642]249    pnav = 4
[6621]250
[6626]251    def update(self, *args, **kw):
252        datatable.need()
253        form = self.request.form
254        self.hitlist = []
[9795]255        if form.get('searchtype', None) == 'suspended':
256            self.searchtype = form['searchtype']
257            self.searchterm = None
258        elif 'searchterm' in form and form['searchterm']:
[6626]259            self.searchterm = form['searchterm']
260            self.searchtype = form['searchtype']
261        elif 'old_searchterm' in form:
262            self.searchterm = form['old_searchterm']
263            self.searchtype = form['old_searchtype']
264        else:
265            if 'search' in form:
[7745]266                self.flash(_('Empty search string'))
[6626]267            return
[7068]268        if self.searchtype == 'current_session':
[8081]269            try:
270                self.searchterm = int(self.searchterm)
271            except ValueError:
[8404]272                self.flash(_('Only year dates allowed (e.g. 2011).'))
[8081]273                return
[6626]274        self.hitlist = search(query=self.searchterm,
275            searchtype=self.searchtype, view=self)
276        if not self.hitlist:
[8404]277            self.flash(_('No student found.'))
[6626]278        return
279
[7819]280class StudentsContainerManagePage(KofaPage):
[6626]281    """The manage page for student containers.
[6622]282    """
283    grok.context(IStudentsContainer)
284    grok.name('manage')
[7136]285    grok.require('waeup.manageStudent')
[6695]286    grok.template('containermanagepage')
[6642]287    pnav = 4
[7723]288    label = _('Manage student section')
[7735]289    search_button = _('Search')
290    remove_button = _('Remove selected')
[6622]291
[6626]292    def update(self, *args, **kw):
293        datatable.need()
[6820]294        toggleall.need()
[7329]295        warning.need()
[6626]296        form = self.request.form
297        self.hitlist = []
[9795]298        if form.get('searchtype', None) == 'suspended':
299            self.searchtype = form['searchtype']
300            self.searchterm = None
301        elif 'searchterm' in form and form['searchterm']:
[6626]302            self.searchterm = form['searchterm']
303            self.searchtype = form['searchtype']
304        elif 'old_searchterm' in form:
305            self.searchterm = form['old_searchterm']
306            self.searchtype = form['old_searchtype']
307        else:
308            if 'search' in form:
[7745]309                self.flash(_('Empty search string'))
[6626]310            return
[8082]311        if self.searchtype == 'current_session':
312            try:
313                self.searchterm = int(self.searchterm)
314            except ValueError:
315                self.flash('Only year dates allowed (e.g. 2011).')
316                return
[6626]317        if not 'entries' in form:
318            self.hitlist = search(query=self.searchterm,
319                searchtype=self.searchtype, view=self)
320            if not self.hitlist:
[7723]321                self.flash(_('No student found.'))
[7459]322            if 'remove' in form:
[7723]323                self.flash(_('No item selected.'))
[6626]324            return
325        entries = form['entries']
326        if isinstance(entries, basestring):
327            entries = [entries]
328        deleted = []
329        for entry in entries:
330            if 'remove' in form:
331                del self.context[entry]
332                deleted.append(entry)
333        self.hitlist = search(query=self.searchterm,
334            searchtype=self.searchtype, view=self)
335        if len(deleted):
[7723]336            self.flash(_('Successfully removed: ${a}',
337                mapping = {'a':', '.join(deleted)}))
[6622]338        return
339
[7819]340class StudentAddFormPage(KofaAddFormPage):
[6622]341    """Add-form to add a student.
342    """
343    grok.context(IStudentsContainer)
[7136]344    grok.require('waeup.manageStudent')
[6622]345    grok.name('addstudent')
[7357]346    form_fields = grok.AutoFields(IStudent).select(
[7520]347        'firstname', 'middlename', 'lastname', 'reg_number')
[7723]348    label = _('Add student')
[6642]349    pnav = 4
[6622]350
[7723]351    @action(_('Create student record'), style='primary')
[6622]352    def addStudent(self, **data):
353        student = createObject(u'waeup.Student')
354        self.applyData(student, **data)
[6652]355        self.context.addStudent(student)
[7723]356        self.flash(_('Student record created.'))
[6651]357        self.redirect(self.url(self.context[student.student_id], 'index'))
[6622]358        return
359
[9338]360class LoginAsStudentStep1(KofaEditFormPage):
361    """ View to temporarily set a student password.
362    """
363    grok.context(IStudent)
364    grok.name('loginasstep1')
365    grok.require('waeup.loginAsStudent')
366    grok.template('loginasstep1')
367    pnav = 4
368
369    def label(self):
370        return _(u'Set temporary password for ${a}',
371            mapping = {'a':self.context.display_fullname})
372
373    @action('Set password now', style='primary')
374    def setPassword(self, *args, **data):
375        kofa_utils = getUtility(IKofaUtils)
376        password = kofa_utils.genPassword()
377        self.context.setTempPassword(self.request.principal.id, password)
378        self.context.writeLogMessage(
379            self, 'temp_password generated: %s' % password)
380        args = {'password':password}
381        self.redirect(self.url(self.context) +
382            '/loginasstep2?%s' % urlencode(args))
383        return
384
385class LoginAsStudentStep2(KofaPage):
386    """ View to temporarily login as student with a temporary password.
387    """
388    grok.context(IStudent)
389    grok.name('loginasstep2')
390    grok.require('waeup.Public')
391    grok.template('loginasstep2')
392    login_button = _('Login now')
393    pnav = 4
394
395    def label(self):
396        return _(u'Login as ${a}',
397            mapping = {'a':self.context.student_id})
398
399    def update(self, SUBMIT=None, password=None):
400        self.password = password
401        if SUBMIT is not None:
402            self.flash(_('You successfully logged in as student.'))
403            self.redirect(self.url(self.context))
404        return
405
[7819]406class StudentBaseDisplayFormPage(KofaDisplayFormPage):
[6631]407    """ Page to display student base data
408    """
[6622]409    grok.context(IStudent)
410    grok.name('index')
[6660]411    grok.require('waeup.viewStudent')
[6695]412    grok.template('basepage')
[9702]413    form_fields = grok.AutoFields(IStudentBase).omit(
414        'password', 'suspended', 'suspended_comment')
[6642]415    pnav = 4
[6622]416
417    @property
418    def label(self):
[8983]419        if self.context.suspended:
[9124]420            return _('${a}: Base Data (account deactivated)',
[8983]421                mapping = {'a':self.context.display_fullname})
422        return  _('${a}: Base Data',
[7723]423            mapping = {'a':self.context.display_fullname})
[6631]424
[6699]425    @property
426    def hasPassword(self):
427        if self.context.password:
[7723]428            return _('set')
429        return _('unset')
[6699]430
[9141]431class StudentBasePDFFormPage(KofaDisplayFormPage):
432    """ Page to display student base data in pdf files.
433    """
434
[10250]435    def __init__(self, context, request, omit_fields=()):
[9374]436        self.omit_fields = omit_fields
437        super(StudentBasePDFFormPage, self).__init__(context, request)
438
439    @property
440    def form_fields(self):
441        form_fields = grok.AutoFields(IStudentBase)
442        for field in self.omit_fields:
443            form_fields = form_fields.omit(field)
444        return form_fields
445
[7229]446class ContactStudentForm(ContactAdminForm):
447    grok.context(IStudent)
[7230]448    grok.name('contactstudent')
[7275]449    grok.require('waeup.viewStudent')
[7229]450    pnav = 4
451    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
452
[9484]453    def update(self, subject=u'', body=u''):
[9857]454        super(ContactStudentForm, self).update()
[7275]455        self.form_fields.get('subject').field.default = subject
[9484]456        self.form_fields.get('body').field.default = body
[9857]457        return
[7275]458
[7229]459    def label(self):
[7723]460        return _(u'Send message to ${a}',
461            mapping = {'a':self.context.display_fullname})
[7229]462
[7459]463    @action('Send message now', style='primary')
[7229]464    def send(self, *args, **data):
[7234]465        try:
[7403]466            email = self.request.principal.email
[7234]467        except AttributeError:
[7403]468            email = self.config.email_admin
469        usertype = getattr(self.request.principal,
470                           'user_type', 'system').title()
[7819]471        kofa_utils = getUtility(IKofaUtils)
[7811]472        success = kofa_utils.sendContactForm(
[7403]473                self.request.principal.title,email,
474                self.context.display_fullname,self.context.email,
475                self.request.principal.id,usertype,
476                self.config.name,
477                data['body'],data['subject'])
[7229]478        if success:
[7723]479            self.flash(_('Your message has been sent.'))
[7229]480        else:
[7723]481            self.flash(_('An smtp server error occurred.'))
[7229]482        return
483
[9191]484class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
485    """Deliver a PDF Admission slip.
486    """
487    grok.context(IStudent)
488    grok.name('admission_slip.pdf')
489    grok.require('waeup.viewStudent')
490    prefix = 'form'
491
492    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
493
494    @property
495    def label(self):
496        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
497        return translate(_('Admission Letter of'),
498            'waeup.kofa', target_language=portal_language) \
499            + ' %s' % self.context.display_fullname
500
501    def render(self):
502        students_utils = getUtility(IStudentsUtils)
503        return students_utils.renderPDFAdmissionLetter(self,
504            self.context.student)
505
[7819]506class StudentBaseManageFormPage(KofaEditFormPage):
[7133]507    """ View to manage student base data
[6631]508    """
509    grok.context(IStudent)
[7133]510    grok.name('manage_base')
[7136]511    grok.require('waeup.manageStudent')
[9124]512    form_fields = grok.AutoFields(IStudentBase).omit(
513        'student_id', 'adm_code', 'suspended')
[6695]514    grok.template('basemanagepage')
[7723]515    label = _('Manage base data')
[6642]516    pnav = 4
[6631]517
[6638]518    def update(self):
519        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]520        tabs.need()
[7490]521        self.tab1 = self.tab2 = ''
522        qs = self.request.get('QUERY_STRING', '')
523        if not qs:
524            qs = 'tab1'
525        setattr(self, qs, 'active')
[6638]526        super(StudentBaseManageFormPage, self).update()
527        self.wf_info = IWorkflowInfo(self.context)
528        return
529
[7723]530    @action(_('Save'), style='primary')
[6638]531    def save(self, **data):
[6701]532        form = self.request.form
[6790]533        password = form.get('password', None)
534        password_ctl = form.get('control_password', None)
535        if password:
[7147]536            validator = getUtility(IPasswordValidator)
537            errors = validator.validate_password(password, password_ctl)
538            if errors:
539                self.flash( ' '.join(errors))
540                return
541        changed_fields = self.applyData(self.context, **data)
[6771]542        # Turn list of lists into single list
543        if changed_fields:
544            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7147]545        else:
546            changed_fields = []
547        if password:
[9273]548            # Now we know that the form has no errors and can set password
[7147]549            IUserAccount(self.context).setPassword(password)
550            changed_fields.append('password')
551        fields_string = ' + '.join(changed_fields)
[7723]552        self.flash(_('Form has been saved.'))
[6644]553        if fields_string:
[8735]554            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[6638]555        return
556
[9273]557class StudentTriggerTransitionFormPage(KofaEditFormPage):
558    """ View to manage student base data
559    """
560    grok.context(IStudent)
561    grok.name('trigtrans')
562    grok.require('waeup.triggerTransition')
563    grok.template('trigtrans')
564    label = _('Trigger registration transition')
565    pnav = 4
566
567    def getTransitions(self):
568        """Return a list of dicts of allowed transition ids and titles.
569
570        Each list entry provides keys ``name`` and ``title`` for
571        internal name and (human readable) title of a single
572        transition.
573        """
574        wf_info = IWorkflowInfo(self.context)
575        allowed_transitions = [t for t in wf_info.getManualTransitions()
576            if not t[0].startswith('pay')]
[10155]577        if self.context.is_postgrad and not self.context.is_special_postgrad:
[9273]578            allowed_transitions = [t for t in allowed_transitions
579                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
580        return [dict(name='', title=_('No transition'))] +[
581            dict(name=x, title=y) for x, y in allowed_transitions]
582
583    @action(_('Save'), style='primary')
584    def save(self, **data):
585        form = self.request.form
[9701]586        if 'transition' in form and form['transition']:
[9273]587            transition_id = form['transition']
588            wf_info = IWorkflowInfo(self.context)
589            wf_info.fireTransition(transition_id)
590        return
591
[9124]592class StudentActivatePage(UtilityView, grok.View):
593    """ Activate student account
594    """
595    grok.context(IStudent)
596    grok.name('activate')
597    grok.require('waeup.manageStudent')
598
599    def update(self):
600        self.context.suspended = False
601        self.context.writeLogMessage(self, 'account activated')
602        history = IObjectHistory(self.context)
603        history.addMessage('Student account activated')
604        self.flash(_('Student account has been activated.'))
605        self.redirect(self.url(self.context))
606        return
607
608    def render(self):
609        return
610
611class StudentDeactivatePage(UtilityView, grok.View):
612    """ Deactivate student account
613    """
614    grok.context(IStudent)
615    grok.name('deactivate')
616    grok.require('waeup.manageStudent')
617
618    def update(self):
619        self.context.suspended = True
620        self.context.writeLogMessage(self, 'account deactivated')
621        history = IObjectHistory(self.context)
622        history.addMessage('Student account deactivated')
623        self.flash(_('Student account has been deactivated.'))
624        self.redirect(self.url(self.context))
625        return
626
627    def render(self):
628        return
629
[7819]630class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
[6631]631    """ Page to display student clearance data
632    """
633    grok.context(IStudent)
634    grok.name('view_clearance')
[6660]635    grok.require('waeup.viewStudent')
[6642]636    pnav = 4
[6631]637
638    @property
[8099]639    def separators(self):
640        return getUtility(IStudentsUtils).SEPARATORS_DICT
641
642    @property
[7993]643    def form_fields(self):
[8472]644        if self.context.is_postgrad:
[8977]645            form_fields = grok.AutoFields(
646                IPGStudentClearance).omit('clearance_locked')
[7993]647        else:
[8977]648            form_fields = grok.AutoFields(
649                IUGStudentClearance).omit('clearance_locked')
[9486]650        if not getattr(self.context, 'officer_comment'):
651            form_fields = form_fields.omit('officer_comment')
[9484]652        else:
[9486]653            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
[7993]654        return form_fields
655
656    @property
[6631]657    def label(self):
[7723]658        return _('${a}: Clearance Data',
659            mapping = {'a':self.context.display_fullname})
[6631]660
[7277]661class ExportPDFClearanceSlipPage(grok.View):
662    """Deliver a PDF slip of the context.
663    """
664    grok.context(IStudent)
[9452]665    grok.name('clearance_slip.pdf')
[7277]666    grok.require('waeup.viewStudent')
667    prefix = 'form'
[9702]668    omit_fields = (
669        'password', 'suspended', 'phone',
670        'adm_code', 'suspended_comment')
[7277]671
672    @property
[7993]673    def form_fields(self):
[8472]674        if self.context.is_postgrad:
[8977]675            form_fields = grok.AutoFields(
676                IPGStudentClearance).omit('clearance_locked')
[7993]677        else:
[8977]678            form_fields = grok.AutoFields(
679                IUGStudentClearance).omit('clearance_locked')
[9486]680        if not getattr(self.context, 'officer_comment'):
681            form_fields = form_fields.omit('officer_comment')
[7993]682        return form_fields
683
684    @property
[7723]685    def title(self):
[7819]686        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]687        return translate(_('Clearance Data'), 'waeup.kofa',
[7723]688            target_language=portal_language)
689
690    @property
[7277]691    def label(self):
[7819]692        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9026]693        return translate(_('Clearance Slip of'),
[7811]694            'waeup.kofa', target_language=portal_language) \
[7723]695            + ' %s' % self.context.display_fullname
[7277]696
[9969]697    # XXX: not used in waeup.kofa and thus not tested
[9010]698    def _signatures(self):
[9548]699        isStudent = getattr(
700            self.request.principal, 'user_type', None) == 'student'
701        if not isStudent and self.context.state in (CLEARED, ):
[9969]702            return ([_('Student Signature')],
703                    [_('Clearance Officer Signature')])
[9010]704        return
705
[9555]706    def _sigsInFooter(self):
[9548]707        isStudent = getattr(
708            self.request.principal, 'user_type', None) == 'student'
709        if not isStudent and self.context.state in (CLEARED, ):
[9555]710            return (_('Date, Student Signature'),
711                    _('Date, Clearance Officer Signature'),
712                    )
[9557]713        return ()
[9548]714
[7277]715    def render(self):
[9141]716        studentview = StudentBasePDFFormPage(self.context.student,
[9375]717            self.request, self.omit_fields)
[7277]718        students_utils = getUtility(IStudentsUtils)
719        return students_utils.renderPDF(
[9452]720            self, 'clearance_slip.pdf',
[9548]721            self.context.student, studentview, signatures=self._signatures(),
[10250]722            sigs_in_footer=self._sigsInFooter(),
723            omit_fields=self.omit_fields)
[7277]724
[7819]725class StudentClearanceManageFormPage(KofaEditFormPage):
[8120]726    """ Page to manage student clearance data
[6631]727    """
728    grok.context(IStudent)
[8119]729    grok.name('manage_clearance')
[7136]730    grok.require('waeup.manageStudent')
[7134]731    grok.template('clearanceeditpage')
[7723]732    label = _('Manage clearance data')
[6642]733    pnav = 4
[6650]734
[7993]735    @property
[8099]736    def separators(self):
737        return getUtility(IStudentsUtils).SEPARATORS_DICT
738
739    @property
[7993]740    def form_fields(self):
[8472]741        if self.context.is_postgrad:
[8977]742            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
[7993]743        else:
[8977]744            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
[7993]745        return form_fields
746
[6650]747    def update(self):
748        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]749        tabs.need()
[7490]750        self.tab1 = self.tab2 = ''
751        qs = self.request.get('QUERY_STRING', '')
752        if not qs:
753            qs = 'tab1'
754        setattr(self, qs, 'active')
[6650]755        return super(StudentClearanceManageFormPage, self).update()
756
[7723]757    @action(_('Save'), style='primary')
[6695]758    def save(self, **data):
[6762]759        msave(self, **data)
[6695]760        return
761
[7459]762class StudentClearPage(UtilityView, grok.View):
[7158]763    """ Clear student by clearance officer
764    """
765    grok.context(IStudent)
766    grok.name('clear')
767    grok.require('waeup.clearStudent')
768
769    def update(self):
[9814]770        if clearance_disabled_message(self.context):
771            self.flash(clearance_disabled_message(self.context))
772            self.redirect(self.url(self.context,'view_clearance'))
773            return
[7158]774        if self.context.state == REQUESTED:
775            IWorkflowInfo(self.context).fireTransition('clear')
[7723]776            self.flash(_('Student has been cleared.'))
[7158]777        else:
[7723]778            self.flash(_('Student is in wrong state.'))
[7158]779        self.redirect(self.url(self.context,'view_clearance'))
780        return
781
782    def render(self):
783        return
784
[9484]785class StudentRejectClearancePage(KofaEditFormPage):
[7158]786    """ Reject clearance by clearance officers
787    """
788    grok.context(IStudent)
789    grok.name('reject_clearance')
[9484]790    label = _('Reject clearance')
[7158]791    grok.require('waeup.clearStudent')
[9484]792    form_fields = grok.AutoFields(
[9486]793        IUGStudentClearance).select('officer_comment')
[7158]794
[9814]795    def update(self):
796        if clearance_disabled_message(self.context):
797            self.flash(clearance_disabled_message(self.context))
798            self.redirect(self.url(self.context,'view_clearance'))
799            return
800        return super(StudentRejectClearancePage, self).update()
801
[9484]802    @action(_('Save comment and reject clearance now'), style='primary')
803    def reject(self, **data):
[7158]804        if self.context.state == CLEARED:
805            IWorkflowInfo(self.context).fireTransition('reset4')
[7723]806            message = _('Clearance has been annulled.')
[7275]807            self.flash(message)
[7158]808        elif self.context.state == REQUESTED:
809            IWorkflowInfo(self.context).fireTransition('reset3')
[7723]810            message = _('Clearance request has been rejected.')
[7275]811            self.flash(message)
[7158]812        else:
[7723]813            self.flash(_('Student is in wrong state.'))
[7334]814            self.redirect(self.url(self.context,'view_clearance'))
[7275]815            return
[9484]816        self.applyData(self.context, **data)
[9486]817        comment = data['officer_comment']
[9556]818        if comment:
819            self.context.writeLogMessage(
820                self, 'comment: %s' % comment.replace('\n', '<br>'))
821            args = {'subject':message, 'body':comment}
822        else:
823            args = {'subject':message,}
[7275]824        self.redirect(self.url(self.context) +
825            '/contactstudent?%s' % urlencode(args))
[7158]826        return
827
828
[7819]829class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
[6631]830    """ Page to display student personal data
831    """
832    grok.context(IStudent)
833    grok.name('view_personal')
[6660]834    grok.require('waeup.viewStudent')
[6631]835    form_fields = grok.AutoFields(IStudentPersonal)
[7386]836    form_fields['perm_address'].custom_widget = BytesDisplayWidget
[9543]837    form_fields[
838        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6642]839    pnav = 4
[6631]840
841    @property
842    def label(self):
[7723]843        return _('${a}: Personal Data',
844            mapping = {'a':self.context.display_fullname})
[6631]845
[8903]846class StudentPersonalManageFormPage(KofaEditFormPage):
847    """ Page to manage personal data
[6631]848    """
849    grok.context(IStudent)
[8903]850    grok.name('manage_personal')
851    grok.require('waeup.manageStudent')
[9553]852    form_fields = grok.AutoFields(IStudentPersonal)
853    form_fields['personal_updated'].for_display = True
[9571]854    form_fields[
855        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[8903]856    label = _('Manage personal data')
[6642]857    pnav = 4
[6631]858
[7723]859    @action(_('Save'), style='primary')
[6762]860    def save(self, **data):
861        msave(self, **data)
862        return
863
[9543]864class StudentPersonalEditFormPage(KofaEditFormPage):
[8903]865    """ Page to edit personal data
866    """
[9543]867    grok.context(IStudent)
[8903]868    grok.name('edit_personal')
869    grok.require('waeup.handleStudent')
[9563]870    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
[8903]871    label = _('Edit personal data')
872    pnav = 4
873
[9543]874    @action(_('Save/Confirm'), style='primary')
875    def save(self, **data):
876        msave(self, **data)
[9569]877        self.context.personal_updated = datetime.utcnow()
[9543]878        return
879
[7819]880class StudyCourseDisplayFormPage(KofaDisplayFormPage):
[6635]881    """ Page to display the student study course data
882    """
883    grok.context(IStudentStudyCourse)
884    grok.name('index')
[6660]885    grok.require('waeup.viewStudent')
[6775]886    grok.template('studycoursepage')
[6642]887    pnav = 4
[6635]888
889    @property
[8972]890    def form_fields(self):
891        if self.context.is_postgrad:
892            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
[9723]893                'previous_verdict')
[8972]894        else:
895            form_fields = grok.AutoFields(IStudentStudyCourse)
896        return form_fields
897
898    @property
[6635]899    def label(self):
[9140]900        if self.context.is_current:
901            return _('${a}: Study Course',
902                mapping = {'a':self.context.__parent__.display_fullname})
903        else:
904            return _('${a}: Previous Study Course',
905                mapping = {'a':self.context.__parent__.display_fullname})
[6635]906
[6912]907    @property
908    def current_mode(self):
[7641]909        if self.context.certificate is not None:
[7841]910            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
[7681]911            return studymodes_dict[self.context.certificate.study_mode]
[7171]912        return
[7642]913
[7171]914    @property
915    def department(self):
[7205]916        if self.context.certificate is not None:
[7171]917            return self.context.certificate.__parent__.__parent__
918        return
[6912]919
[7171]920    @property
921    def faculty(self):
[7205]922        if self.context.certificate is not None:
[7171]923            return self.context.certificate.__parent__.__parent__.__parent__
924        return
925
[9140]926    @property
927    def prev_studycourses(self):
928        if self.context.is_current:
929            if self.context.__parent__.get('studycourse_2', None) is not None:
930                return (
931                        {'href':self.url(self.context.student) + '/studycourse_1',
932                        'title':_('First Study Course, ')},
933                        {'href':self.url(self.context.student) + '/studycourse_2',
934                        'title':_('Second Study Course')}
935                        )
936            if self.context.__parent__.get('studycourse_1', None) is not None:
937                return (
938                        {'href':self.url(self.context.student) + '/studycourse_1',
939                        'title':_('First Study Course')},
940                        )
941        return
942
[7819]943class StudyCourseManageFormPage(KofaEditFormPage):
[6649]944    """ Page to edit the student study course data
945    """
946    grok.context(IStudentStudyCourse)
[6775]947    grok.name('manage')
[7136]948    grok.require('waeup.manageStudent')
[6775]949    grok.template('studycoursemanagepage')
[7723]950    label = _('Manage study course')
[6649]951    pnav = 4
[7723]952    taboneactions = [_('Save'),_('Cancel')]
953    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
954    tabthreeactions = [_('Add study level')]
[6649]955
[8972]956    @property
957    def form_fields(self):
958        if self.context.is_postgrad:
959            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
[9723]960                'previous_verdict')
[8972]961        else:
962            form_fields = grok.AutoFields(IStudentStudyCourse)
963        return form_fields
964
[6775]965    def update(self):
[9139]966        if not self.context.is_current:
967            emit_lock_message(self)
968            return
[6775]969        super(StudyCourseManageFormPage, self).update()
970        tabs.need()
[7484]971        self.tab1 = self.tab2 = ''
972        qs = self.request.get('QUERY_STRING', '')
973        if not qs:
974            qs = 'tab1'
975        setattr(self, qs, 'active')
[7490]976        warning.need()
977        datatable.need()
978        return
[6775]979
[7723]980    @action(_('Save'), style='primary')
[6761]981    def save(self, **data):
[8099]982        try:
983            msave(self, **data)
984        except ConstraintNotSatisfied:
985            # The selected level might not exist in certificate
986            self.flash(_('Current level not available for certificate.'))
987            return
[8081]988        notify(grok.ObjectModifiedEvent(self.context.__parent__))
[6761]989        return
990
[6775]991    @property
992    def level_dict(self):
993        studylevelsource = StudyLevelSource().factory
994        for code in studylevelsource.getValues(self.context):
995            title = studylevelsource.getTitle(self.context, code)
996            yield(dict(code=code, title=title))
997
[9437]998    @property
999    def session_dict(self):
1000        yield(dict(code='', title='--'))
1001        for item in academic_sessions():
1002            code = item[1]
1003            title = item[0]
1004            yield(dict(code=code, title=title))
1005
[7723]1006    @action(_('Add study level'))
[6774]1007    def addStudyLevel(self, **data):
[6775]1008        level_code = self.request.form.get('addlevel', None)
[9437]1009        level_session = self.request.form.get('level_session', None)
1010        if not level_session:
1011            self.flash(_('You must select a session for the level.'))
1012            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1013            return
[8323]1014        studylevel = createObject(u'waeup.StudentStudyLevel')
[6775]1015        studylevel.level = int(level_code)
[9437]1016        studylevel.level_session = int(level_session)
[6775]1017        try:
[6782]1018            self.context.addStudentStudyLevel(
1019                self.context.certificate,studylevel)
[7723]1020            self.flash(_('Study level has been added.'))
[6775]1021        except KeyError:
[7723]1022            self.flash(_('This level exists.'))
[7484]1023        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6774]1024        return
1025
[7723]1026    @jsaction(_('Remove selected levels'))
[6775]1027    def delStudyLevels(self, **data):
1028        form = self.request.form
[9701]1029        if 'val_id' in form:
[6775]1030            child_id = form['val_id']
1031        else:
[7723]1032            self.flash(_('No study level selected.'))
[7484]1033            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6775]1034            return
1035        if not isinstance(child_id, list):
1036            child_id = [child_id]
1037        deleted = []
1038        for id in child_id:
[7723]1039            del self.context[id]
1040            deleted.append(id)
[6775]1041        if len(deleted):
[7723]1042            self.flash(_('Successfully removed: ${a}',
1043                mapping = {'a':', '.join(deleted)}))
[9332]1044            self.context.writeLogMessage(
1045                self,'removed: %s' % ', '.join(deleted))
[7484]1046        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6775]1047        return
[6774]1048
[10178]1049class StudyCourseTranscriptPage(KofaDisplayFormPage):
1050    """ Page to display the student's transcript.
1051    """
[10250]1052    grok.context(IStudentStudyCourseTranscript)
[10178]1053    grok.name('transcript')
1054    grok.require('waeup.viewStudent')
1055    grok.template('transcript')
1056    pnav = 4
1057
1058    def update(self):
1059        super(StudyCourseTranscriptPage, self).update()
1060        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1061        self.session_dict = dict(
1062            [(item[1], item[0]) for item in academic_sessions()])
[10179]1063        self.course_levels = course_levels
[10178]1064        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1065        return
1066
1067    @property
1068    def label(self):
1069        # Here we know that the cookie has been set
1070        lang = self.request.cookies.get('kofa.language')
1071        return _('${a}: Transcript Data', mapping = {
1072            'a':self.context.student.display_fullname})
1073
[10250]1074class ExportPDFTranscriptPage(UtilityView, grok.View):
1075    """Deliver a PDF slip of the context.
1076    """
1077    grok.context(IStudentStudyCourse)
1078    grok.name('transcript.pdf')
1079    grok.require('waeup.viewStudent')
1080    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1081    prefix = 'form'
1082    omit_fields = (
1083        'department', 'faculty', 'entry_session', 'certificate',
1084        'password', 'suspended', 'phone', 'email',
1085        'adm_code', 'sex', 'suspended_comment')
1086
1087    def update(self):
1088        super(ExportPDFTranscriptPage, self).update()
1089        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1090        self.session_dict = dict(
1091            [(item[1], item[0]) for item in academic_sessions()])
1092        self.course_levels = course_levels
1093        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1094        return
1095
1096    @property
1097    def label(self):
1098        # Here we know that the cookie has been set
1099        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1100        return translate(_('Academic Transcript'),
1101            'waeup.kofa', target_language=portal_language)
1102
1103    def render(self):
1104        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1105        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1106        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1107        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1108        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1109        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1110        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1111        studentview = StudentBasePDFFormPage(self.context.student,
1112            self.request, self.omit_fields)
1113        students_utils = getUtility(IStudentsUtils)
1114
1115        tableheader = [(Code,'code', 2.5),
1116                         (Title,'title', 7),
1117                         (Sem, 'semester', 1.5),
1118                         (Cred, 'credits', 1.5),
1119                         (Score, 'score', 1.5),
1120                         (Grade, 'grade', 1.5),
1121                         ]
1122
1123        return students_utils.renderPDFTranscript(
1124            self, 'transcript.pdf',
1125            self.context.student, studentview,
1126            omit_fields=self.omit_fields,
1127            tableheader=tableheader
1128            )
1129
[9138]1130class StudentTransferFormPage(KofaAddFormPage):
1131    """Page to transfer the student.
1132    """
1133    grok.context(IStudent)
1134    grok.name('transfer')
1135    grok.require('waeup.manageStudent')
1136    label = _('Transfer student')
1137    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1138        'entry_mode', 'entry_session')
1139    pnav = 4
1140
1141    def update(self):
1142        super(StudentTransferFormPage, self).update()
1143        warning.need()
1144        return
1145
1146    @jsaction(_('Transfer'))
1147    def transferStudent(self, **data):
1148        error = self.context.transfer(**data)
1149        if error == -1:
1150            self.flash(_('Current level does not match certificate levels.'))
1151        elif error == -2:
1152            self.flash(_('Former study course record incomplete.'))
1153        elif error == -3:
1154            self.flash(_('Maximum number of transfers exceeded.'))
1155        else:
1156            self.flash(_('Successfully transferred.'))
1157        return
1158
[10060]1159class RevertTransferFormPage(KofaEditFormPage):
1160    """View that reverts the previous transfer.
1161    """
1162    grok.context(IStudent)
1163    grok.name('revert_transfer')
1164    grok.require('waeup.manageStudent')
1165    grok.template('reverttransfer')
1166    label = _('Revert previous transfer')
1167
1168    def update(self):
1169        warning.need()
1170        if not self.context.has_key('studycourse_1'):
1171            self.flash(_('No previous transfer.'))
1172            self.redirect(self.url(self.context))
1173            return
1174        return
1175
1176    @jsaction(_('Revert now'))
1177    def transferStudent(self, **data):
1178        self.context.revert_transfer()
1179        self.flash(_('Previous transfer reverted.'))
1180        self.redirect(self.url(self.context, 'studycourse'))
1181        return
1182
[7819]1183class StudyLevelDisplayFormPage(KofaDisplayFormPage):
[6774]1184    """ Page to display student study levels
1185    """
1186    grok.context(IStudentStudyLevel)
1187    grok.name('index')
1188    grok.require('waeup.viewStudent')
[6775]1189    form_fields = grok.AutoFields(IStudentStudyLevel)
[9161]1190    form_fields[
1191        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6783]1192    grok.template('studylevelpage')
[6774]1193    pnav = 4
1194
[7310]1195    def update(self):
1196        super(StudyLevelDisplayFormPage, self).update()
1197        datatable.need()
1198        return
1199
[6774]1200    @property
[8141]1201    def translated_values(self):
[8921]1202        return translated_values(self)
[8141]1203
1204    @property
[6774]1205    def label(self):
[7833]1206        # Here we know that the cookie has been set
1207        lang = self.request.cookies.get('kofa.language')
[7811]1208        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1209            target_language=lang)
1210        return _('${a}: Study Level ${b}', mapping = {
[8736]1211            'a':self.context.student.display_fullname,
[7723]1212            'b':level_title})
[6774]1213
[7459]1214class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
[7028]1215    """Deliver a PDF slip of the context.
1216    """
1217    grok.context(IStudentStudyLevel)
[9452]1218    grok.name('course_registration_slip.pdf')
[7028]1219    grok.require('waeup.viewStudent')
1220    form_fields = grok.AutoFields(IStudentStudyLevel)
[9683]1221    form_fields[
1222        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7028]1223    prefix = 'form'
[9702]1224    omit_fields = (
1225        'password', 'suspended', 'phone',
1226        'adm_code', 'sex', 'suspended_comment')
[7028]1227
1228    @property
[7723]1229    def title(self):
[7819]1230        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1231        return translate(_('Level Data'), 'waeup.kofa',
[7723]1232            target_language=portal_language)
1233
1234    @property
[9906]1235    def content_title_1(self):
[7819]1236        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9906]1237        return translate(_('1st Semester Courses'), 'waeup.kofa',
[7723]1238            target_language=portal_language)
1239
1240    @property
[9906]1241    def content_title_2(self):
1242        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1243        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1244            target_language=portal_language)
1245
1246    @property
[9957]1247    def content_title_3(self):
1248        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1249        return translate(_('Level Courses'), 'waeup.kofa',
1250            target_language=portal_language)
1251
1252    @property
[7028]1253    def label(self):
[7819]1254        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1255        lang = self.request.cookies.get('kofa.language', portal_language)
1256        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1257            target_language=lang)
[8141]1258        return translate(_('Course Registration Slip'),
[7811]1259            'waeup.kofa', target_language=portal_language) \
[7723]1260            + ' %s' % level_title
[7028]1261
1262    def render(self):
[7819]1263        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1264        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1265        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1266        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1267        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1268        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1269        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
[9906]1270        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
[7811]1271        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
[9810]1272        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
[9141]1273        studentview = StudentBasePDFFormPage(self.context.student,
[9375]1274            self.request, self.omit_fields)
[7150]1275        students_utils = getUtility(IStudentsUtils)
[9906]1276        tabledata_1 = sorted(
1277            [value for value in self.context.values() if value.semester == 1],
[7318]1278            key=lambda value: str(value.semester) + value.code)
[9906]1279        tabledata_2 = sorted(
1280            [value for value in self.context.values() if value.semester == 2],
1281            key=lambda value: str(value.semester) + value.code)
[9957]1282        tabledata_3 = sorted(
1283            [value for value in self.context.values() if value.semester == 3],
1284            key=lambda value: str(value.semester) + value.code)
[9906]1285        tableheader = [(Code,'code', 2.5),
[7723]1286                         (Title,'title', 5),
1287                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1288                         (Cred, 'credits', 1.5),
[9906]1289                         #(Mand, 'mandatory', 1.5),
[8141]1290                         (Score, 'score', 1.5),
[9810]1291                         (Grade, 'grade', 1.5),
[8141]1292                         #('Auto', 'automatic', 1.5)
[9906]1293                         ]
1294        return students_utils.renderPDF(
1295            self, 'course_registration_slip.pdf',
1296            self.context.student, studentview,
1297            tableheader_1=tableheader,
1298            tabledata_1=tabledata_1,
1299            tableheader_2=tableheader,
[9957]1300            tabledata_2=tabledata_2,
1301            tableheader_3=tableheader,
[10250]1302            tabledata_3=tabledata_3,
1303            omit_fields=self.omit_fields
[9906]1304            )
[7028]1305
[7819]1306class StudyLevelManageFormPage(KofaEditFormPage):
[6792]1307    """ Page to edit the student study level data
1308    """
1309    grok.context(IStudentStudyLevel)
1310    grok.name('manage')
[7136]1311    grok.require('waeup.manageStudent')
[6792]1312    grok.template('studylevelmanagepage')
[9161]1313    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
[9690]1314        'validation_date', 'validated_by', 'total_credits', 'gpa')
[6792]1315    pnav = 4
[7723]1316    taboneactions = [_('Save'),_('Cancel')]
1317    tabtwoactions = [_('Add course ticket'),
1318        _('Remove selected tickets'),_('Cancel')]
[6792]1319
[9895]1320    def update(self, ADD=None, course=None):
[9139]1321        if not self.context.__parent__.is_current:
1322            emit_lock_message(self)
1323            return
[6792]1324        super(StudyLevelManageFormPage, self).update()
1325        tabs.need()
[7484]1326        self.tab1 = self.tab2 = ''
1327        qs = self.request.get('QUERY_STRING', '')
1328        if not qs:
1329            qs = 'tab1'
1330        setattr(self, qs, 'active')
[7490]1331        warning.need()
1332        datatable.need()
[9895]1333        if ADD is not None:
1334            if not course:
1335                self.flash(_('No valid course code entered.'))
1336                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1337                return
1338            cat = queryUtility(ICatalog, name='courses_catalog')
1339            result = cat.searchResults(code=(course, course))
1340            if len(result) != 1:
1341                self.flash(_('Course not found.'))
1342            else:
1343                course = list(result)[0]
1344                addCourseTicket(self, course)
1345            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]1346        return
1347
1348    @property
[8921]1349    def translated_values(self):
1350        return translated_values(self)
1351
1352    @property
[6792]1353    def label(self):
[7833]1354        # Here we know that the cookie has been set
1355        lang = self.request.cookies.get('kofa.language')
[7811]1356        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1357            target_language=lang)
1358        return _('Manage study level ${a}',
1359            mapping = {'a':level_title})
[6792]1360
[7723]1361    @action(_('Save'), style='primary')
[6792]1362    def save(self, **data):
1363        msave(self, **data)
1364        return
1365
[7723]1366    @jsaction(_('Remove selected tickets'))
[6792]1367    def delCourseTicket(self, **data):
1368        form = self.request.form
[9701]1369        if 'val_id' in form:
[6792]1370            child_id = form['val_id']
1371        else:
[7723]1372            self.flash(_('No ticket selected.'))
[7484]1373            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6792]1374            return
1375        if not isinstance(child_id, list):
1376            child_id = [child_id]
1377        deleted = []
1378        for id in child_id:
[7723]1379            del self.context[id]
1380            deleted.append(id)
[6792]1381        if len(deleted):
[7723]1382            self.flash(_('Successfully removed: ${a}',
1383                mapping = {'a':', '.join(deleted)}))
[9332]1384            self.context.writeLogMessage(
[9924]1385                self,'removed: %s at %s' %
1386                (', '.join(deleted), self.context.level))
[7484]1387        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]1388        return
1389
[7459]1390class ValidateCoursesPage(UtilityView, grok.View):
[7334]1391    """ Validate course list by course adviser
1392    """
1393    grok.context(IStudentStudyLevel)
1394    grok.name('validate_courses')
1395    grok.require('waeup.validateStudent')
1396
1397    def update(self):
[9139]1398        if not self.context.__parent__.is_current:
1399            emit_lock_message(self)
1400            return
[7334]1401        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]1402            self.flash(_('This level does not correspond current level.'))
[8736]1403        elif self.context.student.state == REGISTERED:
1404            IWorkflowInfo(self.context.student).fireTransition(
[7642]1405                'validate_courses')
[7723]1406            self.flash(_('Course list has been validated.'))
[7334]1407        else:
[7723]1408            self.flash(_('Student is in the wrong state.'))
[7334]1409        self.redirect(self.url(self.context))
1410        return
1411
1412    def render(self):
1413        return
1414
[7459]1415class RejectCoursesPage(UtilityView, grok.View):
[7334]1416    """ Reject course list by course adviser
1417    """
1418    grok.context(IStudentStudyLevel)
1419    grok.name('reject_courses')
1420    grok.require('waeup.validateStudent')
1421
1422    def update(self):
[9139]1423        if not self.context.__parent__.is_current:
1424            emit_lock_message(self)
1425            return
[7334]1426        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]1427            self.flash(_('This level does not correspond current level.'))
[7334]1428            self.redirect(self.url(self.context))
1429            return
[8736]1430        elif self.context.student.state == VALIDATED:
1431            IWorkflowInfo(self.context.student).fireTransition('reset8')
[7723]1432            message = _('Course list request has been annulled.')
[7334]1433            self.flash(message)
[8736]1434        elif self.context.student.state == REGISTERED:
1435            IWorkflowInfo(self.context.student).fireTransition('reset7')
[7723]1436            message = _('Course list request has been rejected:')
[7334]1437            self.flash(message)
1438        else:
[7723]1439            self.flash(_('Student is in the wrong state.'))
[7334]1440            self.redirect(self.url(self.context))
1441            return
1442        args = {'subject':message}
[8736]1443        self.redirect(self.url(self.context.student) +
[7334]1444            '/contactstudent?%s' % urlencode(args))
1445        return
1446
1447    def render(self):
1448        return
1449
[7819]1450class CourseTicketAddFormPage(KofaAddFormPage):
[6808]1451    """Add a course ticket.
[6795]1452    """
1453    grok.context(IStudentStudyLevel)
1454    grok.name('add')
[7136]1455    grok.require('waeup.manageStudent')
[7723]1456    label = _('Add course ticket')
[9420]1457    form_fields = grok.AutoFields(ICourseTicketAdd)
[6795]1458    pnav = 4
1459
[9139]1460    def update(self):
1461        if not self.context.__parent__.is_current:
1462            emit_lock_message(self)
1463            return
1464        super(CourseTicketAddFormPage, self).update()
1465        return
1466
[7723]1467    @action(_('Add course ticket'))
[6795]1468    def addCourseTicket(self, **data):
1469        course = data['course']
[9895]1470        success = addCourseTicket(self, course)
1471        if success:
1472            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6795]1473        return
1474
[7834]1475    @action(_('Cancel'), validator=NullValidator)
[6795]1476    def cancel(self, **data):
1477        self.redirect(self.url(self.context))
1478
[7819]1479class CourseTicketDisplayFormPage(KofaDisplayFormPage):
[6796]1480    """ Page to display course tickets
1481    """
1482    grok.context(ICourseTicket)
1483    grok.name('index')
1484    grok.require('waeup.viewStudent')
1485    form_fields = grok.AutoFields(ICourseTicket)
[9684]1486    grok.template('courseticketpage')
[6796]1487    pnav = 4
1488
1489    @property
1490    def label(self):
[7723]1491        return _('${a}: Course Ticket ${b}', mapping = {
[8736]1492            'a':self.context.student.display_fullname,
[7723]1493            'b':self.context.code})
[6796]1494
[7819]1495class CourseTicketManageFormPage(KofaEditFormPage):
[6796]1496    """ Page to manage course tickets
1497    """
1498    grok.context(ICourseTicket)
1499    grok.name('manage')
[7136]1500    grok.require('waeup.manageStudent')
[9420]1501    form_fields = grok.AutoFields(ICourseTicket)
1502    form_fields['title'].for_display = True
1503    form_fields['fcode'].for_display = True
1504    form_fields['dcode'].for_display = True
1505    form_fields['semester'].for_display = True
1506    form_fields['passmark'].for_display = True
1507    form_fields['credits'].for_display = True
[9698]1508    form_fields['mandatory'].for_display = False
[9420]1509    form_fields['automatic'].for_display = True
[9422]1510    form_fields['carry_over'].for_display = True
[6796]1511    pnav = 4
[9697]1512    grok.template('courseticketmanagepage')
[6796]1513
1514    @property
1515    def label(self):
[7723]1516        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
[6796]1517
[7459]1518    @action('Save', style='primary')
[6796]1519    def save(self, **data):
1520        msave(self, **data)
1521        return
1522
[7819]1523class PaymentsManageFormPage(KofaEditFormPage):
[6869]1524    """ Page to manage the student payments
[7642]1525
1526    This manage form page is for both students and students officers.
[6869]1527    """
1528    grok.context(IStudentPaymentsContainer)
[6940]1529    grok.name('index')
[10080]1530    grok.require('waeup.viewStudent')
[6869]1531    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1532    grok.template('paymentsmanagepage')
1533    pnav = 4
1534
[10080]1535    @property
1536    def manage_payments_allowed(self):
1537        return checkPermission('waeup.payStudent', self.context)
1538
[6940]1539    def unremovable(self, ticket):
[7251]1540        usertype = getattr(self.request.principal, 'user_type', None)
1541        if not usertype:
1542            return False
[10080]1543        if not self.manage_payments_allowed:
1544            return True
[7251]1545        return (self.request.principal.user_type == 'student' and ticket.r_code)
[6940]1546
[6869]1547    @property
1548    def label(self):
[7723]1549        return _('${a}: Payments',
1550            mapping = {'a':self.context.__parent__.display_fullname})
[6869]1551
1552    def update(self):
1553        super(PaymentsManageFormPage, self).update()
1554        datatable.need()
[7329]1555        warning.need()
[6869]1556        return
1557
[7723]1558    @jsaction(_('Remove selected tickets'))
[6869]1559    def delPaymentTicket(self, **data):
1560        form = self.request.form
[9701]1561        if 'val_id' in form:
[6869]1562            child_id = form['val_id']
1563        else:
[7723]1564            self.flash(_('No payment selected.'))
[6940]1565            self.redirect(self.url(self.context))
[6869]1566            return
1567        if not isinstance(child_id, list):
1568            child_id = [child_id]
1569        deleted = []
1570        for id in child_id:
[6992]1571            # Students are not allowed to remove used payment tickets
[10001]1572            ticket = self.context.get(id, None)
1573            if ticket is not None and not self.unremovable(ticket):
[7723]1574                del self.context[id]
1575                deleted.append(id)
[6869]1576        if len(deleted):
[7723]1577            self.flash(_('Successfully removed: ${a}',
1578                mapping = {'a': ', '.join(deleted)}))
[8735]1579            self.context.writeLogMessage(
[8885]1580                self,'removed: %s' % ', '.join(deleted))
[6940]1581        self.redirect(self.url(self.context))
[6869]1582        return
1583
[9517]1584    #@action(_('Add online payment ticket'))
1585    #def addPaymentTicket(self, **data):
1586    #    self.redirect(self.url(self.context, '@@addop'))
[6869]1587
[7819]1588class OnlinePaymentAddFormPage(KofaAddFormPage):
[6869]1589    """ Page to add an online payment ticket
1590    """
1591    grok.context(IStudentPaymentsContainer)
1592    grok.name('addop')
[9729]1593    grok.template('onlinepaymentaddform')
[7181]1594    grok.require('waeup.payStudent')
[6877]1595    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]1596        'p_category')
[7723]1597    label = _('Add online payment')
[6869]1598    pnav = 4
[7642]1599
[9729]1600    @property
1601    def selectable_categories(self):
1602        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1603        return sorted(categories.items())
1604
[7723]1605    @action(_('Create ticket'), style='primary')
[6869]1606    def createTicket(self, **data):
[7024]1607        p_category = data['p_category']
[9148]1608        previous_session = data.get('p_session', None)
1609        previous_level = data.get('p_level', None)
[7024]1610        student = self.context.__parent__
1611        if p_category == 'bed_allocation' and student[
1612            'studycourse'].current_session != grok.getSite()[
[8685]1613            'hostels'].accommodation_session:
[7024]1614                self.flash(
[7723]1615                    _('Your current session does not match ' + \
1616                    'accommodation session.'))
[7024]1617                return
[9423]1618        if 'maintenance' in p_category:
1619            current_session = str(student['studycourse'].current_session)
[9701]1620            if not current_session in student['accommodation']:
[9424]1621                self.flash(_('You have not yet booked accommodation.'))
[9423]1622                return
[7150]1623        students_utils = getUtility(IStudentsUtils)
[9148]1624        error, payment = students_utils.setPaymentDetails(
1625            p_category, student, previous_session, previous_level)
[8595]1626        if error is not None:
1627            self.flash(error)
[8081]1628            return
[6869]1629        self.context[payment.p_id] = payment
[7723]1630        self.flash(_('Payment ticket created.'))
[6940]1631        self.redirect(self.url(self.context))
[6869]1632        return
1633
[9383]1634    @action(_('Cancel'), validator=NullValidator)
1635    def cancel(self, **data):
1636        self.redirect(self.url(self.context))
1637
[9862]1638class PreviousPaymentAddFormPage(KofaAddFormPage):
[9148]1639    """ Page to add an online payment ticket for previous sessions
1640    """
1641    grok.context(IStudentPaymentsContainer)
1642    grok.name('addpp')
1643    grok.require('waeup.payStudent')
[9864]1644    form_fields = grok.AutoFields(IStudentPreviousPayment)
[9148]1645    label = _('Add previous session online payment')
1646    pnav = 4
1647
[9517]1648    def update(self):
[9521]1649        if self.context.student.before_payment:
1650            self.flash(_("No previous payment to be made."))
[9517]1651            self.redirect(self.url(self.context))
1652        super(PreviousPaymentAddFormPage, self).update()
1653        return
1654
[9862]1655    @action(_('Create ticket'), style='primary')
1656    def createTicket(self, **data):
1657        p_category = data['p_category']
1658        previous_session = data.get('p_session', None)
1659        previous_level = data.get('p_level', None)
1660        student = self.context.__parent__
1661        students_utils = getUtility(IStudentsUtils)
1662        error, payment = students_utils.setPaymentDetails(
1663            p_category, student, previous_session, previous_level)
1664        if error is not None:
1665            self.flash(error)
1666            return
1667        self.context[payment.p_id] = payment
1668        self.flash(_('Payment ticket created.'))
1669        self.redirect(self.url(self.context))
1670        return
1671
1672    @action(_('Cancel'), validator=NullValidator)
1673    def cancel(self, **data):
1674        self.redirect(self.url(self.context))
1675
[9864]1676class BalancePaymentAddFormPage(KofaAddFormPage):
1677    """ Page to add an online payment ticket for balance sessions
1678    """
1679    grok.context(IStudentPaymentsContainer)
1680    grok.name('addbp')
[9938]1681    grok.require('waeup.manageStudent')
[9864]1682    form_fields = grok.AutoFields(IStudentBalancePayment)
1683    label = _('Add balance')
1684    pnav = 4
1685
1686    @action(_('Create ticket'), style='primary')
1687    def createTicket(self, **data):
[9868]1688        p_category = data['p_category']
[9864]1689        balance_session = data.get('balance_session', None)
1690        balance_level = data.get('balance_level', None)
1691        balance_amount = data.get('balance_amount', None)
1692        student = self.context.__parent__
1693        students_utils = getUtility(IStudentsUtils)
1694        error, payment = students_utils.setBalanceDetails(
[9868]1695            p_category, student, balance_session,
[9864]1696            balance_level, balance_amount)
1697        if error is not None:
1698            self.flash(error)
1699            return
1700        self.context[payment.p_id] = payment
1701        self.flash(_('Payment ticket created.'))
1702        self.redirect(self.url(self.context))
1703        return
1704
1705    @action(_('Cancel'), validator=NullValidator)
1706    def cancel(self, **data):
1707        self.redirect(self.url(self.context))
1708
[7819]1709class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[6869]1710    """ Page to view an online payment ticket
1711    """
[6877]1712    grok.context(IStudentOnlinePayment)
[6869]1713    grok.name('index')
1714    grok.require('waeup.viewStudent')
[9984]1715    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
[8170]1716    form_fields[
1717        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1718    form_fields[
1719        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6869]1720    pnav = 4
1721
1722    @property
1723    def label(self):
[7723]1724        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8736]1725            'a':self.context.student.display_fullname,
[7723]1726            'b':self.context.p_id})
[6869]1727
[8420]1728class OnlinePaymentApprovePage(UtilityView, grok.View):
[6930]1729    """ Callback view
1730    """
1731    grok.context(IStudentOnlinePayment)
[8420]1732    grok.name('approve')
1733    grok.require('waeup.managePortal')
[6930]1734
1735    def update(self):
[8428]1736        success, msg, log = self.context.approveStudentPayment()
1737        if log is not None:
[9770]1738            # Add log message to students.log
[8735]1739            self.context.writeLogMessage(self,log)
[9770]1740            # Add log message to payments.log
1741            self.context.logger.info(
[9779]1742                '%s,%s,%s,%s,%s,,,,,,' % (
[9770]1743                self.context.student.student_id,
1744                self.context.p_id, self.context.p_category,
1745                self.context.amount_auth, self.context.r_code))
[8420]1746        self.flash(msg)
[6940]1747        return
[6930]1748
1749    def render(self):
[6940]1750        self.redirect(self.url(self.context, '@@index'))
[6930]1751        return
1752
[8420]1753class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1754    """ Approval view for students.
1755
1756    This view is used for browser tests only and
1757    must be neutralized in custom pages!
1758    """
1759
1760    grok.name('fake_approve')
1761    grok.require('waeup.payStudent')
1762
[7459]1763class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7019]1764    """Deliver a PDF slip of the context.
1765    """
1766    grok.context(IStudentOnlinePayment)
[8262]1767    grok.name('payment_slip.pdf')
[7019]1768    grok.require('waeup.viewStudent')
[9984]1769    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
[8173]1770    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1771    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7019]1772    prefix = 'form'
[8258]1773    note = None
[9702]1774    omit_fields = (
1775        'password', 'suspended', 'phone',
1776        'adm_code', 'sex', 'suspended_comment')
[7019]1777
1778    @property
[8262]1779    def title(self):
1780        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1781        return translate(_('Payment Data'), 'waeup.kofa',
1782            target_language=portal_language)
1783
1784    @property
[7019]1785    def label(self):
[8262]1786        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1787        return translate(_('Online Payment Slip'),
1788            'waeup.kofa', target_language=portal_language) \
1789            + ' %s' % self.context.p_id
[7019]1790
1791    def render(self):
[8262]1792        #if self.context.p_state != 'paid':
1793        #    self.flash('Ticket not yet paid.')
1794        #    self.redirect(self.url(self.context))
1795        #    return
[9141]1796        studentview = StudentBasePDFFormPage(self.context.student,
[9375]1797            self.request, self.omit_fields)
[7150]1798        students_utils = getUtility(IStudentsUtils)
[8262]1799        return students_utils.renderPDF(self, 'payment_slip.pdf',
[10250]1800            self.context.student, studentview, note=self.note,
1801            omit_fields=self.omit_fields)
[7019]1802
[6992]1803
[7819]1804class AccommodationManageFormPage(KofaEditFormPage):
[7009]1805    """ Page to manage bed tickets.
[7642]1806
1807    This manage form page is for both students and students officers.
[6635]1808    """
1809    grok.context(IStudentAccommodation)
1810    grok.name('index')
[7181]1811    grok.require('waeup.handleAccommodation')
[6635]1812    form_fields = grok.AutoFields(IStudentAccommodation)
[6992]1813    grok.template('accommodationmanagepage')
[6642]1814    pnav = 4
[7723]1815    officers_only_actions = [_('Remove selected')]
[6635]1816
1817    @property
1818    def label(self):
[7723]1819        return _('${a}: Accommodation',
1820            mapping = {'a':self.context.__parent__.display_fullname})
[6637]1821
[6992]1822    def update(self):
1823        super(AccommodationManageFormPage, self).update()
1824        datatable.need()
[7329]1825        warning.need()
[6992]1826        return
1827
[7723]1828    @jsaction(_('Remove selected'))
[7009]1829    def delBedTickets(self, **data):
[7240]1830        if getattr(self.request.principal, 'user_type', None) == 'student':
[7723]1831            self.flash(_('You are not allowed to remove bed tickets.'))
[7017]1832            self.redirect(self.url(self.context))
1833            return
[6992]1834        form = self.request.form
[9701]1835        if 'val_id' in form:
[6992]1836            child_id = form['val_id']
1837        else:
[7723]1838            self.flash(_('No bed ticket selected.'))
[6992]1839            self.redirect(self.url(self.context))
1840            return
1841        if not isinstance(child_id, list):
1842            child_id = [child_id]
1843        deleted = []
1844        for id in child_id:
[7068]1845            del self.context[id]
1846            deleted.append(id)
[6992]1847        if len(deleted):
[7723]1848            self.flash(_('Successfully removed: ${a}',
1849                mapping = {'a':', '.join(deleted)}))
[8735]1850            self.context.writeLogMessage(
1851                self,'removed: % s' % ', '.join(deleted))
[6992]1852        self.redirect(self.url(self.context))
1853        return
1854
[7009]1855    @property
1856    def selected_actions(self):
[7240]1857        if getattr(self.request.principal, 'user_type', None) == 'student':
[7642]1858            return [action for action in self.actions
1859                    if not action.label in self.officers_only_actions]
1860        return self.actions
[7009]1861
[7819]1862class BedTicketAddPage(KofaPage):
[6992]1863    """ Page to add an online payment ticket
1864    """
1865    grok.context(IStudentAccommodation)
1866    grok.name('add')
[7181]1867    grok.require('waeup.handleAccommodation')
[6992]1868    grok.template('enterpin')
[6993]1869    ac_prefix = 'HOS'
[7723]1870    label = _('Add bed ticket')
[6992]1871    pnav = 4
[7723]1872    buttonname = _('Create bed ticket')
[6993]1873    notice = ''
[9188]1874    with_ac = True
[6992]1875
1876    def update(self, SUBMIT=None):
[8736]1877        student = self.context.student
[7150]1878        students_utils = getUtility(IStudentsUtils)
[7186]1879        acc_details  = students_utils.getAccommodationDetails(student)
[8688]1880        if acc_details.get('expired', False):
1881            startdate = acc_details.get('startdate')
1882            enddate = acc_details.get('enddate')
1883            if startdate and enddate:
1884                tz = getUtility(IKofaUtils).tzinfo
1885                startdate = to_timezone(
1886                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1887                enddate = to_timezone(
1888                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1889                self.flash(_("Outside booking period: ${a} - ${b}",
1890                    mapping = {'a': startdate, 'b': enddate}))
1891            else:
1892                self.flash(_("Outside booking period."))
1893            self.redirect(self.url(self.context))
1894            return
[7369]1895        if not acc_details:
[7723]1896            self.flash(_("Your data are incomplete."))
[7369]1897            self.redirect(self.url(self.context))
1898            return
[6996]1899        if not student.state in acc_details['allowed_states']:
[7723]1900            self.flash(_("You are in the wrong registration state."))
[6992]1901            self.redirect(self.url(self.context))
1902            return
[7642]1903        if student['studycourse'].current_session != acc_details[
1904            'booking_session']:
[7061]1905            self.flash(
[7723]1906                _('Your current session does not match accommodation session.'))
[7061]1907            self.redirect(self.url(self.context))
1908            return
1909        if str(acc_details['booking_session']) in self.context.keys():
[7642]1910            self.flash(
[7723]1911                _('You already booked a bed space in current ' \
1912                    + 'accommodation session.'))
[7004]1913            self.redirect(self.url(self.context))
1914            return
[9188]1915        if self.with_ac:
1916            self.ac_series = self.request.form.get('ac_series', None)
1917            self.ac_number = self.request.form.get('ac_number', None)
[6992]1918        if SUBMIT is None:
1919            return
[9188]1920        if self.with_ac:
1921            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1922            code = get_access_code(pin)
1923            if not code:
1924                self.flash(_('Activation code is invalid.'))
1925                return
[7060]1926        # Search and book bed
[6997]1927        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1928        entries = cat.searchResults(
[7003]1929            owner=(student.student_id,student.student_id))
1930        if len(entries):
[9188]1931            # If bed space has been manually allocated use this bed
[7003]1932            bed = [entry for entry in entries][0]
[9424]1933            # Safety belt for paranoids: Does this bed really exist on portal?
1934            # XXX: Can be remove if nobody complains.
1935            if bed.__parent__.__parent__ is None:
1936                self.flash(_('System error: Please contact the adminsitrator.'))
[9428]1937                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
[9424]1938                return
[7060]1939        else:
1940            # else search for other available beds
1941            entries = cat.searchResults(
1942                bed_type=(acc_details['bt'],acc_details['bt']))
1943            available_beds = [
1944                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1945            if available_beds:
[7150]1946                students_utils = getUtility(IStudentsUtils)
[7186]1947                bed = students_utils.selectBed(available_beds)
[9424]1948                # Safety belt for paranoids: Does this bed really exist in portal?
1949                # XXX: Can be remove if nobody complains.
1950                if bed.__parent__.__parent__ is None:
1951                    self.flash(_('System error: Please contact the adminsitrator.'))
[9428]1952                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
[9424]1953                    return
[7060]1954                bed.bookBed(student.student_id)
1955            else:
[7723]1956                self.flash(_('There is no free bed in your category ${a}.',
1957                    mapping = {'a':acc_details['bt']}))
[7060]1958                return
[9188]1959        if self.with_ac:
1960            # Mark pin as used (this also fires a pin related transition)
1961            if code.state == USED:
1962                self.flash(_('Activation code has already been used.'))
[6992]1963                return
[9188]1964            else:
1965                comment = _(u'invalidated')
1966                # Here we know that the ac is in state initialized so we do not
1967                # expect an exception, but the owner might be different
1968                if not invalidate_accesscode(
1969                    pin,comment,self.context.student.student_id):
1970                    self.flash(_('You are not the owner of this access code.'))
1971                    return
[7060]1972        # Create bed ticket
[6992]1973        bedticket = createObject(u'waeup.BedTicket')
[9189]1974        if self.with_ac:
1975            bedticket.booking_code = pin
[6994]1976        bedticket.booking_session = acc_details['booking_session']
[6996]1977        bedticket.bed_type = acc_details['bt']
[7006]1978        bedticket.bed = bed
[6996]1979        hall_title = bed.__parent__.hostel_name
[9199]1980        coordinates = bed.coordinates[1:]
[6996]1981        block, room_nr, bed_nr = coordinates
[7723]1982        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1983            'a':hall_title, 'b':block,
1984            'c':room_nr, 'd':bed_nr,
1985            'e':bed.bed_type})
[7819]1986        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1987        bedticket.bed_coordinates = translate(
[7811]1988            bc, 'waeup.kofa',target_language=portal_language)
[9423]1989        self.context.addBedTicket(bedticket)
[9411]1990        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
[7723]1991        self.flash(_('Bed ticket created and bed booked: ${a}',
[9984]1992            mapping = {'a':bedticket.display_coordinates}))
[6992]1993        self.redirect(self.url(self.context))
1994        return
1995
[7819]1996class BedTicketDisplayFormPage(KofaDisplayFormPage):
[6994]1997    """ Page to display bed tickets
1998    """
1999    grok.context(IBedTicket)
2000    grok.name('index')
[7181]2001    grok.require('waeup.handleAccommodation')
[9984]2002    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
[9201]2003    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6994]2004    pnav = 4
2005
2006    @property
2007    def label(self):
[7723]2008        return _('Bed Ticket for Session ${a}',
2009            mapping = {'a':self.context.getSessionString()})
[6994]2010
[7459]2011class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
[7027]2012    """Deliver a PDF slip of the context.
2013    """
2014    grok.context(IBedTicket)
[9452]2015    grok.name('bed_allocation_slip.pdf')
[7181]2016    grok.require('waeup.handleAccommodation')
[9984]2017    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
[8173]2018    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7027]2019    prefix = 'form'
[9702]2020    omit_fields = (
2021        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
[7027]2022
2023    @property
[7723]2024    def title(self):
[7819]2025        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]2026        return translate(_('Bed Allocation Data'), 'waeup.kofa',
[7723]2027            target_language=portal_language)
2028
2029    @property
[7027]2030    def label(self):
[7819]2031        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9201]2032        #return translate(_('Bed Allocation: '),
2033        #    'waeup.kofa', target_language=portal_language) \
2034        #    + ' %s' % self.context.bed_coordinates
2035        return translate(_('Bed Allocation Slip'),
[7811]2036            'waeup.kofa', target_language=portal_language) \
[9201]2037            + ' %s' % self.context.getSessionString()
[7027]2038
2039    def render(self):
[9141]2040        studentview = StudentBasePDFFormPage(self.context.student,
[9375]2041            self.request, self.omit_fields)
[7150]2042        students_utils = getUtility(IStudentsUtils)
[7186]2043        return students_utils.renderPDF(
[9452]2044            self, 'bed_allocation_slip.pdf',
[10250]2045            self.context.student, studentview,
2046            omit_fields=self.omit_fields)
[7027]2047
[7459]2048class BedTicketRelocationPage(UtilityView, grok.View):
[7015]2049    """ Callback view
2050    """
2051    grok.context(IBedTicket)
2052    grok.name('relocate')
2053    grok.require('waeup.manageHostels')
2054
[7059]2055    # Relocate student if student parameters have changed or the bed_type
2056    # of the bed has changed
[7015]2057    def update(self):
[8736]2058        student = self.context.student
[7150]2059        students_utils = getUtility(IStudentsUtils)
[7186]2060        acc_details  = students_utils.getAccommodationDetails(student)
[7068]2061        if self.context.bed != None and \
2062              'reserved' in self.context.bed.bed_type:
[7723]2063            self.flash(_("Students in reserved beds can't be relocated."))
[7068]2064            self.redirect(self.url(self.context))
2065            return
[7059]2066        if acc_details['bt'] == self.context.bed_type and \
[7068]2067                self.context.bed != None and \
[7059]2068                self.context.bed.bed_type == self.context.bed_type:
[7723]2069            self.flash(_("Student can't be relocated."))
[7068]2070            self.redirect(self.url(self.context))
[7015]2071            return
[7068]2072        # Search a bed
[7015]2073        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2074        entries = cat.searchResults(
[7068]2075            owner=(student.student_id,student.student_id))
2076        if len(entries) and self.context.bed == None:
2077            # If booking has been cancelled but other bed space has been
2078            # manually allocated after cancellation use this bed
2079            new_bed = [entry for entry in entries][0]
2080        else:
2081            # Search for other available beds
2082            entries = cat.searchResults(
2083                bed_type=(acc_details['bt'],acc_details['bt']))
2084            available_beds = [
2085                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2086            if available_beds:
[7150]2087                students_utils = getUtility(IStudentsUtils)
[7186]2088                new_bed = students_utils.selectBed(available_beds)
[7068]2089                new_bed.bookBed(student.student_id)
2090            else:
[7723]2091                self.flash(_('There is no free bed in your category ${a}.',
2092                    mapping = {'a':acc_details['bt']}))
[7068]2093                self.redirect(self.url(self.context))
2094                return
[7642]2095        # Release old bed if exists
[7068]2096        if self.context.bed != None:
2097            self.context.bed.owner = NOT_OCCUPIED
2098            notify(grok.ObjectModifiedEvent(self.context.bed))
[7015]2099        # Alocate new bed
2100        self.context.bed_type = acc_details['bt']
[7068]2101        self.context.bed = new_bed
2102        hall_title = new_bed.__parent__.hostel_name
[9199]2103        coordinates = new_bed.coordinates[1:]
[7015]2104        block, room_nr, bed_nr = coordinates
[7723]2105        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2106            'a':hall_title, 'b':block,
2107            'c':room_nr, 'd':bed_nr,
2108            'e':new_bed.bed_type})
[7819]2109        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]2110        self.context.bed_coordinates = translate(
[7811]2111            bc, 'waeup.kofa',target_language=portal_language)
[9411]2112        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
[7723]2113        self.flash(_('Student relocated: ${a}',
[9984]2114            mapping = {'a':self.context.display_coordinates}))
[7015]2115        self.redirect(self.url(self.context))
2116        return
2117
2118    def render(self):
2119        return
2120
[7819]2121class StudentHistoryPage(KofaPage):
[6637]2122    """ Page to display student clearance data
2123    """
2124    grok.context(IStudent)
2125    grok.name('history')
[6660]2126    grok.require('waeup.viewStudent')
[6637]2127    grok.template('studenthistory')
[6642]2128    pnav = 4
[6637]2129
2130    @property
2131    def label(self):
[7723]2132        return _('${a}: History', mapping = {'a':self.context.display_fullname})
[6694]2133
2134# Pages for students only
2135
[7819]2136class StudentBaseEditFormPage(KofaEditFormPage):
[7133]2137    """ View to edit student base data
2138    """
2139    grok.context(IStudent)
2140    grok.name('edit_base')
2141    grok.require('waeup.handleStudent')
2142    form_fields = grok.AutoFields(IStudentBase).select(
2143        'email', 'phone')
[7723]2144    label = _('Edit base data')
[7133]2145    pnav = 4
2146
[7723]2147    @action(_('Save'), style='primary')
[7133]2148    def save(self, **data):
2149        msave(self, **data)
2150        return
2151
[7819]2152class StudentChangePasswordPage(KofaEditFormPage):
[7144]2153    """ View to manage student base data
[6756]2154    """
2155    grok.context(IStudent)
[7114]2156    grok.name('change_password')
[6694]2157    grok.require('waeup.handleStudent')
[7144]2158    grok.template('change_password')
[7723]2159    label = _('Change password')
[6694]2160    pnav = 4
2161
[7723]2162    @action(_('Save'), style='primary')
[7144]2163    def save(self, **data):
2164        form = self.request.form
2165        password = form.get('change_password', None)
2166        password_ctl = form.get('change_password_repeat', None)
2167        if password:
[7147]2168            validator = getUtility(IPasswordValidator)
2169            errors = validator.validate_password(password, password_ctl)
2170            if not errors:
2171                IUserAccount(self.context).setPassword(password)
[8735]2172                self.context.writeLogMessage(self, 'saved: password')
[7723]2173                self.flash(_('Password changed.'))
[6756]2174            else:
[7147]2175                self.flash( ' '.join(errors))
[6756]2176        return
2177
[7819]2178class StudentFilesUploadPage(KofaPage):
[7114]2179    """ View to upload files by student
2180    """
2181    grok.context(IStudent)
2182    grok.name('change_portrait')
[7127]2183    grok.require('waeup.uploadStudentFile')
[7114]2184    grok.template('filesuploadpage')
[7723]2185    label = _('Upload portrait')
[7114]2186    pnav = 4
2187
[7133]2188    def update(self):
[8736]2189        if self.context.student.state != ADMITTED:
[7145]2190            emit_lock_message(self)
[7133]2191            return
2192        super(StudentFilesUploadPage, self).update()
2193        return
2194
[7819]2195class StartClearancePage(KofaPage):
[6770]2196    grok.context(IStudent)
2197    grok.name('start_clearance')
2198    grok.require('waeup.handleStudent')
2199    grok.template('enterpin')
[7723]2200    label = _('Start clearance')
[6770]2201    ac_prefix = 'CLR'
2202    notice = ''
2203    pnav = 4
[7723]2204    buttonname = _('Start clearance now')
[9952]2205    with_ac = True
[6770]2206
[7133]2207    @property
2208    def all_required_fields_filled(self):
2209        if self.context.email and self.context.phone:
2210            return True
2211        return False
2212
2213    @property
2214    def portrait_uploaded(self):
2215        store = getUtility(IExtFileStore)
2216        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2217            return True
2218        return False
2219
[6770]2220    def update(self, SUBMIT=None):
[7671]2221        if not self.context.state == ADMITTED:
[7745]2222            self.flash(_("Wrong state"))
[6936]2223            self.redirect(self.url(self.context))
2224            return
[7133]2225        if not self.portrait_uploaded:
[7723]2226            self.flash(_("No portrait uploaded."))
[7133]2227            self.redirect(self.url(self.context, 'change_portrait'))
2228            return
2229        if not self.all_required_fields_filled:
[7723]2230            self.flash(_("Not all required fields filled."))
[7133]2231            self.redirect(self.url(self.context, 'edit_base'))
2232            return
[9952]2233        if self.with_ac:
2234            self.ac_series = self.request.form.get('ac_series', None)
2235            self.ac_number = self.request.form.get('ac_number', None)
[6770]2236        if SUBMIT is None:
2237            return
[9952]2238        if self.with_ac:
2239            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2240            code = get_access_code(pin)
2241            if not code:
2242                self.flash(_('Activation code is invalid.'))
2243                return
2244            if code.state == USED:
2245                self.flash(_('Activation code has already been used.'))
2246                return
2247            # Mark pin as used (this also fires a pin related transition)
2248            # and fire transition start_clearance
2249            comment = _(u"invalidated")
2250            # Here we know that the ac is in state initialized so we do not
2251            # expect an exception, but the owner might be different
2252            if not invalidate_accesscode(pin, comment, self.context.student_id):
2253                self.flash(_('You are not the owner of this access code.'))
2254                return
2255            self.context.clr_code = pin
[6770]2256        IWorkflowInfo(self.context).fireTransition('start_clearance')
[7723]2257        self.flash(_('Clearance process has been started.'))
[6770]2258        self.redirect(self.url(self.context,'cedit'))
2259        return
2260
[6695]2261class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2262    """ View to edit student clearance data by student
2263    """
2264    grok.context(IStudent)
2265    grok.name('cedit')
2266    grok.require('waeup.handleStudent')
[7723]2267    label = _('Edit clearance data')
[6718]2268
[7993]2269    @property
2270    def form_fields(self):
[8472]2271        if self.context.is_postgrad:
[8974]2272            form_fields = grok.AutoFields(IPGStudentClearance).omit(
[9486]2273                'clearance_locked', 'clr_code', 'officer_comment')
[7993]2274        else:
[8974]2275            form_fields = grok.AutoFields(IUGStudentClearance).omit(
[9486]2276                'clearance_locked', 'clr_code', 'officer_comment')
[7993]2277        return form_fields
2278
[6718]2279    def update(self):
2280        if self.context.clearance_locked:
[7145]2281            emit_lock_message(self)
[6718]2282            return
2283        return super(StudentClearanceEditFormPage, self).update()
[6719]2284
[7723]2285    @action(_('Save'), style='primary')
[6722]2286    def save(self, **data):
2287        self.applyData(self.context, **data)
[7723]2288        self.flash(_('Clearance form has been saved.'))
[6722]2289        return
2290
[7253]2291    def dataNotComplete(self):
[7642]2292        """To be implemented in the customization package.
2293        """
[7253]2294        return False
2295
[7723]2296    @action(_('Save and request clearance'), style='primary')
[7186]2297    def requestClearance(self, **data):
[6722]2298        self.applyData(self.context, **data)
[7253]2299        if self.dataNotComplete():
2300            self.flash(self.dataNotComplete())
2301            return
[7723]2302        self.flash(_('Clearance form has been saved.'))
[9021]2303        if self.context.clr_code:
2304            self.redirect(self.url(self.context, 'request_clearance'))
2305        else:
2306            # We bypass the request_clearance page if student
2307            # has been imported in state 'clearance started' and
2308            # no clr_code was entered before.
2309            state = IWorkflowState(self.context).getState()
2310            if state != CLEARANCE:
2311                # This shouldn't happen, but the application officer
2312                # might have forgotten to lock the form after changing the state
2313                self.flash(_('This form cannot be submitted. Wrong state!'))
2314                return
2315            IWorkflowInfo(self.context).fireTransition('request_clearance')
2316            self.flash(_('Clearance has been requested.'))
2317            self.redirect(self.url(self.context))
[6722]2318        return
2319
[7819]2320class RequestClearancePage(KofaPage):
[6769]2321    grok.context(IStudent)
2322    grok.name('request_clearance')
2323    grok.require('waeup.handleStudent')
2324    grok.template('enterpin')
[7723]2325    label = _('Request clearance')
2326    notice = _('Enter the CLR access code used for starting clearance.')
[6769]2327    ac_prefix = 'CLR'
2328    pnav = 4
[7723]2329    buttonname = _('Request clearance now')
[9952]2330    with_ac = True
[6769]2331
2332    def update(self, SUBMIT=None):
[9952]2333        if self.with_ac:
2334            self.ac_series = self.request.form.get('ac_series', None)
2335            self.ac_number = self.request.form.get('ac_number', None)
[6769]2336        if SUBMIT is None:
2337            return
[9952]2338        if self.with_ac:
2339            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2340            if self.context.clr_code and self.context.clr_code != pin:
2341                self.flash(_("This isn't your CLR access code."))
2342                return
[6769]2343        state = IWorkflowState(self.context).getState()
2344        if state != CLEARANCE:
[9021]2345            # This shouldn't happen, but the application officer
2346            # might have forgotten to lock the form after changing the state
[7723]2347            self.flash(_('This form cannot be submitted. Wrong state!'))
[6769]2348            return
2349        IWorkflowInfo(self.context).fireTransition('request_clearance')
[7723]2350        self.flash(_('Clearance has been requested.'))
[6769]2351        self.redirect(self.url(self.context))
[6789]2352        return
[6806]2353
[8471]2354class StartSessionPage(KofaPage):
[6944]2355    grok.context(IStudentStudyCourse)
[8471]2356    grok.name('start_session')
[6944]2357    grok.require('waeup.handleStudent')
2358    grok.template('enterpin')
[8471]2359    label = _('Start session')
[6944]2360    ac_prefix = 'SFE'
2361    notice = ''
2362    pnav = 4
[8471]2363    buttonname = _('Start now')
[9952]2364    with_ac = True
[6944]2365
2366    def update(self, SUBMIT=None):
[9139]2367        if not self.context.is_current:
2368            emit_lock_message(self)
2369            return
2370        super(StartSessionPage, self).update()
[8471]2371        if not self.context.next_session_allowed:
2372            self.flash(_("You are not entitled to start session."))
[6944]2373            self.redirect(self.url(self.context))
2374            return
[9952]2375        if self.with_ac:
2376            self.ac_series = self.request.form.get('ac_series', None)
2377            self.ac_number = self.request.form.get('ac_number', None)
[6944]2378        if SUBMIT is None:
2379            return
[9952]2380        if self.with_ac:
2381            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2382            code = get_access_code(pin)
2383            if not code:
2384                self.flash(_('Activation code is invalid.'))
[6944]2385                return
[9952]2386            # Mark pin as used (this also fires a pin related transition)
2387            if code.state == USED:
2388                self.flash(_('Activation code has already been used.'))
2389                return
2390            else:
2391                comment = _(u"invalidated")
2392                # Here we know that the ac is in state initialized so we do not
2393                # expect an error, but the owner might be different
2394                if not invalidate_accesscode(
2395                    pin,comment,self.context.student.student_id):
2396                    self.flash(_('You are not the owner of this access code.'))
2397                    return
[9637]2398        try:
2399            if self.context.student.state == CLEARED:
2400                IWorkflowInfo(self.context.student).fireTransition(
2401                    'pay_first_school_fee')
2402            elif self.context.student.state == RETURNING:
2403                IWorkflowInfo(self.context.student).fireTransition(
2404                    'pay_school_fee')
2405            elif self.context.student.state == PAID:
2406                IWorkflowInfo(self.context.student).fireTransition(
2407                    'pay_pg_fee')
2408        except ConstraintNotSatisfied:
2409            self.flash(_('An error occurred, please contact the system administrator.'))
2410            return
[8471]2411        self.flash(_('Session started.'))
[6944]2412        self.redirect(self.url(self.context))
2413        return
2414
[7819]2415class AddStudyLevelFormPage(KofaEditFormPage):
[6806]2416    """ Page for students to add current study levels
2417    """
2418    grok.context(IStudentStudyCourse)
2419    grok.name('add')
2420    grok.require('waeup.handleStudent')
2421    grok.template('studyleveladdpage')
2422    form_fields = grok.AutoFields(IStudentStudyCourse)
2423    pnav = 4
2424
2425    @property
2426    def label(self):
2427        studylevelsource = StudyLevelSource().factory
2428        code = self.context.current_level
2429        title = studylevelsource.getTitle(self.context, code)
[7723]2430        return _('Add current level ${a}', mapping = {'a':title})
[6806]2431
2432    def update(self):
[9139]2433        if not self.context.is_current:
2434            emit_lock_message(self)
2435            return
[8736]2436        if self.context.student.state != PAID:
[7145]2437            emit_lock_message(self)
[6806]2438            return
2439        super(AddStudyLevelFormPage, self).update()
2440        return
2441
[7723]2442    @action(_('Create course list now'), style='primary')
[6806]2443    def addStudyLevel(self, **data):
[8323]2444        studylevel = createObject(u'waeup.StudentStudyLevel')
[6806]2445        studylevel.level = self.context.current_level
2446        studylevel.level_session = self.context.current_session
2447        try:
2448            self.context.addStudentStudyLevel(
2449                self.context.certificate,studylevel)
2450        except KeyError:
[7723]2451            self.flash(_('This level exists.'))
[9467]2452        except RequiredMissing:
2453            self.flash(_('Your data are incomplete'))
[6806]2454        self.redirect(self.url(self.context))
2455        return
[6808]2456
[7819]2457class StudyLevelEditFormPage(KofaEditFormPage):
[6808]2458    """ Page to edit the student study level data by students
2459    """
2460    grok.context(IStudentStudyLevel)
2461    grok.name('edit')
[9924]2462    grok.require('waeup.editStudyLevel')
[6808]2463    grok.template('studyleveleditpage')
2464    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2465        'level_session', 'level_verdict')
2466    pnav = 4
2467
[9895]2468    def update(self, ADD=None, course=None):
[9139]2469        if not self.context.__parent__.is_current:
2470            emit_lock_message(self)
2471            return
[9257]2472        if self.context.student.state != PAID or \
2473            not self.context.is_current_level:
[7539]2474            emit_lock_message(self)
2475            return
[6808]2476        super(StudyLevelEditFormPage, self).update()
2477        datatable.need()
[7329]2478        warning.need()
[9895]2479        if ADD is not None:
2480            if not course:
2481                self.flash(_('No valid course code entered.'))
2482                return
2483            cat = queryUtility(ICatalog, name='courses_catalog')
2484            result = cat.searchResults(code=(course, course))
2485            if len(result) != 1:
2486                self.flash(_('Course not found.'))
2487                return
2488            course = list(result)[0]
2489            addCourseTicket(self, course)
[6808]2490        return
2491
2492    @property
2493    def label(self):
[7833]2494        # Here we know that the cookie has been set
2495        lang = self.request.cookies.get('kofa.language')
[7811]2496        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]2497            target_language=lang)
[8920]2498        return _('Edit course list of ${a}',
[7723]2499            mapping = {'a':level_title})
[6808]2500
2501    @property
[8921]2502    def translated_values(self):
2503        return translated_values(self)
2504
[9280]2505    def _delCourseTicket(self, **data):
[6808]2506        form = self.request.form
[9701]2507        if 'val_id' in form:
[6808]2508            child_id = form['val_id']
2509        else:
[7723]2510            self.flash(_('No ticket selected.'))
[6808]2511            self.redirect(self.url(self.context, '@@edit'))
2512            return
2513        if not isinstance(child_id, list):
2514            child_id = [child_id]
2515        deleted = []
2516        for id in child_id:
[6940]2517            # Students are not allowed to remove core tickets
[9700]2518            if id in self.context and \
2519                self.context[id].removable_by_student:
[7723]2520                del self.context[id]
2521                deleted.append(id)
[6808]2522        if len(deleted):
[7723]2523            self.flash(_('Successfully removed: ${a}',
2524                mapping = {'a':', '.join(deleted)}))
[9332]2525            self.context.writeLogMessage(
[9924]2526                self,'removed: %s at %s' %
2527                (', '.join(deleted), self.context.level))
[6808]2528        self.redirect(self.url(self.context, u'@@edit'))
2529        return
2530
[9280]2531    @jsaction(_('Remove selected tickets'))
2532    def delCourseTicket(self, **data):
2533        self._delCourseTicket(**data)
2534        return
2535
2536    def _registerCourses(self, **data):
[10155]2537        if self.context.student.is_postgrad and \
2538            not self.context.student.is_special_postgrad:
[9252]2539            self.flash(_(
2540                "You are a postgraduate student, "
2541                "your course list can't bee registered."))
2542            self.redirect(self.url(self.context))
2543            return
[9830]2544        students_utils = getUtility(IStudentsUtils)
2545        max_credits = students_utils.maxCredits(self.context)
2546        if self.context.total_credits > max_credits:
[8642]2547            self.flash(_('Maximum credits of ${a} exceeded.',
[9830]2548                mapping = {'a':max_credits}))
[8642]2549            return
[8736]2550        IWorkflowInfo(self.context.student).fireTransition(
[7642]2551            'register_courses')
[7723]2552        self.flash(_('Course list has been registered.'))
[6810]2553        self.redirect(self.url(self.context))
2554        return
2555
[9895]2556    @action(_('Register course list'))
[9280]2557    def registerCourses(self, **data):
2558        self._registerCourses(**data)
2559        return
2560
[6808]2561class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2562    """Add a course ticket by student.
2563    """
2564    grok.name('ctadd')
2565    grok.require('waeup.handleStudent')
[9420]2566    form_fields = grok.AutoFields(ICourseTicketAdd)
[6808]2567
[7539]2568    def update(self):
[9257]2569        if self.context.student.state != PAID or \
2570            not self.context.is_current_level:
[7539]2571            emit_lock_message(self)
2572            return
2573        super(CourseTicketAddFormPage2, self).update()
2574        return
2575
[7723]2576    @action(_('Add course ticket'))
[6808]2577    def addCourseTicket(self, **data):
[7642]2578        # Safety belt
[8736]2579        if self.context.student.state != PAID:
[7539]2580            return
[6808]2581        course = data['course']
[9895]2582        success = addCourseTicket(self, course)
2583        if success:
2584            self.redirect(self.url(self.context, u'@@edit'))
[6808]2585        return
[7369]2586
[7819]2587class SetPasswordPage(KofaPage):
2588    grok.context(IKofaObject)
[7660]2589    grok.name('setpassword')
2590    grok.require('waeup.Anonymous')
2591    grok.template('setpassword')
[7723]2592    label = _('Set password for first-time login')
[7660]2593    ac_prefix = 'PWD'
2594    pnav = 0
[7738]2595    set_button = _('Set')
[7660]2596
2597    def update(self, SUBMIT=None):
2598        self.reg_number = self.request.form.get('reg_number', None)
2599        self.ac_series = self.request.form.get('ac_series', None)
2600        self.ac_number = self.request.form.get('ac_number', None)
2601
2602        if SUBMIT is None:
2603            return
2604        hitlist = search(query=self.reg_number,
2605            searchtype='reg_number', view=self)
2606        if not hitlist:
[7723]2607            self.flash(_('No student found.'))
[7660]2608            return
2609        if len(hitlist) != 1:   # Cannot happen but anyway
[7723]2610            self.flash(_('More than one student found.'))
[7660]2611            return
2612        student = hitlist[0].context
2613        self.student_id = student.student_id
2614        student_pw = student.password
2615        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2616        code = get_access_code(pin)
2617        if not code:
[7723]2618            self.flash(_('Access code is invalid.'))
[7660]2619            return
2620        if student_pw and pin == student.adm_code:
[7723]2621            self.flash(_(
2622                'Password has already been set. Your Student Id is ${a}',
2623                mapping = {'a':self.student_id}))
[7660]2624            return
2625        elif student_pw:
2626            self.flash(
[7723]2627                _('Password has already been set. You are using the ' +
2628                'wrong Access Code.'))
[7660]2629            return
2630        # Mark pin as used (this also fires a pin related transition)
2631        # and set student password
2632        if code.state == USED:
[7723]2633            self.flash(_('Access code has already been used.'))
[7660]2634            return
2635        else:
[7723]2636            comment = _(u"invalidated")
[7660]2637            # Here we know that the ac is in state initialized so we do not
2638            # expect an exception
2639            invalidate_accesscode(pin,comment)
2640            IUserAccount(student).setPassword(self.ac_number)
2641            student.adm_code = pin
[7723]2642        self.flash(_('Password has been set. Your Student Id is ${a}',
2643            mapping = {'a':self.student_id}))
[7811]2644        return
[8779]2645
2646class StudentRequestPasswordPage(KofaAddFormPage):
2647    """Captcha'd registration page for applicants.
2648    """
2649    grok.name('requestpw')
2650    grok.require('waeup.Anonymous')
2651    grok.template('requestpw')
2652    form_fields = grok.AutoFields(IStudentRequestPW).select(
[8854]2653        'firstname','number','email')
[8779]2654    label = _('Request password for first-time login')
2655
2656    def update(self):
2657        # Handle captcha
2658        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2659        self.captcha_result = self.captcha.verify(self.request)
2660        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2661        return
2662
2663    def _redirect(self, email, password, student_id):
2664        # Forward only email to landing page in base package.
2665        self.redirect(self.url(self.context, 'requestpw_complete',
2666            data = dict(email=email)))
2667        return
2668
2669    def _pw_used(self):
[8780]2670        # XXX: False if password has not been used. We need an extra
2671        #      attribute which remembers if student logged in.
[8779]2672        return True
2673
[8854]2674    @action(_('Send login credentials to email address'), style='primary')
[8779]2675    def get_credentials(self, **data):
2676        if not self.captcha_result.is_valid:
2677            # Captcha will display error messages automatically.
2678            # No need to flash something.
2679            return
[8854]2680        number = data.get('number','')
[8779]2681        firstname = data.get('firstname','')
2682        cat = getUtility(ICatalog, name='students_catalog')
2683        results = list(
[8854]2684            cat.searchResults(reg_number=(number, number)))
2685        if not results:
2686            results = list(
2687                cat.searchResults(matric_number=(number, number)))
[8779]2688        if results:
2689            student = results[0]
2690            if getattr(student,'firstname',None) is None:
2691                self.flash(_('An error occurred.'))
2692                return
2693            elif student.firstname.lower() != firstname.lower():
2694                # Don't tell the truth here. Anonymous must not
2695                # know that a record was found and only the firstname
2696                # verification failed.
2697                self.flash(_('No student record found.'))
2698                return
2699            elif student.password is not None and self._pw_used:
2700                self.flash(_('Your password has already been set and used. '
2701                             'Please proceed to the login page.'))
2702                return
2703            # Store email address but nothing else.
2704            student.email = data['email']
2705            notify(grok.ObjectModifiedEvent(student))
2706        else:
2707            # No record found, this is the truth.
2708            self.flash(_('No student record found.'))
2709            return
2710
2711        kofa_utils = getUtility(IKofaUtils)
2712        password = kofa_utils.genPassword()
[8857]2713        mandate = PasswordMandate()
[8853]2714        mandate.params['password'] = password
[8858]2715        mandate.params['user'] = student
[8853]2716        site = grok.getSite()
2717        site['mandates'].addMandate(mandate)
[8779]2718        # Send email with credentials
[8853]2719        args = {'mandate_id':mandate.mandate_id}
2720        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2721        url_info = u'Confirmation link: %s' % mandate_url
[8779]2722        msg = _('You have successfully requested a password for the')
2723        if kofa_utils.sendCredentials(IUserAccount(student),
[8853]2724            password, url_info, msg):
[8779]2725            email_sent = student.email
2726        else:
2727            email_sent = None
2728        self._redirect(email=email_sent, password=password,
2729            student_id=student.student_id)
[8856]2730        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2731        self.context.logger.info(
2732            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
[8779]2733        return
2734
2735class StudentRequestPasswordEmailSent(KofaPage):
2736    """Landing page after successful password request.
2737
2738    """
2739    grok.name('requestpw_complete')
2740    grok.require('waeup.Public')
2741    grok.template('requestpwmailsent')
2742    label = _('Your password request was successful.')
2743
2744    def update(self, email=None, student_id=None, password=None):
2745        self.email = email
2746        self.password = password
2747        self.student_id = student_id
[8974]2748        return
[9797]2749
[9806]2750class FilterStudentsInDepartmentPage(KofaPage):
2751    """Page that filters and lists students.
2752    """
2753    grok.context(IDepartment)
2754    grok.require('waeup.showStudents')
2755    grok.name('students')
2756    grok.template('filterstudentspage')
2757    pnav = 1
[9819]2758    session_label = _('Current Session')
2759    level_label = _('Current Level')
[9806]2760
2761    def label(self):
[9819]2762        return 'Students in %s' % self.context.longtitle()
[9806]2763
2764    def _set_session_values(self):
2765        vocab_terms = academic_sessions_vocab.by_value.values()
2766        self.sessions = sorted(
2767            [(x.title, x.token) for x in vocab_terms], reverse=True)
2768        self.sessions += [('All Sessions', 'all')]
2769        return
2770
2771    def _set_level_values(self):
2772        vocab_terms = course_levels.by_value.values()
2773        self.levels = sorted(
2774            [(x.title, x.token) for x in vocab_terms])
2775        self.levels += [('All Levels', 'all')]
2776        return
2777
2778    def _searchCatalog(self, session, level):
2779        if level not in (10, 999, None):
2780            start_level = 100 * (level // 100)
2781            end_level = start_level + 90
2782        else:
2783            start_level = end_level = level
2784        cat = queryUtility(ICatalog, name='students_catalog')
2785        students = cat.searchResults(
2786            current_session=(session, session),
2787            current_level=(start_level, end_level),
2788            depcode=(self.context.code, self.context.code)
2789            )
2790        hitlist = []
2791        for student in students:
2792            hitlist.append(StudentQueryResultItem(student, view=self))
2793        return hitlist
2794
2795    def update(self, SHOW=None, session=None, level=None):
2796        datatable.need()
2797        self.parent_url = self.url(self.context.__parent__)
2798        self._set_session_values()
2799        self._set_level_values()
2800        self.hitlist = []
2801        self.session_default = session
2802        self.level_default = level
2803        if SHOW is not None:
2804            if session != 'all':
2805                self.session = int(session)
2806                self.session_string = '%s %s/%s' % (
2807                    self.session_label, self.session, self.session+1)
2808            else:
2809                self.session = None
2810                self.session_string = _('in any session')
2811            if level != 'all':
2812                self.level = int(level)
2813                self.level_string = '%s %s' % (self.level_label, self.level)
2814            else:
2815                self.level = None
2816                self.level_string = _('at any level')
2817            self.hitlist = self._searchCatalog(self.session, self.level)
2818            if not self.hitlist:
2819                self.flash(_('No student found.'))
2820        return
2821
2822class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2823    """Page that filters and lists students.
2824    """
2825    grok.context(ICertificate)
2826
2827    def label(self):
[9819]2828        return 'Students studying %s' % self.context.longtitle()
[9806]2829
2830    def _searchCatalog(self, session, level):
2831        if level not in (10, 999, None):
2832            start_level = 100 * (level // 100)
2833            end_level = start_level + 90
2834        else:
2835            start_level = end_level = level
2836        cat = queryUtility(ICatalog, name='students_catalog')
2837        students = cat.searchResults(
2838            current_session=(session, session),
2839            current_level=(start_level, end_level),
2840            certcode=(self.context.code, self.context.code)
2841            )
2842        hitlist = []
2843        for student in students:
2844            hitlist.append(StudentQueryResultItem(student, view=self))
2845        return hitlist
2846
2847class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2848    """Page that filters and lists students.
2849    """
2850    grok.context(ICourse)
2851
[10024]2852    session_label = _('Session')
2853    level_label = _('Level')
2854
[9806]2855    def label(self):
[9819]2856        return 'Students registered for %s' % self.context.longtitle()
[9806]2857
2858    def _searchCatalog(self, session, level):
2859        if level not in (10, 999, None):
2860            start_level = 100 * (level // 100)
2861            end_level = start_level + 90
2862        else:
2863            start_level = end_level = level
2864        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2865        coursetickets = cat.searchResults(
2866            session=(session, session),
2867            level=(start_level, end_level),
2868            code=(self.context.code, self.context.code)
2869            )
2870        hitlist = []
2871        for ticket in coursetickets:
2872            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
[10039]2873        return list(set(hitlist))
[9806]2874
[9813]2875class ExportJobContainerOverview(KofaPage):
[9835]2876    """Page that lists active student data export jobs and provides links
2877    to discard or download CSV files.
2878
[9797]2879    """
[9813]2880    grok.context(VirtualExportJobContainer)
[9797]2881    grok.require('waeup.showStudents')
2882    grok.name('index.html')
2883    grok.template('exportjobsindex')
[9813]2884    label = _('Student Data Exports')
[9797]2885    pnav = 1
2886
2887    def update(self, CREATE=None, DISCARD=None, job_id=None):
2888        if CREATE:
[9836]2889            self.redirect(self.url('@@exportconfig'))
[9797]2890            return
2891        if DISCARD and job_id:
2892            entry = self.context.entry_from_job_id(job_id)
2893            self.context.delete_export_entry(entry)
[9836]2894            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2895            self.context.logger.info(
2896                '%s - discarded: job_id=%s' % (ob_class, job_id))
[9819]2897            self.flash(_('Discarded export') + ' %s' % job_id)
[9822]2898        self.entries = doll_up(self, user=self.request.principal.id)
[9797]2899        return
2900
[9833]2901class ExportJobContainerJobConfig(KofaPage):
[9797]2902    """Page that configures a students export job.
[9833]2903
2904    This is a baseclass.
[9797]2905    """
[9833]2906    grok.baseclass()
[9836]2907    grok.name('exportconfig')
[9797]2908    grok.require('waeup.showStudents')
[9836]2909    grok.template('exportconfig')
[9833]2910    label = _('Configure student data export')
[9797]2911    pnav = 1
[9835]2912    redirect_target = ''
[9797]2913
2914    def _set_session_values(self):
2915        vocab_terms = academic_sessions_vocab.by_value.values()
2916        self.sessions = sorted(
2917            [(x.title, x.token) for x in vocab_terms], reverse=True)
[9819]2918        self.sessions += [(_('All Sessions'), 'all')]
[9797]2919        return
2920
2921    def _set_level_values(self):
2922        vocab_terms = course_levels.by_value.values()
2923        self.levels = sorted(
2924            [(x.title, x.token) for x in vocab_terms])
[9819]2925        self.levels += [(_('All Levels'), 'all')]
[9797]2926        return
2927
[9803]2928    def _set_mode_values(self):
2929        utils = getUtility(IKofaUtils)
[9838]2930        self.modes = sorted([(value, key) for key, value in
2931                      utils.STUDY_MODES_DICT.items()])
[9819]2932        self.modes +=[(_('All Modes'), 'all')]
[9803]2933        return
2934
[9804]2935    def _set_exporter_values(self):
2936        # We provide all student exporters, nothing else, yet.
[10248]2937        # Bursary Officers don't have the general exportData permission
2938        # and are only allowed to export bursary data.
2939        if not checkPermission('waeup.exportData', self.context):
2940            self.exporters = [('Bursary Data', 'bursary')]
2941            return
[9804]2942        exporters = []
2943        for name in EXPORTER_NAMES:
2944            util = getUtility(ICSVExporter, name=name)
2945            exporters.append((util.title, name),)
2946        self.exporters = exporters
[10247]2947        return
[9804]2948
[9833]2949    @property
2950    def depcode(self):
2951        return None
2952
[9842]2953    @property
2954    def certcode(self):
2955        return None
2956
[9804]2957    def update(self, START=None, session=None, level=None, mode=None,
2958               exporter=None):
[9797]2959        self._set_session_values()
2960        self._set_level_values()
[9803]2961        self._set_mode_values()
[9804]2962        self._set_exporter_values()
[9797]2963        if START is None:
2964            return
2965        if session == 'all':
2966            session=None
2967        if level == 'all':
2968            level = None
[9803]2969        if mode == 'all':
2970            mode = None
[9933]2971        if (mode, level, session,
2972            self.depcode, self.certcode) == (None, None, None, None, None):
2973            # Export all students including those without certificate
2974            job_id = self.context.start_export_job(exporter,
2975                                          self.request.principal.id)
2976        else:
2977            job_id = self.context.start_export_job(exporter,
2978                                          self.request.principal.id,
2979                                          current_session=session,
2980                                          current_level=level,
2981                                          current_mode=mode,
2982                                          depcode=self.depcode,
2983                                          certcode=self.certcode)
[9836]2984        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2985        self.context.logger.info(
[9842]2986            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2987            % (ob_class, exporter, session, level, mode, self.depcode,
2988            self.certcode, job_id))
[9833]2989        self.flash(_('Export started for students with') +
2990                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2991                   session, level, mode))
[9835]2992        self.redirect(self.url(self.redirect_target))
[9797]2993        return
2994
[9822]2995class ExportJobContainerDownload(ExportCSVView):
[9835]2996    """Page that downloads a students export csv file.
2997
[9797]2998    """
[9813]2999    grok.context(VirtualExportJobContainer)
[9797]3000    grok.require('waeup.showStudents')
[9833]3001
3002class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3003    """Page that configures a students export job in datacenter.
3004
3005    """
3006    grok.context(IDataCenter)
[9835]3007    redirect_target = '@@export'
[9833]3008
[10247]3009class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3010    """Page that configures a students export job in facultiescontainer.
3011
3012    """
3013    grok.context(VirtualFacultiesExportJobContainer)
3014
[9833]3015class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3016    """Page that configures a students export job in departments.
3017
3018    """
3019    grok.context(VirtualDepartmentExportJobContainer)
3020
3021    @property
3022    def depcode(self):
[9835]3023        return self.context.__parent__.code
[9842]3024
3025class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3026    """Page that configures a students export job for certificates.
3027
3028    """
3029    grok.context(VirtualCertificateExportJobContainer)
[9843]3030    grok.template('exportconfig_certificate')
[9842]3031
3032    @property
3033    def certcode(self):
3034        return self.context.__parent__.code
[9843]3035
3036class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3037    """Page that configures a students export job for courses.
3038
3039    In contrast to department or certificate student data exports the
3040    coursetickets_catalog is searched here. Therefore the update
3041    method from the base class is customized.
3042    """
3043    grok.context(VirtualCourseExportJobContainer)
3044    grok.template('exportconfig_course')
3045
3046    def _set_exporter_values(self):
[9844]3047        # We provide only two exporters.
[9843]3048        exporters = []
[9844]3049        for name in ('students', 'coursetickets'):
[9843]3050            util = getUtility(ICSVExporter, name=name)
3051            exporters.append((util.title, name),)
3052        self.exporters = exporters
3053
3054    def update(self, START=None, session=None, level=None, mode=None,
3055               exporter=None):
3056        self._set_session_values()
3057        self._set_level_values()
3058        self._set_mode_values()
3059        self._set_exporter_values()
3060        if START is None:
3061            return
3062        if session == 'all':
[10016]3063            session = None
[9843]3064        if level == 'all':
3065            level = None
3066        job_id = self.context.start_export_job(exporter,
3067                                      self.request.principal.id,
3068                                      # Use a different catalog and
3069                                      # pass different keywords than
3070                                      # for the (default) students_catalog
[9845]3071                                      catalog='coursetickets',
[9843]3072                                      session=session,
3073                                      level=level,
3074                                      code=self.context.__parent__.code)
3075        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3076        self.context.logger.info(
3077            '%s - exported: %s (%s, %s, %s), job_id=%s'
3078            % (ob_class, exporter, session, level,
3079            self.context.__parent__.code, job_id))
3080        self.flash(_('Export started for course tickets with') +
3081                   ' level_session=%s, level=%s' % (
3082                   session, level))
3083        self.redirect(self.url(self.redirect_target))
3084        return
Note: See TracBrowser for help on using the repository browser.