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

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

Configure transfer payments and let students enter their desired
study course. Save entered text in p_item attribute.

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