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

Last change on this file since 18096 was 18096, checked in by Henrik Bettermann, 7 days ago

Fix typo.

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