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

Last change on this file since 16299 was 16299, checked in by Henrik Bettermann, 4 years ago

Implement bulk emailing.

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