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

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

Extend IExportJob so that AsyncExportJobs? unveil their status like AsyncReportJobs? do. Use this property attribute to hide buttons.

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