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

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

Add attendance_sheet.pdf view.

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