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

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

More info.

  • Property svn:keywords set to Id
File size: 174.7 KB
Line 
1## $Id: browser.py 17922 2024-09-11 07:53:23Z 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.pdf',
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.pdf',
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.pdf',
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        return
1672
1673    @property
1674    def label(self):
1675        # Here we know that the cookie has been set
1676        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1677        return translate(_('Academic Transcript'),
1678            'waeup.kofa', target_language=portal_language)
1679
1680    def _sigsInFooter(self):
1681        if getattr(
1682            self.context.student['studycourse'], 'transcript_signees', None):
1683            return ()
1684        return (_('CERTIFIED TRUE COPY'),)
1685
1686    def _signatures(self):
1687        return ()
1688
1689    def _digital_sigs(self):
1690        if getattr(
1691            self.context.student['studycourse'], 'transcript_signees', None):
1692            return self.context.student['studycourse'].transcript_signees
1693        return ()
1694
1695    def _save_file(self):
1696        if self.context.student.state == TRANSREL:
1697            return True
1698        return False
1699
1700    def render(self):
1701        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1702        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1703        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1704        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1705        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1706        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1707        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1708        studentview = StudentBasePDFFormPage(self.context.student,
1709            self.request, self.omit_fields)
1710        students_utils = getUtility(IStudentsUtils)
1711
1712        tableheader = [(Code,'code', 2.5),
1713                         (Title,'title', 7),
1714                         (Term, 'semester', 1.5),
1715                         (Cred, 'credits', 1.5),
1716                         (Score, 'total_score', 1.5),
1717                         (Grade, 'grade', 1.5),
1718                         ]
1719
1720        pdfstream = students_utils.renderPDFTranscript(
1721            self, 'transcript.pdf',
1722            self.context.student, studentview,
1723            omit_fields=self.omit_fields,
1724            tableheader=tableheader,
1725            signatures=self._signatures(),
1726            sigs_in_footer=self._sigsInFooter(),
1727            digital_sigs=self._digital_sigs(),
1728            save_file=self._save_file(),
1729            )
1730        if not pdfstream:
1731            self.redirect(self.url(self.context.student))
1732            return
1733        return pdfstream
1734
1735class StudentTransferFormPage(KofaAddFormPage):
1736    """Page to transfer the student.
1737    """
1738    grok.context(IStudent)
1739    grok.name('transfer')
1740    grok.require('waeup.manageStudent')
1741    label = _('Transfer student')
1742    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1743        'entry_mode', 'entry_session')
1744    pnav = 4
1745
1746    @jsaction(_('Transfer'))
1747    def transferStudent(self, **data):
1748        error = self.context.transfer(**data)
1749        if error == -1:
1750            self.flash(_('Current level does not match certificate levels.'),
1751                       type="warning")
1752        elif error == -2:
1753            self.flash(_('Former study course record incomplete.'),
1754                       type="warning")
1755        elif error == -3:
1756            self.flash(_('Maximum number of transfers exceeded.'),
1757                       type="warning")
1758        else:
1759            self.flash(_('Successfully transferred.'))
1760        return
1761
1762class RevertTransferFormPage(KofaEditFormPage):
1763    """View that reverts the previous transfer.
1764    """
1765    grok.context(IStudent)
1766    grok.name('revert_transfer')
1767    grok.require('waeup.manageStudent')
1768    grok.template('reverttransfer')
1769    label = _('Revert previous transfer')
1770
1771    def update(self):
1772        if not self.context.has_key('studycourse_1'):
1773            self.flash(_('No previous transfer.'), type="warning")
1774            self.redirect(self.url(self.context))
1775            return
1776        return
1777
1778    @jsaction(_('Revert now'))
1779    def transferStudent(self, **data):
1780        self.context.revert_transfer()
1781        self.flash(_('Previous transfer reverted.'))
1782        self.redirect(self.url(self.context, 'studycourse'))
1783        return
1784
1785class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1786    """ Page to display student study levels
1787    """
1788    grok.context(IStudentStudyLevel)
1789    grok.name('index')
1790    grok.require('waeup.viewStudent')
1791    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1792    form_fields[
1793        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1794    grok.template('studylevelpage')
1795    pnav = 4
1796
1797    def update(self):
1798        super(StudyLevelDisplayFormPage, self).update()
1799        if self.context.level == 0:
1800            self.form_fields = self.form_fields.omit('gpa')
1801        return
1802
1803    @property
1804    def translated_values(self):
1805        return translated_values(self)
1806
1807    @property
1808    def label(self):
1809        # Here we know that the cookie has been set
1810        lang = self.request.cookies.get('kofa.language')
1811        level_title = translate(self.context.level_title, 'waeup.kofa',
1812            target_language=lang)
1813        return _('${a}: ${b}', mapping = {
1814            'a':self.context.student.display_fullname,
1815            'b':level_title})
1816
1817class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1818    """Deliver a PDF slip of the context.
1819    """
1820    grok.context(IStudentStudyLevel)
1821    grok.name('course_registration_slip.pdf')
1822    grok.require('waeup.viewStudent')
1823    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1824        'level', 'gpa', 'transcript_remark')
1825    form_fields[
1826        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1827    prefix = 'form'
1828    omit_fields = (
1829        'password', 'suspended', 'phone', 'date_of_birth',
1830        'adm_code', 'sex', 'suspended_comment', 'current_level',
1831        'flash_notice')
1832
1833    @property
1834    def title(self):
1835        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1836        return translate(_('Level Data'), 'waeup.kofa',
1837            target_language=portal_language)
1838
1839    @property
1840    def label(self):
1841        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1842        lang = self.request.cookies.get('kofa.language', portal_language)
1843        level_title = translate(self.context.level_title, 'waeup.kofa',
1844            target_language=lang)
1845        return translate(_('Course Registration Slip'),
1846            'waeup.kofa', target_language=portal_language) \
1847            + ' %s' % level_title
1848
1849    @property
1850    def tabletitle(self):
1851        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1852        tabletitle = []
1853        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1854            target_language=portal_language))
1855        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1856            target_language=portal_language))
1857        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1858            target_language=portal_language))
1859        return tabletitle
1860
1861    def _signatures(self):
1862        return ()
1863
1864    def _sigsInFooter(self):
1865        return ()
1866
1867    def render(self):
1868        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1869        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1870        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1871        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1872        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1873        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1874        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1875        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1876        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1877        studentview = StudentBasePDFFormPage(self.context.student,
1878            self.request, self.omit_fields)
1879        students_utils = getUtility(IStudentsUtils)
1880
1881        tabledata = []
1882        tableheader = []
1883        for i in range(1,7):
1884            tabledata.append(sorted(
1885                [value for value in self.context.values() if value.semester == i],
1886                key=lambda value: str(value.semester) + value.code))
1887            tableheader.append([(Code,'code', 2.5),
1888                             (Title,'title', 5),
1889                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1890                             (Cred, 'credits', 1.5),
1891                             #(Mand, 'mandatory', 1.5),
1892                             (Score, 'score', 1.5),
1893                             (Grade, 'grade', 1.5),
1894                             #('Auto', 'automatic', 1.5)
1895                             ])
1896        return students_utils.renderPDF(
1897            self, 'course_registration_slip.pdf',
1898            self.context.student, studentview,
1899            tableheader=tableheader,
1900            tabledata=tabledata,
1901            omit_fields=self.omit_fields,
1902            signatures=self._signatures(),
1903            sigs_in_footer=self._sigsInFooter(),
1904            )
1905
1906class StudyLevelManageFormPage(KofaEditFormPage):
1907    """ Page to edit the student study level data
1908    """
1909    grok.context(IStudentStudyLevel)
1910    grok.name('manage')
1911    grok.require('waeup.manageStudent')
1912    grok.template('studylevelmanagepage')
1913    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1914        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1915    pnav = 4
1916    taboneactions = [_('Save'),_('Cancel')]
1917    tabtwoactions = [_('Add course ticket'),
1918        _('Remove selected tickets'),_('Cancel')]
1919    placeholder = _('Enter valid course code')
1920
1921    def update(self, ADD=None, course=None):
1922        if not self.context.__parent__.is_current \
1923            or self.context.student.studycourse_locked:
1924            emit_lock_message(self)
1925            return
1926        super(StudyLevelManageFormPage, self).update()
1927        if ADD is not None:
1928            if not course:
1929                self.flash(_('No valid course code entered.'), type="warning")
1930                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1931                return
1932            cat = queryUtility(ICatalog, name='courses_catalog')
1933            result = cat.searchResults(code=(course, course))
1934            if len(result) != 1:
1935                self.flash(_('Course not found.'), type="warning")
1936            else:
1937                course = list(result)[0]
1938                addCourseTicket(self, course)
1939            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1940        return
1941
1942    @property
1943    def translated_values(self):
1944        return translated_values(self)
1945
1946    @property
1947    def label(self):
1948        # Here we know that the cookie has been set
1949        lang = self.request.cookies.get('kofa.language')
1950        level_title = translate(self.context.level_title, 'waeup.kofa',
1951            target_language=lang)
1952        return _('Manage ${a}',
1953            mapping = {'a':level_title})
1954
1955    @action(_('Save'), style='primary')
1956    def save(self, **data):
1957        msave(self, **data)
1958        return
1959
1960    @jsaction(_('Remove selected tickets'))
1961    def delCourseTicket(self, **data):
1962        form = self.request.form
1963        if 'val_id' in form:
1964            child_id = form['val_id']
1965        else:
1966            self.flash(_('No ticket selected.'), type="warning")
1967            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1968            return
1969        if not isinstance(child_id, list):
1970            child_id = [child_id]
1971        deleted = []
1972        for id in child_id:
1973            del self.context[id]
1974            deleted.append(id)
1975        if len(deleted):
1976            self.flash(_('Successfully removed: ${a}',
1977                mapping = {'a':', '.join(deleted)}))
1978            self.context.writeLogMessage(
1979                self,'removed: %s' % (', '.join(deleted)))
1980        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1981        return
1982
1983class StudyLevelRemarkFormPage(KofaEditFormPage):
1984    """ Page to edit the student study level transcript remark only
1985    """
1986    grok.context(IStudentStudyLevel)
1987    grok.name('remark')
1988    grok.require('waeup.processTranscript')
1989    grok.template('studylevelremarkpage')
1990    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1991    form_fields['level_session'].for_display = True
1992    form_fields['level_verdict'].for_display = True
1993    form_fields['validation_date'].for_display = True
1994    form_fields['validated_by'].for_display = True
1995
1996    def update(self, ADD=None, course=None):
1997        if self.context.student.studycourse_locked:
1998            emit_lock_message(self)
1999            return
2000        super(StudyLevelRemarkFormPage, self).update()
2001
2002    @property
2003    def label(self):
2004        lang = self.request.cookies.get('kofa.language')
2005        level_title = translate(self.context.level_title, 'waeup.kofa',
2006            target_language=lang)
2007        return _(
2008            'Edit transcript remark of level ${a}', mapping = {'a':level_title})
2009
2010    @property
2011    def translated_values(self):
2012        return translated_values(self)
2013
2014    @action(_('Save remark and go and back to transcript validation page'),
2015        style='primary')
2016    def save(self, **data):
2017        msave(self, **data)
2018        self.redirect(self.url(self.context.student)
2019            + '/studycourse/validate_transcript#tab4')
2020        return
2021
2022class ValidateCoursesView(UtilityView, grok.View):
2023    """ Validate course list by course adviser
2024    """
2025    grok.context(IStudentStudyLevel)
2026    grok.name('validate_courses')
2027    grok.require('waeup.validateStudent')
2028
2029    def update(self):
2030        if not self.context.__parent__.is_current:
2031            emit_lock_message(self)
2032            return
2033        if str(self.context.student.current_level) != self.context.__name__:
2034            self.flash(_('This is not the student\'s current level.'),
2035                       type="danger")
2036        elif self.context.student.state == REGISTERED:
2037            IWorkflowInfo(self.context.student).fireTransition(
2038                'validate_courses')
2039            self.flash(_('Course list has been validated.'))
2040        else:
2041            self.flash(_('Student is in the wrong state.'), type="warning")
2042        self.redirect(self.url(self.context))
2043        return
2044
2045    def render(self):
2046        return
2047
2048class RejectCoursesView(UtilityView, grok.View):
2049    """ Reject course list by course adviser
2050    """
2051    grok.context(IStudentStudyLevel)
2052    grok.name('reject_courses')
2053    grok.require('waeup.validateStudent')
2054
2055    def update(self):
2056        if not self.context.__parent__.is_current:
2057            emit_lock_message(self)
2058            return
2059        if str(self.context.__parent__.current_level) != self.context.__name__:
2060            self.flash(_('This is not the student\'s current level.'),
2061                       type="danger")
2062            self.redirect(self.url(self.context))
2063            return
2064        elif self.context.student.state == VALIDATED:
2065            IWorkflowInfo(self.context.student).fireTransition('reset8')
2066            message = _('Course list request has been annulled.')
2067            self.flash(message)
2068        elif self.context.student.state == REGISTERED:
2069            IWorkflowInfo(self.context.student).fireTransition('reset7')
2070            message = _('Course list has been unregistered.')
2071            self.flash(message)
2072        else:
2073            self.flash(_('Student is in the wrong state.'), type="warning")
2074            self.redirect(self.url(self.context))
2075            return
2076        args = {'subject':message}
2077        self.redirect(self.url(self.context.student) +
2078            '/contactstudent?%s' % urlencode(args))
2079        return
2080
2081    def render(self):
2082        return
2083
2084class UnregisterCoursesView(UtilityView, grok.View):
2085    """Unregister course list by student
2086    """
2087    grok.context(IStudentStudyLevel)
2088    grok.name('unregister_courses')
2089    grok.require('waeup.handleStudent')
2090
2091    def update(self):
2092        if not self.context.__parent__.is_current:
2093            emit_lock_message(self)
2094            return
2095        try:
2096            deadline = grok.getSite()['configuration'][
2097                str(self.context.level_session)].coursereg_deadline
2098        except (TypeError, KeyError):
2099            deadline = None
2100        if deadline and deadline < datetime.now(pytz.utc):
2101            self.flash(_(
2102                "Course registration has ended. "
2103                "Unregistration is disabled."), type="warning")
2104        elif str(self.context.__parent__.current_level) != self.context.__name__:
2105            self.flash(_('This is not your current level.'), type="danger")
2106        elif self.context.student.state == REGISTERED:
2107            IWorkflowInfo(self.context.student).fireTransition('reset7')
2108            message = _('Course list has been unregistered.')
2109            self.flash(message)
2110        else:
2111            self.flash(_('You are in the wrong state.'), type="warning")
2112        self.redirect(self.url(self.context))
2113        return
2114
2115    def render(self):
2116        return
2117
2118class CourseTicketAddFormPage(KofaAddFormPage):
2119    """Add a course ticket.
2120    """
2121    grok.context(IStudentStudyLevel)
2122    grok.name('add')
2123    grok.require('waeup.manageStudent')
2124    label = _('Add course ticket')
2125    form_fields = grok.AutoFields(ICourseTicketAdd)
2126    pnav = 4
2127
2128    def update(self):
2129        if not self.context.__parent__.is_current \
2130            or self.context.student.studycourse_locked:
2131            emit_lock_message(self)
2132            return
2133        super(CourseTicketAddFormPage, self).update()
2134        return
2135
2136    @action(_('Add course ticket'), style='primary')
2137    def addCourseTicket(self, **data):
2138        course = data['course']
2139        success = addCourseTicket(self, course)
2140        if success:
2141            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
2142        return
2143
2144    @action(_('Cancel'), validator=NullValidator)
2145    def cancel(self, **data):
2146        self.redirect(self.url(self.context))
2147
2148class CourseTicketDisplayFormPage(KofaDisplayFormPage):
2149    """ Page to display course tickets
2150    """
2151    grok.context(ICourseTicket)
2152    grok.name('index')
2153    grok.require('waeup.viewStudent')
2154    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
2155        'ticket_session')
2156    grok.template('courseticketpage')
2157    pnav = 4
2158
2159    @property
2160    def label(self):
2161        return _('${a}: Course Ticket ${b}', mapping = {
2162            'a':self.context.student.display_fullname,
2163            'b':self.context.code})
2164
2165class CourseTicketManageFormPage(KofaEditFormPage):
2166    """ Page to manage course tickets
2167    """
2168    grok.context(ICourseTicket)
2169    grok.name('manage')
2170    grok.require('waeup.manageStudent')
2171    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
2172    form_fields['title'].for_display = True
2173    form_fields['fcode'].for_display = True
2174    form_fields['dcode'].for_display = True
2175    form_fields['semester'].for_display = True
2176    form_fields['passmark'].for_display = True
2177    form_fields['credits'].for_display = True
2178    form_fields['mandatory'].for_display = False
2179    form_fields['automatic'].for_display = True
2180    form_fields['carry_over'].for_display = True
2181    form_fields['ticket_session'].for_display = True
2182    pnav = 4
2183    grok.template('courseticketmanagepage')
2184
2185    def update(self):
2186        if not self.context.__parent__.__parent__.is_current \
2187            or self.context.student.studycourse_locked:
2188            emit_lock_message(self)
2189            return
2190        super(CourseTicketManageFormPage, self).update()
2191        return
2192
2193    @property
2194    def label(self):
2195        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
2196
2197    @action('Save', style='primary')
2198    def save(self, **data):
2199        msave(self, **data)
2200        return
2201
2202class PaymentsManageFormPage(KofaEditFormPage):
2203    """ Page to manage the student payments
2204
2205    This manage form page is for both students and students officers.
2206    """
2207    grok.context(IStudentPaymentsContainer)
2208    grok.name('index')
2209    grok.require('waeup.viewStudent')
2210    form_fields = grok.AutoFields(IStudentPaymentsContainer)
2211    grok.template('paymentsmanagepage')
2212    pnav = 4
2213
2214    @property
2215    def manage_payments_allowed(self):
2216        return checkPermission('waeup.payStudent', self.context)
2217
2218    def unremovable(self, ticket):
2219        usertype = getattr(self.request.principal, 'user_type', None)
2220        if not usertype:
2221            return False
2222        if not self.manage_payments_allowed:
2223            return True
2224        return (self.request.principal.user_type == 'student' and ticket.r_code)
2225
2226    @property
2227    def label(self):
2228        return _('${a}: Payments',
2229            mapping = {'a':self.context.__parent__.display_fullname})
2230
2231    @jsaction(_('Remove selected tickets'))
2232    def delPaymentTicket(self, **data):
2233        form = self.request.form
2234        if 'val_id' in form:
2235            child_id = form['val_id']
2236        else:
2237            self.flash(_('No payment selected.'), type="warning")
2238            self.redirect(self.url(self.context))
2239            return
2240        if not isinstance(child_id, list):
2241            child_id = [child_id]
2242        deleted = []
2243        for id in child_id:
2244            # Students are not allowed to remove used payment tickets
2245            ticket = self.context.get(id, None)
2246            if ticket is not None and not self.unremovable(ticket):
2247                del self.context[id]
2248                deleted.append(id)
2249        if len(deleted):
2250            self.flash(_('Successfully removed: ${a}',
2251                mapping = {'a': ', '.join(deleted)}))
2252            self.context.writeLogMessage(
2253                self,'removed: %s' % ', '.join(deleted))
2254        self.redirect(self.url(self.context))
2255        return
2256
2257class OnlinePaymentAddFormPage(KofaAddFormPage):
2258    """ Page to add an online payment ticket
2259    """
2260    grok.context(IStudentPaymentsContainer)
2261    grok.name('addop')
2262    grok.template('onlinepaymentaddform')
2263    grok.require('waeup.payStudent')
2264    form_fields = grok.AutoFields(IStudentOnlinePayment).select('p_combi')
2265    label = _('Add online payment')
2266    pnav = 4
2267
2268    @property
2269    def selectable_categories(self):
2270        student = self.context.__parent__
2271        categories = getUtility(
2272            IKofaUtils).selectable_payment_categories(student)
2273        return sorted(categories.items(), key=lambda value: value[1])
2274
2275    @property
2276    def selectable_payment_options(self):
2277        student = self.context.__parent__
2278        options = getUtility(
2279            IKofaUtils).selectable_payment_options(student)
2280        return sorted(options.items(), key=lambda value: value[1])
2281
2282    @action(_('Create ticket'), style='primary')
2283    def createTicket(self, **data):
2284        form = self.request.form
2285        p_category = form.get('form.p_category', None)
2286        p_option = form.get('form.p_option', None)
2287        p_combi = form.get('form.p_combi', [])
2288        if isinstance(form.get('form.p_combi', None), unicode):
2289            p_combi = [p_combi,]
2290        student = self.context.__parent__
2291        students_utils = getUtility(IStudentsUtils)
2292        # The hostel_application payment category is temporarily used
2293        # by Uniben.
2294        accommodation_session = grok.getSite()['hostels'].accommodation_session
2295        if p_category in ('bed_allocation', 'hostel_application') \
2296            and accommodation_session - student[
2297                'studycourse'].current_session > students_utils.ACCOMMODATION_SPAN:
2298                self.flash(
2299                    _('Your current session does not allow ' + \
2300                    'to book accommodation.'), type="danger")
2301                return
2302        if 'maintenance' in p_category:
2303            if str(accommodation_session) not in student['accommodation']:
2304                self.flash(_('You have not yet booked accommodation.'),
2305                           type="warning")
2306                return
2307        error, payment = students_utils.setPaymentDetails(
2308            p_category, student, None, None, p_combi)
2309        if error is not None:
2310            self.flash(error, type="danger")
2311            return
2312        if p_category == 'transfer':
2313            payment.p_item = form['new_programme']
2314        if p_option:
2315            payment.p_option = p_option
2316        self.context[payment.p_id] = payment
2317        self.flash(_('Payment ticket created.'))
2318        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2319        self.redirect(self.url(payment))
2320        return
2321
2322    @action(_('Cancel'), validator=NullValidator)
2323    def cancel(self, **data):
2324        self.redirect(self.url(self.context))
2325
2326class PreviousPaymentAddFormPage(KofaAddFormPage):
2327    """ Page to add an online payment ticket for previous sessions.
2328    """
2329    grok.context(IStudentPaymentsContainer)
2330    grok.name('addpp')
2331    grok.template('previouspaymentaddform')
2332    grok.require('waeup.payStudent')
2333    form_fields = grok.AutoFields(IStudentPreviousPayment)
2334    label = _('Add previous session online payment')
2335    pnav = 4
2336
2337    def update(self):
2338        if self.context.student.before_payment:
2339            self.flash(_("No previous payment to be made."), type="warning")
2340            self.redirect(self.url(self.context))
2341        super(PreviousPaymentAddFormPage, self).update()
2342        return
2343
2344    @property
2345    def selectable_payment_options(self):
2346        student = self.context.__parent__
2347        options = getUtility(
2348            IKofaUtils).selectable_payment_options(student)
2349        return sorted(options.items(), key=lambda value: value[1])
2350
2351    @action(_('Create ticket'), style='primary')
2352    def createTicket(self, **data):
2353        p_category = data['p_category']
2354        form = self.request.form
2355        p_option = form.get('form.p_option', None)
2356        previous_session = data.get('p_session', None)
2357        previous_level = data.get('p_level', None)
2358        student = self.context.__parent__
2359        students_utils = getUtility(IStudentsUtils)
2360        error, payment = students_utils.setPaymentDetails(
2361            p_category, student, previous_session, previous_level, None)
2362        if error is not None:
2363            self.flash(error, type="danger")
2364            return
2365        if p_option:
2366            payment.p_option = p_option
2367        self.context[payment.p_id] = payment
2368        self.flash(_('Payment ticket created.'))
2369        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2370        self.redirect(self.url(payment))
2371        return
2372
2373    @action(_('Cancel'), validator=NullValidator)
2374    def cancel(self, **data):
2375        self.redirect(self.url(self.context))
2376
2377class BalancePaymentAddFormPage(KofaAddFormPage):
2378    """ Page to add an online payment which can balance s previous session
2379    payment.
2380    """
2381    grok.context(IStudentPaymentsContainer)
2382    grok.name('addbp')
2383    grok.template('previouspaymentaddform')
2384    grok.require('waeup.manageStudent')
2385    form_fields = grok.AutoFields(IStudentBalancePayment)
2386    label = _('Add balance')
2387    pnav = 4
2388
2389    @property
2390    def selectable_payment_options(self):
2391        student = self.context.__parent__
2392        options = getUtility(
2393            IKofaUtils).selectable_payment_options(student)
2394        return sorted(options.items(), key=lambda value: value[1])
2395
2396    @action(_('Create ticket'), style='primary')
2397    def createTicket(self, **data):
2398        p_category = data['p_category']
2399        form = self.request.form
2400        p_option = form.get('form.p_option', None)
2401        balance_session = data.get('balance_session', None)
2402        balance_level = data.get('balance_level', None)
2403        balance_amount = data.get('balance_amount', None)
2404        student = self.context.__parent__
2405        students_utils = getUtility(IStudentsUtils)
2406        error, payment = students_utils.setBalanceDetails(
2407            p_category, student, balance_session,
2408            balance_level, balance_amount)
2409        if error is not None:
2410            self.flash(error, type="danger")
2411            return
2412        if p_option:
2413            payment.p_option = p_option
2414        self.context[payment.p_id] = payment
2415        self.flash(_('Payment ticket created.'))
2416        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2417        self.redirect(self.url(payment))
2418        return
2419
2420    @action(_('Cancel'), validator=NullValidator)
2421    def cancel(self, **data):
2422        self.redirect(self.url(self.context))
2423
2424class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2425    """ Page to view an online payment ticket
2426    """
2427    grok.context(IStudentOnlinePayment)
2428    grok.name('index')
2429    grok.require('waeup.viewStudent')
2430    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2431        'p_item', 'p_combi')
2432    form_fields[
2433        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2434    form_fields[
2435        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2436    pnav = 4
2437
2438    @property
2439    def label(self):
2440        return _('${a}: Online Payment Ticket ${b}', mapping = {
2441            'a':self.context.student.display_fullname,
2442            'b':self.context.p_id})
2443
2444class OnlinePaymentApproveView(UtilityView, grok.View):
2445    """ Callback view
2446    """
2447    grok.context(IStudentOnlinePayment)
2448    grok.name('approve')
2449    grok.require('waeup.managePortal')
2450
2451    def update(self):
2452        flashtype, msg, log = self.context.approveStudentPayment()
2453        if log is not None:
2454            # Add log message to students.log
2455            self.context.writeLogMessage(self,log)
2456            # Add log message to payments.log
2457            self.context.logger.info(
2458                '%s,%s,%s,%s,%s,,,,,,' % (
2459                self.context.student.student_id,
2460                self.context.p_id, self.context.p_category,
2461                self.context.amount_auth, self.context.r_code))
2462        self.flash(msg, type=flashtype)
2463        return
2464
2465    def render(self):
2466        self.redirect(self.url(self.context, '@@index'))
2467        return
2468
2469class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2470    """ Approval view for students.
2471
2472    This view is used for browser tests only and
2473    must be neutralized on custom pages!
2474    """
2475    grok.name('fake_approve')
2476    grok.require('waeup.payStudent')
2477
2478class ExportPDFPaymentSlip(UtilityView, grok.View):
2479    """Deliver a PDF slip of the context.
2480    """
2481    grok.context(IStudentOnlinePayment)
2482    grok.name('payment_slip.pdf')
2483    grok.require('waeup.viewStudent')
2484    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2485        'p_item', 'p_combi')
2486    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2487    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2488    prefix = 'form'
2489    note = None
2490    omit_fields = (
2491        'password', 'suspended', 'phone', 'date_of_birth',
2492        'adm_code', 'sex', 'suspended_comment', 'current_level',
2493        'flash_notice')
2494
2495    @property
2496    def title(self):
2497        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2498        return translate(_('Payment Data'), 'waeup.kofa',
2499            target_language=portal_language)
2500
2501    @property
2502    def label(self):
2503        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2504        return translate(_('Online Payment Slip'),
2505            'waeup.kofa', target_language=portal_language) \
2506            + ' %s' % self.context.p_id
2507
2508    def render(self):
2509        #if self.context.p_state != 'paid':
2510        #    self.flash('Ticket not yet paid.')
2511        #    self.redirect(self.url(self.context))
2512        #    return
2513        studentview = StudentBasePDFFormPage(self.context.student,
2514            self.request, self.omit_fields)
2515        students_utils = getUtility(IStudentsUtils)
2516        return students_utils.renderPDF(self, 'payment_slip.pdf',
2517            self.context.student, studentview, note=self.note,
2518            omit_fields=self.omit_fields)
2519
2520class AccommodationDisplayFormPage(KofaDisplayFormPage):
2521    """ Page to view bed tickets.
2522    This manage form page is for both students and students officers.
2523    """
2524    grok.context(IStudentAccommodation)
2525    grok.name('index')
2526    grok.require('waeup.viewStudent')
2527    form_fields = grok.AutoFields(IStudentAccommodation)
2528    grok.template('accommodationpage')
2529    pnav = 4
2530    with_hostel_selection = True
2531
2532    @property
2533    def label(self):
2534        return _('${a}: Accommodation',
2535            mapping = {'a':self.context.__parent__.display_fullname})
2536
2537    @property
2538    def desired_hostel(self):
2539        if self.context.desired_hostel == 'no':
2540            return _('No favoured hostel')
2541        if self.context.desired_hostel:
2542            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2543            if hostel is not None:
2544                return hostel.hostel_name
2545        return
2546
2547    def update(self):
2548        if checkPermission('waeup.handleAccommodation', self.context):
2549            self.redirect(self.url(self.context, 'manage'))
2550
2551class AccommodationManageFormPage(KofaEditFormPage):
2552    """ Page to manage bed tickets.
2553
2554    This manage form page is for both students and students officers.
2555    """
2556    grok.context(IStudentAccommodation)
2557    grok.name('manage')
2558    grok.require('waeup.handleAccommodation')
2559    form_fields = grok.AutoFields(IStudentAccommodation)
2560    grok.template('accommodationmanagepage')
2561    pnav = 4
2562    with_hostel_selection = True
2563
2564    @property
2565    def booking_allowed(self):
2566        students_utils = getUtility(IStudentsUtils)
2567        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2568        error_message = students_utils.checkAccommodationRequirements(
2569            self.context.student, acc_details)
2570        if error_message:
2571            return False
2572        return True
2573
2574    @property
2575    def actionsgroup1(self):
2576        if not self.booking_allowed:
2577            return []
2578        if not self.with_hostel_selection:
2579            return []
2580        return [_('Save')]
2581
2582    @property
2583    def actionsgroup2(self):
2584        if getattr(self.request.principal, 'user_type', None) == 'student':
2585            ## Book button can be disabled in custom packages by
2586            ## uncommenting the following lines.
2587            #if not self.booking_allowed:
2588            #    return []
2589            return [_('Book accommodation')]
2590        return [_('Book accommodation'), _('Remove selected')]
2591
2592    @property
2593    def label(self):
2594        return _('${a}: Accommodation',
2595            mapping = {'a':self.context.__parent__.display_fullname})
2596
2597    @property
2598    def desired_hostel(self):
2599        if self.context.desired_hostel == 'no':
2600            return _('No favoured hostel')
2601        if self.context.desired_hostel:
2602            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2603            if hostel is not None:
2604                return hostel.hostel_name
2605        return
2606
2607    def getHostels(self):
2608        """Get a list of all stored hostels.
2609        """
2610        yield(dict(name=None, title='--', selected=''))
2611        selected = ''
2612        if self.context.desired_hostel == 'no':
2613          selected = 'selected'
2614        yield(dict(name='no', title=_('No favoured hostel'), selected=selected))
2615        for val in grok.getSite()['hostels'].values():
2616            if val.special_handling == 'blocked':
2617                continue
2618            selected = ''
2619            if val.hostel_id == self.context.desired_hostel:
2620                selected = 'selected'
2621            yield(dict(name=val.hostel_id, title=val.hostel_name,
2622                       selected=selected))
2623
2624    @action(_('Save'), style='primary')
2625    def save(self):
2626        hostel = self.request.form.get('hostel', None)
2627        self.context.desired_hostel = hostel
2628        self.flash(_('Your selection has been saved.'))
2629        return
2630
2631    @action(_('Book accommodation'), style='primary')
2632    def bookAccommodation(self, **data):
2633        self.redirect(self.url(self.context, 'add'))
2634        return
2635
2636    @jsaction(_('Remove selected'))
2637    def delBedTickets(self, **data):
2638        if getattr(self.request.principal, 'user_type', None) == 'student':
2639            self.flash(_('You are not allowed to remove bed tickets.'),
2640                       type="warning")
2641            self.redirect(self.url(self.context))
2642            return
2643        form = self.request.form
2644        if 'val_id' in form:
2645            child_id = form['val_id']
2646        else:
2647            self.flash(_('No bed ticket selected.'), type="warning")
2648            self.redirect(self.url(self.context))
2649            return
2650        if not isinstance(child_id, list):
2651            child_id = [child_id]
2652        deleted = []
2653        for id in child_id:
2654            del self.context[id]
2655            deleted.append(id)
2656        if len(deleted):
2657            self.flash(_('Successfully removed: ${a}',
2658                mapping = {'a':', '.join(deleted)}))
2659            self.context.writeLogMessage(
2660                self,'removed: % s' % ', '.join(deleted))
2661        self.redirect(self.url(self.context))
2662        return
2663
2664class BedTicketAddPage(KofaPage):
2665    """ Page to add a bed ticket
2666    """
2667    grok.context(IStudentAccommodation)
2668    grok.name('add')
2669    grok.require('waeup.handleAccommodation')
2670    #grok.template('enterpin')
2671    ac_prefix = 'HOS'
2672    label = _('Add bed ticket')
2673    pnav = 4
2674    buttonname = _('Create bed ticket')
2675    notice = ''
2676    with_ac = True
2677    with_bedselection = True
2678
2679    @property
2680    def getAvailableBeds(self):
2681        """Get a list of all available beds.
2682        """
2683        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2684        entries = cat.searchResults(
2685            bed_type=(self.acc_details['bt'],self.acc_details['bt']))
2686        available_beds = [
2687            entry for entry in entries if entry.owner == NOT_OCCUPIED]
2688        desired_hostel = self.context.desired_hostel
2689        # Filter desired hostel beds
2690        if desired_hostel and desired_hostel != 'no':
2691            filtered_beds = [bed for bed in available_beds
2692                             if bed.bed_id.split('_')[0] == desired_hostel]
2693            available_beds = filtered_beds
2694        # Add legible bed coordinates
2695        for bed in available_beds:
2696            hall_title = bed.__parent__.hostel_name
2697            coordinates = bed.coordinates[1:]
2698            block, room_nr, bed_nr = coordinates
2699            bed.temp_bed_coordinates = _(
2700                '${a}, Block ${b}, Room ${c}, Bed ${d}', mapping = {
2701                'a':hall_title, 'b':block,
2702                'c':room_nr, 'd':bed_nr})
2703        return available_beds
2704
2705    def update(self, SUBMIT=None):
2706        student = self.context.student
2707        students_utils = getUtility(IStudentsUtils)
2708        self.acc_details  = students_utils.getAccommodationDetails(student)
2709        error_message = students_utils.checkAccommodationRequirements(
2710            student, self.acc_details)
2711        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2712        entries = cat.searchResults(
2713            owner=(student.student_id,student.student_id))
2714        self.show_available_beds = False
2715        if error_message:
2716            self.flash(error_message, type="warning")
2717            self.redirect(self.url(self.context))
2718            return
2719        if self.with_ac:
2720            self.ac_series = self.request.form.get('ac_series', None)
2721            self.ac_number = self.request.form.get('ac_number', None)
2722        available_beds = self.getAvailableBeds
2723        if SUBMIT is None:
2724            if self.with_bedselection and available_beds and not len(entries):
2725                self.show_available_beds = True
2726            return
2727        if self.with_ac:
2728            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2729            code = get_access_code(pin)
2730            if not code:
2731                self.flash(_('Activation code is invalid.'), type="warning")
2732                return
2733        # Search and book bed
2734        if len(entries):
2735            # If bed space has been manually allocated use this bed ...
2736            manual = True
2737            bed = list(entries)[0]
2738        else:
2739            # ... else search for available beds
2740            manual = False
2741            selected_bed = self.request.form.get('bed', None)
2742            if selected_bed:
2743                # Use selected bed
2744                beds = cat.searchResults(
2745                    bed_id=(selected_bed,selected_bed))
2746                bed = list(beds)[0]
2747                bed.bookBed(student.student_id)
2748            elif available_beds:
2749                # Select bed according to selectBed method
2750                students_utils = getUtility(IStudentsUtils)
2751                bed = students_utils.selectBed(available_beds)
2752                bed.bookBed(student.student_id)
2753            else:
2754                self.flash(_('There is no free bed in your category ${a}.',
2755                    mapping = {'a':self.acc_details['bt']}), type="warning")
2756                self.redirect(self.url(self.context))
2757                return
2758        if self.with_ac:
2759            # Mark pin as used (this also fires a pin related transition)
2760            if code.state == USED:
2761                self.flash(_('Activation code has already been used.'),
2762                           type="warning")
2763                if not manual:
2764                    # Release the previously booked bed
2765                    bed.owner = NOT_OCCUPIED
2766                    # Catalog must be informed
2767                    notify(grok.ObjectModifiedEvent(bed))
2768                return
2769            else:
2770                comment = _(u'invalidated')
2771                # Here we know that the ac is in state initialized so we do not
2772                # expect an exception, but the owner might be different
2773                success = invalidate_accesscode(
2774                    pin, comment, self.context.student.student_id)
2775                if not success:
2776                    self.flash(_('You are not the owner of this access code.'),
2777                               type="warning")
2778                    if not manual:
2779                        # Release the previously booked bed
2780                        bed.owner = NOT_OCCUPIED
2781                        # Catalog must be informed
2782                        notify(grok.ObjectModifiedEvent(bed))
2783                    return
2784        # Create bed ticket
2785        bedticket = createObject(u'waeup.BedTicket')
2786        if self.with_ac:
2787            bedticket.booking_code = pin
2788        bedticket.booking_session = self.acc_details['booking_session']
2789        bedticket.bed_type = self.acc_details['bt']
2790        bedticket.bed = bed
2791        hall_title = bed.__parent__.hostel_name
2792        coordinates = bed.coordinates[1:]
2793        block, room_nr, bed_nr = coordinates
2794        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2795            'a':hall_title, 'b':block,
2796            'c':room_nr, 'd':bed_nr,
2797            'e':bed.bed_type})
2798        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2799        bedticket.bed_coordinates = translate(
2800            bc, 'waeup.kofa',target_language=portal_language)
2801        self.context.addBedTicket(bedticket)
2802        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2803        self.flash(_('Bed ticket created and bed booked: ${a}',
2804            mapping = {'a':bedticket.display_coordinates}))
2805        self.redirect(self.url(self.context))
2806        return
2807
2808class BedTicketDisplayFormPage(KofaDisplayFormPage):
2809    """ Page to display bed tickets
2810    """
2811    grok.context(IBedTicket)
2812    grok.name('index')
2813    grok.require('waeup.viewStudent')
2814    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2815    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2816    pnav = 4
2817
2818    @property
2819    def label(self):
2820        return _('Bed Ticket for Session ${a}',
2821            mapping = {'a':self.context.getSessionString()})
2822
2823class ExportPDFBedTicketSlip(UtilityView, grok.View):
2824    """Deliver a PDF slip of the context.
2825    """
2826    grok.context(IBedTicket)
2827    grok.name('bed_allocation_slip.pdf')
2828    grok.require('waeup.viewStudent')
2829    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2830    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2831    prefix = 'form'
2832    omit_fields = (
2833        'password', 'suspended', 'phone', 'adm_code',
2834        'suspended_comment', 'date_of_birth', 'current_level',
2835        'flash_notice')
2836
2837    @property
2838    def title(self):
2839        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2840        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2841            target_language=portal_language)
2842
2843    @property
2844    def label(self):
2845        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2846        #return translate(_('Bed Allocation: '),
2847        #    'waeup.kofa', target_language=portal_language) \
2848        #    + ' %s' % self.context.bed_coordinates
2849        return translate(_('Bed Allocation Slip'),
2850            'waeup.kofa', target_language=portal_language) \
2851            + ' %s' % self.context.getSessionString()
2852
2853    def render(self):
2854        studentview = StudentBasePDFFormPage(self.context.student,
2855            self.request, self.omit_fields)
2856        students_utils = getUtility(IStudentsUtils)
2857        note = None
2858        n = grok.getSite()['hostels'].allocation_expiration
2859        if n:
2860            note = _("""
2861<br /><br /><br /><br /><br /><font size="12">
2862Please endeavour to pay your hostel maintenance charge within ${a} days
2863 of being allocated a space or else you are deemed to have
2864 voluntarily forfeited it and it goes back into circulation to be
2865 available for booking afresh!</font>)
2866""")
2867            note = _(note, mapping={'a': n})
2868            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2869            note = translate(
2870                note, 'waeup.kofa', target_language=portal_language)
2871        return students_utils.renderPDF(
2872            self, 'bed_allocation_slip.pdf',
2873            self.context.student, studentview,
2874            omit_fields=self.omit_fields,
2875            note=note)
2876
2877class BedTicketRelocationView(UtilityView, grok.View):
2878    """ Callback view
2879    """
2880    grok.context(IBedTicket)
2881    grok.name('relocate')
2882    grok.require('waeup.manageHostels')
2883
2884    # Relocate student if student parameters have changed or the bed_type
2885    # of the bed has changed
2886    def update(self):
2887        success, msg = self.context.relocateStudent()
2888        if not success:
2889            self.flash(msg, type="warning")
2890        else:
2891            self.flash(msg)
2892        self.redirect(self.url(self.context))
2893        return
2894
2895    def render(self):
2896        return
2897
2898class StudentHistoryPage(KofaPage):
2899    """ Page to display student history
2900    """
2901    grok.context(IStudent)
2902    grok.name('history')
2903    grok.require('waeup.viewStudent')
2904    grok.template('studenthistory')
2905    pnav = 4
2906
2907    @property
2908    def label(self):
2909        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2910
2911# Pages for students only
2912
2913class StudentBaseEditFormPage(KofaEditFormPage):
2914    """ View to edit student base data
2915    """
2916    grok.context(IStudent)
2917    grok.name('edit_base')
2918    grok.require('waeup.handleStudent')
2919    form_fields = grok.AutoFields(IStudentBase).select(
2920        'email', 'phone', 'parents_email')
2921    label = _('Edit base data')
2922    pnav = 4
2923
2924    @action(_('Save'), style='primary')
2925    def save(self, **data):
2926        msave(self, **data)
2927        return
2928
2929class StudentChangePasswordPage(KofaEditFormPage):
2930    """ View to edit student passwords
2931    """
2932    grok.context(IStudent)
2933    grok.name('change_password')
2934    grok.require('waeup.handleStudent')
2935    grok.template('change_password')
2936    label = _('Change password')
2937    pnav = 4
2938
2939    @action(_('Save'), style='primary')
2940    def save(self, **data):
2941        form = self.request.form
2942        password = form.get('change_password', None)
2943        password_ctl = form.get('change_password_repeat', None)
2944        if password:
2945            validator = getUtility(IPasswordValidator)
2946            errors = validator.validate_password(password, password_ctl)
2947            if not errors:
2948                IUserAccount(self.context).setPassword(password)
2949                # Unset temporary password
2950                self.context.temp_password = None
2951                self.context.writeLogMessage(self, 'saved: password')
2952                self.flash(_('Password changed.'))
2953            else:
2954                self.flash( ' '.join(errors), type="warning")
2955        return
2956
2957class StudentFilesUploadPage(KofaPage):
2958    """ View to upload passport picture by student. The class name
2959    is historical. The page is only used for the student's portrait.
2960    """
2961    grok.context(IStudent)
2962    grok.name('change_portrait')
2963    grok.require('waeup.uploadStudentFile')
2964    grok.template('filesuploadpage')
2965    label = _('Upload portrait')
2966    pnav = 4
2967
2968    def update(self):
2969        if not getUtility(IStudentsUtils).allowPortraitChange(self.context):
2970            emit_lock_message(self,
2971                _('The requested form is locked.'))
2972            return
2973        super(StudentFilesUploadPage, self).update()
2974        return
2975
2976class StudentSignatureUploadPage(KofaPage):
2977    """ View to upload scanned signature by student. Not active
2978    in base package.
2979    """
2980    grok.context(IStudent)
2981    grok.name('change_signature')
2982    grok.require('waeup.uploadStudentFile')
2983    grok.template('filesuploadpage')
2984    label = _('Upload signature')
2985    pnav = 4
2986
2987    def update(self):
2988        SIGNATURE_CHANGE_STATES = getUtility(IStudentsUtils).SIGNATURE_CHANGE_STATES
2989        if self.context.student.state not in SIGNATURE_CHANGE_STATES:
2990            emit_lock_message(self,
2991                _('This form is locked. You are in the wrong state.'))
2992            return
2993        super(StudentSignatureUploadPage, self).update()
2994        return
2995
2996class StudentFinalClearanceUploadPage(KofaPage):
2997    """View to upload scanned final clearance routing slip by student. Not active
2998    in base package.
2999    """
3000    grok.context(IStudent)
3001    grok.name('routing_slip_upload')
3002    grok.require('waeup.uploadStudentFile')
3003    grok.template('finalclearanceuploadpage')
3004    label = _('Upload final clearance routing slip')
3005    deletion_warning = _('Are you sure?')
3006    pnav = 4
3007
3008    def update(self):
3009        final_clearance_enabled = getUtility(
3010            IStudentsUtils).final_clearance_enabled(self.context)
3011        if not final_clearance_enabled:
3012            emit_lock_message(self,
3013                _('This form is locked.'))
3014            return
3015        super(StudentFinalClearanceUploadPage, self).update()
3016        return
3017
3018class ExportPDFFinalClearanceSlip(UtilityView, grok.View):
3019    """Deliver a PDF document.
3020    """
3021    grok.context(IStudent)
3022    grok.name('final_clearance_slip.pdf')
3023    grok.require('waeup.viewStudent')
3024    prefix = 'form'
3025    form_fields = None
3026
3027    omit_fields = (
3028        'suspended',
3029        'adm_code',
3030        'suspended_comment',
3031        'current_level',
3032        'flash_notice',
3033        'entry_session',
3034        'parents_email',
3035        'email',
3036        'phone',
3037        'date_of_birth',
3038        'sex',)
3039
3040    @property
3041    def label(self):
3042        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
3043        return translate(_('Final Document Upload Indicator Slip'), 'waeup.kofa',
3044            target_language=portal_language)
3045
3046    def render(self):
3047        final_slip = getUtility(IExtFileStore).getFileByContext(
3048            self.context.student, attr='routingslip.pdf')
3049        if not final_slip:
3050            emit_lock_message(self,
3051                _('The requested form is locked.'))
3052            return
3053        studentview = StudentBasePDFFormPage(self.context.student,
3054            self.request, self.omit_fields)
3055        students_utils = getUtility(IStudentsUtils)
3056        note = _('''
3057The final clearance routing slip has been successfully uploaded onto the Kofa portal.
3058
3059Take this slip to the Exams and Records Division for the collection of Statement of Results.
3060
3061Exams and Records Division will send notification for date of collection.
3062''')
3063        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
3064        note = translate(
3065            note, 'waeup.kofa', target_language=portal_language)
3066        return students_utils.renderPDF(
3067            self, 'final_clearance_slip.pdf',
3068            self.context.student, studentview,
3069            omit_fields=self.omit_fields,
3070            note=note)
3071
3072class StartClearancePage(KofaPage):
3073    grok.context(IStudent)
3074    grok.name('start_clearance')
3075    grok.require('waeup.handleStudent')
3076    grok.template('enterpin')
3077    label = _('Start clearance')
3078    ac_prefix = 'CLR'
3079    notice = ''
3080    pnav = 4
3081    buttonname = _('Start clearance now')
3082    with_ac = True
3083
3084    @property
3085    def all_required_fields_filled(self):
3086        if not self.context.email:
3087            return _("Email address is missing."), 'edit_base'
3088        if not self.context.phone:
3089            return _("Phone number is missing."), 'edit_base'
3090        return
3091
3092    @property
3093    def portrait_uploaded(self):
3094        store = getUtility(IExtFileStore)
3095        if store.getFileByContext(self.context, attr=u'passport.jpg'):
3096            return True
3097        return False
3098
3099    def update(self, SUBMIT=None):
3100        if not self.context.state == ADMITTED:
3101            self.flash(_("Wrong state"), type="warning")
3102            self.redirect(self.url(self.context))
3103            return
3104        if not self.portrait_uploaded:
3105            self.flash(_("No portrait uploaded."), type="warning")
3106            self.redirect(self.url(self.context, 'change_portrait'))
3107            return
3108        if self.all_required_fields_filled:
3109            arf_warning = self.all_required_fields_filled[0]
3110            arf_redirect = self.all_required_fields_filled[1]
3111            self.flash(arf_warning, type="warning")
3112            self.redirect(self.url(self.context, arf_redirect))
3113            return
3114        if self.with_ac:
3115            self.ac_series = self.request.form.get('ac_series', None)
3116            self.ac_number = self.request.form.get('ac_number', None)
3117        if SUBMIT is None:
3118            return
3119        if self.with_ac:
3120            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3121            code = get_access_code(pin)
3122            if not code:
3123                self.flash(_('Activation code is invalid.'), type="warning")
3124                return
3125            if code.state == USED:
3126                self.flash(_('Activation code has already been used.'),
3127                           type="warning")
3128                return
3129            # Mark pin as used (this also fires a pin related transition)
3130            # and fire transition start_clearance
3131            comment = _(u"invalidated")
3132            # Here we know that the ac is in state initialized so we do not
3133            # expect an exception, but the owner might be different
3134            if not invalidate_accesscode(pin, comment, self.context.student_id):
3135                self.flash(_('You are not the owner of this access code.'),
3136                           type="warning")
3137                return
3138            self.context.clr_code = pin
3139        IWorkflowInfo(self.context).fireTransition('start_clearance')
3140        self.flash(_('Clearance process has been started.'))
3141        self.redirect(self.url(self.context,'cedit'))
3142        return
3143
3144class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
3145    """ View to edit student clearance data by student
3146    """
3147    grok.context(IStudent)
3148    grok.name('cedit')
3149    grok.require('waeup.handleStudent')
3150    label = _('Edit clearance data')
3151
3152    @property
3153    def form_fields(self):
3154        if self.context.is_postgrad:
3155            form_fields = grok.AutoFields(IPGStudentClearance).omit(
3156                'clr_code', 'officer_comment')
3157        else:
3158            form_fields = grok.AutoFields(IUGStudentClearance).omit(
3159                'clr_code', 'officer_comment')
3160        return form_fields
3161
3162    def update(self):
3163        if self.context.clearance_locked:
3164            emit_lock_message(self)
3165            return
3166        return super(StudentClearanceEditFormPage, self).update()
3167
3168    @action(_('Save'), style='primary')
3169    def save(self, **data):
3170        self.applyData(self.context, **data)
3171        self.flash(_('Clearance form has been saved.'))
3172        return
3173
3174    def dataNotComplete(self):
3175        """To be implemented in the customization package.
3176        """
3177        return False
3178
3179    @action(_('Save and request clearance'), style='primary',
3180            warning=_('You can not edit your data after '
3181            'requesting clearance. You really want to request clearance now?'))
3182    def requestClearance(self, **data):
3183        self.applyData(self.context, **data)
3184        if self.dataNotComplete():
3185            self.flash(self.dataNotComplete(), type="warning")
3186            return
3187        self.flash(_('Clearance form has been saved.'))
3188        if self.context.clr_code:
3189            self.redirect(self.url(self.context, 'request_clearance'))
3190        else:
3191            # We bypass the request_clearance page if student
3192            # has been imported in state 'clearance started' and
3193            # no clr_code was entered before.
3194            state = IWorkflowState(self.context).getState()
3195            if state != CLEARANCE:
3196                # This shouldn't happen, but the application officer
3197                # might have forgotten to lock the form after changing the state
3198                self.flash(_('This form cannot be submitted. Wrong state!'),
3199                           type="danger")
3200                return
3201            IWorkflowInfo(self.context).fireTransition('request_clearance')
3202            self.flash(_('Clearance has been requested.'))
3203            self.redirect(self.url(self.context))
3204        return
3205
3206class RequestClearancePage(KofaPage):
3207    grok.context(IStudent)
3208    grok.name('request_clearance')
3209    grok.require('waeup.handleStudent')
3210    grok.template('enterpin')
3211    label = _('Request clearance')
3212    notice = _('Enter the CLR access code used for starting clearance.')
3213    ac_prefix = 'CLR'
3214    pnav = 4
3215    buttonname = _('Request clearance now')
3216    with_ac = True
3217
3218    def update(self, SUBMIT=None):
3219        if self.with_ac:
3220            self.ac_series = self.request.form.get('ac_series', None)
3221            self.ac_number = self.request.form.get('ac_number', None)
3222        if SUBMIT is None:
3223            return
3224        if self.with_ac:
3225            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3226            if self.context.clr_code and self.context.clr_code != pin:
3227                self.flash(_("This isn't your CLR access code."), type="danger")
3228                return
3229        state = IWorkflowState(self.context).getState()
3230        if state != CLEARANCE:
3231            # This shouldn't happen, but the application officer
3232            # might have forgotten to lock the form after changing the state
3233            self.flash(_('This form cannot be submitted. Wrong state!'),
3234                       type="danger")
3235            return
3236        IWorkflowInfo(self.context).fireTransition('request_clearance')
3237        self.flash(_('Clearance has been requested.'))
3238        self.redirect(self.url(self.context))
3239        return
3240
3241class StartSessionPage(KofaPage):
3242    grok.context(IStudentStudyCourse)
3243    grok.name('start_session')
3244    grok.require('waeup.handleStudent')
3245    grok.template('enterpin')
3246    label = _('Start session')
3247    ac_prefix = 'SFE'
3248    notice = ''
3249    pnav = 4
3250    buttonname = _('Start now')
3251    with_ac = True
3252
3253    def update(self, SUBMIT=None):
3254        if not self.context.is_current:
3255            emit_lock_message(self)
3256            return
3257        super(StartSessionPage, self).update()
3258        if not self.context.next_session_allowed:
3259            self.flash(_("You are not entitled to start session."),
3260                       type="warning")
3261            self.redirect(self.url(self.context))
3262            return
3263        if self.with_ac:
3264            self.ac_series = self.request.form.get('ac_series', None)
3265            self.ac_number = self.request.form.get('ac_number', None)
3266        if SUBMIT is None:
3267            return
3268        if self.with_ac:
3269            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3270            code = get_access_code(pin)
3271            if not code:
3272                self.flash(_('Activation code is invalid.'), type="warning")
3273                return
3274            # Mark pin as used (this also fires a pin related transition)
3275            if code.state == USED:
3276                self.flash(_('Activation code has already been used.'),
3277                           type="warning")
3278                return
3279            else:
3280                comment = _(u"invalidated")
3281                # Here we know that the ac is in state initialized so we do not
3282                # expect an error, but the owner might be different
3283                if not invalidate_accesscode(
3284                    pin,comment,self.context.student.student_id):
3285                    self.flash(_('You are not the owner of this access code.'),
3286                               type="warning")
3287                    return
3288        try:
3289            if self.context.student.state == CLEARED:
3290                IWorkflowInfo(self.context.student).fireTransition(
3291                    'pay_first_school_fee')
3292            elif self.context.student.state == RETURNING:
3293                IWorkflowInfo(self.context.student).fireTransition(
3294                    'pay_school_fee')
3295            elif self.context.student.state == PAID:
3296                IWorkflowInfo(self.context.student).fireTransition(
3297                    'pay_pg_fee')
3298        except ConstraintNotSatisfied:
3299            self.flash(_('An error occurred, please contact the system administrator.'),
3300                       type="danger")
3301            return
3302        self.flash(_('Session started.'))
3303        self.redirect(self.url(self.context))
3304        return
3305
3306class AddStudyLevelFormPage(KofaEditFormPage):
3307    """ Page for students to add current study levels
3308    """
3309    grok.context(IStudentStudyCourse)
3310    grok.name('add')
3311    grok.require('waeup.handleStudent')
3312    grok.template('studyleveladdpage')
3313    form_fields = grok.AutoFields(IStudentStudyCourse)
3314    pnav = 4
3315
3316    @property
3317    def label(self):
3318        studylevelsource = StudyLevelSource().factory
3319        code = self.context.current_level
3320        title = studylevelsource.getTitle(self.context, code)
3321        return _('Add current level ${a}', mapping = {'a':title})
3322
3323    def update(self):
3324        if not self.context.is_current \
3325            or self.context.student.studycourse_locked:
3326            emit_lock_message(self)
3327            return
3328        if self.context.student.state != PAID:
3329            emit_lock_message(self)
3330            return
3331        code = self.context.current_level
3332        if code is None:
3333            self.flash(_('Your data are incomplete'), type="danger")
3334            self.redirect(self.url(self.context))
3335            return
3336        super(AddStudyLevelFormPage, self).update()
3337        return
3338
3339    @action(_('Create course list now'), style='primary')
3340    def addStudyLevel(self, **data):
3341        studylevel = createObject(u'waeup.StudentStudyLevel')
3342        studylevel.level = self.context.current_level
3343        studylevel.level_session = self.context.current_session
3344        try:
3345            self.context.addStudentStudyLevel(
3346                self.context.certificate,studylevel)
3347        except KeyError:
3348            self.flash(_('This level exists.'), type="warning")
3349            self.redirect(self.url(self.context))
3350            return
3351        except RequiredMissing:
3352            self.flash(_('Your data are incomplete.'), type="danger")
3353            self.redirect(self.url(self.context))
3354            return
3355        self.flash(_('You successfully created a new course list.'))
3356        self.redirect(self.url(self.context, str(studylevel.level)))
3357        return
3358
3359class StudyLevelEditFormPage(KofaEditFormPage):
3360    """ Page to edit the student study level data by students
3361    """
3362    grok.context(IStudentStudyLevel)
3363    grok.name('edit')
3364    grok.require('waeup.editStudyLevel')
3365    grok.template('studyleveleditpage')
3366    pnav = 4
3367    placeholder = _('Enter valid course code')
3368
3369    def update(self, ADD=None, course=None):
3370        if not self.context.__parent__.is_current:
3371            emit_lock_message(self)
3372            return
3373        if self.context.student.state != PAID or \
3374            not self.context.is_current_level:
3375            emit_lock_message(self)
3376            return
3377        super(StudyLevelEditFormPage, self).update()
3378        if ADD is not None:
3379            if not course:
3380                self.flash(_('No valid course code entered.'), type="warning")
3381                return
3382            cat = queryUtility(ICatalog, name='courses_catalog')
3383            result = cat.searchResults(code=(course, course))
3384            if len(result) != 1:
3385                self.flash(_('Course not found.'), type="warning")
3386                return
3387            course = list(result)[0]
3388            if course.former_course:
3389                self.flash(_('Former courses can\'t be added.'), type="warning")
3390                return
3391            addCourseTicket(self, course)
3392        return
3393
3394    @property
3395    def label(self):
3396        # Here we know that the cookie has been set
3397        lang = self.request.cookies.get('kofa.language')
3398        level_title = translate(self.context.level_title, 'waeup.kofa',
3399            target_language=lang)
3400        return _('Edit course list of ${a}',
3401            mapping = {'a':level_title})
3402
3403    @property
3404    def translated_values(self):
3405        return translated_values(self)
3406
3407    def _delCourseTicket(self, **data):
3408        form = self.request.form
3409        if 'val_id' in form:
3410            child_id = form['val_id']
3411        else:
3412            self.flash(_('No ticket selected.'), type="warning")
3413            self.redirect(self.url(self.context, '@@edit'))
3414            return
3415        if not isinstance(child_id, list):
3416            child_id = [child_id]
3417        deleted = []
3418        for id in child_id:
3419            # Students are not allowed to remove core tickets
3420            if id in self.context and \
3421                self.context[id].removable_by_student:
3422                del self.context[id]
3423                deleted.append(id)
3424        if len(deleted):
3425            self.flash(_('Successfully removed: ${a}',
3426                mapping = {'a':', '.join(deleted)}))
3427            self.context.writeLogMessage(
3428                self,'removed: %s at %s' %
3429                (', '.join(deleted), self.context.level))
3430        self.redirect(self.url(self.context, u'@@edit'))
3431        return
3432
3433    @jsaction(_('Remove selected tickets'))
3434    def delCourseTicket(self, **data):
3435        self._delCourseTicket(**data)
3436        return
3437
3438    def _updateTickets(self, **data):
3439        cat = queryUtility(ICatalog, name='courses_catalog')
3440        invalidated = list()
3441        for value in self.context.values():
3442            result = cat.searchResults(code=(value.code, value.code))
3443            if len(result) != 1:
3444                course = None
3445            else:
3446                course = list(result)[0]
3447            invalid = self.context.updateCourseTicket(value, course)
3448            if invalid:
3449                invalidated.append(invalid)
3450        if invalidated:
3451            invalidated_string = ', '.join(invalidated)
3452            self.context.writeLogMessage(
3453                self, 'course tickets invalidated: %s' % invalidated_string)
3454        self.flash(_('All course tickets updated.'))
3455        return
3456
3457    @action(_('Update all tickets'),
3458        tooltip=_('Update all course parameters including course titles.'))
3459    def updateTickets(self, **data):
3460        self._updateTickets(**data)
3461        return
3462
3463    def _registerCourses(self, **data):
3464        if self.context.student.is_postgrad and \
3465            not self.context.student.is_special_postgrad:
3466            self.flash(_(
3467                "You are a postgraduate student, "
3468                "your course list can't bee registered."), type="warning")
3469            self.redirect(self.url(self.context))
3470            return
3471        students_utils = getUtility(IStudentsUtils)
3472        warning = students_utils.warnCreditsOOR(self.context)
3473        if warning:
3474            self.flash(warning, type="warning")
3475            return
3476        msg = self.context.course_registration_forbidden
3477        if msg:
3478            self.flash(msg, type="warning")
3479            return
3480        IWorkflowInfo(self.context.student).fireTransition(
3481            'register_courses')
3482        self.flash(_('Course list has been registered.'))
3483        self.redirect(self.url(self.context))
3484        return
3485
3486    @action(_('Register course list'), style='primary',
3487        warning=_('You can not edit your course list after registration.'
3488            ' You really want to register?'))
3489    def registerCourses(self, **data):
3490        self._registerCourses(**data)
3491        return
3492
3493class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3494    """Add a course ticket by student.
3495    """
3496    grok.name('ctadd')
3497    grok.require('waeup.handleStudent')
3498    form_fields = grok.AutoFields(ICourseTicketAdd)
3499
3500    def update(self):
3501        if self.context.student.state != PAID or \
3502            not self.context.is_current_level:
3503            emit_lock_message(self)
3504            return
3505        super(CourseTicketAddFormPage2, self).update()
3506        return
3507
3508    @action(_('Add course ticket'))
3509    def addCourseTicket(self, **data):
3510        # Safety belt
3511        if self.context.student.state != PAID:
3512            return
3513        course = data['course']
3514        if course.former_course:
3515            self.flash(_('Former courses can\'t be added.'), type="warning")
3516            return
3517        success = addCourseTicket(self, course)
3518        if success:
3519            self.redirect(self.url(self.context, u'@@edit'))
3520        return
3521
3522class SetPasswordPage(KofaPage):
3523    grok.context(IKofaObject)
3524    grok.name('setpassword')
3525    grok.require('waeup.Anonymous')
3526    grok.template('setpassword')
3527    label = _('Set password for first-time login')
3528    ac_prefix = 'PWD'
3529    pnav = 0
3530    set_button = _('Set')
3531
3532    def update(self, SUBMIT=None):
3533        self.reg_number = self.request.form.get('reg_number', None)
3534        self.ac_series = self.request.form.get('ac_series', None)
3535        self.ac_number = self.request.form.get('ac_number', None)
3536
3537        if SUBMIT is None:
3538            return
3539        hitlist = search(query=self.reg_number,
3540            searchtype='reg_number', view=self)
3541        if not hitlist:
3542            self.flash(_('No student found.'), type="warning")
3543            return
3544        if len(hitlist) != 1:   # Cannot happen but anyway
3545            self.flash(_('More than one student found.'), type="warning")
3546            return
3547        student = hitlist[0].context
3548        self.student_id = student.student_id
3549        student_pw = student.password
3550        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3551        code = get_access_code(pin)
3552        if not code:
3553            self.flash(_('Access code is invalid.'), type="warning")
3554            return
3555        if student_pw and pin == student.adm_code:
3556            self.flash(_(
3557                'Password has already been set. Your Student Id is ${a}',
3558                mapping = {'a':self.student_id}))
3559            return
3560        elif student_pw:
3561            self.flash(
3562                _('Password has already been set. You are using the ' +
3563                'wrong Access Code.'), type="warning")
3564            return
3565        # Mark pin as used (this also fires a pin related transition)
3566        # and set student password
3567        if code.state == USED:
3568            self.flash(_('Access code has already been used.'), type="warning")
3569            return
3570        else:
3571            comment = _(u"invalidated")
3572            # Here we know that the ac is in state initialized so we do not
3573            # expect an exception
3574            invalidate_accesscode(pin,comment)
3575            IUserAccount(student).setPassword(self.ac_number)
3576            student.adm_code = pin
3577        self.flash(_('Password has been set. Your Student Id is ${a}',
3578            mapping = {'a':self.student_id}))
3579        return
3580
3581class StudentRequestPasswordPage(KofaAddFormPage):
3582    """Captcha'd request password page for students.
3583    """
3584    grok.name('requestpw')
3585    grok.require('waeup.Anonymous')
3586    grok.template('requestpw')
3587    form_fields = grok.AutoFields(IStudentRequestPW).select(
3588        'lastname','number','email')
3589    label = _('Request password for first-time login')
3590
3591    def update(self):
3592        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3593        if blocker:
3594            self.flash(_('The portal is in maintenance mode. '
3595                        'Password request forms are temporarily disabled.'),
3596                       type='warning')
3597            self.redirect(self.url(self.context))
3598            return
3599        # Handle captcha
3600        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3601        self.captcha_result = self.captcha.verify(self.request)
3602        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3603        return
3604
3605    def _redirect(self, email, password, student_id):
3606        # Forward only email to landing page in base package.
3607        self.redirect(self.url(self.context, 'requestpw_complete',
3608            data = dict(email=email)))
3609        return
3610
3611    def _redirect_no_student(self):
3612        # No record found, this is the truth. We do not redirect here.
3613        # We are using this method in custom packages
3614        # for redirecting alumni to the application section.
3615        self.flash(_('No student record found.'), type="warning")
3616        return
3617
3618    def _pw_used(self):
3619        # XXX: False if password has not been used. We need an extra
3620        #      attribute which remembers if student logged in.
3621        return True
3622
3623    @action(_('Send login credentials to email address'), style='primary')
3624    def get_credentials(self, **data):
3625        if not self.captcha_result.is_valid:
3626            # Captcha will display error messages automatically.
3627            # No need to flash something.
3628            return
3629        number = data.get('number','')
3630        lastname = data.get('lastname','')
3631        cat = getUtility(ICatalog, name='students_catalog')
3632        results = list(
3633            cat.searchResults(reg_number=(number, number)))
3634        if not results:
3635            results = list(
3636                cat.searchResults(matric_number=(number, number)))
3637        if results:
3638            student = results[0]
3639            if getattr(student,'lastname',None) is None:
3640                self.flash(_('An error occurred.'), type="danger")
3641                return
3642            elif student.lastname.lower() != lastname.lower():
3643                # Don't tell the truth here. Anonymous must not
3644                # know that a record was found and only the lastname
3645                # verification failed.
3646                self.flash(_('No student record found.'), type="warning")
3647                return
3648            elif student.password is not None and self._pw_used:
3649                self.flash(_('Your password has already been set and used. '
3650                             'Please proceed to the login page.'),
3651                           type="warning")
3652                return
3653            # Store email address but nothing else.
3654            student.email = data['email']
3655            notify(grok.ObjectModifiedEvent(student))
3656        else:
3657            self._redirect_no_student()
3658            return
3659
3660        kofa_utils = getUtility(IKofaUtils)
3661        password = kofa_utils.genPassword()
3662        mandate = PasswordMandate()
3663        mandate.params['password'] = password
3664        mandate.params['user'] = student
3665        site = grok.getSite()
3666        site['mandates'].addMandate(mandate)
3667        # Send email with credentials
3668        args = {'mandate_id':mandate.mandate_id}
3669        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3670        url_info = u'Confirmation link: %s' % mandate_url
3671        msg = _('You have successfully requested a password for the')
3672        if kofa_utils.sendCredentials(IUserAccount(student),
3673            password, url_info, msg):
3674            email_sent = student.email
3675        else:
3676            email_sent = None
3677        self._redirect(email=email_sent, password=password,
3678            student_id=student.student_id)
3679        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3680        self.context.logger.info(
3681            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3682        return
3683
3684class ParentsUser:
3685    pass
3686
3687class RequestParentsPasswordPage(StudentRequestPasswordPage):
3688    """Captcha'd request password page for parents.
3689    """
3690    grok.name('requestppw')
3691    grok.template('requestppw')
3692    label = _('Request password for parents access')
3693
3694    def update(self):
3695        super(RequestParentsPasswordPage, self).update()
3696        kofa_utils = getUtility(IKofaUtils)
3697        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3698        return
3699
3700    @action(_('Send temporary login credentials to email address'), style='primary')
3701    def get_credentials(self, **data):
3702        if not self.captcha_result.is_valid:
3703            # Captcha will display error messages automatically.
3704            # No need to flash something.
3705            return
3706        number = data.get('number','')
3707        lastname = data.get('lastname','')
3708        email = data['email']
3709        cat = getUtility(ICatalog, name='students_catalog')
3710        results = list(
3711            cat.searchResults(reg_number=(number, number)))
3712        if not results:
3713            results = list(
3714                cat.searchResults(matric_number=(number, number)))
3715        if results:
3716            student = results[0]
3717            if getattr(student,'lastname',None) is None:
3718                self.flash(_('An error occurred.'), type="danger")
3719                return
3720            elif student.lastname.lower() != lastname.lower():
3721                # Don't tell the truth here. Anonymous must not
3722                # know that a record was found and only the lastname
3723                # verification failed.
3724                self.flash(_('No student record found.'), type="warning")
3725                return
3726            elif email != student.parents_email:
3727                self.flash(_('Wrong email address.'), type="warning")
3728                return
3729        else:
3730            self._redirect_no_student()
3731            return
3732        kofa_utils = getUtility(IKofaUtils)
3733        password = kofa_utils.genPassword()
3734        mandate = ParentsPasswordMandate()
3735        mandate.params['password'] = password
3736        mandate.params['student'] = student
3737        site = grok.getSite()
3738        site['mandates'].addMandate(mandate)
3739        # Send email with credentials
3740        args = {'mandate_id':mandate.mandate_id}
3741        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3742        url_info = u'Confirmation link: %s' % mandate_url
3743        msg = _('You have successfully requested a parents password for the')
3744        # Create a fake user
3745        user = ParentsUser()
3746        user.name = student.student_id
3747        user.title = "Parents of %s" % student.display_fullname
3748        user.email = student.parents_email
3749        if kofa_utils.sendCredentials(user, password, url_info, msg):
3750            email_sent = user.email
3751        else:
3752            email_sent = None
3753        self._redirect(email=email_sent, password=password,
3754            student_id=student.student_id)
3755        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3756        self.context.logger.info(
3757            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3758        return
3759
3760class StudentRequestPasswordEmailSent(KofaPage):
3761    """Landing page after successful password request.
3762
3763    """
3764    grok.name('requestpw_complete')
3765    grok.require('waeup.Public')
3766    grok.template('requestpwmailsent')
3767    label = _('Your password request was successful.')
3768
3769    def update(self, email=None, student_id=None, password=None):
3770        self.email = email
3771        self.password = password
3772        self.student_id = student_id
3773        return
3774
3775class FilterStudentsInDepartmentPage(KofaPage):
3776    """Page that filters and lists students.
3777    """
3778    grok.context(IDepartment)
3779    grok.require('waeup.showStudents')
3780    grok.name('students')
3781    grok.template('filterstudentspage')
3782    pnav = 1
3783    session_label = _('Current Session')
3784    level_label = _('Current Level')
3785
3786    def label(self):
3787        return 'Students in %s' % self.context.longtitle
3788
3789    def _set_session_values(self):
3790        vocab_terms = academic_sessions_vocab.by_value.values()
3791        self.sessions = sorted(
3792            [(x.title, x.token) for x in vocab_terms], reverse=True)
3793        self.sessions += [('All Sessions', 'all')]
3794        return
3795
3796    def _set_level_values(self):
3797        vocab_terms = course_levels.by_value.values()
3798        self.levels = sorted(
3799            [(x.title, x.token) for x in vocab_terms])
3800        self.levels += [('All Levels', 'all')]
3801        return
3802
3803    def _searchCatalog(self, session, level):
3804        if level not in (10, 999, 1000, None):
3805            start_level = 100 * (level // 100)
3806            end_level = start_level + 90
3807        else:
3808            start_level = end_level = level
3809        cat = queryUtility(ICatalog, name='students_catalog')
3810        students = cat.searchResults(
3811            current_session=(session, session),
3812            current_level=(start_level, end_level),
3813            depcode=(self.context.code, self.context.code)
3814            )
3815        hitlist = []
3816        for student in students:
3817            hitlist.append(StudentQueryResultItem(student, view=self))
3818        return hitlist
3819
3820    def update(self, SHOW=None, session=None, level=None):
3821        self.parent_url = self.url(self.context.__parent__)
3822        self._set_session_values()
3823        self._set_level_values()
3824        self.hitlist = []
3825        self.session_default = session
3826        self.level_default = level
3827        if SHOW is not None:
3828            if session != 'all':
3829                self.session = int(session)
3830                self.session_string = '%s %s/%s' % (
3831                    self.session_label, self.session, self.session+1)
3832            else:
3833                self.session = None
3834                self.session_string = _('in any session')
3835            if level != 'all':
3836                self.level = int(level)
3837                self.level_string = '%s %s' % (self.level_label, self.level)
3838            else:
3839                self.level = None
3840                self.level_string = _('at any level')
3841            self.hitlist = self._searchCatalog(self.session, self.level)
3842            if not self.hitlist:
3843                self.flash(_('No student found.'), type="warning")
3844        return
3845
3846class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3847    """Page that filters and lists students.
3848    """
3849    grok.context(ICertificate)
3850
3851    def label(self):
3852        return 'Students studying %s' % self.context.longtitle
3853
3854    def _searchCatalog(self, session, level):
3855        if level not in (10, 999, 1000, None):
3856            start_level = 100 * (level // 100)
3857            end_level = start_level + 90
3858        else:
3859            start_level = end_level = level
3860        cat = queryUtility(ICatalog, name='students_catalog')
3861        students = cat.searchResults(
3862            current_session=(session, session),
3863            current_level=(start_level, end_level),
3864            certcode=(self.context.code, self.context.code)
3865            )
3866        hitlist = []
3867        for student in students:
3868            hitlist.append(StudentQueryResultItem(student, view=self))
3869        return hitlist
3870
3871class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3872    """Page that filters and lists students.
3873    """
3874    grok.context(ICourse)
3875    grok.require('waeup.viewStudent')
3876
3877    session_label = _('Session')
3878    level_label = _('Level')
3879
3880    def label(self):
3881        return 'Students registered for %s' % self.context.longtitle
3882
3883    def _searchCatalog(self, session, level):
3884        if level not in (10, 999, 1000, None):
3885            start_level = 100 * (level // 100)
3886            end_level = start_level + 90
3887        else:
3888            start_level = end_level = level
3889        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3890        coursetickets = cat.searchResults(
3891            session=(session, session),
3892            level=(start_level, end_level),
3893            code=(self.context.code, self.context.code)
3894            )
3895        hitlist = []
3896        for ticket in coursetickets:
3897            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3898        return list(set(hitlist))
3899
3900class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3901    """ Clear all students of a department in state 'clearance requested'.
3902    """
3903    grok.context(IDepartment)
3904    grok.name('clearallstudents')
3905    grok.require('waeup.clearAllStudents')
3906
3907    def update(self):
3908        cat = queryUtility(ICatalog, name='students_catalog')
3909        students = cat.searchResults(
3910            depcode=(self.context.code, self.context.code),
3911            state=(REQUESTED, REQUESTED)
3912            )
3913        num = 0
3914        for student in students:
3915            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3916                continue
3917            IWorkflowInfo(student).fireTransition('clear')
3918            num += 1
3919        self.flash(_('%d students have been cleared.' % num))
3920        self.redirect(self.url(self.context))
3921        return
3922
3923    def render(self):
3924        return
3925
3926#class RemoveFlashNoticeAllStudentsInDepartmentView(UtilityView, grok.View):
3927#    """ Remove flash notices of all students in a department.
3928#    """
3929#    grok.context(IDepartment)
3930#    grok.name('removeflash')
3931#    grok.require('waeup.manageStudent')
3932
3933#    def update(self):
3934#        cat = queryUtility(ICatalog, name='students_catalog')
3935#        students = cat.searchResults(
3936#            depcode=(self.context.code, self.context.code),
3937#            )
3938#        num = 0
3939#        for student in students:
3940#            student.flash_notice = u''
3941#            num += 1
3942#        self.flash(_('%d flash notices have been removed.' % num))
3943#        self.redirect(self.url(self.context))
3944#        return
3945
3946#    def render(self):
3947#        return
3948
3949class EditFlashNoticesFormPage(KofaFormPage):
3950    """Edit all flash notices of students in a department.
3951    """
3952    grok.context(IDepartment)
3953    grok.name('edit_flash_notices')
3954    grok.template('editflashnotices')
3955    grok.require('waeup.manageStudent')
3956    form_fields = grok.AutoFields(IFlashNotice)
3957    pnav = 0
3958
3959    def label(self):
3960        return _(u'Set flash notices for all students in ${a}',
3961            mapping = {'a':self.context.longtitle})
3962
3963    @action('Save flash notices', style='primary')
3964    def save(self, *args, **data):
3965        cat = queryUtility(ICatalog, name='students_catalog')
3966        students = cat.searchResults(
3967            depcode=(self.context.code, self.context.code),
3968            )
3969        num = 0
3970        for student in students:
3971            student.flash_notice = data['flash_notice']
3972            num += 1
3973        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3974        grok.getSite().logger.info(
3975            '%s - %s - saved flash notice: %s'
3976            % (ob_class, self.context.__name__, data['flash_notice']))
3977        self.flash(_('%d flash notices have been edited.' % num))
3978        self.redirect(self.url(self.context))
3979        return
3980
3981    @action(_('Cancel'), validator=NullValidator)
3982    def cancel(self, **data):
3983        self.redirect(self.url(self.context))
3984
3985class EditScoresPage(KofaPage):
3986    """Page that allows to edit batches of scores.
3987    """
3988    grok.context(ICourse)
3989    grok.require('waeup.editScores')
3990    grok.name('edit_scores')
3991    grok.template('editscorespage')
3992    pnav = 1
3993    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3994
3995    def label(self):
3996        return '%s tickets in academic session %s' % (
3997            self.context.code, self.session_title)
3998
3999    def _searchCatalog(self, session):
4000        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4001        # Attention: Also tickets of previous studycourses are found
4002        coursetickets = cat.searchResults(
4003            session=(session, session),
4004            code=(self.context.code, self.context.code)
4005            )
4006        return list(coursetickets)
4007
4008    def _extract_uploadfile(self, uploadfile):
4009        """Get a mapping of student-ids to scores.
4010
4011        The mapping is constructed by reading contents from `uploadfile`.
4012
4013        We expect uploadfile to be a regular CSV file with columns
4014        ``student_id`` and ``score`` (other cols are ignored).
4015        """
4016        result = dict()
4017        data = StringIO(uploadfile.read())  # ensure we have something seekable
4018        reader = csv.DictReader(data)
4019        for row in reader:
4020            if not 'student_id' in row or not 'score' in row:
4021                continue
4022            result[row['student_id']] = row['score']
4023        return result
4024
4025    def _update_scores(self, form):
4026        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
4027        error = ''
4028        if 'UPDATE_FILE' in form:
4029            if form['uploadfile']:
4030                try:
4031                    formvals = self._extract_uploadfile(form['uploadfile'])
4032                except:
4033                    self.flash(
4034                        _('Uploaded file contains illegal data. Ignored'),
4035                        type="danger")
4036                    return False
4037            else:
4038                self.flash(
4039                    _('No file provided.'), type="danger")
4040                return False
4041        else:
4042            formvals = dict(zip(form['sids'], form['scores']))
4043        for ticket in self.editable_tickets:
4044            score = ticket.score
4045            sid = ticket.student.student_id
4046            if sid not in formvals:
4047                continue
4048            if formvals[sid] == '':
4049                score = None
4050            else:
4051                try:
4052                    score = int(formvals[sid])
4053                except ValueError:
4054                    error += '%s, ' % ticket.student.display_fullname
4055            if ticket.score != score:
4056                ticket.score = score
4057                ticket.student.__parent__.logger.info(
4058                    '%s - %s %s/%s score updated (%s)' % (
4059                        ob_class, ticket.student.student_id,
4060                        ticket.level, ticket.code, score)
4061                    )
4062        if error:
4063            self.flash(
4064                _('Error: Score(s) of following students have not been '
4065                    'updated (only integers are allowed): %s.' % error.strip(', ')),
4066                type="danger")
4067        return True
4068
4069    def _validate_results(self, form):
4070        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
4071        user = get_current_principal()
4072        if user is None:
4073            usertitle = 'system'
4074        else:
4075            usertitle = getattr(user, 'public_name', None)
4076            if not usertitle:
4077                usertitle = user.title
4078        self.context.results_validated_by = usertitle
4079        self.context.results_validation_date = datetime.utcnow()
4080        self.context.results_validation_session = self.current_academic_session
4081        return
4082
4083    def _results_editable(self, results_validation_session,
4084                         current_academic_session):
4085        user = get_current_principal()
4086        prm = IPrincipalRoleManager(self.context)
4087        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
4088        if 'waeup.local.LocalStudentsManager' in roles:
4089            return True
4090        if results_validation_session \
4091            and results_validation_session >= current_academic_session:
4092            return False
4093        return True
4094
4095    def update(self,  *args, **kw):
4096        form = self.request.form
4097        self.current_academic_session = grok.getSite()[
4098            'configuration'].current_academic_session
4099        if self.context.__parent__.__parent__.score_editing_disabled \
4100            or self.context.score_editing_disabled:
4101            self.flash(_('Score editing disabled.'), type="warning")
4102            self.redirect(self.url(self.context))
4103            return
4104        if not self.current_academic_session:
4105            self.flash(_('Current academic session not set.'), type="warning")
4106            self.redirect(self.url(self.context))
4107            return
4108        vs = self.context.results_validation_session
4109        if not self._results_editable(vs, self.current_academic_session):
4110            self.flash(
4111                _('Course results have already been '
4112                  'validated and can no longer be changed.'),
4113                type="danger")
4114            self.redirect(self.url(self.context))
4115            return
4116        self.session_title = academic_sessions_vocab.getTerm(
4117            self.current_academic_session).title
4118        self.tickets = self._searchCatalog(self.current_academic_session)
4119        if not self.tickets:
4120            self.flash(_('No student found.'), type="warning")
4121            self.redirect(self.url(self.context))
4122            return
4123        self.editable_tickets = [
4124            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
4125        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
4126            and not 'VALIDATE_RESULTS' in form:
4127            return
4128        if 'VALIDATE_RESULTS' in form:
4129            if vs and vs >= self.current_academic_session:
4130                self.flash(
4131                    _('Course results have already been validated.'),
4132                    type="danger")
4133            for ticket in self.tickets:
4134                if ticket.total_score is not None:
4135                    break
4136                self.flash(
4137                    _('No score has been entered.'),
4138                    type="danger")
4139                return
4140            self._validate_results(form)
4141            self.flash(_('You successfully validated the course results.'))
4142            self.redirect(self.url(self.context))
4143            return
4144        if not self.editable_tickets:
4145            return
4146        success = self._update_scores(form)
4147        if success:
4148            self.flash(_('You successfully updated course results.'))
4149        return
4150
4151class DownloadScoresView(UtilityView, grok.View):
4152    """View that exports scores.
4153    """
4154    grok.context(ICourse)
4155    grok.require('waeup.editScores')
4156    grok.name('download_scores')
4157
4158    def _results_editable(self, results_validation_session,
4159                         current_academic_session):
4160        user = get_current_principal()
4161        prm = IPrincipalRoleManager(self.context)
4162        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
4163        if 'waeup.local.LocalStudentsManager' in roles:
4164            return True
4165        if results_validation_session \
4166            and results_validation_session >= current_academic_session:
4167            return False
4168        return True
4169
4170    def update(self):
4171        self.current_academic_session = grok.getSite()[
4172            'configuration'].current_academic_session
4173        if self.context.__parent__.__parent__.score_editing_disabled \
4174            or self.context.score_editing_disabled:
4175            self.flash(_('Score editing disabled.'), type="warning")
4176            self.redirect(self.url(self.context))
4177            return
4178        if not self.current_academic_session:
4179            self.flash(_('Current academic session not set.'), type="warning")
4180            self.redirect(self.url(self.context))
4181            return
4182        vs = self.context.results_validation_session
4183        if not self._results_editable(vs, self.current_academic_session):
4184            self.flash(
4185                _('Course results have already been '
4186                  'validated and can no longer be changed.'),
4187                type="danger")
4188            self.redirect(self.url(self.context))
4189            return
4190        site = grok.getSite()
4191        exporter = getUtility(ICSVExporter, name='lecturer')
4192        self.csv = exporter.export_filtered(site, filepath=None,
4193                                 catalog='coursetickets',
4194                                 session=self.current_academic_session,
4195                                 level=None,
4196                                 code=self.context.code)
4197        return
4198
4199    def render(self):
4200        filename = 'results_%s_%s.csv' % (
4201            self.context.code, self.current_academic_session)
4202        self.response.setHeader(
4203            'Content-Type', 'text/csv; charset=UTF-8')
4204        self.response.setHeader(
4205            'Content-Disposition:', 'attachment; filename="%s' % filename)
4206        return self.csv
4207
4208class ExportPDFScoresSlip(UtilityView, grok.View,
4209    LocalRoleAssignmentUtilityView):
4210    """Deliver a PDF slip of course tickets for a lecturer.
4211    """
4212    grok.context(ICourse)
4213    grok.name('coursetickets.pdf')
4214    grok.require('waeup.showStudents')
4215
4216    def update(self):
4217        self.current_academic_session = grok.getSite()[
4218            'configuration'].current_academic_session
4219        if not self.current_academic_session:
4220            self.flash(_('Current academic session not set.'), type="danger")
4221            self.redirect(self.url(self.context))
4222            return
4223
4224    @property
4225    def note(self):
4226        return
4227
4228    def data(self, session):
4229        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4230        # Attention: Also tickets of previous studycourses are found
4231        coursetickets = cat.searchResults(
4232            session=(session, session),
4233            code=(self.context.code, self.context.code)
4234            )
4235        header = [[_('S/N'),
4236                   _('Matric No.'),
4237                   _('Reg. No.'),
4238                   _('Fullname'),
4239                   _('Status'),
4240                   _('Course of Studies'),
4241                   _('Level'),
4242                   _('Score') ],]
4243        tickets = []
4244        for ticket in list(coursetickets):
4245            row = [ticket.student.matric_number,
4246                  ticket.student.reg_number,
4247                  ticket.student.display_fullname,
4248                  ticket.student.translated_state,
4249                  ticket.student.certcode,
4250                  ticket.level,
4251                  ticket.score,
4252                  ticket.student.lastname # for sorting only
4253                  ]
4254            tickets.append(row)
4255        data = sorted(tickets, key=lambda value: value[7])
4256        sn = 1
4257        for d in data:
4258            d.pop(7)
4259            d.insert(0, sn)
4260            sn += 1
4261        return header + data, None
4262
4263    def render(self):
4264        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4265                     if i['local_role'] == 'waeup.local.Lecturer']
4266        lecturers = sorted(lecturers)
4267        lecturers =  ', '.join(lecturers)
4268        students_utils = getUtility(IStudentsUtils)
4269        return students_utils.renderPDFCourseticketsOverview(
4270            self, 'coursetickets', self.current_academic_session,
4271            self.data(self.current_academic_session), lecturers,
4272            'landscape', 90, self.note)
4273
4274class ExportAttendanceSlip(UtilityView, grok.View,
4275    LocalRoleAssignmentUtilityView):
4276    """Deliver a PDF slip of course tickets in attendance sheet format.
4277    """
4278    grok.context(ICourse)
4279    grok.name('attendance.pdf')
4280    grok.require('waeup.showStudents')
4281
4282    def update(self):
4283        self.current_academic_session = grok.getSite()[
4284            'configuration'].current_academic_session
4285        if not self.current_academic_session:
4286            self.flash(_('Current academic session not set.'), type="danger")
4287            self.redirect(self.url(self.context))
4288            return
4289
4290    @property
4291    def note(self):
4292        return
4293
4294    def data(self, session):
4295        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4296        # Attention: Also tickets of previous studycourses are found
4297        coursetickets = cat.searchResults(
4298            session=(session, session),
4299            code=(self.context.code, self.context.code)
4300            )
4301        header = [[_('S/N'),
4302                   _('Matric No.'),
4303                   _('Name'),
4304                   _('Level'),
4305                   _('Course of\nStudies'),
4306                   _('Booklet No.'),
4307                   _('Signature'),
4308                   ],]
4309        tickets = []
4310        sn = 1
4311        ctlist = sorted(list(coursetickets),
4312                        key=lambda value: str(value.student.faccode) +
4313                                          str(value.student.depcode) +
4314                                          str(value.student.certcode) +
4315                                          str(value.student.matric_number))
4316        # In AAUE only editable appear on the attendance sheet. Hopefully
4317        # this holds for other universities too.
4318        editable_tickets = [ticket for ticket in ctlist
4319            if ticket.editable_by_lecturer]
4320        for ticket in editable_tickets:
4321            name = textwrap.fill(ticket.student.display_fullname, 20)
4322            row = [sn,
4323                  ticket.student.matric_number,
4324                  name,
4325                  ticket.level,
4326                  ticket.student.certcode,
4327                  20 * ' ',
4328                  27 * ' ',
4329                  ]
4330            tickets.append(row)
4331            sn += 1
4332        return header + tickets, None
4333
4334    def render(self):
4335        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4336                     if i['local_role'] == 'waeup.local.Lecturer']
4337        lecturers =  ', '.join(lecturers)
4338        students_utils = getUtility(IStudentsUtils)
4339        return students_utils.renderPDFCourseticketsOverview(
4340            self, 'attendance', self.current_academic_session,
4341            self.data(self.current_academic_session),
4342            lecturers, '', 65, self.note)
4343
4344class ExportJobContainerOverview(KofaPage):
4345    """Page that lists active student data export jobs and provides links
4346    to discard or download CSV files.
4347
4348    """
4349    grok.context(VirtualExportJobContainer)
4350    grok.require('waeup.showStudents')
4351    grok.name('index.html')
4352    grok.template('exportjobsindex')
4353    label = _('Student Data Exports')
4354    pnav = 1
4355    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4356
4357    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
4358        if CREATE1:
4359            self.redirect(self.url('@@exportconfig'))
4360            return
4361        if CREATE2:
4362            self.redirect(self.url('@@exportselected'))
4363            return
4364        if DISCARD and job_id:
4365            entry = self.context.entry_from_job_id(job_id)
4366            self.context.delete_export_entry(entry)
4367            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4368            self.context.logger.info(
4369                '%s - discarded: job_id=%s' % (ob_class, job_id))
4370            self.flash(_('Discarded export') + ' %s' % job_id)
4371        self.entries = doll_up(self, user=self.request.principal.id)
4372        return
4373
4374class ExportJobContainerJobConfig(KofaPage):
4375    """Page that configures a students export job.
4376
4377    This is a baseclass.
4378    """
4379    grok.baseclass()
4380    grok.require('waeup.showStudents')
4381    grok.template('exportconfig')
4382    label = _('Configure student data export')
4383    pnav = 1
4384    redirect_target = ''
4385    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4386
4387    def _set_session_values(self):
4388        vocab_terms = academic_sessions_vocab.by_value.values()
4389        self.sessions = [(_('All Sessions'), 'all')]
4390        self.sessions += sorted(
4391            [(x.title, x.token) for x in vocab_terms], reverse=True)
4392        return
4393
4394    def _set_level_values(self):
4395        vocab_terms = course_levels.by_value.values()
4396        self.levels = [(_('All Levels'), 'all')]
4397        self.levels += sorted(
4398            [(x.title, x.token) for x in vocab_terms])
4399        return
4400
4401    def _set_semesters_values(self):
4402        utils = getUtility(IKofaUtils)
4403        self.semesters =[(_('All Semesters'), 'all')]
4404        self.semesters += sorted([(value, key) for key, value in
4405                      utils.SEMESTER_DICT.items()])
4406        return
4407
4408    def _set_mode_values(self):
4409        utils = getUtility(IKofaUtils)
4410        self.modes =[(_('All Modes'), 'all')]
4411        self.modes += sorted([(value, key) for key, value in
4412                      utils.STUDY_MODES_DICT.items()])
4413        return
4414
4415    def _set_paycat_values(self):
4416        utils = getUtility(IKofaUtils)
4417        self.paycats =[(_('All Payment Categories'), 'all')]
4418        self.paycats += sorted([(value, key) for key, value in
4419                      utils.PAYMENT_CATEGORIES.items()])
4420        return
4421
4422    def _set_exporter_values(self):
4423        kofa_utils = getUtility(IKofaUtils)
4424        exporters = kofa_utils.collect_exporters(self.context)
4425        if exporters:
4426            self.exporters = exporters
4427            return
4428        exporters = []
4429        STUDENT_EXPORTER_NAMES = getUtility(
4430            IStudentsUtils).STUDENT_EXPORTER_NAMES
4431        for name in STUDENT_EXPORTER_NAMES:
4432            util = getUtility(ICSVExporter, name=name)
4433            exporters.append((util.title, name),)
4434        self.exporters = sorted(exporters)
4435        return
4436
4437    @property
4438    def faccode(self):
4439        return None
4440
4441    @property
4442    def depcode(self):
4443        return None
4444
4445    @property
4446    def certcode(self):
4447        return None
4448
4449    def update(self, START=None, session=None, level=None, mode=None,
4450               payments_start=None, payments_end=None, ct_level=None,
4451               ct_session=None, ct_semester=None, paycat=None,
4452               paysession=None, level_session=None, exporter=None):
4453        self._set_session_values()
4454        self._set_level_values()
4455        self._set_mode_values()
4456        self._set_paycat_values()
4457        self._set_exporter_values()
4458        self._set_semesters_values()
4459        if START is None:
4460            return
4461        ena = exports_not_allowed(self)
4462        if ena:
4463            self.flash(ena, type='danger')
4464            return
4465        if payments_start or payments_end:
4466            date_format = '%d/%m/%Y'
4467            try:
4468                datetime.strptime(payments_start, date_format)
4469                datetime.strptime(payments_end, date_format)
4470            except ValueError:
4471                self.flash(_('Payment dates do not match format d/m/Y.'),
4472                           type="danger")
4473                return
4474        if session == 'all':
4475            session=None
4476        if level == 'all':
4477            level = None
4478        if mode == 'all':
4479            mode = None
4480        if (mode,
4481            level,
4482            session,
4483            self.faccode,
4484            self.depcode,
4485            self.certcode) == (None, None, None, None, None, None):
4486            # Export all students including those without certificate
4487            job_id = self.context.start_export_job(exporter,
4488                                          self.request.principal.id,
4489                                          payments_start = payments_start,
4490                                          payments_end = payments_end,
4491                                          paycat=paycat,
4492                                          paysession=paysession,
4493                                          ct_level = ct_level,
4494                                          ct_session = ct_session,
4495                                          ct_semester = ct_semester,
4496                                          level_session=level_session,
4497                                          )
4498        else:
4499            job_id = self.context.start_export_job(exporter,
4500                                          self.request.principal.id,
4501                                          current_session=session,
4502                                          current_level=level,
4503                                          current_mode=mode,
4504                                          faccode=self.faccode,
4505                                          depcode=self.depcode,
4506                                          certcode=self.certcode,
4507                                          payments_start = payments_start,
4508                                          payments_end = payments_end,
4509                                          paycat=paycat,
4510                                          paysession=paysession,
4511                                          ct_level = ct_level,
4512                                          ct_session = ct_session,
4513                                          ct_semester = ct_semester,
4514                                          level_session=level_session,)
4515        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4516        self.context.logger.info(
4517            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4518            % (ob_class, exporter, session, level, mode, self.faccode,
4519            self.depcode, self.certcode, payments_start, payments_end,
4520            ct_level, ct_session, paycat, paysession, level_session, job_id))
4521        self.flash(_('Export started for students with') +
4522                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4523                   session, level, mode))
4524        self.redirect(self.url(self.redirect_target))
4525        return
4526
4527class ExportJobContainerDownload(ExportCSVView):
4528    """Page that downloads a students export csv file.
4529
4530    """
4531    grok.context(VirtualExportJobContainer)
4532    grok.require('waeup.showStudents')
4533
4534class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4535    """Page that configures a students export job in datacenter.
4536
4537    """
4538    grok.name('exportconfig')
4539    grok.context(IDataCenter)
4540    redirect_target = '@@export'
4541
4542class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4543    """Page that configures a students export job in datacenter.
4544
4545    """
4546    grok.name('exportselected')
4547    grok.context(IDataCenter)
4548    redirect_target = '@@export'
4549    grok.template('exportselected')
4550
4551    def update(self, START=None, students=None, exporter=None):
4552        self._set_exporter_values()
4553        if START is None:
4554            return
4555        ena = exports_not_allowed(self)
4556        if ena:
4557            self.flash(ena, type='danger')
4558            return
4559        try:
4560            ids = students.replace(',', ' ').split()
4561        except:
4562            self.flash(sys.exc_info()[1])
4563            self.redirect(self.url(self.redirect_target))
4564            return
4565        job_id = self.context.start_export_job(
4566            exporter, self.request.principal.id, selected=ids)
4567        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4568        self.context.logger.info(
4569            '%s - selected students exported: %s, job_id=%s' %
4570            (ob_class, exporter, job_id))
4571        self.flash(_('Export of selected students started.'))
4572        self.redirect(self.url(self.redirect_target))
4573        return
4574
4575class FacultiesExportJobContainerJobConfig(
4576    DatacenterExportJobContainerJobConfig):
4577    """Page that configures a students export job in facultiescontainer.
4578
4579    """
4580    grok.context(VirtualFacultiesExportJobContainer)
4581    redirect_target = ''
4582
4583class FacultiesExportJobContainerSelectStudents(
4584    DatacenterExportJobContainerSelectStudents):
4585    """Page that configures a students export job in facultiescontainer.
4586
4587    """
4588    grok.context(VirtualFacultiesExportJobContainer)
4589    redirect_target = ''
4590
4591class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4592    """Page that configures a students export job in faculties.
4593
4594    """
4595    grok.context(VirtualFacultyExportJobContainer)
4596    redirect_target = ''
4597
4598    @property
4599    def faccode(self):
4600        return self.context.__parent__.code
4601
4602class DepartmentExportJobContainerJobConfig(
4603    DatacenterExportJobContainerJobConfig):
4604    """Page that configures a students export job in departments.
4605
4606    """
4607    grok.context(VirtualDepartmentExportJobContainer)
4608    redirect_target = ''
4609
4610    @property
4611    def depcode(self):
4612        return self.context.__parent__.code
4613
4614class CertificateExportJobContainerJobConfig(
4615    DatacenterExportJobContainerJobConfig):
4616    """Page that configures a students export job for certificates.
4617
4618    """
4619    grok.context(VirtualCertificateExportJobContainer)
4620    grok.template('exportconfig_certificate')
4621    redirect_target = ''
4622
4623    @property
4624    def certcode(self):
4625        return self.context.__parent__.code
4626
4627class CourseExportJobContainerJobConfig(
4628    DatacenterExportJobContainerJobConfig):
4629    """Page that configures a students export job for courses.
4630
4631    In contrast to department or certificate student data exports the
4632    coursetickets_catalog is searched here. Therefore the update
4633    method from the base class is customized.
4634    """
4635    grok.context(VirtualCourseExportJobContainer)
4636    grok.template('exportconfig_course')
4637    redirect_target = ''
4638
4639    def _set_exporter_values(self):
4640        # We provide only the 'coursetickets' and 'lecturer' exporter
4641        # but can add more.
4642        exporters = []
4643        for name in ('coursetickets', 'lecturer'):
4644            util = getUtility(ICSVExporter, name=name)
4645            exporters.append((util.title, name),)
4646        self.exporters = exporters
4647        return
4648
4649    # Disabled on 10/03/21
4650    #def _set_session_values(self):
4651    #    # We allow only current academic session
4652    #    academic_session = grok.getSite()['configuration'].current_academic_session
4653    #    if not academic_session:
4654    #        self.sessions = []
4655    #        return
4656    #    x = academic_sessions_vocab.getTerm(academic_session)
4657    #    self.sessions = [(x.title, x.token)]
4658    #    return
4659
4660    def _set_session_values(self):
4661        vocab_terms = academic_sessions_vocab.by_value.values()
4662        self.sessions = sorted(
4663            [(x.title, x.token) for x in vocab_terms], reverse=True)
4664        self.sessions += [('All Sessions', 'all')]
4665        return
4666
4667    def update(self, START=None, session=None, level=None, mode=None,
4668               exporter=None):
4669        if not checkPermission('waeup.exportData', self.context):
4670            self.flash(_('Not permitted.'), type='danger')
4671            self.redirect(self.url(self.context))
4672            return
4673        self._set_session_values()
4674        self._set_level_values()
4675        self._set_mode_values()
4676        self._set_exporter_values()
4677        # Disabled on 10/03/21
4678        #if not self.sessions:
4679        #    self.flash(
4680        #        _('Academic session not set. '
4681        #          'Please contact the administrator.'),
4682        #        type='danger')
4683        #    self.redirect(self.url(self.context))
4684        #    return
4685        if START is None:
4686            return
4687        ena = exports_not_allowed(self)
4688        if ena:
4689            self.flash(ena, type='danger')
4690            return
4691        if session == 'all':
4692            session = None
4693        if level == 'all':
4694            level = None
4695        job_id = self.context.start_export_job(exporter,
4696                                      self.request.principal.id,
4697                                      # Use a different catalog and
4698                                      # pass different keywords than
4699                                      # for the (default) students_catalog
4700                                      catalog='coursetickets',
4701                                      session=session,
4702                                      level=level,
4703                                      code=self.context.__parent__.code)
4704        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4705        self.context.logger.info(
4706            '%s - exported: %s (%s, %s, %s), job_id=%s'
4707            % (ob_class, exporter, session, level,
4708            self.context.__parent__.code, job_id))
4709        self.flash(_('Export started for course tickets with') +
4710                   ' level_session=%s, level=%s' % (
4711                   session, level))
4712        self.redirect(self.url(self.redirect_target))
4713        return
Note: See TracBrowser for help on using the repository browser.