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

Last change on this file since 17917 was 17917, checked in by Henrik Bettermann, 2 months ago

Implement ExportPDFFinalClearanceSlip.

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