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

Last change on this file since 13040 was 13040, checked in by Henrik Bettermann, 9 years ago

More docs.

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