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

Last change on this file since 14519 was 14314, checked in by Henrik Bettermann, 8 years ago

Provide option to render performance data in custom packages.

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