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

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

Add permission, page and button to mass-clear all students in a department.

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