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

Last change on this file since 12707 was 12632, checked in by Henrik Bettermann, 10 years ago

Add components to configure a student export jobs in faculties.

  • Property svn:keywords set to Id
File size: 127.1 KB
Line 
1## $Id: browser.py 12632 2015-02-26 07:35:00Z 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)
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(self.context)
759        if cdm:
760            self.flash(cdm)
761            self.redirect(self.url(self.context,'view_clearance'))
762            return
763        if self.context.state == REQUESTED:
764            IWorkflowInfo(self.context).fireTransition('clear')
765            self.flash(_('Student has been cleared.'))
766        else:
767            self.flash(_('Student is in wrong state.'), type="warning")
768        self.redirect(self.url(self.context,'view_clearance'))
769        return
770
771    def render(self):
772        return
773
774class StudentRejectClearancePage(KofaEditFormPage):
775    """ Reject clearance by clearance officers
776    """
777    grok.context(IStudent)
778    grok.name('reject_clearance')
779    label = _('Reject clearance')
780    grok.require('waeup.clearStudent')
781    form_fields = grok.AutoFields(
782        IUGStudentClearance).select('officer_comment')
783
784    def update(self):
785        cdm = getUtility(IStudentsUtils).clearance_disabled_message(self.context)
786        if cdm:
787            self.flash(cdm, type="warning")
788            self.redirect(self.url(self.context,'view_clearance'))
789            return
790        return super(StudentRejectClearancePage, self).update()
791
792    @action(_('Save comment and reject clearance now'), style='primary')
793    def reject(self, **data):
794        if self.context.state == CLEARED:
795            IWorkflowInfo(self.context).fireTransition('reset4')
796            message = _('Clearance has been annulled.')
797            self.flash(message, type="warning")
798        elif self.context.state == REQUESTED:
799            IWorkflowInfo(self.context).fireTransition('reset3')
800            message = _('Clearance request has been rejected.')
801            self.flash(message, type="warning")
802        else:
803            self.flash(_('Student is in wrong state.'), type="warning")
804            self.redirect(self.url(self.context,'view_clearance'))
805            return
806        self.applyData(self.context, **data)
807        comment = data['officer_comment']
808        if comment:
809            self.context.writeLogMessage(
810                self, 'comment: %s' % comment.replace('\n', '<br>'))
811            args = {'subject':message, 'body':comment}
812        else:
813            args = {'subject':message,}
814        self.redirect(self.url(self.context) +
815            '/contactstudent?%s' % urlencode(args))
816        return
817
818
819class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
820    """ Page to display student personal data
821    """
822    grok.context(IStudent)
823    grok.name('view_personal')
824    grok.require('waeup.viewStudent')
825    form_fields = grok.AutoFields(IStudentPersonal)
826    form_fields['perm_address'].custom_widget = BytesDisplayWidget
827    form_fields[
828        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
829    pnav = 4
830
831    @property
832    def label(self):
833        return _('${a}: Personal Data',
834            mapping = {'a':self.context.display_fullname})
835
836class StudentPersonalManageFormPage(KofaEditFormPage):
837    """ Page to manage personal data
838    """
839    grok.context(IStudent)
840    grok.name('manage_personal')
841    grok.require('waeup.manageStudent')
842    form_fields = grok.AutoFields(IStudentPersonal)
843    form_fields['personal_updated'].for_display = True
844    form_fields[
845        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
846    label = _('Manage personal data')
847    pnav = 4
848
849    @action(_('Save'), style='primary')
850    def save(self, **data):
851        msave(self, **data)
852        return
853
854class StudentPersonalEditFormPage(KofaEditFormPage):
855    """ Page to edit personal data
856    """
857    grok.context(IStudent)
858    grok.name('edit_personal')
859    grok.require('waeup.handleStudent')
860    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
861    label = _('Edit personal data')
862    pnav = 4
863
864    @action(_('Save/Confirm'), style='primary')
865    def save(self, **data):
866        msave(self, **data)
867        self.context.personal_updated = datetime.utcnow()
868        return
869
870class StudyCourseDisplayFormPage(KofaDisplayFormPage):
871    """ Page to display the student study course data
872    """
873    grok.context(IStudentStudyCourse)
874    grok.name('index')
875    grok.require('waeup.viewStudent')
876    grok.template('studycoursepage')
877    pnav = 4
878
879    @property
880    def form_fields(self):
881        if self.context.is_postgrad:
882            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
883                'previous_verdict')
884        else:
885            form_fields = grok.AutoFields(IStudentStudyCourse)
886        return form_fields
887
888    @property
889    def label(self):
890        if self.context.is_current:
891            return _('${a}: Study Course',
892                mapping = {'a':self.context.__parent__.display_fullname})
893        else:
894            return _('${a}: Previous Study Course',
895                mapping = {'a':self.context.__parent__.display_fullname})
896
897    @property
898    def current_mode(self):
899        if self.context.certificate is not None:
900            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
901            return studymodes_dict[self.context.certificate.study_mode]
902        return
903
904    @property
905    def department(self):
906        if self.context.certificate is not None:
907            return self.context.certificate.__parent__.__parent__
908        return
909
910    @property
911    def faculty(self):
912        if self.context.certificate is not None:
913            return self.context.certificate.__parent__.__parent__.__parent__
914        return
915
916    @property
917    def prev_studycourses(self):
918        if self.context.is_current:
919            if self.context.__parent__.get('studycourse_2', None) is not None:
920                return (
921                        {'href':self.url(self.context.student) + '/studycourse_1',
922                        'title':_('First Study Course, ')},
923                        {'href':self.url(self.context.student) + '/studycourse_2',
924                        'title':_('Second Study Course')}
925                        )
926            if self.context.__parent__.get('studycourse_1', None) is not None:
927                return (
928                        {'href':self.url(self.context.student) + '/studycourse_1',
929                        'title':_('First Study Course')},
930                        )
931        return
932
933class StudyCourseManageFormPage(KofaEditFormPage):
934    """ Page to edit the student study course data
935    """
936    grok.context(IStudentStudyCourse)
937    grok.name('manage')
938    grok.require('waeup.manageStudent')
939    grok.template('studycoursemanagepage')
940    label = _('Manage study course')
941    pnav = 4
942    taboneactions = [_('Save'),_('Cancel')]
943    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
944    tabthreeactions = [_('Add study level')]
945
946    @property
947    def form_fields(self):
948        if self.context.is_postgrad:
949            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
950                'previous_verdict')
951        else:
952            form_fields = grok.AutoFields(IStudentStudyCourse)
953        return form_fields
954
955    def update(self):
956        if not self.context.is_current:
957            emit_lock_message(self)
958            return
959        super(StudyCourseManageFormPage, self).update()
960        return
961
962    @action(_('Save'), style='primary')
963    def save(self, **data):
964        try:
965            msave(self, **data)
966        except ConstraintNotSatisfied:
967            # The selected level might not exist in certificate
968            self.flash(_('Current level not available for certificate.'),
969                       type="warning")
970            return
971        notify(grok.ObjectModifiedEvent(self.context.__parent__))
972        return
973
974    @property
975    def level_dicts(self):
976        studylevelsource = StudyLevelSource().factory
977        for code in studylevelsource.getValues(self.context):
978            title = studylevelsource.getTitle(self.context, code)
979            yield(dict(code=code, title=title))
980
981    @property
982    def session_dicts(self):
983        yield(dict(code='', title='--'))
984        for item in academic_sessions():
985            code = item[1]
986            title = item[0]
987            yield(dict(code=code, title=title))
988
989    @action(_('Add study level'), style='primary')
990    def addStudyLevel(self, **data):
991        level_code = self.request.form.get('addlevel', None)
992        level_session = self.request.form.get('level_session', None)
993        if not level_session:
994            self.flash(_('You must select a session for the level.'),
995                       type="warning")
996            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
997            return
998        studylevel = createObject(u'waeup.StudentStudyLevel')
999        studylevel.level = int(level_code)
1000        studylevel.level_session = int(level_session)
1001        try:
1002            self.context.addStudentStudyLevel(
1003                self.context.certificate,studylevel)
1004            self.flash(_('Study level has been added.'))
1005        except KeyError:
1006            self.flash(_('This level exists.'), type="warning")
1007        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1008        return
1009
1010    @jsaction(_('Remove selected levels'))
1011    def delStudyLevels(self, **data):
1012        form = self.request.form
1013        if 'val_id' in form:
1014            child_id = form['val_id']
1015        else:
1016            self.flash(_('No study level selected.'), type="warning")
1017            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1018            return
1019        if not isinstance(child_id, list):
1020            child_id = [child_id]
1021        deleted = []
1022        for id in child_id:
1023            del self.context[id]
1024            deleted.append(id)
1025        if len(deleted):
1026            self.flash(_('Successfully removed: ${a}',
1027                mapping = {'a':', '.join(deleted)}))
1028            self.context.writeLogMessage(
1029                self,'removed: %s' % ', '.join(deleted))
1030        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1031        return
1032
1033class StudentTranscriptRequestPage(KofaPage):
1034    """ Page to transcript by student
1035    """
1036    grok.context(IStudent)
1037    grok.name('request_transcript')
1038    grok.require('waeup.handleStudent')
1039    grok.template('transcriptrequest')
1040    label = _('Request transcript')
1041    ac_prefix = 'TSC'
1042    notice = ''
1043    pnav = 4
1044    buttonname = _('Submit')
1045    with_ac = True
1046
1047    def update(self, SUBMIT=None):
1048        super(StudentTranscriptRequestPage, self).update()
1049        if not self.context.state == GRADUATED:
1050            self.flash(_("Wrong state"), type="danger")
1051            self.redirect(self.url(self.context))
1052            return
1053        if self.with_ac:
1054            self.ac_series = self.request.form.get('ac_series', None)
1055            self.ac_number = self.request.form.get('ac_number', None)
1056        if self.context.transcript_comment is not None:
1057            self.correspondence = self.context.transcript_comment.replace(
1058                '\n', '<br>')
1059        else:
1060            self.correspondence = ''
1061        if SUBMIT is None:
1062            return
1063        if self.with_ac:
1064            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1065            code = get_access_code(pin)
1066            if not code:
1067                self.flash(_('Activation code is invalid.'), type="warning")
1068                return
1069            if code.state == USED:
1070                self.flash(_('Activation code has already been used.'),
1071                           type="warning")
1072                return
1073            # Mark pin as used (this also fires a pin related transition)
1074            # and fire transition start_clearance
1075            comment = _(u"invalidated")
1076            # Here we know that the ac is in state initialized so we do not
1077            # expect an exception, but the owner might be different
1078            if not invalidate_accesscode(pin, comment, self.context.student_id):
1079                self.flash(_('You are not the owner of this access code.'),
1080                           type="warning")
1081                return
1082            self.context.clr_code = pin
1083        IWorkflowInfo(self.context).fireTransition('request_transcript')
1084        comment = self.request.form.get('comment', '').replace('\r', '')
1085        address = self.request.form.get('address', '').replace('\r', '')
1086        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1087        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1088        old_transcript_comment = self.context.transcript_comment
1089        if old_transcript_comment == None:
1090            old_transcript_comment = ''
1091        self.context.transcript_comment = '''On %s %s wrote:
1092
1093%s
1094
1095Dispatch Address:
1096%s
1097
1098%s''' % (today, self.request.principal.id, comment, address,
1099         old_transcript_comment)
1100        self.context.writeLogMessage(
1101            self, 'comment: %s' % comment.replace('\n', '<br>'))
1102        self.flash(_('Transcript processing has been started.'))
1103        self.redirect(self.url(self.context))
1104        return
1105
1106class StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
1107    """ Page to process transcript requests
1108    """
1109    grok.context(IStudent)
1110    grok.name('process_transcript_request')
1111    grok.require('waeup.viewTranscript')
1112    grok.template('transcriptprocess')
1113    form_fields = grok.AutoFields(IStudentTranscript)
1114    label = _('Process transcript request')
1115    buttonname = _('Save comment and mark as processed')
1116    pnav = 4
1117
1118    def update(self, SUBMIT=None):
1119        super(StudentTranscriptRequestProcessFormPage, self).update()
1120        if self.context.state != TRANSCRIPT:
1121            self.flash(_('Student is in wrong state.'), type="warning")
1122            self.redirect(self.url(self.context))
1123            return
1124        if self.context.transcript_comment is not None:
1125            self.correspondence = self.context.transcript_comment.replace(
1126                '\n', '<br>')
1127        else:
1128            self.correspondence = ''
1129        if SUBMIT is None:
1130            return
1131        IWorkflowInfo(self.context).fireTransition('process_transcript')
1132        self.flash(_('Transcript request processed.'))
1133        comment = self.request.form.get('comment', '').replace('\r', '')
1134        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1135        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1136        old_transcript_comment = self.context.transcript_comment
1137        if old_transcript_comment == None:
1138            old_transcript_comment = ''
1139        self.context.transcript_comment = '''On %s %s wrote:
1140
1141%s
1142
1143%s''' % (today, self.request.principal.id, comment,
1144         old_transcript_comment)
1145        self.context.writeLogMessage(
1146            self, 'comment: %s' % comment.replace('\n', '<br>'))
1147        subject = _('Transcript processed')
1148        args = {'subject':subject, 'body':comment}
1149        self.redirect(self.url(self.context) +
1150            '/contactstudent?%s' % urlencode(args))
1151        return
1152
1153class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1154    """ Page to edit personal data by student
1155    """
1156    grok.context(IStudent)
1157    grok.name('manage_transcript_request')
1158    grok.require('waeup.manageStudent')
1159    form_fields = grok.AutoFields(IStudentTranscript)
1160    label = _('Manage transcript request')
1161    pnav = 4
1162
1163    @action(_('Save'), style='primary')
1164    def save(self, **data):
1165        msave(self, **data)
1166        return
1167
1168class StudyCourseTranscriptPage(KofaDisplayFormPage):
1169    """ Page to display the student's transcript.
1170    """
1171    grok.context(IStudentStudyCourseTranscript)
1172    grok.name('transcript')
1173    grok.require('waeup.viewTranscript')
1174    grok.template('transcript')
1175    pnav = 4
1176
1177    def update(self):
1178        if not self.context.student.transcript_enabled:
1179            self.flash(_('You are not allowed to view the transcript.'),
1180                       type="warning")
1181            self.redirect(self.url(self.context))
1182            return
1183        super(StudyCourseTranscriptPage, self).update()
1184        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1185        self.level_dict = level_dict(self.context)
1186        self.session_dict = dict(
1187            [(item[1], item[0]) for item in academic_sessions()])
1188        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1189        return
1190
1191    @property
1192    def label(self):
1193        # Here we know that the cookie has been set
1194        lang = self.request.cookies.get('kofa.language')
1195        return _('${a}: Transcript Data', mapping = {
1196            'a':self.context.student.display_fullname})
1197
1198class ExportPDFTranscriptPage(UtilityView, grok.View):
1199    """Deliver a PDF slip of the context.
1200    """
1201    grok.context(IStudentStudyCourse)
1202    grok.name('transcript.pdf')
1203    grok.require('waeup.viewTranscript')
1204    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1205    prefix = 'form'
1206    omit_fields = (
1207        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1208        'password', 'suspended', 'phone', 'email',
1209        'adm_code', 'suspended_comment', 'current_level')
1210
1211    def update(self):
1212        if not self.context.student.transcript_enabled:
1213            self.flash(_('You are not allowed to download the transcript.'),
1214                       type="warning")
1215            self.redirect(self.url(self.context))
1216            return
1217        super(ExportPDFTranscriptPage, self).update()
1218        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1219        self.level_dict = level_dict(self.context)
1220        self.session_dict = dict(
1221            [(item[1], item[0]) for item in academic_sessions()])
1222        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1223        return
1224
1225    @property
1226    def label(self):
1227        # Here we know that the cookie has been set
1228        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1229        return translate(_('Academic Transcript'),
1230            'waeup.kofa', target_language=portal_language)
1231
1232    def _sigsInFooter(self):
1233        return (_('CERTIFIED TRUE COPY'),)
1234
1235    def _signatures(self):
1236        return None
1237
1238    def render(self):
1239        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1240        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1241        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1242        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1243        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1244        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1245        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1246        studentview = StudentBasePDFFormPage(self.context.student,
1247            self.request, self.omit_fields)
1248        students_utils = getUtility(IStudentsUtils)
1249
1250        tableheader = [(Code,'code', 2.5),
1251                         (Title,'title', 7),
1252                         (Term, 'semester', 1.5),
1253                         (Cred, 'credits', 1.5),
1254                         (Score, 'score', 1.5),
1255                         (Grade, 'grade', 1.5),
1256                         ]
1257
1258        return students_utils.renderPDFTranscript(
1259            self, 'transcript.pdf',
1260            self.context.student, studentview,
1261            omit_fields=self.omit_fields,
1262            tableheader=tableheader,
1263            signatures=self._signatures(),
1264            sigs_in_footer=self._sigsInFooter(),
1265            )
1266
1267class StudentTransferFormPage(KofaAddFormPage):
1268    """Page to transfer the student.
1269    """
1270    grok.context(IStudent)
1271    grok.name('transfer')
1272    grok.require('waeup.manageStudent')
1273    label = _('Transfer student')
1274    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1275        'entry_mode', 'entry_session')
1276    pnav = 4
1277
1278    @jsaction(_('Transfer'))
1279    def transferStudent(self, **data):
1280        error = self.context.transfer(**data)
1281        if error == -1:
1282            self.flash(_('Current level does not match certificate levels.'),
1283                       type="warning")
1284        elif error == -2:
1285            self.flash(_('Former study course record incomplete.'),
1286                       type="warning")
1287        elif error == -3:
1288            self.flash(_('Maximum number of transfers exceeded.'),
1289                       type="warning")
1290        else:
1291            self.flash(_('Successfully transferred.'))
1292        return
1293
1294class RevertTransferFormPage(KofaEditFormPage):
1295    """View that reverts the previous transfer.
1296    """
1297    grok.context(IStudent)
1298    grok.name('revert_transfer')
1299    grok.require('waeup.manageStudent')
1300    grok.template('reverttransfer')
1301    label = _('Revert previous transfer')
1302
1303    def update(self):
1304        if not self.context.has_key('studycourse_1'):
1305            self.flash(_('No previous transfer.'), type="warning")
1306            self.redirect(self.url(self.context))
1307            return
1308        return
1309
1310    @jsaction(_('Revert now'))
1311    def transferStudent(self, **data):
1312        self.context.revert_transfer()
1313        self.flash(_('Previous transfer reverted.'))
1314        self.redirect(self.url(self.context, 'studycourse'))
1315        return
1316
1317class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1318    """ Page to display student study levels
1319    """
1320    grok.context(IStudentStudyLevel)
1321    grok.name('index')
1322    grok.require('waeup.viewStudent')
1323    form_fields = grok.AutoFields(IStudentStudyLevel)
1324    form_fields[
1325        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1326    grok.template('studylevelpage')
1327    pnav = 4
1328
1329    def update(self):
1330        super(StudyLevelDisplayFormPage, self).update()
1331        return
1332
1333    @property
1334    def translated_values(self):
1335        return translated_values(self)
1336
1337    @property
1338    def label(self):
1339        # Here we know that the cookie has been set
1340        lang = self.request.cookies.get('kofa.language')
1341        level_title = translate(self.context.level_title, 'waeup.kofa',
1342            target_language=lang)
1343        return _('${a}: Study Level ${b}', mapping = {
1344            'a':self.context.student.display_fullname,
1345            'b':level_title})
1346
1347class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1348    """Deliver a PDF slip of the context.
1349    """
1350    grok.context(IStudentStudyLevel)
1351    grok.name('course_registration_slip.pdf')
1352    grok.require('waeup.viewStudent')
1353    form_fields = grok.AutoFields(IStudentStudyLevel)
1354    form_fields[
1355        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1356    prefix = 'form'
1357    omit_fields = (
1358        'password', 'suspended', 'phone', 'date_of_birth',
1359        'adm_code', 'sex', 'suspended_comment', 'current_level')
1360
1361    @property
1362    def title(self):
1363        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1364        return translate(_('Level Data'), 'waeup.kofa',
1365            target_language=portal_language)
1366
1367    @property
1368    def label(self):
1369        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1370        lang = self.request.cookies.get('kofa.language', portal_language)
1371        level_title = translate(self.context.level_title, 'waeup.kofa',
1372            target_language=lang)
1373        return translate(_('Course Registration Slip'),
1374            'waeup.kofa', target_language=portal_language) \
1375            + ' %s' % level_title
1376
1377    @property
1378    def tabletitle(self):
1379        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1380        tabletitle = []
1381        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1382            target_language=portal_language))
1383        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1384            target_language=portal_language))
1385        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1386            target_language=portal_language))
1387        return tabletitle
1388
1389    def render(self):
1390        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1391        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1392        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1393        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1394        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1395        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1396        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1397        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1398        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1399        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1400        studentview = StudentBasePDFFormPage(self.context.student,
1401            self.request, self.omit_fields)
1402        students_utils = getUtility(IStudentsUtils)
1403
1404        tabledata = []
1405        tableheader = []
1406        contenttitle = []
1407        for i in range(1,7):
1408            tabledata.append(sorted(
1409                [value for value in self.context.values() if value.semester == i],
1410                key=lambda value: str(value.semester) + value.code))
1411            tableheader.append([(Code,'code', 2.5),
1412                             (Title,'title', 5),
1413                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1414                             (Cred, 'credits', 1.5),
1415                             #(Mand, 'mandatory', 1.5),
1416                             (Score, 'score', 1.5),
1417                             (Grade, 'grade', 1.5),
1418                             #('Auto', 'automatic', 1.5)
1419                             ])
1420        return students_utils.renderPDF(
1421            self, 'course_registration_slip.pdf',
1422            self.context.student, studentview,
1423            tableheader=tableheader,
1424            tabledata=tabledata,
1425            omit_fields=self.omit_fields
1426            )
1427
1428class StudyLevelManageFormPage(KofaEditFormPage):
1429    """ Page to edit the student study level data
1430    """
1431    grok.context(IStudentStudyLevel)
1432    grok.name('manage')
1433    grok.require('waeup.manageStudent')
1434    grok.template('studylevelmanagepage')
1435    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1436        'validation_date', 'validated_by', 'total_credits', 'gpa')
1437    pnav = 4
1438    taboneactions = [_('Save'),_('Cancel')]
1439    tabtwoactions = [_('Add course ticket'),
1440        _('Remove selected tickets'),_('Cancel')]
1441    placeholder = _('Enter valid course code')
1442
1443    def update(self, ADD=None, course=None):
1444        if not self.context.__parent__.is_current:
1445            emit_lock_message(self)
1446            return
1447        super(StudyLevelManageFormPage, self).update()
1448        if ADD is not None:
1449            if not course:
1450                self.flash(_('No valid course code entered.'), type="warning")
1451                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1452                return
1453            cat = queryUtility(ICatalog, name='courses_catalog')
1454            result = cat.searchResults(code=(course, course))
1455            if len(result) != 1:
1456                self.flash(_('Course not found.'), type="warning")
1457            else:
1458                course = list(result)[0]
1459                addCourseTicket(self, course)
1460            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1461        return
1462
1463    @property
1464    def translated_values(self):
1465        return translated_values(self)
1466
1467    @property
1468    def label(self):
1469        # Here we know that the cookie has been set
1470        lang = self.request.cookies.get('kofa.language')
1471        level_title = translate(self.context.level_title, 'waeup.kofa',
1472            target_language=lang)
1473        return _('Manage study level ${a}',
1474            mapping = {'a':level_title})
1475
1476    @action(_('Save'), style='primary')
1477    def save(self, **data):
1478        msave(self, **data)
1479        return
1480
1481    @jsaction(_('Remove selected tickets'))
1482    def delCourseTicket(self, **data):
1483        form = self.request.form
1484        if 'val_id' in form:
1485            child_id = form['val_id']
1486        else:
1487            self.flash(_('No ticket selected.'), type="warning")
1488            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1489            return
1490        if not isinstance(child_id, list):
1491            child_id = [child_id]
1492        deleted = []
1493        for id in child_id:
1494            del self.context[id]
1495            deleted.append(id)
1496        if len(deleted):
1497            self.flash(_('Successfully removed: ${a}',
1498                mapping = {'a':', '.join(deleted)}))
1499            self.context.writeLogMessage(
1500                self,'removed: %s at %s' %
1501                (', '.join(deleted), self.context.level))
1502        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1503        return
1504
1505class ValidateCoursesPage(UtilityView, grok.View):
1506    """ Validate course list by course adviser
1507    """
1508    grok.context(IStudentStudyLevel)
1509    grok.name('validate_courses')
1510    grok.require('waeup.validateStudent')
1511
1512    def update(self):
1513        if not self.context.__parent__.is_current:
1514            emit_lock_message(self)
1515            return
1516        if str(self.context.__parent__.current_level) != self.context.__name__:
1517            self.flash(_('This level does not correspond current level.'),
1518                       type="danger")
1519        elif self.context.student.state == REGISTERED:
1520            IWorkflowInfo(self.context.student).fireTransition(
1521                'validate_courses')
1522            self.flash(_('Course list has been validated.'))
1523        else:
1524            self.flash(_('Student is in the wrong state.'), type="warning")
1525        self.redirect(self.url(self.context))
1526        return
1527
1528    def render(self):
1529        return
1530
1531class RejectCoursesPage(UtilityView, grok.View):
1532    """ Reject course list by course adviser
1533    """
1534    grok.context(IStudentStudyLevel)
1535    grok.name('reject_courses')
1536    grok.require('waeup.validateStudent')
1537
1538    def update(self):
1539        if not self.context.__parent__.is_current:
1540            emit_lock_message(self)
1541            return
1542        if str(self.context.__parent__.current_level) != self.context.__name__:
1543            self.flash(_('This level does not correspond current level.'),
1544                       type="danger")
1545            self.redirect(self.url(self.context))
1546            return
1547        elif self.context.student.state == VALIDATED:
1548            IWorkflowInfo(self.context.student).fireTransition('reset8')
1549            message = _('Course list request has been annulled.')
1550            self.flash(message)
1551        elif self.context.student.state == REGISTERED:
1552            IWorkflowInfo(self.context.student).fireTransition('reset7')
1553            message = _('Course list request has been rejected:')
1554            self.flash(message)
1555        else:
1556            self.flash(_('Student is in the wrong state.'), type="warning")
1557            self.redirect(self.url(self.context))
1558            return
1559        args = {'subject':message}
1560        self.redirect(self.url(self.context.student) +
1561            '/contactstudent?%s' % urlencode(args))
1562        return
1563
1564    def render(self):
1565        return
1566
1567class CourseTicketAddFormPage(KofaAddFormPage):
1568    """Add a course ticket.
1569    """
1570    grok.context(IStudentStudyLevel)
1571    grok.name('add')
1572    grok.require('waeup.manageStudent')
1573    label = _('Add course ticket')
1574    form_fields = grok.AutoFields(ICourseTicketAdd)
1575    pnav = 4
1576
1577    def update(self):
1578        if not self.context.__parent__.is_current:
1579            emit_lock_message(self)
1580            return
1581        super(CourseTicketAddFormPage, self).update()
1582        return
1583
1584    @action(_('Add course ticket'), style='primary')
1585    def addCourseTicket(self, **data):
1586        course = data['course']
1587        success = addCourseTicket(self, course)
1588        if success:
1589            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1590        return
1591
1592    @action(_('Cancel'), validator=NullValidator)
1593    def cancel(self, **data):
1594        self.redirect(self.url(self.context))
1595
1596class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1597    """ Page to display course tickets
1598    """
1599    grok.context(ICourseTicket)
1600    grok.name('index')
1601    grok.require('waeup.viewStudent')
1602    form_fields = grok.AutoFields(ICourseTicket)
1603    grok.template('courseticketpage')
1604    pnav = 4
1605
1606    @property
1607    def label(self):
1608        return _('${a}: Course Ticket ${b}', mapping = {
1609            'a':self.context.student.display_fullname,
1610            'b':self.context.code})
1611
1612class CourseTicketManageFormPage(KofaEditFormPage):
1613    """ Page to manage course tickets
1614    """
1615    grok.context(ICourseTicket)
1616    grok.name('manage')
1617    grok.require('waeup.manageStudent')
1618    form_fields = grok.AutoFields(ICourseTicket)
1619    form_fields['title'].for_display = True
1620    form_fields['fcode'].for_display = True
1621    form_fields['dcode'].for_display = True
1622    form_fields['semester'].for_display = True
1623    form_fields['passmark'].for_display = True
1624    form_fields['credits'].for_display = True
1625    form_fields['mandatory'].for_display = False
1626    form_fields['automatic'].for_display = True
1627    form_fields['carry_over'].for_display = True
1628    pnav = 4
1629    grok.template('courseticketmanagepage')
1630
1631    @property
1632    def label(self):
1633        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1634
1635    @action('Save', style='primary')
1636    def save(self, **data):
1637        msave(self, **data)
1638        return
1639
1640class PaymentsManageFormPage(KofaEditFormPage):
1641    """ Page to manage the student payments
1642
1643    This manage form page is for both students and students officers.
1644    """
1645    grok.context(IStudentPaymentsContainer)
1646    grok.name('index')
1647    grok.require('waeup.viewStudent')
1648    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1649    grok.template('paymentsmanagepage')
1650    pnav = 4
1651
1652    @property
1653    def manage_payments_allowed(self):
1654        return checkPermission('waeup.payStudent', self.context)
1655
1656    def unremovable(self, ticket):
1657        usertype = getattr(self.request.principal, 'user_type', None)
1658        if not usertype:
1659            return False
1660        if not self.manage_payments_allowed:
1661            return True
1662        return (self.request.principal.user_type == 'student' and ticket.r_code)
1663
1664    @property
1665    def label(self):
1666        return _('${a}: Payments',
1667            mapping = {'a':self.context.__parent__.display_fullname})
1668
1669    @jsaction(_('Remove selected tickets'))
1670    def delPaymentTicket(self, **data):
1671        form = self.request.form
1672        if 'val_id' in form:
1673            child_id = form['val_id']
1674        else:
1675            self.flash(_('No payment selected.'), type="warning")
1676            self.redirect(self.url(self.context))
1677            return
1678        if not isinstance(child_id, list):
1679            child_id = [child_id]
1680        deleted = []
1681        for id in child_id:
1682            # Students are not allowed to remove used payment tickets
1683            ticket = self.context.get(id, None)
1684            if ticket is not None and not self.unremovable(ticket):
1685                del self.context[id]
1686                deleted.append(id)
1687        if len(deleted):
1688            self.flash(_('Successfully removed: ${a}',
1689                mapping = {'a': ', '.join(deleted)}))
1690            self.context.writeLogMessage(
1691                self,'removed: %s' % ', '.join(deleted))
1692        self.redirect(self.url(self.context))
1693        return
1694
1695    #@action(_('Add online payment ticket'))
1696    #def addPaymentTicket(self, **data):
1697    #    self.redirect(self.url(self.context, '@@addop'))
1698
1699class OnlinePaymentAddFormPage(KofaAddFormPage):
1700    """ Page to add an online payment ticket
1701    """
1702    grok.context(IStudentPaymentsContainer)
1703    grok.name('addop')
1704    grok.template('onlinepaymentaddform')
1705    grok.require('waeup.payStudent')
1706    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1707        'p_category')
1708    label = _('Add online payment')
1709    pnav = 4
1710
1711    @property
1712    def selectable_categories(self):
1713        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1714        return sorted(categories.items())
1715
1716    @action(_('Create ticket'), style='primary')
1717    def createTicket(self, **data):
1718        p_category = data['p_category']
1719        previous_session = data.get('p_session', None)
1720        previous_level = data.get('p_level', None)
1721        student = self.context.__parent__
1722        # The hostel_application payment category is temporarily used
1723        # by Uniben.
1724        if p_category in ('bed_allocation', 'hostel_application') and student[
1725            'studycourse'].current_session != grok.getSite()[
1726            'hostels'].accommodation_session:
1727                self.flash(
1728                    _('Your current session does not match ' + \
1729                    'accommodation session.'), type="danger")
1730                return
1731        if 'maintenance' in p_category:
1732            current_session = str(student['studycourse'].current_session)
1733            if not current_session in student['accommodation']:
1734                self.flash(_('You have not yet booked accommodation.'),
1735                           type="warning")
1736                return
1737        students_utils = getUtility(IStudentsUtils)
1738        error, payment = students_utils.setPaymentDetails(
1739            p_category, student, previous_session, previous_level)
1740        if error is not None:
1741            self.flash(error, type="danger")
1742            return
1743        self.context[payment.p_id] = payment
1744        self.flash(_('Payment ticket created.'))
1745        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1746        self.redirect(self.url(self.context))
1747        return
1748
1749    @action(_('Cancel'), validator=NullValidator)
1750    def cancel(self, **data):
1751        self.redirect(self.url(self.context))
1752
1753class PreviousPaymentAddFormPage(KofaAddFormPage):
1754    """ Page to add an online payment ticket for previous sessions
1755    """
1756    grok.context(IStudentPaymentsContainer)
1757    grok.name('addpp')
1758    grok.require('waeup.payStudent')
1759    form_fields = grok.AutoFields(IStudentPreviousPayment)
1760    label = _('Add previous session online payment')
1761    pnav = 4
1762
1763    def update(self):
1764        if self.context.student.before_payment:
1765            self.flash(_("No previous payment to be made."), type="warning")
1766            self.redirect(self.url(self.context))
1767        super(PreviousPaymentAddFormPage, self).update()
1768        return
1769
1770    @action(_('Create ticket'), style='primary')
1771    def createTicket(self, **data):
1772        p_category = data['p_category']
1773        previous_session = data.get('p_session', None)
1774        previous_level = data.get('p_level', None)
1775        student = self.context.__parent__
1776        students_utils = getUtility(IStudentsUtils)
1777        error, payment = students_utils.setPaymentDetails(
1778            p_category, student, previous_session, previous_level)
1779        if error is not None:
1780            self.flash(error, type="danger")
1781            return
1782        self.context[payment.p_id] = payment
1783        self.flash(_('Payment ticket created.'))
1784        self.redirect(self.url(self.context))
1785        return
1786
1787    @action(_('Cancel'), validator=NullValidator)
1788    def cancel(self, **data):
1789        self.redirect(self.url(self.context))
1790
1791class BalancePaymentAddFormPage(KofaAddFormPage):
1792    """ Page to add an online payment ticket for balance sessions
1793    """
1794    grok.context(IStudentPaymentsContainer)
1795    grok.name('addbp')
1796    grok.require('waeup.manageStudent')
1797    form_fields = grok.AutoFields(IStudentBalancePayment)
1798    label = _('Add balance')
1799    pnav = 4
1800
1801    @action(_('Create ticket'), style='primary')
1802    def createTicket(self, **data):
1803        p_category = data['p_category']
1804        balance_session = data.get('balance_session', None)
1805        balance_level = data.get('balance_level', None)
1806        balance_amount = data.get('balance_amount', None)
1807        student = self.context.__parent__
1808        students_utils = getUtility(IStudentsUtils)
1809        error, payment = students_utils.setBalanceDetails(
1810            p_category, student, balance_session,
1811            balance_level, balance_amount)
1812        if error is not None:
1813            self.flash(error, type="danger")
1814            return
1815        self.context[payment.p_id] = payment
1816        self.flash(_('Payment ticket created.'))
1817        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1818        self.redirect(self.url(self.context))
1819        return
1820
1821    @action(_('Cancel'), validator=NullValidator)
1822    def cancel(self, **data):
1823        self.redirect(self.url(self.context))
1824
1825class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1826    """ Page to view an online payment ticket
1827    """
1828    grok.context(IStudentOnlinePayment)
1829    grok.name('index')
1830    grok.require('waeup.viewStudent')
1831    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1832    form_fields[
1833        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1834    form_fields[
1835        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1836    pnav = 4
1837
1838    @property
1839    def label(self):
1840        return _('${a}: Online Payment Ticket ${b}', mapping = {
1841            'a':self.context.student.display_fullname,
1842            'b':self.context.p_id})
1843
1844class OnlinePaymentApprovePage(UtilityView, grok.View):
1845    """ Callback view
1846    """
1847    grok.context(IStudentOnlinePayment)
1848    grok.name('approve')
1849    grok.require('waeup.managePortal')
1850
1851    def update(self):
1852        flashtype, msg, log = self.context.approveStudentPayment()
1853        if log is not None:
1854            # Add log message to students.log
1855            self.context.writeLogMessage(self,log)
1856            # Add log message to payments.log
1857            self.context.logger.info(
1858                '%s,%s,%s,%s,%s,,,,,,' % (
1859                self.context.student.student_id,
1860                self.context.p_id, self.context.p_category,
1861                self.context.amount_auth, self.context.r_code))
1862        self.flash(msg, type=flashtype)
1863        return
1864
1865    def render(self):
1866        self.redirect(self.url(self.context, '@@index'))
1867        return
1868
1869class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1870    """ Approval view for students.
1871
1872    This view is used for browser tests only and
1873    must be neutralized in custom pages!
1874    """
1875
1876    grok.name('fake_approve')
1877    grok.require('waeup.payStudent')
1878
1879class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1880    """Deliver a PDF slip of the context.
1881    """
1882    grok.context(IStudentOnlinePayment)
1883    grok.name('payment_slip.pdf')
1884    grok.require('waeup.viewStudent')
1885    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1886    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1887    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1888    prefix = 'form'
1889    note = None
1890    omit_fields = (
1891        'password', 'suspended', 'phone', 'date_of_birth',
1892        'adm_code', 'sex', 'suspended_comment', 'current_level')
1893
1894    @property
1895    def title(self):
1896        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1897        return translate(_('Payment Data'), 'waeup.kofa',
1898            target_language=portal_language)
1899
1900    @property
1901    def label(self):
1902        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1903        return translate(_('Online Payment Slip'),
1904            'waeup.kofa', target_language=portal_language) \
1905            + ' %s' % self.context.p_id
1906
1907    def render(self):
1908        #if self.context.p_state != 'paid':
1909        #    self.flash('Ticket not yet paid.')
1910        #    self.redirect(self.url(self.context))
1911        #    return
1912        studentview = StudentBasePDFFormPage(self.context.student,
1913            self.request, self.omit_fields)
1914        students_utils = getUtility(IStudentsUtils)
1915        return students_utils.renderPDF(self, 'payment_slip.pdf',
1916            self.context.student, studentview, note=self.note,
1917            omit_fields=self.omit_fields)
1918
1919
1920class AccommodationManageFormPage(KofaEditFormPage):
1921    """ Page to manage bed tickets.
1922
1923    This manage form page is for both students and students officers.
1924    """
1925    grok.context(IStudentAccommodation)
1926    grok.name('index')
1927    grok.require('waeup.handleAccommodation')
1928    form_fields = grok.AutoFields(IStudentAccommodation)
1929    grok.template('accommodationmanagepage')
1930    pnav = 4
1931    officers_only_actions = [_('Remove selected')]
1932
1933    @property
1934    def label(self):
1935        return _('${a}: Accommodation',
1936            mapping = {'a':self.context.__parent__.display_fullname})
1937
1938    @jsaction(_('Remove selected'))
1939    def delBedTickets(self, **data):
1940        if getattr(self.request.principal, 'user_type', None) == 'student':
1941            self.flash(_('You are not allowed to remove bed tickets.'),
1942                       type="warning")
1943            self.redirect(self.url(self.context))
1944            return
1945        form = self.request.form
1946        if 'val_id' in form:
1947            child_id = form['val_id']
1948        else:
1949            self.flash(_('No bed ticket selected.'), type="warning")
1950            self.redirect(self.url(self.context))
1951            return
1952        if not isinstance(child_id, list):
1953            child_id = [child_id]
1954        deleted = []
1955        for id in child_id:
1956            del self.context[id]
1957            deleted.append(id)
1958        if len(deleted):
1959            self.flash(_('Successfully removed: ${a}',
1960                mapping = {'a':', '.join(deleted)}))
1961            self.context.writeLogMessage(
1962                self,'removed: % s' % ', '.join(deleted))
1963        self.redirect(self.url(self.context))
1964        return
1965
1966    @property
1967    def selected_actions(self):
1968        if getattr(self.request.principal, 'user_type', None) == 'student':
1969            return [action for action in self.actions
1970                    if not action.label in self.officers_only_actions]
1971        return self.actions
1972
1973class BedTicketAddPage(KofaPage):
1974    """ Page to add an online payment ticket
1975    """
1976    grok.context(IStudentAccommodation)
1977    grok.name('add')
1978    grok.require('waeup.handleAccommodation')
1979    grok.template('enterpin')
1980    ac_prefix = 'HOS'
1981    label = _('Add bed ticket')
1982    pnav = 4
1983    buttonname = _('Create bed ticket')
1984    notice = ''
1985    with_ac = True
1986
1987    def update(self, SUBMIT=None):
1988        student = self.context.student
1989        students_utils = getUtility(IStudentsUtils)
1990        acc_details  = students_utils.getAccommodationDetails(student)
1991        if acc_details.get('expired', False):
1992            startdate = acc_details.get('startdate')
1993            enddate = acc_details.get('enddate')
1994            if startdate and enddate:
1995                tz = getUtility(IKofaUtils).tzinfo
1996                startdate = to_timezone(
1997                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1998                enddate = to_timezone(
1999                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
2000                self.flash(_("Outside booking period: ${a} - ${b}",
2001                    mapping = {'a': startdate, 'b': enddate}), type="warning")
2002            else:
2003                self.flash(_("Outside booking period."), type="warning")
2004            self.redirect(self.url(self.context))
2005            return
2006        if not acc_details:
2007            self.flash(_("Your data are incomplete."), type="warning")
2008            self.redirect(self.url(self.context))
2009            return
2010        if not student.state in acc_details['allowed_states']:
2011            self.flash(_("You are in the wrong registration state."),
2012                       type="warning")
2013            self.redirect(self.url(self.context))
2014            return
2015        if student['studycourse'].current_session != acc_details[
2016            'booking_session']:
2017            self.flash(
2018                _('Your current session does not match accommodation session.'),
2019                type="warning")
2020            self.redirect(self.url(self.context))
2021            return
2022        if str(acc_details['booking_session']) in self.context.keys():
2023            self.flash(
2024                _('You already booked a bed space in current ' \
2025                    + 'accommodation session.'), type="warning")
2026            self.redirect(self.url(self.context))
2027            return
2028        if self.with_ac:
2029            self.ac_series = self.request.form.get('ac_series', None)
2030            self.ac_number = self.request.form.get('ac_number', None)
2031        if SUBMIT is None:
2032            return
2033        if self.with_ac:
2034            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2035            code = get_access_code(pin)
2036            if not code:
2037                self.flash(_('Activation code is invalid.'), type="warning")
2038                return
2039        # Search and book bed
2040        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2041        entries = cat.searchResults(
2042            owner=(student.student_id,student.student_id))
2043        if len(entries):
2044            # If bed space has been manually allocated use this bed
2045            bed = [entry for entry in entries][0]
2046            # Safety belt for paranoids: Does this bed really exist on portal?
2047            # XXX: Can be remove if nobody complains.
2048            if bed.__parent__.__parent__ is None:
2049                self.flash(_('System error: Please contact the adminsitrator.'),
2050                           type="danger")
2051                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2052                return
2053        else:
2054            # else search for other available beds
2055            entries = cat.searchResults(
2056                bed_type=(acc_details['bt'],acc_details['bt']))
2057            available_beds = [
2058                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2059            if available_beds:
2060                students_utils = getUtility(IStudentsUtils)
2061                bed = students_utils.selectBed(available_beds)
2062                # Safety belt for paranoids: Does this bed really exist in portal?
2063                # XXX: Can be remove if nobody complains.
2064                if bed.__parent__.__parent__ is None:
2065                    self.flash(_('System error: Please contact the adminsitrator.'),
2066                               type="warning")
2067                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2068                    return
2069                bed.bookBed(student.student_id)
2070            else:
2071                self.flash(_('There is no free bed in your category ${a}.',
2072                    mapping = {'a':acc_details['bt']}), type="warning")
2073                return
2074        if self.with_ac:
2075            # Mark pin as used (this also fires a pin related transition)
2076            if code.state == USED:
2077                self.flash(_('Activation code has already been used.'),
2078                           type="warning")
2079                return
2080            else:
2081                comment = _(u'invalidated')
2082                # Here we know that the ac is in state initialized so we do not
2083                # expect an exception, but the owner might be different
2084                if not invalidate_accesscode(
2085                    pin,comment,self.context.student.student_id):
2086                    self.flash(_('You are not the owner of this access code.'),
2087                               type="warning")
2088                    return
2089        # Create bed ticket
2090        bedticket = createObject(u'waeup.BedTicket')
2091        if self.with_ac:
2092            bedticket.booking_code = pin
2093        bedticket.booking_session = acc_details['booking_session']
2094        bedticket.bed_type = acc_details['bt']
2095        bedticket.bed = bed
2096        hall_title = bed.__parent__.hostel_name
2097        coordinates = bed.coordinates[1:]
2098        block, room_nr, bed_nr = coordinates
2099        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2100            'a':hall_title, 'b':block,
2101            'c':room_nr, 'd':bed_nr,
2102            'e':bed.bed_type})
2103        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2104        bedticket.bed_coordinates = translate(
2105            bc, 'waeup.kofa',target_language=portal_language)
2106        self.context.addBedTicket(bedticket)
2107        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2108        self.flash(_('Bed ticket created and bed booked: ${a}',
2109            mapping = {'a':bedticket.display_coordinates}))
2110        self.redirect(self.url(self.context))
2111        return
2112
2113class BedTicketDisplayFormPage(KofaDisplayFormPage):
2114    """ Page to display bed tickets
2115    """
2116    grok.context(IBedTicket)
2117    grok.name('index')
2118    grok.require('waeup.handleAccommodation')
2119    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2120    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2121    pnav = 4
2122
2123    @property
2124    def label(self):
2125        return _('Bed Ticket for Session ${a}',
2126            mapping = {'a':self.context.getSessionString()})
2127
2128class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
2129    """Deliver a PDF slip of the context.
2130    """
2131    grok.context(IBedTicket)
2132    grok.name('bed_allocation_slip.pdf')
2133    grok.require('waeup.handleAccommodation')
2134    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2135    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2136    prefix = 'form'
2137    omit_fields = (
2138        'password', 'suspended', 'phone', 'adm_code',
2139        'suspended_comment', 'date_of_birth', 'current_level')
2140
2141    @property
2142    def title(self):
2143        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2144        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2145            target_language=portal_language)
2146
2147    @property
2148    def label(self):
2149        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2150        #return translate(_('Bed Allocation: '),
2151        #    'waeup.kofa', target_language=portal_language) \
2152        #    + ' %s' % self.context.bed_coordinates
2153        return translate(_('Bed Allocation Slip'),
2154            'waeup.kofa', target_language=portal_language) \
2155            + ' %s' % self.context.getSessionString()
2156
2157    def render(self):
2158        studentview = StudentBasePDFFormPage(self.context.student,
2159            self.request, self.omit_fields)
2160        students_utils = getUtility(IStudentsUtils)
2161        return students_utils.renderPDF(
2162            self, 'bed_allocation_slip.pdf',
2163            self.context.student, studentview,
2164            omit_fields=self.omit_fields)
2165
2166class BedTicketRelocationPage(UtilityView, grok.View):
2167    """ Callback view
2168    """
2169    grok.context(IBedTicket)
2170    grok.name('relocate')
2171    grok.require('waeup.manageHostels')
2172
2173    # Relocate student if student parameters have changed or the bed_type
2174    # of the bed has changed
2175    def update(self):
2176        student = self.context.student
2177        students_utils = getUtility(IStudentsUtils)
2178        acc_details  = students_utils.getAccommodationDetails(student)
2179        if self.context.bed != None and \
2180              'reserved' in self.context.bed.bed_type:
2181            self.flash(_("Students in reserved beds can't be relocated."),
2182                       type="warning")
2183            self.redirect(self.url(self.context))
2184            return
2185        if acc_details['bt'] == self.context.bed_type and \
2186                self.context.bed != None and \
2187                self.context.bed.bed_type == self.context.bed_type:
2188            self.flash(_("Student can't be relocated."), type="warning")
2189            self.redirect(self.url(self.context))
2190            return
2191        # Search a bed
2192        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2193        entries = cat.searchResults(
2194            owner=(student.student_id,student.student_id))
2195        if len(entries) and self.context.bed == None:
2196            # If booking has been cancelled but other bed space has been
2197            # manually allocated after cancellation use this bed
2198            new_bed = [entry for entry in entries][0]
2199        else:
2200            # Search for other available beds
2201            entries = cat.searchResults(
2202                bed_type=(acc_details['bt'],acc_details['bt']))
2203            available_beds = [
2204                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2205            if available_beds:
2206                students_utils = getUtility(IStudentsUtils)
2207                new_bed = students_utils.selectBed(available_beds)
2208                new_bed.bookBed(student.student_id)
2209            else:
2210                self.flash(_('There is no free bed in your category ${a}.',
2211                    mapping = {'a':acc_details['bt']}), type="warning")
2212                self.redirect(self.url(self.context))
2213                return
2214        # Release old bed if exists
2215        if self.context.bed != None:
2216            self.context.bed.owner = NOT_OCCUPIED
2217            notify(grok.ObjectModifiedEvent(self.context.bed))
2218        # Alocate new bed
2219        self.context.bed_type = acc_details['bt']
2220        self.context.bed = new_bed
2221        hall_title = new_bed.__parent__.hostel_name
2222        coordinates = new_bed.coordinates[1:]
2223        block, room_nr, bed_nr = coordinates
2224        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2225            'a':hall_title, 'b':block,
2226            'c':room_nr, 'd':bed_nr,
2227            'e':new_bed.bed_type})
2228        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2229        self.context.bed_coordinates = translate(
2230            bc, 'waeup.kofa',target_language=portal_language)
2231        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2232        self.flash(_('Student relocated: ${a}',
2233            mapping = {'a':self.context.display_coordinates}))
2234        self.redirect(self.url(self.context))
2235        return
2236
2237    def render(self):
2238        return
2239
2240class StudentHistoryPage(KofaPage):
2241    """ Page to display student history
2242    """
2243    grok.context(IStudent)
2244    grok.name('history')
2245    grok.require('waeup.viewStudent')
2246    grok.template('studenthistory')
2247    pnav = 4
2248
2249    @property
2250    def label(self):
2251        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2252
2253# Pages for students only
2254
2255class StudentBaseEditFormPage(KofaEditFormPage):
2256    """ View to edit student base data
2257    """
2258    grok.context(IStudent)
2259    grok.name('edit_base')
2260    grok.require('waeup.handleStudent')
2261    form_fields = grok.AutoFields(IStudentBase).select(
2262        'email', 'phone')
2263    label = _('Edit base data')
2264    pnav = 4
2265
2266    @action(_('Save'), style='primary')
2267    def save(self, **data):
2268        msave(self, **data)
2269        return
2270
2271class StudentChangePasswordPage(KofaEditFormPage):
2272    """ View to edit student passwords
2273    """
2274    grok.context(IStudent)
2275    grok.name('change_password')
2276    grok.require('waeup.handleStudent')
2277    grok.template('change_password')
2278    label = _('Change password')
2279    pnav = 4
2280
2281    @action(_('Save'), style='primary')
2282    def save(self, **data):
2283        form = self.request.form
2284        password = form.get('change_password', None)
2285        password_ctl = form.get('change_password_repeat', None)
2286        if password:
2287            validator = getUtility(IPasswordValidator)
2288            errors = validator.validate_password(password, password_ctl)
2289            if not errors:
2290                IUserAccount(self.context).setPassword(password)
2291                self.context.writeLogMessage(self, 'saved: password')
2292                self.flash(_('Password changed.'))
2293            else:
2294                self.flash( ' '.join(errors), type="warning")
2295        return
2296
2297class StudentFilesUploadPage(KofaPage):
2298    """ View to upload files by student
2299    """
2300    grok.context(IStudent)
2301    grok.name('change_portrait')
2302    grok.require('waeup.uploadStudentFile')
2303    grok.template('filesuploadpage')
2304    label = _('Upload portrait')
2305    pnav = 4
2306
2307    def update(self):
2308        PWCHANGE_STATES = getUtility(IStudentsUtils).PWCHANGE_STATES
2309        if self.context.student.state not in PWCHANGE_STATES:
2310            emit_lock_message(self)
2311            return
2312        super(StudentFilesUploadPage, self).update()
2313        return
2314
2315class StartClearancePage(KofaPage):
2316    grok.context(IStudent)
2317    grok.name('start_clearance')
2318    grok.require('waeup.handleStudent')
2319    grok.template('enterpin')
2320    label = _('Start clearance')
2321    ac_prefix = 'CLR'
2322    notice = ''
2323    pnav = 4
2324    buttonname = _('Start clearance now')
2325    with_ac = True
2326
2327    @property
2328    def all_required_fields_filled(self):
2329        if self.context.email and self.context.phone:
2330            return True
2331        return False
2332
2333    @property
2334    def portrait_uploaded(self):
2335        store = getUtility(IExtFileStore)
2336        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2337            return True
2338        return False
2339
2340    def update(self, SUBMIT=None):
2341        if not self.context.state == ADMITTED:
2342            self.flash(_("Wrong state"), type="warning")
2343            self.redirect(self.url(self.context))
2344            return
2345        if not self.portrait_uploaded:
2346            self.flash(_("No portrait uploaded."), type="warning")
2347            self.redirect(self.url(self.context, 'change_portrait'))
2348            return
2349        if not self.all_required_fields_filled:
2350            self.flash(_("Not all required fields filled."), type="warning")
2351            self.redirect(self.url(self.context, 'edit_base'))
2352            return
2353        if self.with_ac:
2354            self.ac_series = self.request.form.get('ac_series', None)
2355            self.ac_number = self.request.form.get('ac_number', None)
2356        if SUBMIT is None:
2357            return
2358        if self.with_ac:
2359            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2360            code = get_access_code(pin)
2361            if not code:
2362                self.flash(_('Activation code is invalid.'), type="warning")
2363                return
2364            if code.state == USED:
2365                self.flash(_('Activation code has already been used.'),
2366                           type="warning")
2367                return
2368            # Mark pin as used (this also fires a pin related transition)
2369            # and fire transition start_clearance
2370            comment = _(u"invalidated")
2371            # Here we know that the ac is in state initialized so we do not
2372            # expect an exception, but the owner might be different
2373            if not invalidate_accesscode(pin, comment, self.context.student_id):
2374                self.flash(_('You are not the owner of this access code.'),
2375                           type="warning")
2376                return
2377            self.context.clr_code = pin
2378        IWorkflowInfo(self.context).fireTransition('start_clearance')
2379        self.flash(_('Clearance process has been started.'))
2380        self.redirect(self.url(self.context,'cedit'))
2381        return
2382
2383class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2384    """ View to edit student clearance data by student
2385    """
2386    grok.context(IStudent)
2387    grok.name('cedit')
2388    grok.require('waeup.handleStudent')
2389    label = _('Edit clearance data')
2390
2391    @property
2392    def form_fields(self):
2393        if self.context.is_postgrad:
2394            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2395                'clearance_locked', 'clr_code', 'officer_comment')
2396        else:
2397            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2398                'clearance_locked', 'clr_code', 'officer_comment')
2399        return form_fields
2400
2401    def update(self):
2402        if self.context.clearance_locked:
2403            emit_lock_message(self)
2404            return
2405        return super(StudentClearanceEditFormPage, self).update()
2406
2407    @action(_('Save'), style='primary')
2408    def save(self, **data):
2409        self.applyData(self.context, **data)
2410        self.flash(_('Clearance form has been saved.'))
2411        return
2412
2413    def dataNotComplete(self):
2414        """To be implemented in the customization package.
2415        """
2416        return False
2417
2418    @action(_('Save and request clearance'), style='primary')
2419    def requestClearance(self, **data):
2420        self.applyData(self.context, **data)
2421        if self.dataNotComplete():
2422            self.flash(self.dataNotComplete(), type="warning")
2423            return
2424        self.flash(_('Clearance form has been saved.'))
2425        if self.context.clr_code:
2426            self.redirect(self.url(self.context, 'request_clearance'))
2427        else:
2428            # We bypass the request_clearance page if student
2429            # has been imported in state 'clearance started' and
2430            # no clr_code was entered before.
2431            state = IWorkflowState(self.context).getState()
2432            if state != CLEARANCE:
2433                # This shouldn't happen, but the application officer
2434                # might have forgotten to lock the form after changing the state
2435                self.flash(_('This form cannot be submitted. Wrong state!'),
2436                           type="danger")
2437                return
2438            IWorkflowInfo(self.context).fireTransition('request_clearance')
2439            self.flash(_('Clearance has been requested.'))
2440            self.redirect(self.url(self.context))
2441        return
2442
2443class RequestClearancePage(KofaPage):
2444    grok.context(IStudent)
2445    grok.name('request_clearance')
2446    grok.require('waeup.handleStudent')
2447    grok.template('enterpin')
2448    label = _('Request clearance')
2449    notice = _('Enter the CLR access code used for starting clearance.')
2450    ac_prefix = 'CLR'
2451    pnav = 4
2452    buttonname = _('Request clearance now')
2453    with_ac = True
2454
2455    def update(self, SUBMIT=None):
2456        if self.with_ac:
2457            self.ac_series = self.request.form.get('ac_series', None)
2458            self.ac_number = self.request.form.get('ac_number', None)
2459        if SUBMIT is None:
2460            return
2461        if self.with_ac:
2462            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2463            if self.context.clr_code and self.context.clr_code != pin:
2464                self.flash(_("This isn't your CLR access code."), type="danger")
2465                return
2466        state = IWorkflowState(self.context).getState()
2467        if state != CLEARANCE:
2468            # This shouldn't happen, but the application officer
2469            # might have forgotten to lock the form after changing the state
2470            self.flash(_('This form cannot be submitted. Wrong state!'),
2471                       type="danger")
2472            return
2473        IWorkflowInfo(self.context).fireTransition('request_clearance')
2474        self.flash(_('Clearance has been requested.'))
2475        self.redirect(self.url(self.context))
2476        return
2477
2478class StartSessionPage(KofaPage):
2479    grok.context(IStudentStudyCourse)
2480    grok.name('start_session')
2481    grok.require('waeup.handleStudent')
2482    grok.template('enterpin')
2483    label = _('Start session')
2484    ac_prefix = 'SFE'
2485    notice = ''
2486    pnav = 4
2487    buttonname = _('Start now')
2488    with_ac = True
2489
2490    def update(self, SUBMIT=None):
2491        if not self.context.is_current:
2492            emit_lock_message(self)
2493            return
2494        super(StartSessionPage, self).update()
2495        if not self.context.next_session_allowed:
2496            self.flash(_("You are not entitled to start session."),
2497                       type="warning")
2498            self.redirect(self.url(self.context))
2499            return
2500        if self.with_ac:
2501            self.ac_series = self.request.form.get('ac_series', None)
2502            self.ac_number = self.request.form.get('ac_number', None)
2503        if SUBMIT is None:
2504            return
2505        if self.with_ac:
2506            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2507            code = get_access_code(pin)
2508            if not code:
2509                self.flash(_('Activation code is invalid.'), type="warning")
2510                return
2511            # Mark pin as used (this also fires a pin related transition)
2512            if code.state == USED:
2513                self.flash(_('Activation code has already been used.'),
2514                           type="warning")
2515                return
2516            else:
2517                comment = _(u"invalidated")
2518                # Here we know that the ac is in state initialized so we do not
2519                # expect an error, but the owner might be different
2520                if not invalidate_accesscode(
2521                    pin,comment,self.context.student.student_id):
2522                    self.flash(_('You are not the owner of this access code.'),
2523                               type="warning")
2524                    return
2525        try:
2526            if self.context.student.state == CLEARED:
2527                IWorkflowInfo(self.context.student).fireTransition(
2528                    'pay_first_school_fee')
2529            elif self.context.student.state == RETURNING:
2530                IWorkflowInfo(self.context.student).fireTransition(
2531                    'pay_school_fee')
2532            elif self.context.student.state == PAID:
2533                IWorkflowInfo(self.context.student).fireTransition(
2534                    'pay_pg_fee')
2535        except ConstraintNotSatisfied:
2536            self.flash(_('An error occurred, please contact the system administrator.'),
2537                       type="danger")
2538            return
2539        self.flash(_('Session started.'))
2540        self.redirect(self.url(self.context))
2541        return
2542
2543class AddStudyLevelFormPage(KofaEditFormPage):
2544    """ Page for students to add current study levels
2545    """
2546    grok.context(IStudentStudyCourse)
2547    grok.name('add')
2548    grok.require('waeup.handleStudent')
2549    grok.template('studyleveladdpage')
2550    form_fields = grok.AutoFields(IStudentStudyCourse)
2551    pnav = 4
2552
2553    @property
2554    def label(self):
2555        studylevelsource = StudyLevelSource().factory
2556        code = self.context.current_level
2557        title = studylevelsource.getTitle(self.context, code)
2558        return _('Add current level ${a}', mapping = {'a':title})
2559
2560    def update(self):
2561        if not self.context.is_current:
2562            emit_lock_message(self)
2563            return
2564        if self.context.student.state != PAID:
2565            emit_lock_message(self)
2566            return
2567        code = self.context.current_level
2568        if code is None:
2569            self.flash(_('Your data are incomplete'), type="danger")
2570            self.redirect(self.url(self.context))
2571            return
2572        super(AddStudyLevelFormPage, self).update()
2573        return
2574
2575    @action(_('Create course list now'), style='primary')
2576    def addStudyLevel(self, **data):
2577        studylevel = createObject(u'waeup.StudentStudyLevel')
2578        studylevel.level = self.context.current_level
2579        studylevel.level_session = self.context.current_session
2580        try:
2581            self.context.addStudentStudyLevel(
2582                self.context.certificate,studylevel)
2583        except KeyError:
2584            self.flash(_('This level exists.'), type="warning")
2585        except RequiredMissing:
2586            self.flash(_('Your data are incomplete'), type="danger")
2587        self.redirect(self.url(self.context))
2588        return
2589
2590class StudyLevelEditFormPage(KofaEditFormPage):
2591    """ Page to edit the student study level data by students
2592    """
2593    grok.context(IStudentStudyLevel)
2594    grok.name('edit')
2595    grok.require('waeup.editStudyLevel')
2596    grok.template('studyleveleditpage')
2597    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2598        'level_session', 'level_verdict')
2599    pnav = 4
2600    placeholder = _('Enter valid course code')
2601
2602    def update(self, ADD=None, course=None):
2603        if not self.context.__parent__.is_current:
2604            emit_lock_message(self)
2605            return
2606        if self.context.student.state != PAID or \
2607            not self.context.is_current_level:
2608            emit_lock_message(self)
2609            return
2610        super(StudyLevelEditFormPage, self).update()
2611        if ADD is not None:
2612            if not course:
2613                self.flash(_('No valid course code entered.'), type="warning")
2614                return
2615            cat = queryUtility(ICatalog, name='courses_catalog')
2616            result = cat.searchResults(code=(course, course))
2617            if len(result) != 1:
2618                self.flash(_('Course not found.'), type="warning")
2619                return
2620            course = list(result)[0]
2621            addCourseTicket(self, course)
2622        return
2623
2624    @property
2625    def label(self):
2626        # Here we know that the cookie has been set
2627        lang = self.request.cookies.get('kofa.language')
2628        level_title = translate(self.context.level_title, 'waeup.kofa',
2629            target_language=lang)
2630        return _('Edit course list of ${a}',
2631            mapping = {'a':level_title})
2632
2633    @property
2634    def translated_values(self):
2635        return translated_values(self)
2636
2637    def _delCourseTicket(self, **data):
2638        form = self.request.form
2639        if 'val_id' in form:
2640            child_id = form['val_id']
2641        else:
2642            self.flash(_('No ticket selected.'), type="warning")
2643            self.redirect(self.url(self.context, '@@edit'))
2644            return
2645        if not isinstance(child_id, list):
2646            child_id = [child_id]
2647        deleted = []
2648        for id in child_id:
2649            # Students are not allowed to remove core tickets
2650            if id in self.context and \
2651                self.context[id].removable_by_student:
2652                del self.context[id]
2653                deleted.append(id)
2654        if len(deleted):
2655            self.flash(_('Successfully removed: ${a}',
2656                mapping = {'a':', '.join(deleted)}))
2657            self.context.writeLogMessage(
2658                self,'removed: %s at %s' %
2659                (', '.join(deleted), self.context.level))
2660        self.redirect(self.url(self.context, u'@@edit'))
2661        return
2662
2663    @jsaction(_('Remove selected tickets'))
2664    def delCourseTicket(self, **data):
2665        self._delCourseTicket(**data)
2666        return
2667
2668    def _registerCourses(self, **data):
2669        if self.context.student.is_postgrad and \
2670            not self.context.student.is_special_postgrad:
2671            self.flash(_(
2672                "You are a postgraduate student, "
2673                "your course list can't bee registered."), type="warning")
2674            self.redirect(self.url(self.context))
2675            return
2676        students_utils = getUtility(IStudentsUtils)
2677        max_credits = students_utils.maxCredits(self.context)
2678        if max_credits and self.context.total_credits > max_credits:
2679            self.flash(_('Maximum credits of ${a} exceeded.',
2680                mapping = {'a':max_credits}), type="warning")
2681            return
2682        IWorkflowInfo(self.context.student).fireTransition(
2683            'register_courses')
2684        self.flash(_('Course list has been registered.'))
2685        self.redirect(self.url(self.context))
2686        return
2687
2688    @action(_('Register course list'), style='primary',
2689        warning=_('You can not edit your course list after registration.'
2690            ' You really want to register?'))
2691    def registerCourses(self, **data):
2692        self._registerCourses(**data)
2693        return
2694
2695class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2696    """Add a course ticket by student.
2697    """
2698    grok.name('ctadd')
2699    grok.require('waeup.handleStudent')
2700    form_fields = grok.AutoFields(ICourseTicketAdd)
2701
2702    def update(self):
2703        if self.context.student.state != PAID or \
2704            not self.context.is_current_level:
2705            emit_lock_message(self)
2706            return
2707        super(CourseTicketAddFormPage2, self).update()
2708        return
2709
2710    @action(_('Add course ticket'))
2711    def addCourseTicket(self, **data):
2712        # Safety belt
2713        if self.context.student.state != PAID:
2714            return
2715        course = data['course']
2716        success = addCourseTicket(self, course)
2717        if success:
2718            self.redirect(self.url(self.context, u'@@edit'))
2719        return
2720
2721class SetPasswordPage(KofaPage):
2722    grok.context(IKofaObject)
2723    grok.name('setpassword')
2724    grok.require('waeup.Anonymous')
2725    grok.template('setpassword')
2726    label = _('Set password for first-time login')
2727    ac_prefix = 'PWD'
2728    pnav = 0
2729    set_button = _('Set')
2730
2731    def update(self, SUBMIT=None):
2732        self.reg_number = self.request.form.get('reg_number', None)
2733        self.ac_series = self.request.form.get('ac_series', None)
2734        self.ac_number = self.request.form.get('ac_number', None)
2735
2736        if SUBMIT is None:
2737            return
2738        hitlist = search(query=self.reg_number,
2739            searchtype='reg_number', view=self)
2740        if not hitlist:
2741            self.flash(_('No student found.'), type="warning")
2742            return
2743        if len(hitlist) != 1:   # Cannot happen but anyway
2744            self.flash(_('More than one student found.'), type="warning")
2745            return
2746        student = hitlist[0].context
2747        self.student_id = student.student_id
2748        student_pw = student.password
2749        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2750        code = get_access_code(pin)
2751        if not code:
2752            self.flash(_('Access code is invalid.'), type="warning")
2753            return
2754        if student_pw and pin == student.adm_code:
2755            self.flash(_(
2756                'Password has already been set. Your Student Id is ${a}',
2757                mapping = {'a':self.student_id}))
2758            return
2759        elif student_pw:
2760            self.flash(
2761                _('Password has already been set. You are using the ' +
2762                'wrong Access Code.'), type="warning")
2763            return
2764        # Mark pin as used (this also fires a pin related transition)
2765        # and set student password
2766        if code.state == USED:
2767            self.flash(_('Access code has already been used.'), type="warning")
2768            return
2769        else:
2770            comment = _(u"invalidated")
2771            # Here we know that the ac is in state initialized so we do not
2772            # expect an exception
2773            invalidate_accesscode(pin,comment)
2774            IUserAccount(student).setPassword(self.ac_number)
2775            student.adm_code = pin
2776        self.flash(_('Password has been set. Your Student Id is ${a}',
2777            mapping = {'a':self.student_id}))
2778        return
2779
2780class StudentRequestPasswordPage(KofaAddFormPage):
2781    """Captcha'd registration page for applicants.
2782    """
2783    grok.name('requestpw')
2784    grok.require('waeup.Anonymous')
2785    grok.template('requestpw')
2786    form_fields = grok.AutoFields(IStudentRequestPW).select(
2787        'firstname','number','email')
2788    label = _('Request password for first-time login')
2789
2790    def update(self):
2791        # Handle captcha
2792        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2793        self.captcha_result = self.captcha.verify(self.request)
2794        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2795        return
2796
2797    def _redirect(self, email, password, student_id):
2798        # Forward only email to landing page in base package.
2799        self.redirect(self.url(self.context, 'requestpw_complete',
2800            data = dict(email=email)))
2801        return
2802
2803    def _pw_used(self):
2804        # XXX: False if password has not been used. We need an extra
2805        #      attribute which remembers if student logged in.
2806        return True
2807
2808    @action(_('Send login credentials to email address'), style='primary')
2809    def get_credentials(self, **data):
2810        if not self.captcha_result.is_valid:
2811            # Captcha will display error messages automatically.
2812            # No need to flash something.
2813            return
2814        number = data.get('number','')
2815        firstname = data.get('firstname','')
2816        cat = getUtility(ICatalog, name='students_catalog')
2817        results = list(
2818            cat.searchResults(reg_number=(number, number)))
2819        if not results:
2820            results = list(
2821                cat.searchResults(matric_number=(number, number)))
2822        if results:
2823            student = results[0]
2824            if getattr(student,'firstname',None) is None:
2825                self.flash(_('An error occurred.'), type="danger")
2826                return
2827            elif student.firstname.lower() != firstname.lower():
2828                # Don't tell the truth here. Anonymous must not
2829                # know that a record was found and only the firstname
2830                # verification failed.
2831                self.flash(_('No student record found.'), type="warning")
2832                return
2833            elif student.password is not None and self._pw_used:
2834                self.flash(_('Your password has already been set and used. '
2835                             'Please proceed to the login page.'),
2836                           type="warning")
2837                return
2838            # Store email address but nothing else.
2839            student.email = data['email']
2840            notify(grok.ObjectModifiedEvent(student))
2841        else:
2842            # No record found, this is the truth.
2843            self.flash(_('No student record found.'), type="warning")
2844            return
2845
2846        kofa_utils = getUtility(IKofaUtils)
2847        password = kofa_utils.genPassword()
2848        mandate = PasswordMandate()
2849        mandate.params['password'] = password
2850        mandate.params['user'] = student
2851        site = grok.getSite()
2852        site['mandates'].addMandate(mandate)
2853        # Send email with credentials
2854        args = {'mandate_id':mandate.mandate_id}
2855        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2856        url_info = u'Confirmation link: %s' % mandate_url
2857        msg = _('You have successfully requested a password for the')
2858        if kofa_utils.sendCredentials(IUserAccount(student),
2859            password, url_info, msg):
2860            email_sent = student.email
2861        else:
2862            email_sent = None
2863        self._redirect(email=email_sent, password=password,
2864            student_id=student.student_id)
2865        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2866        self.context.logger.info(
2867            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2868        return
2869
2870class StudentRequestPasswordEmailSent(KofaPage):
2871    """Landing page after successful password request.
2872
2873    """
2874    grok.name('requestpw_complete')
2875    grok.require('waeup.Public')
2876    grok.template('requestpwmailsent')
2877    label = _('Your password request was successful.')
2878
2879    def update(self, email=None, student_id=None, password=None):
2880        self.email = email
2881        self.password = password
2882        self.student_id = student_id
2883        return
2884
2885class FilterStudentsInDepartmentPage(KofaPage):
2886    """Page that filters and lists students.
2887    """
2888    grok.context(IDepartment)
2889    grok.require('waeup.showStudents')
2890    grok.name('students')
2891    grok.template('filterstudentspage')
2892    pnav = 1
2893    session_label = _('Current Session')
2894    level_label = _('Current Level')
2895
2896    def label(self):
2897        return 'Students in %s' % self.context.longtitle
2898
2899    def _set_session_values(self):
2900        vocab_terms = academic_sessions_vocab.by_value.values()
2901        self.sessions = sorted(
2902            [(x.title, x.token) for x in vocab_terms], reverse=True)
2903        self.sessions += [('All Sessions', 'all')]
2904        return
2905
2906    def _set_level_values(self):
2907        vocab_terms = course_levels.by_value.values()
2908        self.levels = sorted(
2909            [(x.title, x.token) for x in vocab_terms])
2910        self.levels += [('All Levels', 'all')]
2911        return
2912
2913    def _searchCatalog(self, session, level):
2914        if level not in (10, 999, None):
2915            start_level = 100 * (level // 100)
2916            end_level = start_level + 90
2917        else:
2918            start_level = end_level = level
2919        cat = queryUtility(ICatalog, name='students_catalog')
2920        students = cat.searchResults(
2921            current_session=(session, session),
2922            current_level=(start_level, end_level),
2923            depcode=(self.context.code, self.context.code)
2924            )
2925        hitlist = []
2926        for student in students:
2927            hitlist.append(StudentQueryResultItem(student, view=self))
2928        return hitlist
2929
2930    def update(self, SHOW=None, session=None, level=None):
2931        self.parent_url = self.url(self.context.__parent__)
2932        self._set_session_values()
2933        self._set_level_values()
2934        self.hitlist = []
2935        self.session_default = session
2936        self.level_default = level
2937        if SHOW is not None:
2938            if session != 'all':
2939                self.session = int(session)
2940                self.session_string = '%s %s/%s' % (
2941                    self.session_label, self.session, self.session+1)
2942            else:
2943                self.session = None
2944                self.session_string = _('in any session')
2945            if level != 'all':
2946                self.level = int(level)
2947                self.level_string = '%s %s' % (self.level_label, self.level)
2948            else:
2949                self.level = None
2950                self.level_string = _('at any level')
2951            self.hitlist = self._searchCatalog(self.session, self.level)
2952            if not self.hitlist:
2953                self.flash(_('No student found.'), type="warning")
2954        return
2955
2956class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2957    """Page that filters and lists students.
2958    """
2959    grok.context(ICertificate)
2960
2961    def label(self):
2962        return 'Students studying %s' % self.context.longtitle
2963
2964    def _searchCatalog(self, session, level):
2965        if level not in (10, 999, None):
2966            start_level = 100 * (level // 100)
2967            end_level = start_level + 90
2968        else:
2969            start_level = end_level = level
2970        cat = queryUtility(ICatalog, name='students_catalog')
2971        students = cat.searchResults(
2972            current_session=(session, session),
2973            current_level=(start_level, end_level),
2974            certcode=(self.context.code, self.context.code)
2975            )
2976        hitlist = []
2977        for student in students:
2978            hitlist.append(StudentQueryResultItem(student, view=self))
2979        return hitlist
2980
2981class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2982    """Page that filters and lists students.
2983    """
2984    grok.context(ICourse)
2985
2986    session_label = _('Session')
2987    level_label = _('Level')
2988
2989    def label(self):
2990        return 'Students registered for %s' % self.context.longtitle
2991
2992    def _searchCatalog(self, session, level):
2993        if level not in (10, 999, None):
2994            start_level = 100 * (level // 100)
2995            end_level = start_level + 90
2996        else:
2997            start_level = end_level = level
2998        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2999        coursetickets = cat.searchResults(
3000            session=(session, session),
3001            level=(start_level, end_level),
3002            code=(self.context.code, self.context.code)
3003            )
3004        hitlist = []
3005        for ticket in coursetickets:
3006            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3007        return list(set(hitlist))
3008
3009class ClearAllStudentsInDepartmentPage(UtilityView, grok.View):
3010    """ Clear all students of a department in state 'clearance requested'.
3011    """
3012    grok.context(IDepartment)
3013    grok.name('clearallstudents')
3014    grok.require('waeup.clearAllStudents')
3015
3016    def update(self):
3017        cat = queryUtility(ICatalog, name='students_catalog')
3018        students = cat.searchResults(
3019            depcode=(self.context.code, self.context.code),
3020            state=(REQUESTED, REQUESTED)
3021            )
3022        num = 0
3023        for student in students:
3024            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3025                continue
3026            IWorkflowInfo(student).fireTransition('clear')
3027            num += 1
3028        self.flash(_('%d students have been cleared.' % num))
3029        self.redirect(self.url(self.context))
3030        return
3031
3032    def render(self):
3033        return
3034
3035
3036class EditScoresPage(KofaPage):
3037    """Page that filters and lists students.
3038    """
3039    grok.context(ICourse)
3040    grok.require('waeup.editScores')
3041    grok.name('edit_scores')
3042    grok.template('editscorespage')
3043    pnav = 1
3044
3045    def label(self):
3046        session = academic_sessions_vocab.getTerm(
3047            self.current_academic_session).title
3048        return '%s tickets in academic session %s' % (
3049            self.context.code, session)
3050
3051    def _searchCatalog(self, session):
3052        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3053        coursetickets = cat.searchResults(
3054            session=(session, session),
3055            code=(self.context.code, self.context.code)
3056            )
3057        return list(coursetickets)
3058
3059    def update(self,  *args, **kw):
3060        form = self.request.form
3061        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3062        self.current_academic_session = grok.getSite()[
3063            'configuration'].current_academic_session
3064        if self.context.__parent__.__parent__.score_editing_disabled:
3065            self.flash(_('Score editing disabled.'), type="warning")
3066            self.redirect(self.url(self.context))
3067            return
3068        if not self.current_academic_session:
3069            self.flash(_('Current academic session not set.'), type="warning")
3070            self.redirect(self.url(self.context))
3071            return
3072        self.tickets = self._searchCatalog(self.current_academic_session)
3073        editable_tickets = [
3074            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3075        if not self.tickets:
3076            self.flash(_('No student found.'), type="warning")
3077            self.redirect(self.url(self.context))
3078            return
3079        if 'UPDATE' in form:
3080            tno = 0
3081            error = ''
3082            if not editable_tickets:
3083                return
3084            scores = form['scores']
3085            if isinstance(scores, basestring):
3086                scores = [scores]
3087            for ticket in editable_tickets:
3088                score = ticket.score
3089                if scores[tno] == '':
3090                    score = None
3091                else:
3092                    try:
3093                        score = int(scores[tno])
3094                    except ValueError:
3095                        error += '%s, ' % ticket.student.display_fullname
3096                if ticket.score != score:
3097                    ticket.score = score
3098                    ticket.student.__parent__.logger.info(
3099                        '%s - %s %s/%s score updated (%s)' %
3100                        (ob_class, ticket.student.student_id,
3101                         ticket.level, ticket.code, score))
3102                    #notify(grok.ObjectModifiedEvent(ticket))
3103                tno += 1
3104            if error:
3105                self.flash(_('Error: Score(s) of %s have not be updated. '
3106                  'Only integers are allowed.' % error.strip(', ')),
3107                  type="danger")
3108        return
3109
3110class ExportJobContainerOverview(KofaPage):
3111    """Page that lists active student data export jobs and provides links
3112    to discard or download CSV files.
3113
3114    """
3115    grok.context(VirtualExportJobContainer)
3116    grok.require('waeup.showStudents')
3117    grok.name('index.html')
3118    grok.template('exportjobsindex')
3119    label = _('Student Data Exports')
3120    pnav = 1
3121
3122    def update(self, CREATE=None, DISCARD=None, job_id=None):
3123        if CREATE:
3124            self.redirect(self.url('@@exportconfig'))
3125            return
3126        if DISCARD and job_id:
3127            entry = self.context.entry_from_job_id(job_id)
3128            self.context.delete_export_entry(entry)
3129            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3130            self.context.logger.info(
3131                '%s - discarded: job_id=%s' % (ob_class, job_id))
3132            self.flash(_('Discarded export') + ' %s' % job_id)
3133        self.entries = doll_up(self, user=self.request.principal.id)
3134        return
3135
3136class ExportJobContainerJobConfig(KofaPage):
3137    """Page that configures a students export job.
3138
3139    This is a baseclass.
3140    """
3141    grok.baseclass()
3142    grok.name('exportconfig')
3143    grok.require('waeup.showStudents')
3144    grok.template('exportconfig')
3145    label = _('Configure student data export')
3146    pnav = 1
3147    redirect_target = ''
3148
3149    def _set_session_values(self):
3150        vocab_terms = academic_sessions_vocab.by_value.values()
3151        self.sessions = sorted(
3152            [(x.title, x.token) for x in vocab_terms], reverse=True)
3153        self.sessions += [(_('All Sessions'), 'all')]
3154        return
3155
3156    def _set_level_values(self):
3157        vocab_terms = course_levels.by_value.values()
3158        self.levels = sorted(
3159            [(x.title, x.token) for x in vocab_terms])
3160        self.levels += [(_('All Levels'), 'all')]
3161        return
3162
3163    def _set_mode_values(self):
3164        utils = getUtility(IKofaUtils)
3165        self.modes = sorted([(value, key) for key, value in
3166                      utils.STUDY_MODES_DICT.items()])
3167        self.modes +=[(_('All Modes'), 'all')]
3168        return
3169
3170    def _set_exporter_values(self):
3171        # We provide all student exporters, nothing else, yet.
3172        # Bursary or Department Officers don't have the general exportData
3173        # permission and are only allowed to export bursary or payments
3174        # overview data respectively. This is the only place where
3175        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3176        # are used.
3177        exporters = []
3178        if not checkPermission('waeup.exportData', self.context):
3179            if checkPermission('waeup.exportBursaryData', self.context):
3180                exporters += [('Bursary Data', 'bursary')]
3181            if checkPermission('waeup.exportPaymentsOverview', self.context):
3182                exporters += [('Student Payments Overview', 'paymentsoverview')]
3183            self.exporters = exporters
3184            return
3185        STUDENT_EXPORTER_NAMES = getUtility(
3186            IStudentsUtils).STUDENT_EXPORTER_NAMES
3187        for name in STUDENT_EXPORTER_NAMES:
3188            util = getUtility(ICSVExporter, name=name)
3189            exporters.append((util.title, name),)
3190        self.exporters = exporters
3191        return
3192
3193    @property
3194    def faccode(self):
3195        return None
3196
3197    @property
3198    def depcode(self):
3199        return None
3200
3201    @property
3202    def certcode(self):
3203        return None
3204
3205    def update(self, START=None, session=None, level=None, mode=None,
3206               payments_start=None, payments_end=None,
3207               exporter=None):
3208        self._set_session_values()
3209        self._set_level_values()
3210        self._set_mode_values()
3211        self._set_exporter_values()
3212        if START is None:
3213            return
3214        if payments_start or payments_end:
3215            date_format = '%d/%m/%Y'
3216            try:
3217                dummy = datetime.strptime(payments_start, date_format)
3218                dummy = datetime.strptime(payments_end, date_format)
3219            except ValueError:
3220                self.flash(_('Payment dates do not match format d/m/Y.'),
3221                           type="danger")
3222                return
3223        if session == 'all':
3224            session=None
3225        if level == 'all':
3226            level = None
3227        if mode == 'all':
3228            mode = None
3229        if payments_start == '':
3230            payments_start = None
3231        if payments_end == '':
3232            payments_end = None
3233        if (mode,
3234            level,
3235            session,
3236            self.faccode,
3237            self.depcode,
3238            self.certcode) == (None, None, None, None, None, None):
3239            # Export all students including those without certificate
3240            if payments_start:
3241                job_id = self.context.start_export_job(exporter,
3242                                              self.request.principal.id,
3243                                              payments_start = payments_start,
3244                                              payments_end = payments_end)
3245            else:
3246                job_id = self.context.start_export_job(exporter,
3247                                              self.request.principal.id)
3248        else:
3249            if payments_start:
3250                job_id = self.context.start_export_job(exporter,
3251                                              self.request.principal.id,
3252                                              current_session=session,
3253                                              current_level=level,
3254                                              current_mode=mode,
3255                                              faccode=self.faccode,
3256                                              depcode=self.depcode,
3257                                              certcode=self.certcode,
3258                                              payments_start = payments_start,
3259                                              payments_end = payments_end)
3260            else:
3261                job_id = self.context.start_export_job(exporter,
3262                                              self.request.principal.id,
3263                                              current_session=session,
3264                                              current_level=level,
3265                                              current_mode=mode,
3266                                              faccode=self.faccode,
3267                                              depcode=self.depcode,
3268                                              certcode=self.certcode)
3269        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3270        self.context.logger.info(
3271            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3272            % (ob_class, exporter, session, level, mode, self.faccode,
3273            self.depcode, self.certcode, payments_start, payments_end, job_id))
3274        self.flash(_('Export started for students with') +
3275                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3276                   session, level, mode))
3277        self.redirect(self.url(self.redirect_target))
3278        return
3279
3280class ExportJobContainerDownload(ExportCSVView):
3281    """Page that downloads a students export csv file.
3282
3283    """
3284    grok.context(VirtualExportJobContainer)
3285    grok.require('waeup.showStudents')
3286
3287class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3288    """Page that configures a students export job in datacenter.
3289
3290    """
3291    grok.context(IDataCenter)
3292    redirect_target = '@@export'
3293
3294class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3295    """Page that configures a students export job in datacenter.
3296
3297    """
3298    grok.name('exportselected')
3299    grok.context(IDataCenter)
3300    redirect_target = '@@export'
3301    grok.template('exportselected')
3302    label = _('Configure student data export')
3303
3304    def update(self, START=None, students=None, exporter=None):
3305        self._set_exporter_values()
3306        if START is None:
3307            return
3308        try:
3309            ids = students.replace(',', ' ').split()
3310        except:
3311            self.flash(sys.exc_info()[1])
3312            self.redirect(self.url(self.redirect_target))
3313            return
3314        job_id = self.context.start_export_job(
3315            exporter, self.request.principal.id, selected=ids)
3316        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3317        self.context.logger.info(
3318            '%s - selected students exported: %s, job_id=%s' %
3319            (ob_class, exporter, job_id))
3320        self.flash(_('Export of selected students started.'))
3321        self.redirect(self.url(self.redirect_target))
3322        return
3323
3324class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3325    """Page that configures a students export job in facultiescontainer.
3326
3327    """
3328    grok.context(VirtualFacultiesExportJobContainer)
3329
3330
3331class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3332    """Page that configures a students export job in faculties.
3333
3334    """
3335    grok.context(VirtualFacultyExportJobContainer)
3336
3337    @property
3338    def faccode(self):
3339        return self.context.__parent__.code
3340
3341class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3342    """Page that configures a students export job in departments.
3343
3344    """
3345    grok.context(VirtualDepartmentExportJobContainer)
3346
3347    @property
3348    def depcode(self):
3349        return self.context.__parent__.code
3350
3351class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3352    """Page that configures a students export job for certificates.
3353
3354    """
3355    grok.context(VirtualCertificateExportJobContainer)
3356    grok.template('exportconfig_certificate')
3357
3358    @property
3359    def certcode(self):
3360        return self.context.__parent__.code
3361
3362class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3363    """Page that configures a students export job for courses.
3364
3365    In contrast to department or certificate student data exports the
3366    coursetickets_catalog is searched here. Therefore the update
3367    method from the base class is customized.
3368    """
3369    grok.context(VirtualCourseExportJobContainer)
3370    grok.template('exportconfig_course')
3371
3372    def _set_exporter_values(self):
3373        # We provide only two exporters.
3374        exporters = []
3375        for name in ('students', 'coursetickets'):
3376            util = getUtility(ICSVExporter, name=name)
3377            exporters.append((util.title, name),)
3378        self.exporters = exporters
3379
3380    def update(self, START=None, session=None, level=None, mode=None,
3381               exporter=None):
3382        self._set_session_values()
3383        self._set_level_values()
3384        self._set_mode_values()
3385        self._set_exporter_values()
3386        if START is None:
3387            return
3388        if session == 'all':
3389            session = None
3390        if level == 'all':
3391            level = None
3392        job_id = self.context.start_export_job(exporter,
3393                                      self.request.principal.id,
3394                                      # Use a different catalog and
3395                                      # pass different keywords than
3396                                      # for the (default) students_catalog
3397                                      catalog='coursetickets',
3398                                      session=session,
3399                                      level=level,
3400                                      code=self.context.__parent__.code)
3401        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3402        self.context.logger.info(
3403            '%s - exported: %s (%s, %s, %s), job_id=%s'
3404            % (ob_class, exporter, session, level,
3405            self.context.__parent__.code, job_id))
3406        self.flash(_('Export started for course tickets with') +
3407                   ' level_session=%s, level=%s' % (
3408                   session, level))
3409        self.redirect(self.url(self.redirect_target))
3410        return
Note: See TracBrowser for help on using the repository browser.