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

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

Show grade on StudyLevelDisplayFormPage? and corresponding slip.

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