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

Last change on this file since 11737 was 11732, checked in by Henrik Bettermann, 10 years ago

Start export job without payment date parameters if not selected.

  • Property svn:keywords set to Id
File size: 124.5 KB
Line 
1## $Id: browser.py 11732 2014-07-04 09:14:12Z 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 sys
21import grok
22import pytz
23from urllib import urlencode
24from datetime import datetime
25from zope.event import notify
26from zope.i18n import translate
27from zope.catalog.interfaces import ICatalog
28from zope.component import queryUtility, getUtility, createObject
29from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
30from zope.formlib.textwidgets import BytesDisplayWidget
31from zope.security import checkPermission
32from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
33from waeup.kofa.accesscodes import (
34    invalidate_accesscode, get_access_code)
35from waeup.kofa.accesscodes.workflow import USED
36from waeup.kofa.browser.layout import (
37    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
38    KofaForm, NullValidator)
39from waeup.kofa.browser.breadcrumbs import Breadcrumb
40from waeup.kofa.browser.pages import ContactAdminForm, ExportCSVView, doll_up
41from waeup.kofa.browser.layout import jsaction, action, UtilityView
42from waeup.kofa.browser.interfaces import ICaptchaManager
43from waeup.kofa.hostels.hostel import NOT_OCCUPIED
44from waeup.kofa.interfaces import (
45    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
46    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
47    academic_sessions_vocab, IJobManager, IDataCenter)
48from waeup.kofa.interfaces import MessageFactory as _
49from waeup.kofa.widgets.datewidget import (
50    FriendlyDateWidget, FriendlyDateDisplayWidget,
51    FriendlyDatetimeDisplayWidget)
52from waeup.kofa.mandates.mandate import PasswordMandate
53from waeup.kofa.university.interfaces import (
54    IDepartment, ICertificate, ICourse)
55from waeup.kofa.university.department import (
56    VirtualDepartmentExportJobContainer,)
57from waeup.kofa.university.facultiescontainer import (
58    VirtualFacultiesExportJobContainer, FacultiesContainer)
59from waeup.kofa.university.certificate import (
60    VirtualCertificateExportJobContainer,)
61from waeup.kofa.university.course import (
62    VirtualCourseExportJobContainer,)
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.utils.batching import VirtualExportJobContainer
65from waeup.kofa.utils.helpers import get_current_principal, to_timezone, now
66from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
67from waeup.kofa.students.interfaces import (
68    IStudentsContainer, IStudent,
69    IUGStudentClearance,IPGStudentClearance,
70    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
71    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
72    IStudentAccommodation, IStudentStudyLevel,
73    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
74    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
75    IBedTicket, IStudentsUtils, IStudentRequestPW, IStudentTranscript
76    )
77from waeup.kofa.students.catalog import search, StudentQueryResultItem
78from waeup.kofa.students.export import EXPORTER_NAMES
79from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
80from waeup.kofa.students.vocabularies import StudyLevelSource
81from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
82    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
83    GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS)
84
85
86grok.context(IKofaObject) # Make IKofaObject the default context
87
88# Save function used for save methods in pages
89def msave(view, **data):
90    changed_fields = view.applyData(view.context, **data)
91    # Turn list of lists into single list
92    if changed_fields:
93        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
94    # Inform catalog if certificate has changed
95    # (applyData does this only for the context)
96    if 'certificate' in changed_fields:
97        notify(grok.ObjectModifiedEvent(view.context.student))
98    fields_string = ' + '.join(changed_fields)
99    view.flash(_('Form has been saved.'))
100    if fields_string:
101        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
102    return
103
104def emit_lock_message(view):
105    """Flash a lock message.
106    """
107    view.flash(_('The requested form is locked (read-only).'), type="warning")
108    view.redirect(view.url(view.context))
109    return
110
111def translated_values(view):
112    """Translate course ticket attribute values to be displayed on
113    studylevel pages.
114    """
115    lang = view.request.cookies.get('kofa.language')
116    for value in view.context.values():
117        # We have to unghostify (according to Tres Seaver) the __dict__
118        # by activating the object, otherwise value_dict will be empty
119        # when calling the first time.
120        value._p_activate()
121        value_dict = dict([i for i in value.__dict__.items()])
122        value_dict['url'] = view.url(value)
123        value_dict['removable_by_student'] = value.removable_by_student
124        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
125            target_language=lang)
126        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
127            target_language=lang)
128        value_dict['automatic'] = translate(str(value.automatic), 'zope',
129            target_language=lang)
130        value_dict['grade'] = value.grade
131        value_dict['weight'] = value.weight
132        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
133        value_dict['semester'] = semester_dict[
134            value.semester].replace('mester', 'm.')
135        yield value_dict
136
137def clearance_disabled_message(student):
138    try:
139        session_config = grok.getSite()[
140            'configuration'][str(student.current_session)]
141    except KeyError:
142        return _('Session configuration object is not available.')
143    if not session_config.clearance_enabled:
144        return _('Clearance is disabled for this session.')
145    return None
146
147def addCourseTicket(view, course=None):
148    students_utils = getUtility(IStudentsUtils)
149    ticket = createObject(u'waeup.CourseTicket')
150    ticket.automatic = False
151    ticket.carry_over = False
152    max_credits = students_utils.maxCreditsExceeded(view.context, course)
153    if max_credits:
154        view.flash(_(
155            'Total credits exceed ${a}.',
156            mapping = {'a': max_credits}), type="warning")
157        return False
158    try:
159        view.context.addCourseTicket(ticket, course)
160    except KeyError:
161        view.flash(_('The ticket exists.'), 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) == 'suspended':
267            self.searchtype = form['searchtype']
268            self.searchterm = None
269        elif form.get('searchtype', None) == 'transcript':
270            self.searchtype = form['searchtype']
271            self.searchterm = None
272        elif 'searchterm' in form and form['searchterm']:
273            self.searchterm = form['searchterm']
274            self.searchtype = form['searchtype']
275        elif 'old_searchterm' in form:
276            self.searchterm = form['old_searchterm']
277            self.searchtype = form['old_searchtype']
278        else:
279            if 'search' in form:
280                self.flash(_('Empty search string'), type="warning")
281            return
282        if self.searchtype == 'current_session':
283            try:
284                self.searchterm = int(self.searchterm)
285            except ValueError:
286                self.flash(_('Only year dates allowed (e.g. 2011).'),
287                           type="danger")
288                return
289        self.hitlist = search(query=self.searchterm,
290            searchtype=self.searchtype, view=self)
291        if not self.hitlist:
292            self.flash(_('No student found.'), type="warning")
293        return
294
295class StudentsContainerManagePage(KofaPage):
296    """The manage page for student containers.
297    """
298    grok.context(IStudentsContainer)
299    grok.name('manage')
300    grok.require('waeup.manageStudent')
301    grok.template('containermanagepage')
302    pnav = 4
303    label = _('Manage student section')
304    search_button = _('Find student(s)')
305    remove_button = _('Remove selected')
306
307    def update(self, *args, **kw):
308        form = self.request.form
309        self.hitlist = []
310        if form.get('searchtype', None) == 'suspended':
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 record'), 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
373class LoginAsStudentStep1(KofaEditFormPage):
374    """ View to temporarily set a student password.
375    """
376    grok.context(IStudent)
377    grok.name('loginasstep1')
378    grok.require('waeup.loginAsStudent')
379    grok.template('loginasstep1')
380    pnav = 4
381
382    def label(self):
383        return _(u'Set temporary password for ${a}',
384            mapping = {'a':self.context.display_fullname})
385
386    @action('Set password now', style='primary')
387    def setPassword(self, *args, **data):
388        kofa_utils = getUtility(IKofaUtils)
389        password = kofa_utils.genPassword()
390        self.context.setTempPassword(self.request.principal.id, password)
391        self.context.writeLogMessage(
392            self, 'temp_password generated: %s' % password)
393        args = {'password':password}
394        self.redirect(self.url(self.context) +
395            '/loginasstep2?%s' % urlencode(args))
396        return
397
398class LoginAsStudentStep2(KofaPage):
399    """ View to temporarily login as student with a temporary password.
400    """
401    grok.context(IStudent)
402    grok.name('loginasstep2')
403    grok.require('waeup.Public')
404    grok.template('loginasstep2')
405    login_button = _('Login now')
406    pnav = 4
407
408    def label(self):
409        return _(u'Login as ${a}',
410            mapping = {'a':self.context.student_id})
411
412    def update(self, SUBMIT=None, password=None):
413        self.password = password
414        if SUBMIT is not None:
415            self.flash(_('You successfully logged in as student.'))
416            self.redirect(self.url(self.context))
417        return
418
419class StudentBaseDisplayFormPage(KofaDisplayFormPage):
420    """ Page to display student base data
421    """
422    grok.context(IStudent)
423    grok.name('index')
424    grok.require('waeup.viewStudent')
425    grok.template('basepage')
426    form_fields = grok.AutoFields(IStudentBase).omit(
427        'password', 'suspended', 'suspended_comment')
428    pnav = 4
429
430    @property
431    def label(self):
432        if self.context.suspended:
433            return _('${a}: Base Data (account deactivated)',
434                mapping = {'a':self.context.display_fullname})
435        return  _('${a}: Base Data',
436            mapping = {'a':self.context.display_fullname})
437
438    @property
439    def hasPassword(self):
440        if self.context.password:
441            return _('set')
442        return _('unset')
443
444class StudentBasePDFFormPage(KofaDisplayFormPage):
445    """ Page to display student base data in pdf files.
446    """
447
448    def __init__(self, context, request, omit_fields=()):
449        self.omit_fields = omit_fields
450        super(StudentBasePDFFormPage, self).__init__(context, request)
451
452    @property
453    def form_fields(self):
454        form_fields = grok.AutoFields(IStudentBase)
455        for field in self.omit_fields:
456            form_fields = form_fields.omit(field)
457        return form_fields
458
459class ContactStudentForm(ContactAdminForm):
460    grok.context(IStudent)
461    grok.name('contactstudent')
462    grok.require('waeup.viewStudent')
463    pnav = 4
464    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
465
466    def update(self, subject=u'', body=u''):
467        super(ContactStudentForm, self).update()
468        self.form_fields.get('subject').field.default = subject
469        self.form_fields.get('body').field.default = body
470        return
471
472    def label(self):
473        return _(u'Send message to ${a}',
474            mapping = {'a':self.context.display_fullname})
475
476    @action('Send message now', style='primary')
477    def send(self, *args, **data):
478        try:
479            email = self.request.principal.email
480        except AttributeError:
481            email = self.config.email_admin
482        usertype = getattr(self.request.principal,
483                           'user_type', 'system').title()
484        kofa_utils = getUtility(IKofaUtils)
485        success = kofa_utils.sendContactForm(
486                self.request.principal.title,email,
487                self.context.display_fullname,self.context.email,
488                self.request.principal.id,usertype,
489                self.config.name,
490                data['body'],data['subject'])
491        if success:
492            self.flash(_('Your message has been sent.'))
493        else:
494            self.flash(_('An smtp server error occurred.'), type="danger")
495        return
496
497class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
498    """Deliver a PDF Admission slip.
499    """
500    grok.context(IStudent)
501    grok.name('admission_slip.pdf')
502    grok.require('waeup.viewStudent')
503    prefix = 'form'
504
505    omit_fields = ('date_of_birth', 'current_level')
506
507    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
508
509    @property
510    def label(self):
511        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
512        return translate(_('Admission Letter of'),
513            'waeup.kofa', target_language=portal_language) \
514            + ' %s' % self.context.display_fullname
515
516    def render(self):
517        students_utils = getUtility(IStudentsUtils)
518        return students_utils.renderPDFAdmissionLetter(self,
519            self.context.student, omit_fields=self.omit_fields)
520
521class StudentBaseManageFormPage(KofaEditFormPage):
522    """ View to manage student base data
523    """
524    grok.context(IStudent)
525    grok.name('manage_base')
526    grok.require('waeup.manageStudent')
527    form_fields = grok.AutoFields(IStudentBase).omit(
528        'student_id', 'adm_code', 'suspended')
529    grok.template('basemanagepage')
530    label = _('Manage base data')
531    pnav = 4
532
533    def update(self):
534        super(StudentBaseManageFormPage, self).update()
535        self.wf_info = IWorkflowInfo(self.context)
536        return
537
538    @action(_('Save'), style='primary')
539    def save(self, **data):
540        form = self.request.form
541        password = form.get('password', None)
542        password_ctl = form.get('control_password', None)
543        if password:
544            validator = getUtility(IPasswordValidator)
545            errors = validator.validate_password(password, password_ctl)
546            if errors:
547                self.flash( ' '.join(errors), type="danger")
548                return
549        changed_fields = self.applyData(self.context, **data)
550        # Turn list of lists into single list
551        if changed_fields:
552            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
553        else:
554            changed_fields = []
555        if password:
556            # Now we know that the form has no errors and can set password
557            IUserAccount(self.context).setPassword(password)
558            changed_fields.append('password')
559        fields_string = ' + '.join(changed_fields)
560        self.flash(_('Form has been saved.'))
561        if fields_string:
562            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
563        return
564
565class StudentTriggerTransitionFormPage(KofaEditFormPage):
566    """ View to manage student base data
567    """
568    grok.context(IStudent)
569    grok.name('trigtrans')
570    grok.require('waeup.triggerTransition')
571    grok.template('trigtrans')
572    label = _('Trigger registration transition')
573    pnav = 4
574
575    def getTransitions(self):
576        """Return a list of dicts of allowed transition ids and titles.
577
578        Each list entry provides keys ``name`` and ``title`` for
579        internal name and (human readable) title of a single
580        transition.
581        """
582        wf_info = IWorkflowInfo(self.context)
583        allowed_transitions = [t for t in wf_info.getManualTransitions()
584            if not t[0].startswith('pay')]
585        if self.context.is_postgrad and not self.context.is_special_postgrad:
586            allowed_transitions = [t for t in allowed_transitions
587                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
588        return [dict(name='', title=_('No transition'))] +[
589            dict(name=x, title=y) for x, y in allowed_transitions]
590
591    @action(_('Save'), style='primary')
592    def save(self, **data):
593        form = self.request.form
594        if 'transition' in form and form['transition']:
595            transition_id = form['transition']
596            wf_info = IWorkflowInfo(self.context)
597            wf_info.fireTransition(transition_id)
598        return
599
600class StudentActivatePage(UtilityView, grok.View):
601    """ Activate student account
602    """
603    grok.context(IStudent)
604    grok.name('activate')
605    grok.require('waeup.manageStudent')
606
607    def update(self):
608        self.context.suspended = False
609        self.context.writeLogMessage(self, 'account activated')
610        history = IObjectHistory(self.context)
611        history.addMessage('Student account activated')
612        self.flash(_('Student account has been activated.'))
613        self.redirect(self.url(self.context))
614        return
615
616    def render(self):
617        return
618
619class StudentDeactivatePage(UtilityView, grok.View):
620    """ Deactivate student account
621    """
622    grok.context(IStudent)
623    grok.name('deactivate')
624    grok.require('waeup.manageStudent')
625
626    def update(self):
627        self.context.suspended = True
628        self.context.writeLogMessage(self, 'account deactivated')
629        history = IObjectHistory(self.context)
630        history.addMessage('Student account deactivated')
631        self.flash(_('Student account has been deactivated.'))
632        self.redirect(self.url(self.context))
633        return
634
635    def render(self):
636        return
637
638class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
639    """ Page to display student clearance data
640    """
641    grok.context(IStudent)
642    grok.name('view_clearance')
643    grok.require('waeup.viewStudent')
644    pnav = 4
645
646    @property
647    def separators(self):
648        return getUtility(IStudentsUtils).SEPARATORS_DICT
649
650    @property
651    def form_fields(self):
652        if self.context.is_postgrad:
653            form_fields = grok.AutoFields(
654                IPGStudentClearance).omit('clearance_locked')
655        else:
656            form_fields = grok.AutoFields(
657                IUGStudentClearance).omit('clearance_locked')
658        if not getattr(self.context, 'officer_comment'):
659            form_fields = form_fields.omit('officer_comment')
660        else:
661            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
662        return form_fields
663
664    @property
665    def label(self):
666        return _('${a}: Clearance Data',
667            mapping = {'a':self.context.display_fullname})
668
669class ExportPDFClearanceSlipPage(grok.View):
670    """Deliver a PDF slip of the context.
671    """
672    grok.context(IStudent)
673    grok.name('clearance_slip.pdf')
674    grok.require('waeup.viewStudent')
675    prefix = 'form'
676    omit_fields = (
677        'suspended', 'phone',
678        'adm_code', 'suspended_comment',
679        'date_of_birth', 'current_level')
680
681    @property
682    def form_fields(self):
683        if self.context.is_postgrad:
684            form_fields = grok.AutoFields(
685                IPGStudentClearance).omit('clearance_locked')
686        else:
687            form_fields = grok.AutoFields(
688                IUGStudentClearance).omit('clearance_locked')
689        if not getattr(self.context, 'officer_comment'):
690            form_fields = form_fields.omit('officer_comment')
691        return form_fields
692
693    @property
694    def title(self):
695        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
696        return translate(_('Clearance Data'), 'waeup.kofa',
697            target_language=portal_language)
698
699    @property
700    def label(self):
701        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
702        return translate(_('Clearance Slip of'),
703            'waeup.kofa', target_language=portal_language) \
704            + ' %s' % self.context.display_fullname
705
706    # XXX: not used in waeup.kofa and thus not tested
707    def _signatures(self):
708        isStudent = getattr(
709            self.request.principal, 'user_type', None) == 'student'
710        if not isStudent and self.context.state in (CLEARED, ):
711            return ([_('Student Signature')],
712                    [_('Clearance Officer Signature')])
713        return
714
715    def _sigsInFooter(self):
716        isStudent = getattr(
717            self.request.principal, 'user_type', None) == 'student'
718        if not isStudent and self.context.state in (CLEARED, ):
719            return (_('Date, Student Signature'),
720                    _('Date, Clearance Officer Signature'),
721                    )
722        return ()
723
724    def render(self):
725        studentview = StudentBasePDFFormPage(self.context.student,
726            self.request, self.omit_fields)
727        students_utils = getUtility(IStudentsUtils)
728        return students_utils.renderPDF(
729            self, 'clearance_slip.pdf',
730            self.context.student, studentview, signatures=self._signatures(),
731            sigs_in_footer=self._sigsInFooter(),
732            omit_fields=self.omit_fields)
733
734class StudentClearanceManageFormPage(KofaEditFormPage):
735    """ Page to manage student clearance data
736    """
737    grok.context(IStudent)
738    grok.name('manage_clearance')
739    grok.require('waeup.manageStudent')
740    grok.template('clearanceeditpage')
741    label = _('Manage clearance data')
742    deletion_warning = _('Are you sure?')
743    pnav = 4
744
745    @property
746    def separators(self):
747        return getUtility(IStudentsUtils).SEPARATORS_DICT
748
749    @property
750    def form_fields(self):
751        if self.context.is_postgrad:
752            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
753        else:
754            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
755        return form_fields
756
757    @action(_('Save'), style='primary')
758    def save(self, **data):
759        msave(self, **data)
760        return
761
762class StudentClearPage(UtilityView, grok.View):
763    """ Clear student by clearance officer
764    """
765    grok.context(IStudent)
766    grok.name('clear')
767    grok.require('waeup.clearStudent')
768
769    def update(self):
770        if clearance_disabled_message(self.context):
771            self.flash(clearance_disabled_message(self.context))
772            self.redirect(self.url(self.context,'view_clearance'))
773            return
774        if self.context.state == REQUESTED:
775            IWorkflowInfo(self.context).fireTransition('clear')
776            self.flash(_('Student has been cleared.'))
777        else:
778            self.flash(_('Student is in wrong state.'), type="warning")
779        self.redirect(self.url(self.context,'view_clearance'))
780        return
781
782    def render(self):
783        return
784
785class StudentRejectClearancePage(KofaEditFormPage):
786    """ Reject clearance by clearance officers
787    """
788    grok.context(IStudent)
789    grok.name('reject_clearance')
790    label = _('Reject clearance')
791    grok.require('waeup.clearStudent')
792    form_fields = grok.AutoFields(
793        IUGStudentClearance).select('officer_comment')
794
795    def update(self):
796        if clearance_disabled_message(self.context):
797            self.flash(clearance_disabled_message(self.context), type="warning")
798            self.redirect(self.url(self.context,'view_clearance'))
799            return
800        return super(StudentRejectClearancePage, self).update()
801
802    @action(_('Save comment and reject clearance now'), style='primary')
803    def reject(self, **data):
804        if self.context.state == CLEARED:
805            IWorkflowInfo(self.context).fireTransition('reset4')
806            message = _('Clearance has been annulled.')
807            self.flash(message, type="warning")
808        elif self.context.state == REQUESTED:
809            IWorkflowInfo(self.context).fireTransition('reset3')
810            message = _('Clearance request has been rejected.')
811            self.flash(message, type="warning")
812        else:
813            self.flash(_('Student is in wrong state.'), type="warning")
814            self.redirect(self.url(self.context,'view_clearance'))
815            return
816        self.applyData(self.context, **data)
817        comment = data['officer_comment']
818        if comment:
819            self.context.writeLogMessage(
820                self, 'comment: %s' % comment.replace('\n', '<br>'))
821            args = {'subject':message, 'body':comment}
822        else:
823            args = {'subject':message,}
824        self.redirect(self.url(self.context) +
825            '/contactstudent?%s' % urlencode(args))
826        return
827
828
829class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
830    """ Page to display student personal data
831    """
832    grok.context(IStudent)
833    grok.name('view_personal')
834    grok.require('waeup.viewStudent')
835    form_fields = grok.AutoFields(IStudentPersonal)
836    form_fields['perm_address'].custom_widget = BytesDisplayWidget
837    form_fields[
838        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
839    pnav = 4
840
841    @property
842    def label(self):
843        return _('${a}: Personal Data',
844            mapping = {'a':self.context.display_fullname})
845
846class StudentPersonalManageFormPage(KofaEditFormPage):
847    """ Page to manage personal data
848    """
849    grok.context(IStudent)
850    grok.name('manage_personal')
851    grok.require('waeup.manageStudent')
852    form_fields = grok.AutoFields(IStudentPersonal)
853    form_fields['personal_updated'].for_display = True
854    form_fields[
855        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
856    label = _('Manage personal data')
857    pnav = 4
858
859    @action(_('Save'), style='primary')
860    def save(self, **data):
861        msave(self, **data)
862        return
863
864class StudentPersonalEditFormPage(KofaEditFormPage):
865    """ Page to edit personal data
866    """
867    grok.context(IStudent)
868    grok.name('edit_personal')
869    grok.require('waeup.handleStudent')
870    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
871    label = _('Edit personal data')
872    pnav = 4
873
874    @action(_('Save/Confirm'), style='primary')
875    def save(self, **data):
876        msave(self, **data)
877        self.context.personal_updated = datetime.utcnow()
878        return
879
880class StudyCourseDisplayFormPage(KofaDisplayFormPage):
881    """ Page to display the student study course data
882    """
883    grok.context(IStudentStudyCourse)
884    grok.name('index')
885    grok.require('waeup.viewStudent')
886    grok.template('studycoursepage')
887    pnav = 4
888
889    @property
890    def form_fields(self):
891        if self.context.is_postgrad:
892            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
893                'previous_verdict')
894        else:
895            form_fields = grok.AutoFields(IStudentStudyCourse)
896        return form_fields
897
898    @property
899    def label(self):
900        if self.context.is_current:
901            return _('${a}: Study Course',
902                mapping = {'a':self.context.__parent__.display_fullname})
903        else:
904            return _('${a}: Previous Study Course',
905                mapping = {'a':self.context.__parent__.display_fullname})
906
907    @property
908    def current_mode(self):
909        if self.context.certificate is not None:
910            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
911            return studymodes_dict[self.context.certificate.study_mode]
912        return
913
914    @property
915    def department(self):
916        if self.context.certificate is not None:
917            return self.context.certificate.__parent__.__parent__
918        return
919
920    @property
921    def faculty(self):
922        if self.context.certificate is not None:
923            return self.context.certificate.__parent__.__parent__.__parent__
924        return
925
926    @property
927    def prev_studycourses(self):
928        if self.context.is_current:
929            if self.context.__parent__.get('studycourse_2', None) is not None:
930                return (
931                        {'href':self.url(self.context.student) + '/studycourse_1',
932                        'title':_('First Study Course, ')},
933                        {'href':self.url(self.context.student) + '/studycourse_2',
934                        'title':_('Second Study Course')}
935                        )
936            if self.context.__parent__.get('studycourse_1', None) is not None:
937                return (
938                        {'href':self.url(self.context.student) + '/studycourse_1',
939                        'title':_('First Study Course')},
940                        )
941        return
942
943class StudyCourseManageFormPage(KofaEditFormPage):
944    """ Page to edit the student study course data
945    """
946    grok.context(IStudentStudyCourse)
947    grok.name('manage')
948    grok.require('waeup.manageStudent')
949    grok.template('studycoursemanagepage')
950    label = _('Manage study course')
951    pnav = 4
952    taboneactions = [_('Save'),_('Cancel')]
953    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
954    tabthreeactions = [_('Add study level')]
955
956    @property
957    def form_fields(self):
958        if self.context.is_postgrad:
959            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
960                'previous_verdict')
961        else:
962            form_fields = grok.AutoFields(IStudentStudyCourse)
963        return form_fields
964
965    def update(self):
966        if not self.context.is_current:
967            emit_lock_message(self)
968            return
969        super(StudyCourseManageFormPage, self).update()
970        return
971
972    @action(_('Save'), style='primary')
973    def save(self, **data):
974        try:
975            msave(self, **data)
976        except ConstraintNotSatisfied:
977            # The selected level might not exist in certificate
978            self.flash(_('Current level not available for certificate.'),
979                       type="warning")
980            return
981        notify(grok.ObjectModifiedEvent(self.context.__parent__))
982        return
983
984    @property
985    def level_dicts(self):
986        studylevelsource = StudyLevelSource().factory
987        for code in studylevelsource.getValues(self.context):
988            title = studylevelsource.getTitle(self.context, code)
989            yield(dict(code=code, title=title))
990
991    @property
992    def session_dicts(self):
993        yield(dict(code='', title='--'))
994        for item in academic_sessions():
995            code = item[1]
996            title = item[0]
997            yield(dict(code=code, title=title))
998
999    @action(_('Add study level'), style='primary')
1000    def addStudyLevel(self, **data):
1001        level_code = self.request.form.get('addlevel', None)
1002        level_session = self.request.form.get('level_session', None)
1003        if not level_session:
1004            self.flash(_('You must select a session for the level.'),
1005                       type="warning")
1006            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1007            return
1008        studylevel = createObject(u'waeup.StudentStudyLevel')
1009        studylevel.level = int(level_code)
1010        studylevel.level_session = int(level_session)
1011        try:
1012            self.context.addStudentStudyLevel(
1013                self.context.certificate,studylevel)
1014            self.flash(_('Study level has been added.'))
1015        except KeyError:
1016            self.flash(_('This level exists.'), type="warning")
1017        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1018        return
1019
1020    @jsaction(_('Remove selected levels'))
1021    def delStudyLevels(self, **data):
1022        form = self.request.form
1023        if 'val_id' in form:
1024            child_id = form['val_id']
1025        else:
1026            self.flash(_('No study level selected.'), type="warning")
1027            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1028            return
1029        if not isinstance(child_id, list):
1030            child_id = [child_id]
1031        deleted = []
1032        for id in child_id:
1033            del self.context[id]
1034            deleted.append(id)
1035        if len(deleted):
1036            self.flash(_('Successfully removed: ${a}',
1037                mapping = {'a':', '.join(deleted)}))
1038            self.context.writeLogMessage(
1039                self,'removed: %s' % ', '.join(deleted))
1040        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1041        return
1042
1043class StudentTranscriptRequestPage(KofaPage):
1044    """ Page to transcript by student
1045    """
1046    grok.context(IStudent)
1047    grok.name('request_transcript')
1048    grok.require('waeup.handleStudent')
1049    grok.template('transcriptrequest')
1050    label = _('Request transcript')
1051    ac_prefix = 'TSC'
1052    notice = ''
1053    pnav = 4
1054    buttonname = _('Submit')
1055    with_ac = True
1056
1057    def update(self, SUBMIT=None):
1058        super(StudentTranscriptRequestPage, self).update()
1059        if not self.context.state == GRADUATED:
1060            self.flash(_("Wrong state"), type="danger")
1061            self.redirect(self.url(self.context))
1062            return
1063        if self.with_ac:
1064            self.ac_series = self.request.form.get('ac_series', None)
1065            self.ac_number = self.request.form.get('ac_number', None)
1066        if self.context.transcript_comment is not None:
1067            self.correspondence = self.context.transcript_comment.replace(
1068                '\n', '<br>')
1069        else:
1070            self.correspondence = ''
1071        if SUBMIT is None:
1072            return
1073        if self.with_ac:
1074            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1075            code = get_access_code(pin)
1076            if not code:
1077                self.flash(_('Activation code is invalid.'), type="warning")
1078                return
1079            if code.state == USED:
1080                self.flash(_('Activation code has already been used.'),
1081                           type="warning")
1082                return
1083            # Mark pin as used (this also fires a pin related transition)
1084            # and fire transition start_clearance
1085            comment = _(u"invalidated")
1086            # Here we know that the ac is in state initialized so we do not
1087            # expect an exception, but the owner might be different
1088            if not invalidate_accesscode(pin, comment, self.context.student_id):
1089                self.flash(_('You are not the owner of this access code.'),
1090                           type="warning")
1091                return
1092            self.context.clr_code = pin
1093        IWorkflowInfo(self.context).fireTransition('request_transcript')
1094        comment = self.request.form.get('comment', '').replace('\r', '')
1095        address = self.request.form.get('address', '').replace('\r', '')
1096        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1097        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1098        old_transcript_comment = self.context.transcript_comment
1099        if old_transcript_comment == None:
1100            old_transcript_comment = ''
1101        self.context.transcript_comment = '''On %s %s wrote:
1102
1103%s
1104
1105Dispatch Address:
1106%s
1107
1108%s''' % (today, self.request.principal.id, comment, address,
1109         old_transcript_comment)
1110        self.context.writeLogMessage(
1111            self, 'comment: %s' % comment.replace('\n', '<br>'))
1112        self.flash(_('Transcript processing has been started.'))
1113        self.redirect(self.url(self.context))
1114        return
1115
1116class StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
1117    """ Page to process transcript requests
1118    """
1119    grok.context(IStudent)
1120    grok.name('process_transcript_request')
1121    grok.require('waeup.viewTranscript')
1122    grok.template('transcriptprocess')
1123    form_fields = grok.AutoFields(IStudentTranscript)
1124    label = _('Process transcript request')
1125    buttonname = _('Save comment and mark as processed')
1126    pnav = 4
1127
1128    def update(self, SUBMIT=None):
1129        super(StudentTranscriptRequestProcessFormPage, self).update()
1130        if self.context.state != TRANSCRIPT:
1131            self.flash(_('Student is in wrong state.'), type="warning")
1132            self.redirect(self.url(self.context))
1133            return
1134        if self.context.transcript_comment is not None:
1135            self.correspondence = self.context.transcript_comment.replace(
1136                '\n', '<br>')
1137        else:
1138            self.correspondence = ''
1139        if SUBMIT is None:
1140            return
1141        IWorkflowInfo(self.context).fireTransition('process_transcript')
1142        self.flash(_('Transcript request processed.'))
1143        comment = self.request.form.get('comment', '').replace('\r', '')
1144        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1145        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1146        old_transcript_comment = self.context.transcript_comment
1147        if old_transcript_comment == None:
1148            old_transcript_comment = ''
1149        self.context.transcript_comment = '''On %s %s wrote:
1150
1151%s
1152
1153%s''' % (today, self.request.principal.id, comment,
1154         old_transcript_comment)
1155        self.context.writeLogMessage(
1156            self, 'comment: %s' % comment.replace('\n', '<br>'))
1157        subject = _('Transcript processed')
1158        args = {'subject':subject, 'body':comment}
1159        self.redirect(self.url(self.context) +
1160            '/contactstudent?%s' % urlencode(args))
1161        return
1162
1163class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1164    """ Page to edit personal data by student
1165    """
1166    grok.context(IStudent)
1167    grok.name('manage_transcript_request')
1168    grok.require('waeup.manageStudent')
1169    form_fields = grok.AutoFields(IStudentTranscript)
1170    label = _('Manage transcript request')
1171    pnav = 4
1172
1173    @action(_('Save'), style='primary')
1174    def save(self, **data):
1175        msave(self, **data)
1176        return
1177
1178class StudyCourseTranscriptPage(KofaDisplayFormPage):
1179    """ Page to display the student's transcript.
1180    """
1181    grok.context(IStudentStudyCourseTranscript)
1182    grok.name('transcript')
1183    grok.require('waeup.viewTranscript')
1184    grok.template('transcript')
1185    pnav = 4
1186
1187    def update(self):
1188        if not self.context.student.transcript_enabled:
1189            self.flash(_('You are not allowed to view the transcript.'),
1190                       type="warning")
1191            self.redirect(self.url(self.context))
1192            return
1193        super(StudyCourseTranscriptPage, self).update()
1194        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1195        self.level_dict = level_dict(self.context)
1196        self.session_dict = dict(
1197            [(item[1], item[0]) for item in academic_sessions()])
1198        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1199        return
1200
1201    @property
1202    def label(self):
1203        # Here we know that the cookie has been set
1204        lang = self.request.cookies.get('kofa.language')
1205        return _('${a}: Transcript Data', mapping = {
1206            'a':self.context.student.display_fullname})
1207
1208class ExportPDFTranscriptPage(UtilityView, grok.View):
1209    """Deliver a PDF slip of the context.
1210    """
1211    grok.context(IStudentStudyCourse)
1212    grok.name('transcript.pdf')
1213    grok.require('waeup.viewTranscript')
1214    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1215    prefix = 'form'
1216    omit_fields = (
1217        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1218        'password', 'suspended', 'phone', 'email',
1219        'adm_code', 'suspended_comment', 'current_level')
1220
1221    def update(self):
1222        if not self.context.student.transcript_enabled:
1223            self.flash(_('You are not allowed to download the transcript.'),
1224                       type="warning")
1225            self.redirect(self.url(self.context))
1226            return
1227        super(ExportPDFTranscriptPage, self).update()
1228        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1229        self.level_dict = level_dict(self.context)
1230        self.session_dict = dict(
1231            [(item[1], item[0]) for item in academic_sessions()])
1232        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1233        return
1234
1235    @property
1236    def label(self):
1237        # Here we know that the cookie has been set
1238        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1239        return translate(_('Academic Transcript'),
1240            'waeup.kofa', target_language=portal_language)
1241
1242    def _sigsInFooter(self):
1243        return (_('CERTIFIED TRUE COPY'),)
1244
1245    def _signatures(self):
1246        return None
1247
1248    def render(self):
1249        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1250        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1251        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1252        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1253        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1254        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1255        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1256        studentview = StudentBasePDFFormPage(self.context.student,
1257            self.request, self.omit_fields)
1258        students_utils = getUtility(IStudentsUtils)
1259
1260        tableheader = [(Code,'code', 2.5),
1261                         (Title,'title', 7),
1262                         (Term, 'semester', 1.5),
1263                         (Cred, 'credits', 1.5),
1264                         (Score, 'score', 1.5),
1265                         (Grade, 'grade', 1.5),
1266                         ]
1267
1268        return students_utils.renderPDFTranscript(
1269            self, 'transcript.pdf',
1270            self.context.student, studentview,
1271            omit_fields=self.omit_fields,
1272            tableheader=tableheader,
1273            signatures=self._signatures(),
1274            sigs_in_footer=self._sigsInFooter(),
1275            )
1276
1277class StudentTransferFormPage(KofaAddFormPage):
1278    """Page to transfer the student.
1279    """
1280    grok.context(IStudent)
1281    grok.name('transfer')
1282    grok.require('waeup.manageStudent')
1283    label = _('Transfer student')
1284    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1285        'entry_mode', 'entry_session')
1286    pnav = 4
1287
1288    @jsaction(_('Transfer'))
1289    def transferStudent(self, **data):
1290        error = self.context.transfer(**data)
1291        if error == -1:
1292            self.flash(_('Current level does not match certificate levels.'),
1293                       type="warning")
1294        elif error == -2:
1295            self.flash(_('Former study course record incomplete.'),
1296                       type="warning")
1297        elif error == -3:
1298            self.flash(_('Maximum number of transfers exceeded.'),
1299                       type="warning")
1300        else:
1301            self.flash(_('Successfully transferred.'))
1302        return
1303
1304class RevertTransferFormPage(KofaEditFormPage):
1305    """View that reverts the previous transfer.
1306    """
1307    grok.context(IStudent)
1308    grok.name('revert_transfer')
1309    grok.require('waeup.manageStudent')
1310    grok.template('reverttransfer')
1311    label = _('Revert previous transfer')
1312
1313    def update(self):
1314        if not self.context.has_key('studycourse_1'):
1315            self.flash(_('No previous transfer.'), type="warning")
1316            self.redirect(self.url(self.context))
1317            return
1318        return
1319
1320    @jsaction(_('Revert now'))
1321    def transferStudent(self, **data):
1322        self.context.revert_transfer()
1323        self.flash(_('Previous transfer reverted.'))
1324        self.redirect(self.url(self.context, 'studycourse'))
1325        return
1326
1327class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1328    """ Page to display student study levels
1329    """
1330    grok.context(IStudentStudyLevel)
1331    grok.name('index')
1332    grok.require('waeup.viewStudent')
1333    form_fields = grok.AutoFields(IStudentStudyLevel)
1334    form_fields[
1335        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1336    grok.template('studylevelpage')
1337    pnav = 4
1338
1339    def update(self):
1340        super(StudyLevelDisplayFormPage, self).update()
1341        return
1342
1343    @property
1344    def translated_values(self):
1345        return translated_values(self)
1346
1347    @property
1348    def label(self):
1349        # Here we know that the cookie has been set
1350        lang = self.request.cookies.get('kofa.language')
1351        level_title = translate(self.context.level_title, 'waeup.kofa',
1352            target_language=lang)
1353        return _('${a}: Study Level ${b}', mapping = {
1354            'a':self.context.student.display_fullname,
1355            'b':level_title})
1356
1357class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1358    """Deliver a PDF slip of the context.
1359    """
1360    grok.context(IStudentStudyLevel)
1361    grok.name('course_registration_slip.pdf')
1362    grok.require('waeup.viewStudent')
1363    form_fields = grok.AutoFields(IStudentStudyLevel)
1364    form_fields[
1365        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1366    prefix = 'form'
1367    omit_fields = (
1368        'password', 'suspended', 'phone', 'date_of_birth',
1369        'adm_code', 'sex', 'suspended_comment', 'current_level')
1370
1371    @property
1372    def title(self):
1373        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1374        return translate(_('Level Data'), 'waeup.kofa',
1375            target_language=portal_language)
1376
1377    @property
1378    def label(self):
1379        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1380        lang = self.request.cookies.get('kofa.language', portal_language)
1381        level_title = translate(self.context.level_title, 'waeup.kofa',
1382            target_language=lang)
1383        return translate(_('Course Registration Slip'),
1384            'waeup.kofa', target_language=portal_language) \
1385            + ' %s' % level_title
1386
1387    @property
1388    def tabletitle(self):
1389        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1390        tabletitle = []
1391        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1392            target_language=portal_language))
1393        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1394            target_language=portal_language))
1395        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1396            target_language=portal_language))
1397        return tabletitle
1398
1399    def render(self):
1400        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1401        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1402        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1403        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1404        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1405        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1406        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1407        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1408        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1409        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1410        studentview = StudentBasePDFFormPage(self.context.student,
1411            self.request, self.omit_fields)
1412        students_utils = getUtility(IStudentsUtils)
1413
1414        tabledata = []
1415        tableheader = []
1416        contenttitle = []
1417        for i in range(1,7):
1418            tabledata.append(sorted(
1419                [value for value in self.context.values() if value.semester == i],
1420                key=lambda value: str(value.semester) + value.code))
1421            tableheader.append([(Code,'code', 2.5),
1422                             (Title,'title', 5),
1423                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1424                             (Cred, 'credits', 1.5),
1425                             #(Mand, 'mandatory', 1.5),
1426                             (Score, 'score', 1.5),
1427                             (Grade, 'grade', 1.5),
1428                             #('Auto', 'automatic', 1.5)
1429                             ])
1430        return students_utils.renderPDF(
1431            self, 'course_registration_slip.pdf',
1432            self.context.student, studentview,
1433            tableheader=tableheader,
1434            tabledata=tabledata,
1435            omit_fields=self.omit_fields
1436            )
1437
1438class StudyLevelManageFormPage(KofaEditFormPage):
1439    """ Page to edit the student study level data
1440    """
1441    grok.context(IStudentStudyLevel)
1442    grok.name('manage')
1443    grok.require('waeup.manageStudent')
1444    grok.template('studylevelmanagepage')
1445    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1446        'validation_date', 'validated_by', 'total_credits', 'gpa')
1447    pnav = 4
1448    taboneactions = [_('Save'),_('Cancel')]
1449    tabtwoactions = [_('Add course ticket'),
1450        _('Remove selected tickets'),_('Cancel')]
1451    placeholder = _('Enter valid course code')
1452
1453    def update(self, ADD=None, course=None):
1454        if not self.context.__parent__.is_current:
1455            emit_lock_message(self)
1456            return
1457        super(StudyLevelManageFormPage, self).update()
1458        if ADD is not None:
1459            if not course:
1460                self.flash(_('No valid course code entered.'), type="warning")
1461                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1462                return
1463            cat = queryUtility(ICatalog, name='courses_catalog')
1464            result = cat.searchResults(code=(course, course))
1465            if len(result) != 1:
1466                self.flash(_('Course not found.'), type="warning")
1467            else:
1468                course = list(result)[0]
1469                addCourseTicket(self, course)
1470            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1471        return
1472
1473    @property
1474    def translated_values(self):
1475        return translated_values(self)
1476
1477    @property
1478    def label(self):
1479        # Here we know that the cookie has been set
1480        lang = self.request.cookies.get('kofa.language')
1481        level_title = translate(self.context.level_title, 'waeup.kofa',
1482            target_language=lang)
1483        return _('Manage study level ${a}',
1484            mapping = {'a':level_title})
1485
1486    @action(_('Save'), style='primary')
1487    def save(self, **data):
1488        msave(self, **data)
1489        return
1490
1491    @jsaction(_('Remove selected tickets'))
1492    def delCourseTicket(self, **data):
1493        form = self.request.form
1494        if 'val_id' in form:
1495            child_id = form['val_id']
1496        else:
1497            self.flash(_('No ticket selected.'), type="warning")
1498            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1499            return
1500        if not isinstance(child_id, list):
1501            child_id = [child_id]
1502        deleted = []
1503        for id in child_id:
1504            del self.context[id]
1505            deleted.append(id)
1506        if len(deleted):
1507            self.flash(_('Successfully removed: ${a}',
1508                mapping = {'a':', '.join(deleted)}))
1509            self.context.writeLogMessage(
1510                self,'removed: %s at %s' %
1511                (', '.join(deleted), self.context.level))
1512        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1513        return
1514
1515class ValidateCoursesPage(UtilityView, grok.View):
1516    """ Validate course list by course adviser
1517    """
1518    grok.context(IStudentStudyLevel)
1519    grok.name('validate_courses')
1520    grok.require('waeup.validateStudent')
1521
1522    def update(self):
1523        if not self.context.__parent__.is_current:
1524            emit_lock_message(self)
1525            return
1526        if str(self.context.__parent__.current_level) != self.context.__name__:
1527            self.flash(_('This level does not correspond current level.'),
1528                       type="danger")
1529        elif self.context.student.state == REGISTERED:
1530            IWorkflowInfo(self.context.student).fireTransition(
1531                'validate_courses')
1532            self.flash(_('Course list has been validated.'))
1533        else:
1534            self.flash(_('Student is in the wrong state.'), type="warning")
1535        self.redirect(self.url(self.context))
1536        return
1537
1538    def render(self):
1539        return
1540
1541class RejectCoursesPage(UtilityView, grok.View):
1542    """ Reject course list by course adviser
1543    """
1544    grok.context(IStudentStudyLevel)
1545    grok.name('reject_courses')
1546    grok.require('waeup.validateStudent')
1547
1548    def update(self):
1549        if not self.context.__parent__.is_current:
1550            emit_lock_message(self)
1551            return
1552        if str(self.context.__parent__.current_level) != self.context.__name__:
1553            self.flash(_('This level does not correspond current level.'),
1554                       type="danger")
1555            self.redirect(self.url(self.context))
1556            return
1557        elif self.context.student.state == VALIDATED:
1558            IWorkflowInfo(self.context.student).fireTransition('reset8')
1559            message = _('Course list request has been annulled.')
1560            self.flash(message)
1561        elif self.context.student.state == REGISTERED:
1562            IWorkflowInfo(self.context.student).fireTransition('reset7')
1563            message = _('Course list request has been rejected:')
1564            self.flash(message)
1565        else:
1566            self.flash(_('Student is in the wrong state.'), type="warning")
1567            self.redirect(self.url(self.context))
1568            return
1569        args = {'subject':message}
1570        self.redirect(self.url(self.context.student) +
1571            '/contactstudent?%s' % urlencode(args))
1572        return
1573
1574    def render(self):
1575        return
1576
1577class CourseTicketAddFormPage(KofaAddFormPage):
1578    """Add a course ticket.
1579    """
1580    grok.context(IStudentStudyLevel)
1581    grok.name('add')
1582    grok.require('waeup.manageStudent')
1583    label = _('Add course ticket')
1584    form_fields = grok.AutoFields(ICourseTicketAdd)
1585    pnav = 4
1586
1587    def update(self):
1588        if not self.context.__parent__.is_current:
1589            emit_lock_message(self)
1590            return
1591        super(CourseTicketAddFormPage, self).update()
1592        return
1593
1594    @action(_('Add course ticket'), style='primary')
1595    def addCourseTicket(self, **data):
1596        course = data['course']
1597        success = addCourseTicket(self, course)
1598        if success:
1599            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1600        return
1601
1602    @action(_('Cancel'), validator=NullValidator)
1603    def cancel(self, **data):
1604        self.redirect(self.url(self.context))
1605
1606class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1607    """ Page to display course tickets
1608    """
1609    grok.context(ICourseTicket)
1610    grok.name('index')
1611    grok.require('waeup.viewStudent')
1612    form_fields = grok.AutoFields(ICourseTicket)
1613    grok.template('courseticketpage')
1614    pnav = 4
1615
1616    @property
1617    def label(self):
1618        return _('${a}: Course Ticket ${b}', mapping = {
1619            'a':self.context.student.display_fullname,
1620            'b':self.context.code})
1621
1622class CourseTicketManageFormPage(KofaEditFormPage):
1623    """ Page to manage course tickets
1624    """
1625    grok.context(ICourseTicket)
1626    grok.name('manage')
1627    grok.require('waeup.manageStudent')
1628    form_fields = grok.AutoFields(ICourseTicket)
1629    form_fields['title'].for_display = True
1630    form_fields['fcode'].for_display = True
1631    form_fields['dcode'].for_display = True
1632    form_fields['semester'].for_display = True
1633    form_fields['passmark'].for_display = True
1634    form_fields['credits'].for_display = True
1635    form_fields['mandatory'].for_display = False
1636    form_fields['automatic'].for_display = True
1637    form_fields['carry_over'].for_display = True
1638    pnav = 4
1639    grok.template('courseticketmanagepage')
1640
1641    @property
1642    def label(self):
1643        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1644
1645    @action('Save', style='primary')
1646    def save(self, **data):
1647        msave(self, **data)
1648        return
1649
1650class PaymentsManageFormPage(KofaEditFormPage):
1651    """ Page to manage the student payments
1652
1653    This manage form page is for both students and students officers.
1654    """
1655    grok.context(IStudentPaymentsContainer)
1656    grok.name('index')
1657    grok.require('waeup.viewStudent')
1658    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1659    grok.template('paymentsmanagepage')
1660    pnav = 4
1661
1662    @property
1663    def manage_payments_allowed(self):
1664        return checkPermission('waeup.payStudent', self.context)
1665
1666    def unremovable(self, ticket):
1667        usertype = getattr(self.request.principal, 'user_type', None)
1668        if not usertype:
1669            return False
1670        if not self.manage_payments_allowed:
1671            return True
1672        return (self.request.principal.user_type == 'student' and ticket.r_code)
1673
1674    @property
1675    def label(self):
1676        return _('${a}: Payments',
1677            mapping = {'a':self.context.__parent__.display_fullname})
1678
1679    @jsaction(_('Remove selected tickets'))
1680    def delPaymentTicket(self, **data):
1681        form = self.request.form
1682        if 'val_id' in form:
1683            child_id = form['val_id']
1684        else:
1685            self.flash(_('No payment selected.'), type="warning")
1686            self.redirect(self.url(self.context))
1687            return
1688        if not isinstance(child_id, list):
1689            child_id = [child_id]
1690        deleted = []
1691        for id in child_id:
1692            # Students are not allowed to remove used payment tickets
1693            ticket = self.context.get(id, None)
1694            if ticket is not None and not self.unremovable(ticket):
1695                del self.context[id]
1696                deleted.append(id)
1697        if len(deleted):
1698            self.flash(_('Successfully removed: ${a}',
1699                mapping = {'a': ', '.join(deleted)}))
1700            self.context.writeLogMessage(
1701                self,'removed: %s' % ', '.join(deleted))
1702        self.redirect(self.url(self.context))
1703        return
1704
1705    #@action(_('Add online payment ticket'))
1706    #def addPaymentTicket(self, **data):
1707    #    self.redirect(self.url(self.context, '@@addop'))
1708
1709class OnlinePaymentAddFormPage(KofaAddFormPage):
1710    """ Page to add an online payment ticket
1711    """
1712    grok.context(IStudentPaymentsContainer)
1713    grok.name('addop')
1714    grok.template('onlinepaymentaddform')
1715    grok.require('waeup.payStudent')
1716    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1717        'p_category')
1718    label = _('Add online payment')
1719    pnav = 4
1720
1721    @property
1722    def selectable_categories(self):
1723        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1724        return sorted(categories.items())
1725
1726    @action(_('Create ticket'), style='primary')
1727    def createTicket(self, **data):
1728        p_category = data['p_category']
1729        previous_session = data.get('p_session', None)
1730        previous_level = data.get('p_level', None)
1731        student = self.context.__parent__
1732        if p_category == 'bed_allocation' and student[
1733            'studycourse'].current_session != grok.getSite()[
1734            'hostels'].accommodation_session:
1735                self.flash(
1736                    _('Your current session does not match ' + \
1737                    'accommodation session.'), type="danger")
1738                return
1739        if 'maintenance' in p_category:
1740            current_session = str(student['studycourse'].current_session)
1741            if not current_session in student['accommodation']:
1742                self.flash(_('You have not yet booked accommodation.'),
1743                           type="warning")
1744                return
1745        students_utils = getUtility(IStudentsUtils)
1746        error, payment = students_utils.setPaymentDetails(
1747            p_category, student, previous_session, previous_level)
1748        if error is not None:
1749            self.flash(error, type="danger")
1750            return
1751        self.context[payment.p_id] = payment
1752        self.flash(_('Payment ticket created.'))
1753        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1754        self.redirect(self.url(self.context))
1755        return
1756
1757    @action(_('Cancel'), validator=NullValidator)
1758    def cancel(self, **data):
1759        self.redirect(self.url(self.context))
1760
1761class PreviousPaymentAddFormPage(KofaAddFormPage):
1762    """ Page to add an online payment ticket for previous sessions
1763    """
1764    grok.context(IStudentPaymentsContainer)
1765    grok.name('addpp')
1766    grok.require('waeup.payStudent')
1767    form_fields = grok.AutoFields(IStudentPreviousPayment)
1768    label = _('Add previous session online payment')
1769    pnav = 4
1770
1771    def update(self):
1772        if self.context.student.before_payment:
1773            self.flash(_("No previous payment to be made."), type="warning")
1774            self.redirect(self.url(self.context))
1775        super(PreviousPaymentAddFormPage, self).update()
1776        return
1777
1778    @action(_('Create ticket'), style='primary')
1779    def createTicket(self, **data):
1780        p_category = data['p_category']
1781        previous_session = data.get('p_session', None)
1782        previous_level = data.get('p_level', None)
1783        student = self.context.__parent__
1784        students_utils = getUtility(IStudentsUtils)
1785        error, payment = students_utils.setPaymentDetails(
1786            p_category, student, previous_session, previous_level)
1787        if error is not None:
1788            self.flash(error, type="danger")
1789            return
1790        self.context[payment.p_id] = payment
1791        self.flash(_('Payment ticket created.'))
1792        self.redirect(self.url(self.context))
1793        return
1794
1795    @action(_('Cancel'), validator=NullValidator)
1796    def cancel(self, **data):
1797        self.redirect(self.url(self.context))
1798
1799class BalancePaymentAddFormPage(KofaAddFormPage):
1800    """ Page to add an online payment ticket for balance sessions
1801    """
1802    grok.context(IStudentPaymentsContainer)
1803    grok.name('addbp')
1804    grok.require('waeup.manageStudent')
1805    form_fields = grok.AutoFields(IStudentBalancePayment)
1806    label = _('Add balance')
1807    pnav = 4
1808
1809    @action(_('Create ticket'), style='primary')
1810    def createTicket(self, **data):
1811        p_category = data['p_category']
1812        balance_session = data.get('balance_session', None)
1813        balance_level = data.get('balance_level', None)
1814        balance_amount = data.get('balance_amount', None)
1815        student = self.context.__parent__
1816        students_utils = getUtility(IStudentsUtils)
1817        error, payment = students_utils.setBalanceDetails(
1818            p_category, student, balance_session,
1819            balance_level, balance_amount)
1820        if error is not None:
1821            self.flash(error, type="danger")
1822            return
1823        self.context[payment.p_id] = payment
1824        self.flash(_('Payment ticket created.'))
1825        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1826        self.redirect(self.url(self.context))
1827        return
1828
1829    @action(_('Cancel'), validator=NullValidator)
1830    def cancel(self, **data):
1831        self.redirect(self.url(self.context))
1832
1833class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1834    """ Page to view an online payment ticket
1835    """
1836    grok.context(IStudentOnlinePayment)
1837    grok.name('index')
1838    grok.require('waeup.viewStudent')
1839    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1840    form_fields[
1841        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1842    form_fields[
1843        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1844    pnav = 4
1845
1846    @property
1847    def label(self):
1848        return _('${a}: Online Payment Ticket ${b}', mapping = {
1849            'a':self.context.student.display_fullname,
1850            'b':self.context.p_id})
1851
1852class OnlinePaymentApprovePage(UtilityView, grok.View):
1853    """ Callback view
1854    """
1855    grok.context(IStudentOnlinePayment)
1856    grok.name('approve')
1857    grok.require('waeup.managePortal')
1858
1859    def update(self):
1860        flashtype, msg, log = self.context.approveStudentPayment()
1861        if log is not None:
1862            # Add log message to students.log
1863            self.context.writeLogMessage(self,log)
1864            # Add log message to payments.log
1865            self.context.logger.info(
1866                '%s,%s,%s,%s,%s,,,,,,' % (
1867                self.context.student.student_id,
1868                self.context.p_id, self.context.p_category,
1869                self.context.amount_auth, self.context.r_code))
1870        self.flash(msg, type=flashtype)
1871        return
1872
1873    def render(self):
1874        self.redirect(self.url(self.context, '@@index'))
1875        return
1876
1877class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1878    """ Approval view for students.
1879
1880    This view is used for browser tests only and
1881    must be neutralized in custom pages!
1882    """
1883
1884    grok.name('fake_approve')
1885    grok.require('waeup.payStudent')
1886
1887class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1888    """Deliver a PDF slip of the context.
1889    """
1890    grok.context(IStudentOnlinePayment)
1891    grok.name('payment_slip.pdf')
1892    grok.require('waeup.viewStudent')
1893    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1894    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1895    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1896    prefix = 'form'
1897    note = None
1898    omit_fields = (
1899        'password', 'suspended', 'phone', 'date_of_birth',
1900        'adm_code', 'sex', 'suspended_comment', 'current_level')
1901
1902    @property
1903    def title(self):
1904        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1905        return translate(_('Payment Data'), 'waeup.kofa',
1906            target_language=portal_language)
1907
1908    @property
1909    def label(self):
1910        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1911        return translate(_('Online Payment Slip'),
1912            'waeup.kofa', target_language=portal_language) \
1913            + ' %s' % self.context.p_id
1914
1915    def render(self):
1916        #if self.context.p_state != 'paid':
1917        #    self.flash('Ticket not yet paid.')
1918        #    self.redirect(self.url(self.context))
1919        #    return
1920        studentview = StudentBasePDFFormPage(self.context.student,
1921            self.request, self.omit_fields)
1922        students_utils = getUtility(IStudentsUtils)
1923        return students_utils.renderPDF(self, 'payment_slip.pdf',
1924            self.context.student, studentview, note=self.note,
1925            omit_fields=self.omit_fields)
1926
1927
1928class AccommodationManageFormPage(KofaEditFormPage):
1929    """ Page to manage bed tickets.
1930
1931    This manage form page is for both students and students officers.
1932    """
1933    grok.context(IStudentAccommodation)
1934    grok.name('index')
1935    grok.require('waeup.handleAccommodation')
1936    form_fields = grok.AutoFields(IStudentAccommodation)
1937    grok.template('accommodationmanagepage')
1938    pnav = 4
1939    officers_only_actions = [_('Remove selected')]
1940
1941    @property
1942    def label(self):
1943        return _('${a}: Accommodation',
1944            mapping = {'a':self.context.__parent__.display_fullname})
1945
1946    @jsaction(_('Remove selected'))
1947    def delBedTickets(self, **data):
1948        if getattr(self.request.principal, 'user_type', None) == 'student':
1949            self.flash(_('You are not allowed to remove bed tickets.'),
1950                       type="warning")
1951            self.redirect(self.url(self.context))
1952            return
1953        form = self.request.form
1954        if 'val_id' in form:
1955            child_id = form['val_id']
1956        else:
1957            self.flash(_('No bed ticket selected.'), type="warning")
1958            self.redirect(self.url(self.context))
1959            return
1960        if not isinstance(child_id, list):
1961            child_id = [child_id]
1962        deleted = []
1963        for id in child_id:
1964            del self.context[id]
1965            deleted.append(id)
1966        if len(deleted):
1967            self.flash(_('Successfully removed: ${a}',
1968                mapping = {'a':', '.join(deleted)}))
1969            self.context.writeLogMessage(
1970                self,'removed: % s' % ', '.join(deleted))
1971        self.redirect(self.url(self.context))
1972        return
1973
1974    @property
1975    def selected_actions(self):
1976        if getattr(self.request.principal, 'user_type', None) == 'student':
1977            return [action for action in self.actions
1978                    if not action.label in self.officers_only_actions]
1979        return self.actions
1980
1981class BedTicketAddPage(KofaPage):
1982    """ Page to add an online payment ticket
1983    """
1984    grok.context(IStudentAccommodation)
1985    grok.name('add')
1986    grok.require('waeup.handleAccommodation')
1987    grok.template('enterpin')
1988    ac_prefix = 'HOS'
1989    label = _('Add bed ticket')
1990    pnav = 4
1991    buttonname = _('Create bed ticket')
1992    notice = ''
1993    with_ac = True
1994
1995    def update(self, SUBMIT=None):
1996        student = self.context.student
1997        students_utils = getUtility(IStudentsUtils)
1998        acc_details  = students_utils.getAccommodationDetails(student)
1999        if acc_details.get('expired', False):
2000            startdate = acc_details.get('startdate')
2001            enddate = acc_details.get('enddate')
2002            if startdate and enddate:
2003                tz = getUtility(IKofaUtils).tzinfo
2004                startdate = to_timezone(
2005                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
2006                enddate = to_timezone(
2007                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
2008                self.flash(_("Outside booking period: ${a} - ${b}",
2009                    mapping = {'a': startdate, 'b': enddate}), type="warning")
2010            else:
2011                self.flash(_("Outside booking period."), type="warning")
2012            self.redirect(self.url(self.context))
2013            return
2014        if not acc_details:
2015            self.flash(_("Your data are incomplete."), type="warning")
2016            self.redirect(self.url(self.context))
2017            return
2018        if not student.state in acc_details['allowed_states']:
2019            self.flash(_("You are in the wrong registration state."),
2020                       type="warning")
2021            self.redirect(self.url(self.context))
2022            return
2023        if student['studycourse'].current_session != acc_details[
2024            'booking_session']:
2025            self.flash(
2026                _('Your current session does not match accommodation session.'),
2027                type="warning")
2028            self.redirect(self.url(self.context))
2029            return
2030        if str(acc_details['booking_session']) in self.context.keys():
2031            self.flash(
2032                _('You already booked a bed space in current ' \
2033                    + 'accommodation session.'), type="warning")
2034            self.redirect(self.url(self.context))
2035            return
2036        if self.with_ac:
2037            self.ac_series = self.request.form.get('ac_series', None)
2038            self.ac_number = self.request.form.get('ac_number', None)
2039        if SUBMIT is None:
2040            return
2041        if self.with_ac:
2042            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2043            code = get_access_code(pin)
2044            if not code:
2045                self.flash(_('Activation code is invalid.'), type="warning")
2046                return
2047        # Search and book bed
2048        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2049        entries = cat.searchResults(
2050            owner=(student.student_id,student.student_id))
2051        if len(entries):
2052            # If bed space has been manually allocated use this bed
2053            bed = [entry for entry in entries][0]
2054            # Safety belt for paranoids: Does this bed really exist on portal?
2055            # XXX: Can be remove if nobody complains.
2056            if bed.__parent__.__parent__ is None:
2057                self.flash(_('System error: Please contact the adminsitrator.'),
2058                           type="danger")
2059                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2060                return
2061        else:
2062            # else search for other available beds
2063            entries = cat.searchResults(
2064                bed_type=(acc_details['bt'],acc_details['bt']))
2065            available_beds = [
2066                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2067            if available_beds:
2068                students_utils = getUtility(IStudentsUtils)
2069                bed = students_utils.selectBed(available_beds)
2070                # Safety belt for paranoids: Does this bed really exist in portal?
2071                # XXX: Can be remove if nobody complains.
2072                if bed.__parent__.__parent__ is None:
2073                    self.flash(_('System error: Please contact the adminsitrator.'),
2074                               type="warning")
2075                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2076                    return
2077                bed.bookBed(student.student_id)
2078            else:
2079                self.flash(_('There is no free bed in your category ${a}.',
2080                    mapping = {'a':acc_details['bt']}), type="warning")
2081                return
2082        if self.with_ac:
2083            # Mark pin as used (this also fires a pin related transition)
2084            if code.state == USED:
2085                self.flash(_('Activation code has already been used.'),
2086                           type="warning")
2087                return
2088            else:
2089                comment = _(u'invalidated')
2090                # Here we know that the ac is in state initialized so we do not
2091                # expect an exception, but the owner might be different
2092                if not invalidate_accesscode(
2093                    pin,comment,self.context.student.student_id):
2094                    self.flash(_('You are not the owner of this access code.'),
2095                               type="warning")
2096                    return
2097        # Create bed ticket
2098        bedticket = createObject(u'waeup.BedTicket')
2099        if self.with_ac:
2100            bedticket.booking_code = pin
2101        bedticket.booking_session = acc_details['booking_session']
2102        bedticket.bed_type = acc_details['bt']
2103        bedticket.bed = bed
2104        hall_title = bed.__parent__.hostel_name
2105        coordinates = bed.coordinates[1:]
2106        block, room_nr, bed_nr = coordinates
2107        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2108            'a':hall_title, 'b':block,
2109            'c':room_nr, 'd':bed_nr,
2110            'e':bed.bed_type})
2111        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2112        bedticket.bed_coordinates = translate(
2113            bc, 'waeup.kofa',target_language=portal_language)
2114        self.context.addBedTicket(bedticket)
2115        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2116        self.flash(_('Bed ticket created and bed booked: ${a}',
2117            mapping = {'a':bedticket.display_coordinates}))
2118        self.redirect(self.url(self.context))
2119        return
2120
2121class BedTicketDisplayFormPage(KofaDisplayFormPage):
2122    """ Page to display bed tickets
2123    """
2124    grok.context(IBedTicket)
2125    grok.name('index')
2126    grok.require('waeup.handleAccommodation')
2127    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2128    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2129    pnav = 4
2130
2131    @property
2132    def label(self):
2133        return _('Bed Ticket for Session ${a}',
2134            mapping = {'a':self.context.getSessionString()})
2135
2136class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
2137    """Deliver a PDF slip of the context.
2138    """
2139    grok.context(IBedTicket)
2140    grok.name('bed_allocation_slip.pdf')
2141    grok.require('waeup.handleAccommodation')
2142    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2143    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2144    prefix = 'form'
2145    omit_fields = (
2146        'password', 'suspended', 'phone', 'adm_code',
2147        'suspended_comment', 'date_of_birth', 'current_level')
2148
2149    @property
2150    def title(self):
2151        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2152        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2153            target_language=portal_language)
2154
2155    @property
2156    def label(self):
2157        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2158        #return translate(_('Bed Allocation: '),
2159        #    'waeup.kofa', target_language=portal_language) \
2160        #    + ' %s' % self.context.bed_coordinates
2161        return translate(_('Bed Allocation Slip'),
2162            'waeup.kofa', target_language=portal_language) \
2163            + ' %s' % self.context.getSessionString()
2164
2165    def render(self):
2166        studentview = StudentBasePDFFormPage(self.context.student,
2167            self.request, self.omit_fields)
2168        students_utils = getUtility(IStudentsUtils)
2169        return students_utils.renderPDF(
2170            self, 'bed_allocation_slip.pdf',
2171            self.context.student, studentview,
2172            omit_fields=self.omit_fields)
2173
2174class BedTicketRelocationPage(UtilityView, grok.View):
2175    """ Callback view
2176    """
2177    grok.context(IBedTicket)
2178    grok.name('relocate')
2179    grok.require('waeup.manageHostels')
2180
2181    # Relocate student if student parameters have changed or the bed_type
2182    # of the bed has changed
2183    def update(self):
2184        student = self.context.student
2185        students_utils = getUtility(IStudentsUtils)
2186        acc_details  = students_utils.getAccommodationDetails(student)
2187        if self.context.bed != None and \
2188              'reserved' in self.context.bed.bed_type:
2189            self.flash(_("Students in reserved beds can't be relocated."),
2190                       type="warning")
2191            self.redirect(self.url(self.context))
2192            return
2193        if acc_details['bt'] == self.context.bed_type and \
2194                self.context.bed != None and \
2195                self.context.bed.bed_type == self.context.bed_type:
2196            self.flash(_("Student can't be relocated."), type="warning")
2197            self.redirect(self.url(self.context))
2198            return
2199        # Search a bed
2200        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2201        entries = cat.searchResults(
2202            owner=(student.student_id,student.student_id))
2203        if len(entries) and self.context.bed == None:
2204            # If booking has been cancelled but other bed space has been
2205            # manually allocated after cancellation use this bed
2206            new_bed = [entry for entry in entries][0]
2207        else:
2208            # Search for other available beds
2209            entries = cat.searchResults(
2210                bed_type=(acc_details['bt'],acc_details['bt']))
2211            available_beds = [
2212                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2213            if available_beds:
2214                students_utils = getUtility(IStudentsUtils)
2215                new_bed = students_utils.selectBed(available_beds)
2216                new_bed.bookBed(student.student_id)
2217            else:
2218                self.flash(_('There is no free bed in your category ${a}.',
2219                    mapping = {'a':acc_details['bt']}), type="warning")
2220                self.redirect(self.url(self.context))
2221                return
2222        # Release old bed if exists
2223        if self.context.bed != None:
2224            self.context.bed.owner = NOT_OCCUPIED
2225            notify(grok.ObjectModifiedEvent(self.context.bed))
2226        # Alocate new bed
2227        self.context.bed_type = acc_details['bt']
2228        self.context.bed = new_bed
2229        hall_title = new_bed.__parent__.hostel_name
2230        coordinates = new_bed.coordinates[1:]
2231        block, room_nr, bed_nr = coordinates
2232        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2233            'a':hall_title, 'b':block,
2234            'c':room_nr, 'd':bed_nr,
2235            'e':new_bed.bed_type})
2236        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2237        self.context.bed_coordinates = translate(
2238            bc, 'waeup.kofa',target_language=portal_language)
2239        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2240        self.flash(_('Student relocated: ${a}',
2241            mapping = {'a':self.context.display_coordinates}))
2242        self.redirect(self.url(self.context))
2243        return
2244
2245    def render(self):
2246        return
2247
2248class StudentHistoryPage(KofaPage):
2249    """ Page to display student clearance data
2250    """
2251    grok.context(IStudent)
2252    grok.name('history')
2253    grok.require('waeup.viewStudent')
2254    grok.template('studenthistory')
2255    pnav = 4
2256
2257    @property
2258    def label(self):
2259        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2260
2261# Pages for students only
2262
2263class StudentBaseEditFormPage(KofaEditFormPage):
2264    """ View to edit student base data
2265    """
2266    grok.context(IStudent)
2267    grok.name('edit_base')
2268    grok.require('waeup.handleStudent')
2269    form_fields = grok.AutoFields(IStudentBase).select(
2270        'email', 'phone')
2271    label = _('Edit base data')
2272    pnav = 4
2273
2274    @action(_('Save'), style='primary')
2275    def save(self, **data):
2276        msave(self, **data)
2277        return
2278
2279class StudentChangePasswordPage(KofaEditFormPage):
2280    """ View to manage student base data
2281    """
2282    grok.context(IStudent)
2283    grok.name('change_password')
2284    grok.require('waeup.handleStudent')
2285    grok.template('change_password')
2286    label = _('Change password')
2287    pnav = 4
2288
2289    @action(_('Save'), style='primary')
2290    def save(self, **data):
2291        form = self.request.form
2292        password = form.get('change_password', None)
2293        password_ctl = form.get('change_password_repeat', None)
2294        if password:
2295            validator = getUtility(IPasswordValidator)
2296            errors = validator.validate_password(password, password_ctl)
2297            if not errors:
2298                IUserAccount(self.context).setPassword(password)
2299                self.context.writeLogMessage(self, 'saved: password')
2300                self.flash(_('Password changed.'))
2301            else:
2302                self.flash( ' '.join(errors), type="warning")
2303        return
2304
2305class StudentFilesUploadPage(KofaPage):
2306    """ View to upload files by student
2307    """
2308    grok.context(IStudent)
2309    grok.name('change_portrait')
2310    grok.require('waeup.uploadStudentFile')
2311    grok.template('filesuploadpage')
2312    label = _('Upload portrait')
2313    pnav = 4
2314
2315    def update(self):
2316        PWCHANGE_STATES = getUtility(IStudentsUtils).PWCHANGE_STATES
2317        if self.context.student.state not in PWCHANGE_STATES:
2318            emit_lock_message(self)
2319            return
2320        super(StudentFilesUploadPage, self).update()
2321        return
2322
2323class StartClearancePage(KofaPage):
2324    grok.context(IStudent)
2325    grok.name('start_clearance')
2326    grok.require('waeup.handleStudent')
2327    grok.template('enterpin')
2328    label = _('Start clearance')
2329    ac_prefix = 'CLR'
2330    notice = ''
2331    pnav = 4
2332    buttonname = _('Start clearance now')
2333    with_ac = True
2334
2335    @property
2336    def all_required_fields_filled(self):
2337        if self.context.email and self.context.phone:
2338            return True
2339        return False
2340
2341    @property
2342    def portrait_uploaded(self):
2343        store = getUtility(IExtFileStore)
2344        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2345            return True
2346        return False
2347
2348    def update(self, SUBMIT=None):
2349        if not self.context.state == ADMITTED:
2350            self.flash(_("Wrong state"), type="warning")
2351            self.redirect(self.url(self.context))
2352            return
2353        if not self.portrait_uploaded:
2354            self.flash(_("No portrait uploaded."), type="warning")
2355            self.redirect(self.url(self.context, 'change_portrait'))
2356            return
2357        if not self.all_required_fields_filled:
2358            self.flash(_("Not all required fields filled."), type="warning")
2359            self.redirect(self.url(self.context, 'edit_base'))
2360            return
2361        if self.with_ac:
2362            self.ac_series = self.request.form.get('ac_series', None)
2363            self.ac_number = self.request.form.get('ac_number', None)
2364        if SUBMIT is None:
2365            return
2366        if self.with_ac:
2367            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2368            code = get_access_code(pin)
2369            if not code:
2370                self.flash(_('Activation code is invalid.'), type="warning")
2371                return
2372            if code.state == USED:
2373                self.flash(_('Activation code has already been used.'),
2374                           type="warning")
2375                return
2376            # Mark pin as used (this also fires a pin related transition)
2377            # and fire transition start_clearance
2378            comment = _(u"invalidated")
2379            # Here we know that the ac is in state initialized so we do not
2380            # expect an exception, but the owner might be different
2381            if not invalidate_accesscode(pin, comment, self.context.student_id):
2382                self.flash(_('You are not the owner of this access code.'),
2383                           type="warning")
2384                return
2385            self.context.clr_code = pin
2386        IWorkflowInfo(self.context).fireTransition('start_clearance')
2387        self.flash(_('Clearance process has been started.'))
2388        self.redirect(self.url(self.context,'cedit'))
2389        return
2390
2391class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2392    """ View to edit student clearance data by student
2393    """
2394    grok.context(IStudent)
2395    grok.name('cedit')
2396    grok.require('waeup.handleStudent')
2397    label = _('Edit clearance data')
2398
2399    @property
2400    def form_fields(self):
2401        if self.context.is_postgrad:
2402            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2403                'clearance_locked', 'clr_code', 'officer_comment')
2404        else:
2405            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2406                'clearance_locked', 'clr_code', 'officer_comment')
2407        return form_fields
2408
2409    def update(self):
2410        if self.context.clearance_locked:
2411            emit_lock_message(self)
2412            return
2413        return super(StudentClearanceEditFormPage, self).update()
2414
2415    @action(_('Save'), style='primary')
2416    def save(self, **data):
2417        self.applyData(self.context, **data)
2418        self.flash(_('Clearance form has been saved.'))
2419        return
2420
2421    def dataNotComplete(self):
2422        """To be implemented in the customization package.
2423        """
2424        return False
2425
2426    @action(_('Save and request clearance'), style='primary')
2427    def requestClearance(self, **data):
2428        self.applyData(self.context, **data)
2429        if self.dataNotComplete():
2430            self.flash(self.dataNotComplete(), type="warning")
2431            return
2432        self.flash(_('Clearance form has been saved.'))
2433        if self.context.clr_code:
2434            self.redirect(self.url(self.context, 'request_clearance'))
2435        else:
2436            # We bypass the request_clearance page if student
2437            # has been imported in state 'clearance started' and
2438            # no clr_code was entered before.
2439            state = IWorkflowState(self.context).getState()
2440            if state != CLEARANCE:
2441                # This shouldn't happen, but the application officer
2442                # might have forgotten to lock the form after changing the state
2443                self.flash(_('This form cannot be submitted. Wrong state!'),
2444                           type="danger")
2445                return
2446            IWorkflowInfo(self.context).fireTransition('request_clearance')
2447            self.flash(_('Clearance has been requested.'))
2448            self.redirect(self.url(self.context))
2449        return
2450
2451class RequestClearancePage(KofaPage):
2452    grok.context(IStudent)
2453    grok.name('request_clearance')
2454    grok.require('waeup.handleStudent')
2455    grok.template('enterpin')
2456    label = _('Request clearance')
2457    notice = _('Enter the CLR access code used for starting clearance.')
2458    ac_prefix = 'CLR'
2459    pnav = 4
2460    buttonname = _('Request clearance now')
2461    with_ac = True
2462
2463    def update(self, SUBMIT=None):
2464        if self.with_ac:
2465            self.ac_series = self.request.form.get('ac_series', None)
2466            self.ac_number = self.request.form.get('ac_number', None)
2467        if SUBMIT is None:
2468            return
2469        if self.with_ac:
2470            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2471            if self.context.clr_code and self.context.clr_code != pin:
2472                self.flash(_("This isn't your CLR access code."), type="danger")
2473                return
2474        state = IWorkflowState(self.context).getState()
2475        if state != CLEARANCE:
2476            # This shouldn't happen, but the application officer
2477            # might have forgotten to lock the form after changing the state
2478            self.flash(_('This form cannot be submitted. Wrong state!'),
2479                       type="danger")
2480            return
2481        IWorkflowInfo(self.context).fireTransition('request_clearance')
2482        self.flash(_('Clearance has been requested.'))
2483        self.redirect(self.url(self.context))
2484        return
2485
2486class StartSessionPage(KofaPage):
2487    grok.context(IStudentStudyCourse)
2488    grok.name('start_session')
2489    grok.require('waeup.handleStudent')
2490    grok.template('enterpin')
2491    label = _('Start session')
2492    ac_prefix = 'SFE'
2493    notice = ''
2494    pnav = 4
2495    buttonname = _('Start now')
2496    with_ac = True
2497
2498    def update(self, SUBMIT=None):
2499        if not self.context.is_current:
2500            emit_lock_message(self)
2501            return
2502        super(StartSessionPage, self).update()
2503        if not self.context.next_session_allowed:
2504            self.flash(_("You are not entitled to start session."),
2505                       type="warning")
2506            self.redirect(self.url(self.context))
2507            return
2508        if self.with_ac:
2509            self.ac_series = self.request.form.get('ac_series', None)
2510            self.ac_number = self.request.form.get('ac_number', None)
2511        if SUBMIT is None:
2512            return
2513        if self.with_ac:
2514            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2515            code = get_access_code(pin)
2516            if not code:
2517                self.flash(_('Activation code is invalid.'), type="warning")
2518                return
2519            # Mark pin as used (this also fires a pin related transition)
2520            if code.state == USED:
2521                self.flash(_('Activation code has already been used.'),
2522                           type="warning")
2523                return
2524            else:
2525                comment = _(u"invalidated")
2526                # Here we know that the ac is in state initialized so we do not
2527                # expect an error, but the owner might be different
2528                if not invalidate_accesscode(
2529                    pin,comment,self.context.student.student_id):
2530                    self.flash(_('You are not the owner of this access code.'),
2531                               type="warning")
2532                    return
2533        try:
2534            if self.context.student.state == CLEARED:
2535                IWorkflowInfo(self.context.student).fireTransition(
2536                    'pay_first_school_fee')
2537            elif self.context.student.state == RETURNING:
2538                IWorkflowInfo(self.context.student).fireTransition(
2539                    'pay_school_fee')
2540            elif self.context.student.state == PAID:
2541                IWorkflowInfo(self.context.student).fireTransition(
2542                    'pay_pg_fee')
2543        except ConstraintNotSatisfied:
2544            self.flash(_('An error occurred, please contact the system administrator.'),
2545                       type="danger")
2546            return
2547        self.flash(_('Session started.'))
2548        self.redirect(self.url(self.context))
2549        return
2550
2551class AddStudyLevelFormPage(KofaEditFormPage):
2552    """ Page for students to add current study levels
2553    """
2554    grok.context(IStudentStudyCourse)
2555    grok.name('add')
2556    grok.require('waeup.handleStudent')
2557    grok.template('studyleveladdpage')
2558    form_fields = grok.AutoFields(IStudentStudyCourse)
2559    pnav = 4
2560
2561    @property
2562    def label(self):
2563        studylevelsource = StudyLevelSource().factory
2564        code = self.context.current_level
2565        title = studylevelsource.getTitle(self.context, code)
2566        return _('Add current level ${a}', mapping = {'a':title})
2567
2568    def update(self):
2569        if not self.context.is_current:
2570            emit_lock_message(self)
2571            return
2572        if self.context.student.state != PAID:
2573            emit_lock_message(self)
2574            return
2575        code = self.context.current_level
2576        if code is None:
2577            self.flash(_('Your data are incomplete'), type="danger")
2578            self.redirect(self.url(self.context))
2579            return
2580        super(AddStudyLevelFormPage, self).update()
2581        return
2582
2583    @action(_('Create course list now'), style='primary')
2584    def addStudyLevel(self, **data):
2585        studylevel = createObject(u'waeup.StudentStudyLevel')
2586        studylevel.level = self.context.current_level
2587        studylevel.level_session = self.context.current_session
2588        try:
2589            self.context.addStudentStudyLevel(
2590                self.context.certificate,studylevel)
2591        except KeyError:
2592            self.flash(_('This level exists.'), type="warning")
2593        except RequiredMissing:
2594            self.flash(_('Your data are incomplete'), type="danger")
2595        self.redirect(self.url(self.context))
2596        return
2597
2598class StudyLevelEditFormPage(KofaEditFormPage):
2599    """ Page to edit the student study level data by students
2600    """
2601    grok.context(IStudentStudyLevel)
2602    grok.name('edit')
2603    grok.require('waeup.editStudyLevel')
2604    grok.template('studyleveleditpage')
2605    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2606        'level_session', 'level_verdict')
2607    pnav = 4
2608
2609    def update(self, ADD=None, course=None):
2610        if not self.context.__parent__.is_current:
2611            emit_lock_message(self)
2612            return
2613        if self.context.student.state != PAID or \
2614            not self.context.is_current_level:
2615            emit_lock_message(self)
2616            return
2617        super(StudyLevelEditFormPage, self).update()
2618        if ADD is not None:
2619            if not course:
2620                self.flash(_('No valid course code entered.'), type="warning")
2621                return
2622            cat = queryUtility(ICatalog, name='courses_catalog')
2623            result = cat.searchResults(code=(course, course))
2624            if len(result) != 1:
2625                self.flash(_('Course not found.'), type="warning")
2626                return
2627            course = list(result)[0]
2628            addCourseTicket(self, course)
2629        return
2630
2631    @property
2632    def label(self):
2633        # Here we know that the cookie has been set
2634        lang = self.request.cookies.get('kofa.language')
2635        level_title = translate(self.context.level_title, 'waeup.kofa',
2636            target_language=lang)
2637        return _('Edit course list of ${a}',
2638            mapping = {'a':level_title})
2639
2640    @property
2641    def translated_values(self):
2642        return translated_values(self)
2643
2644    def _delCourseTicket(self, **data):
2645        form = self.request.form
2646        if 'val_id' in form:
2647            child_id = form['val_id']
2648        else:
2649            self.flash(_('No ticket selected.'), type="warning")
2650            self.redirect(self.url(self.context, '@@edit'))
2651            return
2652        if not isinstance(child_id, list):
2653            child_id = [child_id]
2654        deleted = []
2655        for id in child_id:
2656            # Students are not allowed to remove core tickets
2657            if id in self.context and \
2658                self.context[id].removable_by_student:
2659                del self.context[id]
2660                deleted.append(id)
2661        if len(deleted):
2662            self.flash(_('Successfully removed: ${a}',
2663                mapping = {'a':', '.join(deleted)}))
2664            self.context.writeLogMessage(
2665                self,'removed: %s at %s' %
2666                (', '.join(deleted), self.context.level))
2667        self.redirect(self.url(self.context, u'@@edit'))
2668        return
2669
2670    @jsaction(_('Remove selected tickets'))
2671    def delCourseTicket(self, **data):
2672        self._delCourseTicket(**data)
2673        return
2674
2675    def _registerCourses(self, **data):
2676        if self.context.student.is_postgrad and \
2677            not self.context.student.is_special_postgrad:
2678            self.flash(_(
2679                "You are a postgraduate student, "
2680                "your course list can't bee registered."), type="warning")
2681            self.redirect(self.url(self.context))
2682            return
2683        students_utils = getUtility(IStudentsUtils)
2684        max_credits = students_utils.maxCredits(self.context)
2685        if self.context.total_credits > max_credits:
2686            self.flash(_('Maximum credits of ${a} exceeded.',
2687                mapping = {'a':max_credits}), type="warning")
2688            return
2689        IWorkflowInfo(self.context.student).fireTransition(
2690            'register_courses')
2691        self.flash(_('Course list has been registered.'))
2692        self.redirect(self.url(self.context))
2693        return
2694
2695    @action(_('Register course list'))
2696    def registerCourses(self, **data):
2697        self._registerCourses(**data)
2698        return
2699
2700class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2701    """Add a course ticket by student.
2702    """
2703    grok.name('ctadd')
2704    grok.require('waeup.handleStudent')
2705    form_fields = grok.AutoFields(ICourseTicketAdd)
2706
2707    def update(self):
2708        if self.context.student.state != PAID or \
2709            not self.context.is_current_level:
2710            emit_lock_message(self)
2711            return
2712        super(CourseTicketAddFormPage2, self).update()
2713        return
2714
2715    @action(_('Add course ticket'))
2716    def addCourseTicket(self, **data):
2717        # Safety belt
2718        if self.context.student.state != PAID:
2719            return
2720        course = data['course']
2721        success = addCourseTicket(self, course)
2722        if success:
2723            self.redirect(self.url(self.context, u'@@edit'))
2724        return
2725
2726class SetPasswordPage(KofaPage):
2727    grok.context(IKofaObject)
2728    grok.name('setpassword')
2729    grok.require('waeup.Anonymous')
2730    grok.template('setpassword')
2731    label = _('Set password for first-time login')
2732    ac_prefix = 'PWD'
2733    pnav = 0
2734    set_button = _('Set')
2735
2736    def update(self, SUBMIT=None):
2737        self.reg_number = self.request.form.get('reg_number', None)
2738        self.ac_series = self.request.form.get('ac_series', None)
2739        self.ac_number = self.request.form.get('ac_number', None)
2740
2741        if SUBMIT is None:
2742            return
2743        hitlist = search(query=self.reg_number,
2744            searchtype='reg_number', view=self)
2745        if not hitlist:
2746            self.flash(_('No student found.'), type="warning")
2747            return
2748        if len(hitlist) != 1:   # Cannot happen but anyway
2749            self.flash(_('More than one student found.'), type="warning")
2750            return
2751        student = hitlist[0].context
2752        self.student_id = student.student_id
2753        student_pw = student.password
2754        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2755        code = get_access_code(pin)
2756        if not code:
2757            self.flash(_('Access code is invalid.'), type="warning")
2758            return
2759        if student_pw and pin == student.adm_code:
2760            self.flash(_(
2761                'Password has already been set. Your Student Id is ${a}',
2762                mapping = {'a':self.student_id}))
2763            return
2764        elif student_pw:
2765            self.flash(
2766                _('Password has already been set. You are using the ' +
2767                'wrong Access Code.'), type="warning")
2768            return
2769        # Mark pin as used (this also fires a pin related transition)
2770        # and set student password
2771        if code.state == USED:
2772            self.flash(_('Access code has already been used.'), type="warning")
2773            return
2774        else:
2775            comment = _(u"invalidated")
2776            # Here we know that the ac is in state initialized so we do not
2777            # expect an exception
2778            invalidate_accesscode(pin,comment)
2779            IUserAccount(student).setPassword(self.ac_number)
2780            student.adm_code = pin
2781        self.flash(_('Password has been set. Your Student Id is ${a}',
2782            mapping = {'a':self.student_id}))
2783        return
2784
2785class StudentRequestPasswordPage(KofaAddFormPage):
2786    """Captcha'd registration page for applicants.
2787    """
2788    grok.name('requestpw')
2789    grok.require('waeup.Anonymous')
2790    grok.template('requestpw')
2791    form_fields = grok.AutoFields(IStudentRequestPW).select(
2792        'firstname','number','email')
2793    label = _('Request password for first-time login')
2794
2795    def update(self):
2796        # Handle captcha
2797        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2798        self.captcha_result = self.captcha.verify(self.request)
2799        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2800        return
2801
2802    def _redirect(self, email, password, student_id):
2803        # Forward only email to landing page in base package.
2804        self.redirect(self.url(self.context, 'requestpw_complete',
2805            data = dict(email=email)))
2806        return
2807
2808    def _pw_used(self):
2809        # XXX: False if password has not been used. We need an extra
2810        #      attribute which remembers if student logged in.
2811        return True
2812
2813    @action(_('Send login credentials to email address'), style='primary')
2814    def get_credentials(self, **data):
2815        if not self.captcha_result.is_valid:
2816            # Captcha will display error messages automatically.
2817            # No need to flash something.
2818            return
2819        number = data.get('number','')
2820        firstname = data.get('firstname','')
2821        cat = getUtility(ICatalog, name='students_catalog')
2822        results = list(
2823            cat.searchResults(reg_number=(number, number)))
2824        if not results:
2825            results = list(
2826                cat.searchResults(matric_number=(number, number)))
2827        if results:
2828            student = results[0]
2829            if getattr(student,'firstname',None) is None:
2830                self.flash(_('An error occurred.'), type="danger")
2831                return
2832            elif student.firstname.lower() != firstname.lower():
2833                # Don't tell the truth here. Anonymous must not
2834                # know that a record was found and only the firstname
2835                # verification failed.
2836                self.flash(_('No student record found.'), type="warning")
2837                return
2838            elif student.password is not None and self._pw_used:
2839                self.flash(_('Your password has already been set and used. '
2840                             'Please proceed to the login page.'),
2841                           type="warning")
2842                return
2843            # Store email address but nothing else.
2844            student.email = data['email']
2845            notify(grok.ObjectModifiedEvent(student))
2846        else:
2847            # No record found, this is the truth.
2848            self.flash(_('No student record found.'), type="warning")
2849            return
2850
2851        kofa_utils = getUtility(IKofaUtils)
2852        password = kofa_utils.genPassword()
2853        mandate = PasswordMandate()
2854        mandate.params['password'] = password
2855        mandate.params['user'] = student
2856        site = grok.getSite()
2857        site['mandates'].addMandate(mandate)
2858        # Send email with credentials
2859        args = {'mandate_id':mandate.mandate_id}
2860        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2861        url_info = u'Confirmation link: %s' % mandate_url
2862        msg = _('You have successfully requested a password for the')
2863        if kofa_utils.sendCredentials(IUserAccount(student),
2864            password, url_info, msg):
2865            email_sent = student.email
2866        else:
2867            email_sent = None
2868        self._redirect(email=email_sent, password=password,
2869            student_id=student.student_id)
2870        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2871        self.context.logger.info(
2872            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2873        return
2874
2875class StudentRequestPasswordEmailSent(KofaPage):
2876    """Landing page after successful password request.
2877
2878    """
2879    grok.name('requestpw_complete')
2880    grok.require('waeup.Public')
2881    grok.template('requestpwmailsent')
2882    label = _('Your password request was successful.')
2883
2884    def update(self, email=None, student_id=None, password=None):
2885        self.email = email
2886        self.password = password
2887        self.student_id = student_id
2888        return
2889
2890class FilterStudentsInDepartmentPage(KofaPage):
2891    """Page that filters and lists students.
2892    """
2893    grok.context(IDepartment)
2894    grok.require('waeup.showStudents')
2895    grok.name('students')
2896    grok.template('filterstudentspage')
2897    pnav = 1
2898    session_label = _('Current Session')
2899    level_label = _('Current Level')
2900
2901    def label(self):
2902        return 'Students in %s' % self.context.longtitle
2903
2904    def _set_session_values(self):
2905        vocab_terms = academic_sessions_vocab.by_value.values()
2906        self.sessions = sorted(
2907            [(x.title, x.token) for x in vocab_terms], reverse=True)
2908        self.sessions += [('All Sessions', 'all')]
2909        return
2910
2911    def _set_level_values(self):
2912        vocab_terms = course_levels.by_value.values()
2913        self.levels = sorted(
2914            [(x.title, x.token) for x in vocab_terms])
2915        self.levels += [('All Levels', 'all')]
2916        return
2917
2918    def _searchCatalog(self, session, level):
2919        if level not in (10, 999, None):
2920            start_level = 100 * (level // 100)
2921            end_level = start_level + 90
2922        else:
2923            start_level = end_level = level
2924        cat = queryUtility(ICatalog, name='students_catalog')
2925        students = cat.searchResults(
2926            current_session=(session, session),
2927            current_level=(start_level, end_level),
2928            depcode=(self.context.code, self.context.code)
2929            )
2930        hitlist = []
2931        for student in students:
2932            hitlist.append(StudentQueryResultItem(student, view=self))
2933        return hitlist
2934
2935    def update(self, SHOW=None, session=None, level=None):
2936        self.parent_url = self.url(self.context.__parent__)
2937        self._set_session_values()
2938        self._set_level_values()
2939        self.hitlist = []
2940        self.session_default = session
2941        self.level_default = level
2942        if SHOW is not None:
2943            if session != 'all':
2944                self.session = int(session)
2945                self.session_string = '%s %s/%s' % (
2946                    self.session_label, self.session, self.session+1)
2947            else:
2948                self.session = None
2949                self.session_string = _('in any session')
2950            if level != 'all':
2951                self.level = int(level)
2952                self.level_string = '%s %s' % (self.level_label, self.level)
2953            else:
2954                self.level = None
2955                self.level_string = _('at any level')
2956            self.hitlist = self._searchCatalog(self.session, self.level)
2957            if not self.hitlist:
2958                self.flash(_('No student found.'), type="warning")
2959        return
2960
2961class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2962    """Page that filters and lists students.
2963    """
2964    grok.context(ICertificate)
2965
2966    def label(self):
2967        return 'Students studying %s' % self.context.longtitle
2968
2969    def _searchCatalog(self, session, level):
2970        if level not in (10, 999, None):
2971            start_level = 100 * (level // 100)
2972            end_level = start_level + 90
2973        else:
2974            start_level = end_level = level
2975        cat = queryUtility(ICatalog, name='students_catalog')
2976        students = cat.searchResults(
2977            current_session=(session, session),
2978            current_level=(start_level, end_level),
2979            certcode=(self.context.code, self.context.code)
2980            )
2981        hitlist = []
2982        for student in students:
2983            hitlist.append(StudentQueryResultItem(student, view=self))
2984        return hitlist
2985
2986class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2987    """Page that filters and lists students.
2988    """
2989    grok.context(ICourse)
2990
2991    session_label = _('Session')
2992    level_label = _('Level')
2993
2994    def label(self):
2995        return 'Students registered for %s' % self.context.longtitle
2996
2997    def _searchCatalog(self, session, level):
2998        if level not in (10, 999, None):
2999            start_level = 100 * (level // 100)
3000            end_level = start_level + 90
3001        else:
3002            start_level = end_level = level
3003        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3004        coursetickets = cat.searchResults(
3005            session=(session, session),
3006            level=(start_level, end_level),
3007            code=(self.context.code, self.context.code)
3008            )
3009        hitlist = []
3010        for ticket in coursetickets:
3011            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3012        return list(set(hitlist))
3013
3014class EditScoresPage(KofaPage):
3015    """Page that filters and lists students.
3016    """
3017    grok.context(ICourse)
3018    grok.require('waeup.editScores')
3019    grok.name('edit_scores')
3020    grok.template('editscorespage')
3021    pnav = 1
3022
3023    def label(self):
3024        session = academic_sessions_vocab.getTerm(
3025            self.current_academic_session).title
3026        return '%s tickets in academic session %s' % (
3027            self.context.code, session)
3028
3029    def _searchCatalog(self, session):
3030        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3031        coursetickets = cat.searchResults(
3032            session=(session, session),
3033            code=(self.context.code, self.context.code)
3034            )
3035        return list(coursetickets)
3036
3037    def update(self,  *args, **kw):
3038        form = self.request.form
3039        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3040        self.current_academic_session = grok.getSite()[
3041            'configuration'].current_academic_session
3042        if self.context.__parent__.__parent__.score_editing_disabled:
3043            self.flash(_('Score editing disabled.'), type="warning")
3044            self.redirect(self.url(self.context))
3045            return
3046        if not self.current_academic_session:
3047            self.flash(_('Current academic session not set.'), type="warning")
3048            self.redirect(self.url(self.context))
3049            return
3050        self.tickets = self._searchCatalog(self.current_academic_session)
3051        editable_tickets = [
3052            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3053        if not self.tickets:
3054            self.flash(_('No student found.'), type="warning")
3055            self.redirect(self.url(self.context))
3056            return
3057        if 'UPDATE' in form:
3058            tno = 0
3059            error = ''
3060            if not editable_tickets:
3061                return
3062            scores = form['scores']
3063            if isinstance(scores, basestring):
3064                scores = [scores]
3065            for ticket in editable_tickets:
3066                score = ticket.score
3067                if scores[tno] == '':
3068                    score = None
3069                else:
3070                    try:
3071                        score = int(scores[tno])
3072                    except ValueError:
3073                        error += '%s, ' % ticket.student.display_fullname
3074                if ticket.score != score:
3075                    ticket.score = score
3076                    ticket.student.__parent__.logger.info(
3077                        '%s - %s %s/%s score updated (%s)' %
3078                        (ob_class, ticket.student.student_id,
3079                         ticket.level, ticket.code, score))
3080                    #notify(grok.ObjectModifiedEvent(ticket))
3081                tno += 1
3082            if error:
3083                self.flash(_('Error: Score(s) of %s have not be updated. '
3084                  'Only integers are allowed.' % error.strip(', ')),
3085                  type="danger")
3086        return
3087
3088class ExportJobContainerOverview(KofaPage):
3089    """Page that lists active student data export jobs and provides links
3090    to discard or download CSV files.
3091
3092    """
3093    grok.context(VirtualExportJobContainer)
3094    grok.require('waeup.showStudents')
3095    grok.name('index.html')
3096    grok.template('exportjobsindex')
3097    label = _('Student Data Exports')
3098    pnav = 1
3099
3100    def update(self, CREATE=None, DISCARD=None, job_id=None):
3101        if CREATE:
3102            self.redirect(self.url('@@exportconfig'))
3103            return
3104        if DISCARD and job_id:
3105            entry = self.context.entry_from_job_id(job_id)
3106            self.context.delete_export_entry(entry)
3107            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3108            self.context.logger.info(
3109                '%s - discarded: job_id=%s' % (ob_class, job_id))
3110            self.flash(_('Discarded export') + ' %s' % job_id)
3111        self.entries = doll_up(self, user=self.request.principal.id)
3112        return
3113
3114class ExportJobContainerJobConfig(KofaPage):
3115    """Page that configures a students export job.
3116
3117    This is a baseclass.
3118    """
3119    grok.baseclass()
3120    grok.name('exportconfig')
3121    grok.require('waeup.showStudents')
3122    grok.template('exportconfig')
3123    label = _('Configure student data export')
3124    pnav = 1
3125    redirect_target = ''
3126
3127    def _set_session_values(self):
3128        vocab_terms = academic_sessions_vocab.by_value.values()
3129        self.sessions = sorted(
3130            [(x.title, x.token) for x in vocab_terms], reverse=True)
3131        self.sessions += [(_('All Sessions'), 'all')]
3132        return
3133
3134    def _set_level_values(self):
3135        vocab_terms = course_levels.by_value.values()
3136        self.levels = sorted(
3137            [(x.title, x.token) for x in vocab_terms])
3138        self.levels += [(_('All Levels'), 'all')]
3139        return
3140
3141    def _set_mode_values(self):
3142        utils = getUtility(IKofaUtils)
3143        self.modes = sorted([(value, key) for key, value in
3144                      utils.STUDY_MODES_DICT.items()])
3145        self.modes +=[(_('All Modes'), 'all')]
3146        return
3147
3148    def _set_exporter_values(self):
3149        # We provide all student exporters, nothing else, yet.
3150        # Bursary or Department Officers don't have the general exportData
3151        # permission and are only allowed to export bursary or payments
3152        # overview data respectively. This is the only place where
3153        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3154        # are used.
3155        exporters = []
3156        if not checkPermission('waeup.exportData', self.context):
3157            if checkPermission('waeup.exportBursaryData', self.context):
3158                exporters += [('Bursary Data', 'bursary')]
3159            if checkPermission('waeup.exportPaymentsOverview', self.context):
3160                exporters += [('Student Payments Overview', 'paymentsoverview')]
3161            self.exporters = exporters
3162            return
3163        for name in EXPORTER_NAMES:
3164            util = getUtility(ICSVExporter, name=name)
3165            exporters.append((util.title, name),)
3166        self.exporters = exporters
3167        return
3168
3169    @property
3170    def depcode(self):
3171        return None
3172
3173    @property
3174    def certcode(self):
3175        return None
3176
3177    def update(self, START=None, session=None, level=None, mode=None,
3178               payments_start=None, payments_end=None,
3179               exporter=None):
3180        self._set_session_values()
3181        self._set_level_values()
3182        self._set_mode_values()
3183        self._set_exporter_values()
3184        if START is None:
3185            return
3186        if payments_start or payments_end:
3187            date_format = '%d/%m/%Y'
3188            try:
3189                dummy = datetime.strptime(payments_start, date_format)
3190                dummy = datetime.strptime(payments_end, date_format)
3191            except ValueError:
3192                self.flash(_('Payment dates do not match format d/m/Y.'),
3193                           type="danger")
3194                return
3195        if session == 'all':
3196            session=None
3197        if level == 'all':
3198            level = None
3199        if mode == 'all':
3200            mode = None
3201        if payments_start == '':
3202            payments_start = None
3203        if payments_end == '':
3204            payments_end = None
3205        if (mode, level, session,
3206            self.depcode, self.certcode) == (None, None, None, None, None):
3207            # Export all students including those without certificate
3208            if payments_start:
3209                job_id = self.context.start_export_job(exporter,
3210                                              self.request.principal.id,
3211                                              payments_start = payments_start,
3212                                              payments_end = payments_end)
3213            else:
3214                job_id = self.context.start_export_job(exporter,
3215                                              self.request.principal.id)
3216        else:
3217            if payments_start:
3218                job_id = self.context.start_export_job(exporter,
3219                                              self.request.principal.id,
3220                                              current_session=session,
3221                                              current_level=level,
3222                                              current_mode=mode,
3223                                              depcode=self.depcode,
3224                                              certcode=self.certcode,
3225                                              payments_start = payments_start,
3226                                              payments_end = payments_end)
3227            else:
3228                job_id = self.context.start_export_job(exporter,
3229                                              self.request.principal.id,
3230                                              current_session=session,
3231                                              current_level=level,
3232                                              current_mode=mode,
3233                                              depcode=self.depcode,
3234                                              certcode=self.certcode)
3235        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3236        self.context.logger.info(
3237            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s), job_id=%s'
3238            % (ob_class, exporter, session, level, mode, self.depcode,
3239            self.certcode, payments_start, payments_end, job_id))
3240        self.flash(_('Export started for students with') +
3241                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3242                   session, level, mode))
3243        self.redirect(self.url(self.redirect_target))
3244        return
3245
3246class ExportJobContainerDownload(ExportCSVView):
3247    """Page that downloads a students export csv file.
3248
3249    """
3250    grok.context(VirtualExportJobContainer)
3251    grok.require('waeup.showStudents')
3252
3253class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3254    """Page that configures a students export job in datacenter.
3255
3256    """
3257    grok.context(IDataCenter)
3258    redirect_target = '@@export'
3259
3260class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3261    """Page that configures a students export job in facultiescontainer.
3262
3263    """
3264    grok.context(VirtualFacultiesExportJobContainer)
3265
3266class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3267    """Page that configures a students export job in departments.
3268
3269    """
3270    grok.context(VirtualDepartmentExportJobContainer)
3271
3272    @property
3273    def depcode(self):
3274        return self.context.__parent__.code
3275
3276class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3277    """Page that configures a students export job for certificates.
3278
3279    """
3280    grok.context(VirtualCertificateExportJobContainer)
3281    grok.template('exportconfig_certificate')
3282
3283    @property
3284    def certcode(self):
3285        return self.context.__parent__.code
3286
3287class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3288    """Page that configures a students export job for courses.
3289
3290    In contrast to department or certificate student data exports the
3291    coursetickets_catalog is searched here. Therefore the update
3292    method from the base class is customized.
3293    """
3294    grok.context(VirtualCourseExportJobContainer)
3295    grok.template('exportconfig_course')
3296
3297    def _set_exporter_values(self):
3298        # We provide only two exporters.
3299        exporters = []
3300        for name in ('students', 'coursetickets'):
3301            util = getUtility(ICSVExporter, name=name)
3302            exporters.append((util.title, name),)
3303        self.exporters = exporters
3304
3305    def update(self, START=None, session=None, level=None, mode=None,
3306               exporter=None):
3307        self._set_session_values()
3308        self._set_level_values()
3309        self._set_mode_values()
3310        self._set_exporter_values()
3311        if START is None:
3312            return
3313        if session == 'all':
3314            session = None
3315        if level == 'all':
3316            level = None
3317        job_id = self.context.start_export_job(exporter,
3318                                      self.request.principal.id,
3319                                      # Use a different catalog and
3320                                      # pass different keywords than
3321                                      # for the (default) students_catalog
3322                                      catalog='coursetickets',
3323                                      session=session,
3324                                      level=level,
3325                                      code=self.context.__parent__.code)
3326        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3327        self.context.logger.info(
3328            '%s - exported: %s (%s, %s, %s), job_id=%s'
3329            % (ob_class, exporter, session, level,
3330            self.context.__parent__.code, job_id))
3331        self.flash(_('Export started for course tickets with') +
3332                   ' level_session=%s, level=%s' % (
3333                   session, level))
3334        self.redirect(self.url(self.redirect_target))
3335        return
Note: See TracBrowser for help on using the repository browser.