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

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

Remove deprecated HTML and REST widgets and use html2dict instead.

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