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

Last change on this file since 9813 was 9813, checked in by Henrik Bettermann, 12 years ago

ExportJobContainerOverview? and ExportJobContainerDownload? can be used for all local exporter.

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