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

Last change on this file since 15980 was 15972, checked in by Henrik Bettermann, 5 years ago

Add AccommodationDisplayFormPage

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