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

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

Sort modes.

  • Property svn:keywords set to Id
File size: 100.0 KB
Line 
1## $Id: browser.py 9838 2013-01-07 08:35:10Z 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, ExportCSVView, doll_up
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, IDataCenter)
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
2240    def update(self):
2241        if not self.context.__parent__.is_current:
2242            emit_lock_message(self)
2243            return
2244        if self.context.student.state != PAID or \
2245            not self.context.is_current_level:
2246            emit_lock_message(self)
2247            return
2248        super(StudyLevelEditFormPage, self).update()
2249        datatable.need()
2250        warning.need()
2251        return
2252
2253    @property
2254    def label(self):
2255        # Here we know that the cookie has been set
2256        lang = self.request.cookies.get('kofa.language')
2257        level_title = translate(self.context.level_title, 'waeup.kofa',
2258            target_language=lang)
2259        return _('Edit course list of ${a}',
2260            mapping = {'a':level_title})
2261
2262    @property
2263    def translated_values(self):
2264        return translated_values(self)
2265
2266    @action(_('Add course ticket'))
2267    def addCourseTicket(self, **data):
2268        self.redirect(self.url(self.context, 'ctadd'))
2269
2270    def _delCourseTicket(self, **data):
2271        form = self.request.form
2272        if 'val_id' in form:
2273            child_id = form['val_id']
2274        else:
2275            self.flash(_('No ticket selected.'))
2276            self.redirect(self.url(self.context, '@@edit'))
2277            return
2278        if not isinstance(child_id, list):
2279            child_id = [child_id]
2280        deleted = []
2281        for id in child_id:
2282            # Students are not allowed to remove core tickets
2283            if id in self.context and \
2284                self.context[id].removable_by_student:
2285                del self.context[id]
2286                deleted.append(id)
2287        if len(deleted):
2288            self.flash(_('Successfully removed: ${a}',
2289                mapping = {'a':', '.join(deleted)}))
2290            self.context.writeLogMessage(
2291                self,'removed: %s' % ', '.join(deleted))
2292        self.redirect(self.url(self.context, u'@@edit'))
2293        return
2294
2295    @jsaction(_('Remove selected tickets'))
2296    def delCourseTicket(self, **data):
2297        self._delCourseTicket(**data)
2298        return
2299
2300    def _registerCourses(self, **data):
2301        if self.context.student.is_postgrad:
2302            self.flash(_(
2303                "You are a postgraduate student, "
2304                "your course list can't bee registered."))
2305            self.redirect(self.url(self.context))
2306            return
2307        students_utils = getUtility(IStudentsUtils)
2308        max_credits = students_utils.maxCredits(self.context)
2309        if self.context.total_credits > max_credits:
2310            self.flash(_('Maximum credits of ${a} exceeded.',
2311                mapping = {'a':max_credits}))
2312            return
2313        IWorkflowInfo(self.context.student).fireTransition(
2314            'register_courses')
2315        self.flash(_('Course list has been registered.'))
2316        self.redirect(self.url(self.context))
2317        return
2318
2319    @action(_('Register course list'), style='primary')
2320    def registerCourses(self, **data):
2321        self._registerCourses(**data)
2322        return
2323
2324
2325class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2326    """Add a course ticket by student.
2327    """
2328    grok.name('ctadd')
2329    grok.require('waeup.handleStudent')
2330    form_fields = grok.AutoFields(ICourseTicketAdd)
2331
2332    def update(self):
2333        if self.context.student.state != PAID or \
2334            not self.context.is_current_level:
2335            emit_lock_message(self)
2336            return
2337        super(CourseTicketAddFormPage2, self).update()
2338        return
2339
2340    @action(_('Add course ticket'))
2341    def addCourseTicket(self, **data):
2342        students_utils = getUtility(IStudentsUtils)
2343        # Safety belt
2344        if self.context.student.state != PAID:
2345            return
2346        ticket = createObject(u'waeup.CourseTicket')
2347        course = data['course']
2348        ticket.automatic = False
2349        ticket.carry_over = False
2350        max_credits = students_utils.maxCreditsExceeded(self.context, course)
2351        if max_credits:
2352            self.flash(_(
2353                'Your total credits exceed ${a}.',
2354                mapping = {'a': max_credits}))
2355            return
2356        try:
2357            self.context.addCourseTicket(ticket, course)
2358        except KeyError:
2359            self.flash(_('The ticket exists.'))
2360            return
2361        self.flash(_('Successfully added ${a}.',
2362            mapping = {'a':ticket.code}))
2363        self.redirect(self.url(self.context, u'@@edit'))
2364        return
2365
2366
2367class SetPasswordPage(KofaPage):
2368    grok.context(IKofaObject)
2369    grok.name('setpassword')
2370    grok.require('waeup.Anonymous')
2371    grok.template('setpassword')
2372    label = _('Set password for first-time login')
2373    ac_prefix = 'PWD'
2374    pnav = 0
2375    set_button = _('Set')
2376
2377    def update(self, SUBMIT=None):
2378        self.reg_number = self.request.form.get('reg_number', None)
2379        self.ac_series = self.request.form.get('ac_series', None)
2380        self.ac_number = self.request.form.get('ac_number', None)
2381
2382        if SUBMIT is None:
2383            return
2384        hitlist = search(query=self.reg_number,
2385            searchtype='reg_number', view=self)
2386        if not hitlist:
2387            self.flash(_('No student found.'))
2388            return
2389        if len(hitlist) != 1:   # Cannot happen but anyway
2390            self.flash(_('More than one student found.'))
2391            return
2392        student = hitlist[0].context
2393        self.student_id = student.student_id
2394        student_pw = student.password
2395        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2396        code = get_access_code(pin)
2397        if not code:
2398            self.flash(_('Access code is invalid.'))
2399            return
2400        if student_pw and pin == student.adm_code:
2401            self.flash(_(
2402                'Password has already been set. Your Student Id is ${a}',
2403                mapping = {'a':self.student_id}))
2404            return
2405        elif student_pw:
2406            self.flash(
2407                _('Password has already been set. You are using the ' +
2408                'wrong Access Code.'))
2409            return
2410        # Mark pin as used (this also fires a pin related transition)
2411        # and set student password
2412        if code.state == USED:
2413            self.flash(_('Access code has already been used.'))
2414            return
2415        else:
2416            comment = _(u"invalidated")
2417            # Here we know that the ac is in state initialized so we do not
2418            # expect an exception
2419            invalidate_accesscode(pin,comment)
2420            IUserAccount(student).setPassword(self.ac_number)
2421            student.adm_code = pin
2422        self.flash(_('Password has been set. Your Student Id is ${a}',
2423            mapping = {'a':self.student_id}))
2424        return
2425
2426class StudentRequestPasswordPage(KofaAddFormPage):
2427    """Captcha'd registration page for applicants.
2428    """
2429    grok.name('requestpw')
2430    grok.require('waeup.Anonymous')
2431    grok.template('requestpw')
2432    form_fields = grok.AutoFields(IStudentRequestPW).select(
2433        'firstname','number','email')
2434    label = _('Request password for first-time login')
2435
2436    def update(self):
2437        # Handle captcha
2438        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2439        self.captcha_result = self.captcha.verify(self.request)
2440        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2441        return
2442
2443    def _redirect(self, email, password, student_id):
2444        # Forward only email to landing page in base package.
2445        self.redirect(self.url(self.context, 'requestpw_complete',
2446            data = dict(email=email)))
2447        return
2448
2449    def _pw_used(self):
2450        # XXX: False if password has not been used. We need an extra
2451        #      attribute which remembers if student logged in.
2452        return True
2453
2454    @action(_('Send login credentials to email address'), style='primary')
2455    def get_credentials(self, **data):
2456        if not self.captcha_result.is_valid:
2457            # Captcha will display error messages automatically.
2458            # No need to flash something.
2459            return
2460        number = data.get('number','')
2461        firstname = data.get('firstname','')
2462        cat = getUtility(ICatalog, name='students_catalog')
2463        results = list(
2464            cat.searchResults(reg_number=(number, number)))
2465        if not results:
2466            results = list(
2467                cat.searchResults(matric_number=(number, number)))
2468        if results:
2469            student = results[0]
2470            if getattr(student,'firstname',None) is None:
2471                self.flash(_('An error occurred.'))
2472                return
2473            elif student.firstname.lower() != firstname.lower():
2474                # Don't tell the truth here. Anonymous must not
2475                # know that a record was found and only the firstname
2476                # verification failed.
2477                self.flash(_('No student record found.'))
2478                return
2479            elif student.password is not None and self._pw_used:
2480                self.flash(_('Your password has already been set and used. '
2481                             'Please proceed to the login page.'))
2482                return
2483            # Store email address but nothing else.
2484            student.email = data['email']
2485            notify(grok.ObjectModifiedEvent(student))
2486        else:
2487            # No record found, this is the truth.
2488            self.flash(_('No student record found.'))
2489            return
2490
2491        kofa_utils = getUtility(IKofaUtils)
2492        password = kofa_utils.genPassword()
2493        mandate = PasswordMandate()
2494        mandate.params['password'] = password
2495        mandate.params['user'] = student
2496        site = grok.getSite()
2497        site['mandates'].addMandate(mandate)
2498        # Send email with credentials
2499        args = {'mandate_id':mandate.mandate_id}
2500        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2501        url_info = u'Confirmation link: %s' % mandate_url
2502        msg = _('You have successfully requested a password for the')
2503        if kofa_utils.sendCredentials(IUserAccount(student),
2504            password, url_info, msg):
2505            email_sent = student.email
2506        else:
2507            email_sent = None
2508        self._redirect(email=email_sent, password=password,
2509            student_id=student.student_id)
2510        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2511        self.context.logger.info(
2512            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2513        return
2514
2515class StudentRequestPasswordEmailSent(KofaPage):
2516    """Landing page after successful password request.
2517
2518    """
2519    grok.name('requestpw_complete')
2520    grok.require('waeup.Public')
2521    grok.template('requestpwmailsent')
2522    label = _('Your password request was successful.')
2523
2524    def update(self, email=None, student_id=None, password=None):
2525        self.email = email
2526        self.password = password
2527        self.student_id = student_id
2528        return
2529
2530class FilterStudentsInDepartmentPage(KofaPage):
2531    """Page that filters and lists students.
2532    """
2533    grok.context(IDepartment)
2534    grok.require('waeup.showStudents')
2535    grok.name('students')
2536    grok.template('filterstudentspage')
2537    pnav = 1
2538    session_label = _('Current Session')
2539    level_label = _('Current Level')
2540
2541    def label(self):
2542        return 'Students in %s' % self.context.longtitle()
2543
2544    def _set_session_values(self):
2545        vocab_terms = academic_sessions_vocab.by_value.values()
2546        self.sessions = sorted(
2547            [(x.title, x.token) for x in vocab_terms], reverse=True)
2548        self.sessions += [('All Sessions', 'all')]
2549        return
2550
2551    def _set_level_values(self):
2552        vocab_terms = course_levels.by_value.values()
2553        self.levels = sorted(
2554            [(x.title, x.token) for x in vocab_terms])
2555        self.levels += [('All Levels', 'all')]
2556        return
2557
2558    def _searchCatalog(self, session, level):
2559        if level not in (10, 999, None):
2560            start_level = 100 * (level // 100)
2561            end_level = start_level + 90
2562        else:
2563            start_level = end_level = level
2564        cat = queryUtility(ICatalog, name='students_catalog')
2565        students = cat.searchResults(
2566            current_session=(session, session),
2567            current_level=(start_level, end_level),
2568            depcode=(self.context.code, self.context.code)
2569            )
2570        hitlist = []
2571        for student in students:
2572            hitlist.append(StudentQueryResultItem(student, view=self))
2573        return hitlist
2574
2575    def update(self, SHOW=None, session=None, level=None):
2576        datatable.need()
2577        self.parent_url = self.url(self.context.__parent__)
2578        self._set_session_values()
2579        self._set_level_values()
2580        self.hitlist = []
2581        self.session_default = session
2582        self.level_default = level
2583        if SHOW is not None:
2584            if session != 'all':
2585                self.session = int(session)
2586                self.session_string = '%s %s/%s' % (
2587                    self.session_label, self.session, self.session+1)
2588            else:
2589                self.session = None
2590                self.session_string = _('in any session')
2591            if level != 'all':
2592                self.level = int(level)
2593                self.level_string = '%s %s' % (self.level_label, self.level)
2594            else:
2595                self.level = None
2596                self.level_string = _('at any level')
2597            self.hitlist = self._searchCatalog(self.session, self.level)
2598            if not self.hitlist:
2599                self.flash(_('No student found.'))
2600        return
2601
2602class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2603    """Page that filters and lists students.
2604    """
2605    grok.context(ICertificate)
2606
2607    def label(self):
2608        return 'Students studying %s' % self.context.longtitle()
2609
2610    def _searchCatalog(self, session, level):
2611        if level not in (10, 999, None):
2612            start_level = 100 * (level // 100)
2613            end_level = start_level + 90
2614        else:
2615            start_level = end_level = level
2616        cat = queryUtility(ICatalog, name='students_catalog')
2617        students = cat.searchResults(
2618            current_session=(session, session),
2619            current_level=(start_level, end_level),
2620            certcode=(self.context.code, self.context.code)
2621            )
2622        hitlist = []
2623        for student in students:
2624            hitlist.append(StudentQueryResultItem(student, view=self))
2625        return hitlist
2626
2627class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2628    """Page that filters and lists students.
2629    """
2630    grok.context(ICourse)
2631
2632    def label(self):
2633        return 'Students registered for %s' % self.context.longtitle()
2634
2635    def _searchCatalog(self, session, level):
2636        if level not in (10, 999, None):
2637            start_level = 100 * (level // 100)
2638            end_level = start_level + 90
2639        else:
2640            start_level = end_level = level
2641        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2642        coursetickets = cat.searchResults(
2643            session=(session, session),
2644            level=(start_level, end_level),
2645            code=(self.context.code, self.context.code)
2646            )
2647        hitlist = []
2648        for ticket in coursetickets:
2649            # XXX: If students have registered the same courses twice
2650            # they will be listed twice.
2651            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2652        return hitlist
2653
2654class ExportJobContainerOverview(KofaPage):
2655    """Page that lists active student data export jobs and provides links
2656    to discard or download CSV files.
2657
2658    """
2659    grok.context(VirtualExportJobContainer)
2660    grok.require('waeup.showStudents')
2661    grok.name('index.html')
2662    grok.template('exportjobsindex')
2663    label = _('Student Data Exports')
2664    pnav = 1
2665
2666    def update(self, CREATE=None, DISCARD=None, job_id=None):
2667        if CREATE:
2668            self.redirect(self.url('@@exportconfig'))
2669            return
2670        if DISCARD and job_id:
2671            entry = self.context.entry_from_job_id(job_id)
2672            self.context.delete_export_entry(entry)
2673            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2674            self.context.logger.info(
2675                '%s - discarded: job_id=%s' % (ob_class, job_id))
2676            self.flash(_('Discarded export') + ' %s' % job_id)
2677        self.entries = doll_up(self, user=self.request.principal.id)
2678        return
2679
2680class ExportJobContainerJobConfig(KofaPage):
2681    """Page that configures a students export job.
2682
2683    This is a baseclass.
2684    """
2685    grok.baseclass()
2686    grok.name('exportconfig')
2687    grok.require('waeup.showStudents')
2688    grok.template('exportconfig')
2689    label = _('Configure student data export')
2690    pnav = 1
2691    redirect_target = ''
2692
2693    def _set_session_values(self):
2694        vocab_terms = academic_sessions_vocab.by_value.values()
2695        self.sessions = sorted(
2696            [(x.title, x.token) for x in vocab_terms], reverse=True)
2697        self.sessions += [(_('All Sessions'), 'all')]
2698        return
2699
2700    def _set_level_values(self):
2701        vocab_terms = course_levels.by_value.values()
2702        self.levels = sorted(
2703            [(x.title, x.token) for x in vocab_terms])
2704        self.levels += [(_('All Levels'), 'all')]
2705        return
2706
2707    def _set_mode_values(self):
2708        utils = getUtility(IKofaUtils)
2709        self.modes = sorted([(value, key) for key, value in
2710                      utils.STUDY_MODES_DICT.items()])
2711        self.modes +=[(_('All Modes'), 'all')]
2712        return
2713
2714    def _set_exporter_values(self):
2715        # We provide all student exporters, nothing else, yet.
2716        exporters = []
2717        for name in EXPORTER_NAMES:
2718            util = getUtility(ICSVExporter, name=name)
2719            exporters.append((util.title, name),)
2720        self.exporters = exporters
2721
2722    @property
2723    def depcode(self):
2724        return None
2725
2726    def update(self, START=None, session=None, level=None, mode=None,
2727               exporter=None):
2728        self._set_session_values()
2729        self._set_level_values()
2730        self._set_mode_values()
2731        self._set_exporter_values()
2732        if START is None:
2733            return
2734        if session == 'all':
2735            session=None
2736        if level == 'all':
2737            level = None
2738        if mode == 'all':
2739            mode = None
2740        job_id = self.context.start_export_job(exporter,
2741                                      self.request.principal.id,
2742                                      current_session=session,
2743                                      current_level=level,
2744                                      current_mode=mode,
2745                                      depcode=self.depcode)
2746        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2747        self.context.logger.info(
2748            '%s - exported: %s (%s, %s, %s, %s), job_id=%s'
2749            % (ob_class, exporter, session, level, mode, self.depcode, job_id))
2750        self.flash(_('Export started for students with') +
2751                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2752                   session, level, mode))
2753        self.redirect(self.url(self.redirect_target))
2754        return
2755
2756class ExportJobContainerDownload(ExportCSVView):
2757    """Page that downloads a students export csv file.
2758
2759    """
2760    grok.context(VirtualExportJobContainer)
2761    grok.require('waeup.showStudents')
2762
2763class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
2764    """Page that configures a students export job in datacenter.
2765
2766    """
2767    grok.context(IDataCenter)
2768    redirect_target = '@@export'
2769
2770class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
2771    """Page that configures a students export job in departments.
2772
2773    """
2774    grok.context(VirtualDepartmentExportJobContainer)
2775
2776    @property
2777    def depcode(self):
2778        return self.context.__parent__.code
Note: See TracBrowser for help on using the repository browser.