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

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

Ease customization of STUDENT_EXPORTER_NAMES.

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