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

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

Copy also files from applicants to students section.

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