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

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

Add button and view which allow students to unregister their
current course list unless the courses have not been validated.

  • Property svn:keywords set to Id
File size: 127.8 KB
Line 
1## $Id: browser.py 13610 2016-01-13 20:59: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 is not the student\'s 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 is not the student\'s 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 has been unregistered.')
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 UnregisterCoursesView(UtilityView, grok.View):
1566    """Unegister course list by student
1567    """
1568    grok.context(IStudentStudyLevel)
1569    grok.name('unregister_courses')
1570    grok.require('waeup.handleStudent')
1571
1572    def update(self):
1573        if not self.context.__parent__.is_current:
1574            emit_lock_message(self)
1575            return
1576        elif not self.context.course_registration_allowed:
1577            self.flash(_(
1578                "Course registration has ended. "
1579                "Unregistration is disabled."), type="warning")
1580        elif str(self.context.__parent__.current_level) != self.context.__name__:
1581            self.flash(_('This is not your current level.'), type="danger")
1582        elif self.context.student.state == REGISTERED:
1583            IWorkflowInfo(self.context.student).fireTransition('reset7')
1584            message = _('Course list has been unregistered.')
1585            self.flash(message)
1586        else:
1587            self.flash(_('You are in the wrong state.'), type="warning")
1588        self.redirect(self.url(self.context))
1589        return
1590
1591    def render(self):
1592        return
1593
1594class CourseTicketAddFormPage(KofaAddFormPage):
1595    """Add a course ticket.
1596    """
1597    grok.context(IStudentStudyLevel)
1598    grok.name('add')
1599    grok.require('waeup.manageStudent')
1600    label = _('Add course ticket')
1601    form_fields = grok.AutoFields(ICourseTicketAdd)
1602    pnav = 4
1603
1604    def update(self):
1605        if not self.context.__parent__.is_current:
1606            emit_lock_message(self)
1607            return
1608        super(CourseTicketAddFormPage, self).update()
1609        return
1610
1611    @action(_('Add course ticket'), style='primary')
1612    def addCourseTicket(self, **data):
1613        course = data['course']
1614        success = addCourseTicket(self, course)
1615        if success:
1616            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1617        return
1618
1619    @action(_('Cancel'), validator=NullValidator)
1620    def cancel(self, **data):
1621        self.redirect(self.url(self.context))
1622
1623class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1624    """ Page to display course tickets
1625    """
1626    grok.context(ICourseTicket)
1627    grok.name('index')
1628    grok.require('waeup.viewStudent')
1629    form_fields = grok.AutoFields(ICourseTicket)
1630    grok.template('courseticketpage')
1631    pnav = 4
1632
1633    @property
1634    def label(self):
1635        return _('${a}: Course Ticket ${b}', mapping = {
1636            'a':self.context.student.display_fullname,
1637            'b':self.context.code})
1638
1639class CourseTicketManageFormPage(KofaEditFormPage):
1640    """ Page to manage course tickets
1641    """
1642    grok.context(ICourseTicket)
1643    grok.name('manage')
1644    grok.require('waeup.manageStudent')
1645    form_fields = grok.AutoFields(ICourseTicket)
1646    form_fields['title'].for_display = True
1647    form_fields['fcode'].for_display = True
1648    form_fields['dcode'].for_display = True
1649    form_fields['semester'].for_display = True
1650    form_fields['passmark'].for_display = True
1651    form_fields['credits'].for_display = True
1652    form_fields['mandatory'].for_display = False
1653    form_fields['automatic'].for_display = True
1654    form_fields['carry_over'].for_display = True
1655    pnav = 4
1656    grok.template('courseticketmanagepage')
1657
1658    @property
1659    def label(self):
1660        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1661
1662    @action('Save', style='primary')
1663    def save(self, **data):
1664        msave(self, **data)
1665        return
1666
1667class PaymentsManageFormPage(KofaEditFormPage):
1668    """ Page to manage the student payments
1669
1670    This manage form page is for both students and students officers.
1671    """
1672    grok.context(IStudentPaymentsContainer)
1673    grok.name('index')
1674    grok.require('waeup.viewStudent')
1675    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1676    grok.template('paymentsmanagepage')
1677    pnav = 4
1678
1679    @property
1680    def manage_payments_allowed(self):
1681        return checkPermission('waeup.payStudent', self.context)
1682
1683    def unremovable(self, ticket):
1684        usertype = getattr(self.request.principal, 'user_type', None)
1685        if not usertype:
1686            return False
1687        if not self.manage_payments_allowed:
1688            return True
1689        return (self.request.principal.user_type == 'student' and ticket.r_code)
1690
1691    @property
1692    def label(self):
1693        return _('${a}: Payments',
1694            mapping = {'a':self.context.__parent__.display_fullname})
1695
1696    @jsaction(_('Remove selected tickets'))
1697    def delPaymentTicket(self, **data):
1698        form = self.request.form
1699        if 'val_id' in form:
1700            child_id = form['val_id']
1701        else:
1702            self.flash(_('No payment selected.'), type="warning")
1703            self.redirect(self.url(self.context))
1704            return
1705        if not isinstance(child_id, list):
1706            child_id = [child_id]
1707        deleted = []
1708        for id in child_id:
1709            # Students are not allowed to remove used payment tickets
1710            ticket = self.context.get(id, None)
1711            if ticket is not None and not self.unremovable(ticket):
1712                del self.context[id]
1713                deleted.append(id)
1714        if len(deleted):
1715            self.flash(_('Successfully removed: ${a}',
1716                mapping = {'a': ', '.join(deleted)}))
1717            self.context.writeLogMessage(
1718                self,'removed: %s' % ', '.join(deleted))
1719        self.redirect(self.url(self.context))
1720        return
1721
1722    #@action(_('Add online payment ticket'))
1723    #def addPaymentTicket(self, **data):
1724    #    self.redirect(self.url(self.context, '@@addop'))
1725
1726class OnlinePaymentAddFormPage(KofaAddFormPage):
1727    """ Page to add an online payment ticket
1728    """
1729    grok.context(IStudentPaymentsContainer)
1730    grok.name('addop')
1731    grok.template('onlinepaymentaddform')
1732    grok.require('waeup.payStudent')
1733    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1734        'p_category')
1735    label = _('Add online payment')
1736    pnav = 4
1737
1738    @property
1739    def selectable_categories(self):
1740        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1741        return sorted(categories.items())
1742
1743    @action(_('Create ticket'), style='primary')
1744    def createTicket(self, **data):
1745        p_category = data['p_category']
1746        previous_session = data.get('p_session', None)
1747        previous_level = data.get('p_level', None)
1748        student = self.context.__parent__
1749        # The hostel_application payment category is temporarily used
1750        # by Uniben.
1751        if p_category in ('bed_allocation', 'hostel_application') and student[
1752            'studycourse'].current_session != grok.getSite()[
1753            'hostels'].accommodation_session:
1754                self.flash(
1755                    _('Your current session does not match ' + \
1756                    'accommodation session.'), type="danger")
1757                return
1758        if 'maintenance' in p_category:
1759            current_session = str(student['studycourse'].current_session)
1760            if not current_session in student['accommodation']:
1761                self.flash(_('You have not yet booked accommodation.'),
1762                           type="warning")
1763                return
1764        students_utils = getUtility(IStudentsUtils)
1765        error, payment = students_utils.setPaymentDetails(
1766            p_category, student, previous_session, previous_level)
1767        if error is not None:
1768            self.flash(error, type="danger")
1769            return
1770        if p_category == 'transfer':
1771            payment.p_item = self.request.form['new_programme']
1772        self.context[payment.p_id] = payment
1773        self.flash(_('Payment ticket created.'))
1774        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1775        self.redirect(self.url(self.context))
1776        return
1777
1778    @action(_('Cancel'), validator=NullValidator)
1779    def cancel(self, **data):
1780        self.redirect(self.url(self.context))
1781
1782class PreviousPaymentAddFormPage(KofaAddFormPage):
1783    """ Page to add an online payment ticket for previous sessions.
1784    """
1785    grok.context(IStudentPaymentsContainer)
1786    grok.name('addpp')
1787    grok.require('waeup.payStudent')
1788    form_fields = grok.AutoFields(IStudentPreviousPayment)
1789    label = _('Add previous session online payment')
1790    pnav = 4
1791
1792    def update(self):
1793        if self.context.student.before_payment:
1794            self.flash(_("No previous payment to be made."), type="warning")
1795            self.redirect(self.url(self.context))
1796        super(PreviousPaymentAddFormPage, self).update()
1797        return
1798
1799    @action(_('Create ticket'), style='primary')
1800    def createTicket(self, **data):
1801        p_category = data['p_category']
1802        previous_session = data.get('p_session', None)
1803        previous_level = data.get('p_level', None)
1804        student = self.context.__parent__
1805        students_utils = getUtility(IStudentsUtils)
1806        error, payment = students_utils.setPaymentDetails(
1807            p_category, student, previous_session, previous_level)
1808        if error is not None:
1809            self.flash(error, type="danger")
1810            return
1811        self.context[payment.p_id] = payment
1812        self.flash(_('Payment ticket created.'))
1813        self.redirect(self.url(self.context))
1814        return
1815
1816    @action(_('Cancel'), validator=NullValidator)
1817    def cancel(self, **data):
1818        self.redirect(self.url(self.context))
1819
1820class BalancePaymentAddFormPage(KofaAddFormPage):
1821    """ Page to add an online payment which can balance s previous session
1822    payment.
1823    """
1824    grok.context(IStudentPaymentsContainer)
1825    grok.name('addbp')
1826    grok.require('waeup.manageStudent')
1827    form_fields = grok.AutoFields(IStudentBalancePayment)
1828    label = _('Add balance')
1829    pnav = 4
1830
1831    @action(_('Create ticket'), style='primary')
1832    def createTicket(self, **data):
1833        p_category = data['p_category']
1834        balance_session = data.get('balance_session', None)
1835        balance_level = data.get('balance_level', None)
1836        balance_amount = data.get('balance_amount', None)
1837        student = self.context.__parent__
1838        students_utils = getUtility(IStudentsUtils)
1839        error, payment = students_utils.setBalanceDetails(
1840            p_category, student, balance_session,
1841            balance_level, balance_amount)
1842        if error is not None:
1843            self.flash(error, type="danger")
1844            return
1845        self.context[payment.p_id] = payment
1846        self.flash(_('Payment ticket created.'))
1847        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1848        self.redirect(self.url(self.context))
1849        return
1850
1851    @action(_('Cancel'), validator=NullValidator)
1852    def cancel(self, **data):
1853        self.redirect(self.url(self.context))
1854
1855class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1856    """ Page to view an online payment ticket
1857    """
1858    grok.context(IStudentOnlinePayment)
1859    grok.name('index')
1860    grok.require('waeup.viewStudent')
1861    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1862    form_fields[
1863        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1864    form_fields[
1865        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1866    pnav = 4
1867
1868    @property
1869    def label(self):
1870        return _('${a}: Online Payment Ticket ${b}', mapping = {
1871            'a':self.context.student.display_fullname,
1872            'b':self.context.p_id})
1873
1874class OnlinePaymentApproveView(UtilityView, grok.View):
1875    """ Callback view
1876    """
1877    grok.context(IStudentOnlinePayment)
1878    grok.name('approve')
1879    grok.require('waeup.managePortal')
1880
1881    def update(self):
1882        flashtype, msg, log = self.context.approveStudentPayment()
1883        if log is not None:
1884            # Add log message to students.log
1885            self.context.writeLogMessage(self,log)
1886            # Add log message to payments.log
1887            self.context.logger.info(
1888                '%s,%s,%s,%s,%s,,,,,,' % (
1889                self.context.student.student_id,
1890                self.context.p_id, self.context.p_category,
1891                self.context.amount_auth, self.context.r_code))
1892        self.flash(msg, type=flashtype)
1893        return
1894
1895    def render(self):
1896        self.redirect(self.url(self.context, '@@index'))
1897        return
1898
1899class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
1900    """ Approval view for students.
1901
1902    This view is used for browser tests only and
1903    must be neutralized in custom pages!
1904    """
1905    grok.name('fake_approve')
1906    grok.require('waeup.payStudent')
1907
1908class ExportPDFPaymentSlip(UtilityView, grok.View):
1909    """Deliver a PDF slip of the context.
1910    """
1911    grok.context(IStudentOnlinePayment)
1912    grok.name('payment_slip.pdf')
1913    grok.require('waeup.viewStudent')
1914    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1915    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1916    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1917    prefix = 'form'
1918    note = None
1919    omit_fields = (
1920        'password', 'suspended', 'phone', 'date_of_birth',
1921        'adm_code', 'sex', 'suspended_comment', 'current_level')
1922
1923    @property
1924    def title(self):
1925        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1926        return translate(_('Payment Data'), 'waeup.kofa',
1927            target_language=portal_language)
1928
1929    @property
1930    def label(self):
1931        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1932        return translate(_('Online Payment Slip'),
1933            'waeup.kofa', target_language=portal_language) \
1934            + ' %s' % self.context.p_id
1935
1936    def render(self):
1937        #if self.context.p_state != 'paid':
1938        #    self.flash('Ticket not yet paid.')
1939        #    self.redirect(self.url(self.context))
1940        #    return
1941        studentview = StudentBasePDFFormPage(self.context.student,
1942            self.request, self.omit_fields)
1943        students_utils = getUtility(IStudentsUtils)
1944        return students_utils.renderPDF(self, 'payment_slip.pdf',
1945            self.context.student, studentview, note=self.note,
1946            omit_fields=self.omit_fields)
1947
1948
1949class AccommodationManageFormPage(KofaEditFormPage):
1950    """ Page to manage bed tickets.
1951
1952    This manage form page is for both students and students officers.
1953    """
1954    grok.context(IStudentAccommodation)
1955    grok.name('index')
1956    grok.require('waeup.handleAccommodation')
1957    form_fields = grok.AutoFields(IStudentAccommodation)
1958    grok.template('accommodationmanagepage')
1959    pnav = 4
1960    with_hostel_selection = True
1961
1962    @property
1963    def actionsgroup1(self):
1964        if not self.with_hostel_selection:
1965            return []
1966        students_utils = getUtility(IStudentsUtils)
1967        acc_details  = students_utils.getAccommodationDetails(self.context.student)
1968        error_message = students_utils.checkAccommodationRequirements(
1969            self.context.student, acc_details)
1970        if error_message:
1971            return []
1972        return [_('Save')]
1973
1974    @property
1975    def actionsgroup2(self):
1976        if getattr(self.request.principal, 'user_type', None) == 'student':
1977            return [_('Book accommodation')]
1978        return [_('Book accommodation'), _('Remove selected')]
1979
1980    @property
1981    def label(self):
1982        return _('${a}: Accommodation',
1983            mapping = {'a':self.context.__parent__.display_fullname})
1984
1985    @property
1986    def desired_hostel(self):
1987        if self.context.desired_hostel:
1988            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
1989            if hostel is not None:
1990                return hostel.hostel_name
1991        return
1992
1993    def getHostels(self):
1994        """Get a list of all stored hostels.
1995        """
1996        yield(dict(name=None, title='--', selected=''))
1997        for val in grok.getSite()['hostels'].values():
1998            selected = ''
1999            if val.hostel_id == self.context.desired_hostel:
2000                selected = 'selected'
2001            yield(dict(name=val.hostel_id, title=val.hostel_name,
2002                       selected=selected))
2003
2004    @action(_('Save'), style='primary')
2005    def save(self):
2006        hostel = self.request.form.get('hostel', None)
2007        self.context.desired_hostel = hostel
2008        self.flash(_('Your selection has been saved.'))
2009        return
2010
2011    @action(_('Book accommodation'), style='primary')
2012    def bookAccommodation(self, **data):
2013        self.redirect(self.url(self.context, 'add'))
2014        return
2015
2016    @jsaction(_('Remove selected'))
2017    def delBedTickets(self, **data):
2018        if getattr(self.request.principal, 'user_type', None) == 'student':
2019            self.flash(_('You are not allowed to remove bed tickets.'),
2020                       type="warning")
2021            self.redirect(self.url(self.context))
2022            return
2023        form = self.request.form
2024        if 'val_id' in form:
2025            child_id = form['val_id']
2026        else:
2027            self.flash(_('No bed ticket selected.'), type="warning")
2028            self.redirect(self.url(self.context))
2029            return
2030        if not isinstance(child_id, list):
2031            child_id = [child_id]
2032        deleted = []
2033        for id in child_id:
2034            del self.context[id]
2035            deleted.append(id)
2036        if len(deleted):
2037            self.flash(_('Successfully removed: ${a}',
2038                mapping = {'a':', '.join(deleted)}))
2039            self.context.writeLogMessage(
2040                self,'removed: % s' % ', '.join(deleted))
2041        self.redirect(self.url(self.context))
2042        return
2043
2044class BedTicketAddPage(KofaPage):
2045    """ Page to add an online payment ticket
2046    """
2047    grok.context(IStudentAccommodation)
2048    grok.name('add')
2049    grok.require('waeup.handleAccommodation')
2050    grok.template('enterpin')
2051    ac_prefix = 'HOS'
2052    label = _('Add bed ticket')
2053    pnav = 4
2054    buttonname = _('Create bed ticket')
2055    notice = ''
2056    with_ac = True
2057
2058    def update(self, SUBMIT=None):
2059        student = self.context.student
2060        students_utils = getUtility(IStudentsUtils)
2061        acc_details  = students_utils.getAccommodationDetails(student)
2062        error_message = students_utils.checkAccommodationRequirements(
2063            student, acc_details)
2064        if error_message:
2065            self.flash(error_message, type="warning")
2066            self.redirect(self.url(self.context))
2067            return
2068        if self.with_ac:
2069            self.ac_series = self.request.form.get('ac_series', None)
2070            self.ac_number = self.request.form.get('ac_number', None)
2071        if SUBMIT is None:
2072            return
2073        if self.with_ac:
2074            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2075            code = get_access_code(pin)
2076            if not code:
2077                self.flash(_('Activation code is invalid.'), type="warning")
2078                return
2079        # Search and book bed
2080        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2081        entries = cat.searchResults(
2082            owner=(student.student_id,student.student_id))
2083        if len(entries):
2084            # If bed space has been manually allocated use this bed
2085            manual = True
2086            bed = [entry for entry in entries][0]
2087            # Safety belt for paranoids: Does this bed really exist on portal?
2088            # XXX: Can be remove if nobody complains.
2089            if bed.__parent__.__parent__ is None:
2090                self.flash(_('System error: Please contact the adminsitrator.'),
2091                           type="danger")
2092                self.context.writeLogMessage(
2093                    self, 'fatal error: %s' % bed.bed_id)
2094                return
2095        else:
2096            # else search for other available beds
2097            manual = False
2098            entries = cat.searchResults(
2099                bed_type=(acc_details['bt'],acc_details['bt']))
2100            available_beds = [
2101                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2102            if available_beds:
2103                students_utils = getUtility(IStudentsUtils)
2104                bed = students_utils.selectBed(
2105                    available_beds, self.context.desired_hostel)
2106                if bed is None:
2107                    self.flash(_(
2108                        'There is no free bed in your desired hostel. '
2109                        'Please try another hostel.'),
2110                        type="warning")
2111                    self.redirect(self.url(self.context))
2112                    return
2113                # Safety belt for paranoids: Does this bed really exist
2114                # in portal?
2115                # XXX: Can be remove if nobody complains.
2116                if bed.__parent__.__parent__ is None:
2117                    self.flash(_(
2118                        'System error: Please contact the administrator.'),
2119                        type="warning")
2120                    self.context.writeLogMessage(
2121                        self, 'fatal error: %s' % bed.bed_id)
2122                    return
2123                bed.bookBed(student.student_id)
2124            else:
2125                self.flash(_('There is no free bed in your category ${a}.',
2126                    mapping = {'a':acc_details['bt']}), type="warning")
2127                self.redirect(self.url(self.context))
2128                return
2129        if self.with_ac:
2130            # Mark pin as used (this also fires a pin related transition)
2131            if code.state == USED:
2132                self.flash(_('Activation code has already been used.'),
2133                           type="warning")
2134                if not manual:
2135                    # Release the previously booked bed
2136                    bed.owner = NOT_OCCUPIED
2137                    # Catalog must be informed
2138                    notify(grok.ObjectModifiedEvent(bed))
2139                return
2140            else:
2141                comment = _(u'invalidated')
2142                # Here we know that the ac is in state initialized so we do not
2143                # expect an exception, but the owner might be different
2144                success = invalidate_accesscode(
2145                    pin, comment, self.context.student.student_id)
2146                if not success:
2147                    self.flash(_('You are not the owner of this access code.'),
2148                               type="warning")
2149                    if not manual:
2150                        # Release the previously booked bed
2151                        bed.owner = NOT_OCCUPIED
2152                        # Catalog must be informed
2153                        notify(grok.ObjectModifiedEvent(bed))
2154                    return
2155        # Create bed ticket
2156        bedticket = createObject(u'waeup.BedTicket')
2157        if self.with_ac:
2158            bedticket.booking_code = pin
2159        bedticket.booking_session = acc_details['booking_session']
2160        bedticket.bed_type = acc_details['bt']
2161        bedticket.bed = bed
2162        hall_title = bed.__parent__.hostel_name
2163        coordinates = bed.coordinates[1:]
2164        block, room_nr, bed_nr = coordinates
2165        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2166            'a':hall_title, 'b':block,
2167            'c':room_nr, 'd':bed_nr,
2168            'e':bed.bed_type})
2169        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2170        bedticket.bed_coordinates = translate(
2171            bc, 'waeup.kofa',target_language=portal_language)
2172        self.context.addBedTicket(bedticket)
2173        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2174        self.flash(_('Bed ticket created and bed booked: ${a}',
2175            mapping = {'a':bedticket.display_coordinates}))
2176        self.redirect(self.url(self.context))
2177        return
2178
2179class BedTicketDisplayFormPage(KofaDisplayFormPage):
2180    """ Page to display bed tickets
2181    """
2182    grok.context(IBedTicket)
2183    grok.name('index')
2184    grok.require('waeup.handleAccommodation')
2185    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2186    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2187    pnav = 4
2188
2189    @property
2190    def label(self):
2191        return _('Bed Ticket for Session ${a}',
2192            mapping = {'a':self.context.getSessionString()})
2193
2194class ExportPDFBedTicketSlip(UtilityView, grok.View):
2195    """Deliver a PDF slip of the context.
2196    """
2197    grok.context(IBedTicket)
2198    grok.name('bed_allocation_slip.pdf')
2199    grok.require('waeup.handleAccommodation')
2200    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2201    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2202    prefix = 'form'
2203    omit_fields = (
2204        'password', 'suspended', 'phone', 'adm_code',
2205        'suspended_comment', 'date_of_birth', 'current_level')
2206
2207    @property
2208    def title(self):
2209        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2210        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2211            target_language=portal_language)
2212
2213    @property
2214    def label(self):
2215        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2216        #return translate(_('Bed Allocation: '),
2217        #    'waeup.kofa', target_language=portal_language) \
2218        #    + ' %s' % self.context.bed_coordinates
2219        return translate(_('Bed Allocation Slip'),
2220            'waeup.kofa', target_language=portal_language) \
2221            + ' %s' % self.context.getSessionString()
2222
2223    def render(self):
2224        studentview = StudentBasePDFFormPage(self.context.student,
2225            self.request, self.omit_fields)
2226        students_utils = getUtility(IStudentsUtils)
2227        return students_utils.renderPDF(
2228            self, 'bed_allocation_slip.pdf',
2229            self.context.student, studentview,
2230            omit_fields=self.omit_fields)
2231
2232class BedTicketRelocationView(UtilityView, grok.View):
2233    """ Callback view
2234    """
2235    grok.context(IBedTicket)
2236    grok.name('relocate')
2237    grok.require('waeup.manageHostels')
2238
2239    # Relocate student if student parameters have changed or the bed_type
2240    # of the bed has changed
2241    def update(self):
2242        success, msg = self.context.relocateStudent()
2243        if not success:
2244            self.flash(msg, type="warning")
2245        else:
2246            self.flash(msg)
2247        self.redirect(self.url(self.context))
2248        return
2249
2250    def render(self):
2251        return
2252
2253class StudentHistoryPage(KofaPage):
2254    """ Page to display student history
2255    """
2256    grok.context(IStudent)
2257    grok.name('history')
2258    grok.require('waeup.viewStudent')
2259    grok.template('studenthistory')
2260    pnav = 4
2261
2262    @property
2263    def label(self):
2264        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2265
2266# Pages for students only
2267
2268class StudentBaseEditFormPage(KofaEditFormPage):
2269    """ View to edit student base data
2270    """
2271    grok.context(IStudent)
2272    grok.name('edit_base')
2273    grok.require('waeup.handleStudent')
2274    form_fields = grok.AutoFields(IStudentBase).select(
2275        'email', 'phone')
2276    label = _('Edit base data')
2277    pnav = 4
2278
2279    @action(_('Save'), style='primary')
2280    def save(self, **data):
2281        msave(self, **data)
2282        return
2283
2284class StudentChangePasswordPage(KofaEditFormPage):
2285    """ View to edit student passwords
2286    """
2287    grok.context(IStudent)
2288    grok.name('change_password')
2289    grok.require('waeup.handleStudent')
2290    grok.template('change_password')
2291    label = _('Change password')
2292    pnav = 4
2293
2294    @action(_('Save'), style='primary')
2295    def save(self, **data):
2296        form = self.request.form
2297        password = form.get('change_password', None)
2298        password_ctl = form.get('change_password_repeat', None)
2299        if password:
2300            validator = getUtility(IPasswordValidator)
2301            errors = validator.validate_password(password, password_ctl)
2302            if not errors:
2303                IUserAccount(self.context).setPassword(password)
2304                # Unset temporary password
2305                self.context.temp_password = None
2306                self.context.writeLogMessage(self, 'saved: password')
2307                self.flash(_('Password changed.'))
2308            else:
2309                self.flash( ' '.join(errors), type="warning")
2310        return
2311
2312class StudentFilesUploadPage(KofaPage):
2313    """ View to upload files by student
2314    """
2315    grok.context(IStudent)
2316    grok.name('change_portrait')
2317    grok.require('waeup.uploadStudentFile')
2318    grok.template('filesuploadpage')
2319    label = _('Upload portrait')
2320    pnav = 4
2321
2322    def update(self):
2323        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2324        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2325            emit_lock_message(self)
2326            return
2327        super(StudentFilesUploadPage, self).update()
2328        return
2329
2330class StartClearancePage(KofaPage):
2331    grok.context(IStudent)
2332    grok.name('start_clearance')
2333    grok.require('waeup.handleStudent')
2334    grok.template('enterpin')
2335    label = _('Start clearance')
2336    ac_prefix = 'CLR'
2337    notice = ''
2338    pnav = 4
2339    buttonname = _('Start clearance now')
2340    with_ac = True
2341
2342    @property
2343    def all_required_fields_filled(self):
2344        if not self.context.email:
2345            return _("Email address is missing."), 'edit_base'
2346        if not self.context.phone:
2347            return _("Phone number is missing."), 'edit_base'
2348        return
2349
2350    @property
2351    def portrait_uploaded(self):
2352        store = getUtility(IExtFileStore)
2353        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2354            return True
2355        return False
2356
2357    def update(self, SUBMIT=None):
2358        if not self.context.state == ADMITTED:
2359            self.flash(_("Wrong state"), type="warning")
2360            self.redirect(self.url(self.context))
2361            return
2362        if not self.portrait_uploaded:
2363            self.flash(_("No portrait uploaded."), type="warning")
2364            self.redirect(self.url(self.context, 'change_portrait'))
2365            return
2366        if self.all_required_fields_filled:
2367            arf_warning = self.all_required_fields_filled[0]
2368            arf_redirect = self.all_required_fields_filled[1]
2369            self.flash(arf_warning, type="warning")
2370            self.redirect(self.url(self.context, arf_redirect))
2371            return
2372        if self.with_ac:
2373            self.ac_series = self.request.form.get('ac_series', None)
2374            self.ac_number = self.request.form.get('ac_number', None)
2375        if SUBMIT is None:
2376            return
2377        if self.with_ac:
2378            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2379            code = get_access_code(pin)
2380            if not code:
2381                self.flash(_('Activation code is invalid.'), type="warning")
2382                return
2383            if code.state == USED:
2384                self.flash(_('Activation code has already been used.'),
2385                           type="warning")
2386                return
2387            # Mark pin as used (this also fires a pin related transition)
2388            # and fire transition start_clearance
2389            comment = _(u"invalidated")
2390            # Here we know that the ac is in state initialized so we do not
2391            # expect an exception, but the owner might be different
2392            if not invalidate_accesscode(pin, comment, self.context.student_id):
2393                self.flash(_('You are not the owner of this access code.'),
2394                           type="warning")
2395                return
2396            self.context.clr_code = pin
2397        IWorkflowInfo(self.context).fireTransition('start_clearance')
2398        self.flash(_('Clearance process has been started.'))
2399        self.redirect(self.url(self.context,'cedit'))
2400        return
2401
2402class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2403    """ View to edit student clearance data by student
2404    """
2405    grok.context(IStudent)
2406    grok.name('cedit')
2407    grok.require('waeup.handleStudent')
2408    label = _('Edit clearance data')
2409
2410    @property
2411    def form_fields(self):
2412        if self.context.is_postgrad:
2413            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2414                'clr_code', 'officer_comment')
2415        else:
2416            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2417                'clr_code', 'officer_comment')
2418        return form_fields
2419
2420    def update(self):
2421        if self.context.clearance_locked:
2422            emit_lock_message(self)
2423            return
2424        return super(StudentClearanceEditFormPage, self).update()
2425
2426    @action(_('Save'), style='primary')
2427    def save(self, **data):
2428        self.applyData(self.context, **data)
2429        self.flash(_('Clearance form has been saved.'))
2430        return
2431
2432    def dataNotComplete(self):
2433        """To be implemented in the customization package.
2434        """
2435        return False
2436
2437    @action(_('Save and request clearance'), style='primary')
2438    def requestClearance(self, **data):
2439        self.applyData(self.context, **data)
2440        if self.dataNotComplete():
2441            self.flash(self.dataNotComplete(), type="warning")
2442            return
2443        self.flash(_('Clearance form has been saved.'))
2444        if self.context.clr_code:
2445            self.redirect(self.url(self.context, 'request_clearance'))
2446        else:
2447            # We bypass the request_clearance page if student
2448            # has been imported in state 'clearance started' and
2449            # no clr_code was entered before.
2450            state = IWorkflowState(self.context).getState()
2451            if state != CLEARANCE:
2452                # This shouldn't happen, but the application officer
2453                # might have forgotten to lock the form after changing the state
2454                self.flash(_('This form cannot be submitted. Wrong state!'),
2455                           type="danger")
2456                return
2457            IWorkflowInfo(self.context).fireTransition('request_clearance')
2458            self.flash(_('Clearance has been requested.'))
2459            self.redirect(self.url(self.context))
2460        return
2461
2462class RequestClearancePage(KofaPage):
2463    grok.context(IStudent)
2464    grok.name('request_clearance')
2465    grok.require('waeup.handleStudent')
2466    grok.template('enterpin')
2467    label = _('Request clearance')
2468    notice = _('Enter the CLR access code used for starting clearance.')
2469    ac_prefix = 'CLR'
2470    pnav = 4
2471    buttonname = _('Request clearance now')
2472    with_ac = True
2473
2474    def update(self, SUBMIT=None):
2475        if self.with_ac:
2476            self.ac_series = self.request.form.get('ac_series', None)
2477            self.ac_number = self.request.form.get('ac_number', None)
2478        if SUBMIT is None:
2479            return
2480        if self.with_ac:
2481            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2482            if self.context.clr_code and self.context.clr_code != pin:
2483                self.flash(_("This isn't your CLR access code."), type="danger")
2484                return
2485        state = IWorkflowState(self.context).getState()
2486        if state != CLEARANCE:
2487            # This shouldn't happen, but the application officer
2488            # might have forgotten to lock the form after changing the state
2489            self.flash(_('This form cannot be submitted. Wrong state!'),
2490                       type="danger")
2491            return
2492        IWorkflowInfo(self.context).fireTransition('request_clearance')
2493        self.flash(_('Clearance has been requested.'))
2494        self.redirect(self.url(self.context))
2495        return
2496
2497class StartSessionPage(KofaPage):
2498    grok.context(IStudentStudyCourse)
2499    grok.name('start_session')
2500    grok.require('waeup.handleStudent')
2501    grok.template('enterpin')
2502    label = _('Start session')
2503    ac_prefix = 'SFE'
2504    notice = ''
2505    pnav = 4
2506    buttonname = _('Start now')
2507    with_ac = True
2508
2509    def update(self, SUBMIT=None):
2510        if not self.context.is_current:
2511            emit_lock_message(self)
2512            return
2513        super(StartSessionPage, self).update()
2514        if not self.context.next_session_allowed:
2515            self.flash(_("You are not entitled to start session."),
2516                       type="warning")
2517            self.redirect(self.url(self.context))
2518            return
2519        if self.with_ac:
2520            self.ac_series = self.request.form.get('ac_series', None)
2521            self.ac_number = self.request.form.get('ac_number', None)
2522        if SUBMIT is None:
2523            return
2524        if self.with_ac:
2525            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2526            code = get_access_code(pin)
2527            if not code:
2528                self.flash(_('Activation code is invalid.'), type="warning")
2529                return
2530            # Mark pin as used (this also fires a pin related transition)
2531            if code.state == USED:
2532                self.flash(_('Activation code has already been used.'),
2533                           type="warning")
2534                return
2535            else:
2536                comment = _(u"invalidated")
2537                # Here we know that the ac is in state initialized so we do not
2538                # expect an error, but the owner might be different
2539                if not invalidate_accesscode(
2540                    pin,comment,self.context.student.student_id):
2541                    self.flash(_('You are not the owner of this access code.'),
2542                               type="warning")
2543                    return
2544        try:
2545            if self.context.student.state == CLEARED:
2546                IWorkflowInfo(self.context.student).fireTransition(
2547                    'pay_first_school_fee')
2548            elif self.context.student.state == RETURNING:
2549                IWorkflowInfo(self.context.student).fireTransition(
2550                    'pay_school_fee')
2551            elif self.context.student.state == PAID:
2552                IWorkflowInfo(self.context.student).fireTransition(
2553                    'pay_pg_fee')
2554        except ConstraintNotSatisfied:
2555            self.flash(_('An error occurred, please contact the system administrator.'),
2556                       type="danger")
2557            return
2558        self.flash(_('Session started.'))
2559        self.redirect(self.url(self.context))
2560        return
2561
2562class AddStudyLevelFormPage(KofaEditFormPage):
2563    """ Page for students to add current study levels
2564    """
2565    grok.context(IStudentStudyCourse)
2566    grok.name('add')
2567    grok.require('waeup.handleStudent')
2568    grok.template('studyleveladdpage')
2569    form_fields = grok.AutoFields(IStudentStudyCourse)
2570    pnav = 4
2571
2572    @property
2573    def label(self):
2574        studylevelsource = StudyLevelSource().factory
2575        code = self.context.current_level
2576        title = studylevelsource.getTitle(self.context, code)
2577        return _('Add current level ${a}', mapping = {'a':title})
2578
2579    def update(self):
2580        if not self.context.is_current:
2581            emit_lock_message(self)
2582            return
2583        if self.context.student.state != PAID:
2584            emit_lock_message(self)
2585            return
2586        code = self.context.current_level
2587        if code is None:
2588            self.flash(_('Your data are incomplete'), type="danger")
2589            self.redirect(self.url(self.context))
2590            return
2591        super(AddStudyLevelFormPage, self).update()
2592        return
2593
2594    @action(_('Create course list now'), style='primary')
2595    def addStudyLevel(self, **data):
2596        studylevel = createObject(u'waeup.StudentStudyLevel')
2597        studylevel.level = self.context.current_level
2598        studylevel.level_session = self.context.current_session
2599        try:
2600            self.context.addStudentStudyLevel(
2601                self.context.certificate,studylevel)
2602        except KeyError:
2603            self.flash(_('This level exists.'), type="warning")
2604        except RequiredMissing:
2605            self.flash(_('Your data are incomplete'), type="danger")
2606        self.redirect(self.url(self.context))
2607        return
2608
2609class StudyLevelEditFormPage(KofaEditFormPage):
2610    """ Page to edit the student study level data by students
2611    """
2612    grok.context(IStudentStudyLevel)
2613    grok.name('edit')
2614    grok.require('waeup.editStudyLevel')
2615    grok.template('studyleveleditpage')
2616    pnav = 4
2617    placeholder = _('Enter valid course code')
2618
2619    def update(self, ADD=None, course=None):
2620        if not self.context.__parent__.is_current:
2621            emit_lock_message(self)
2622            return
2623        if self.context.student.state != PAID or \
2624            not self.context.is_current_level:
2625            emit_lock_message(self)
2626            return
2627        super(StudyLevelEditFormPage, self).update()
2628        if ADD is not None:
2629            if not course:
2630                self.flash(_('No valid course code entered.'), type="warning")
2631                return
2632            cat = queryUtility(ICatalog, name='courses_catalog')
2633            result = cat.searchResults(code=(course, course))
2634            if len(result) != 1:
2635                self.flash(_('Course not found.'), type="warning")
2636                return
2637            course = list(result)[0]
2638            addCourseTicket(self, course)
2639        return
2640
2641    @property
2642    def label(self):
2643        # Here we know that the cookie has been set
2644        lang = self.request.cookies.get('kofa.language')
2645        level_title = translate(self.context.level_title, 'waeup.kofa',
2646            target_language=lang)
2647        return _('Edit course list of ${a}',
2648            mapping = {'a':level_title})
2649
2650    @property
2651    def translated_values(self):
2652        return translated_values(self)
2653
2654    def _delCourseTicket(self, **data):
2655        form = self.request.form
2656        if 'val_id' in form:
2657            child_id = form['val_id']
2658        else:
2659            self.flash(_('No ticket selected.'), type="warning")
2660            self.redirect(self.url(self.context, '@@edit'))
2661            return
2662        if not isinstance(child_id, list):
2663            child_id = [child_id]
2664        deleted = []
2665        for id in child_id:
2666            # Students are not allowed to remove core tickets
2667            if id in self.context and \
2668                self.context[id].removable_by_student:
2669                del self.context[id]
2670                deleted.append(id)
2671        if len(deleted):
2672            self.flash(_('Successfully removed: ${a}',
2673                mapping = {'a':', '.join(deleted)}))
2674            self.context.writeLogMessage(
2675                self,'removed: %s at %s' %
2676                (', '.join(deleted), self.context.level))
2677        self.redirect(self.url(self.context, u'@@edit'))
2678        return
2679
2680    @jsaction(_('Remove selected tickets'))
2681    def delCourseTicket(self, **data):
2682        self._delCourseTicket(**data)
2683        return
2684
2685    def _registerCourses(self, **data):
2686        if self.context.student.is_postgrad and \
2687            not self.context.student.is_special_postgrad:
2688            self.flash(_(
2689                "You are a postgraduate student, "
2690                "your course list can't bee registered."), type="warning")
2691            self.redirect(self.url(self.context))
2692            return
2693        students_utils = getUtility(IStudentsUtils)
2694        max_credits = students_utils.maxCredits(self.context)
2695        if max_credits and self.context.total_credits > max_credits:
2696            self.flash(_('Maximum credits of ${a} exceeded.',
2697                mapping = {'a':max_credits}), type="warning")
2698            return
2699        if not self.context.course_registration_allowed:
2700            self.flash(_(
2701                "Course registration has ended. "
2702                "Please pay the late registration fee."), type="warning")
2703            #self.redirect(self.url(self.context))
2704            return
2705        IWorkflowInfo(self.context.student).fireTransition(
2706            'register_courses')
2707        self.flash(_('Course list has been registered.'))
2708        self.redirect(self.url(self.context))
2709        return
2710
2711    @action(_('Register course list'), style='primary',
2712        warning=_('You can not edit your course list after registration.'
2713            ' You really want to register?'))
2714    def registerCourses(self, **data):
2715        self._registerCourses(**data)
2716        return
2717
2718class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2719    """Add a course ticket by student.
2720    """
2721    grok.name('ctadd')
2722    grok.require('waeup.handleStudent')
2723    form_fields = grok.AutoFields(ICourseTicketAdd)
2724
2725    def update(self):
2726        if self.context.student.state != PAID or \
2727            not self.context.is_current_level:
2728            emit_lock_message(self)
2729            return
2730        super(CourseTicketAddFormPage2, self).update()
2731        return
2732
2733    @action(_('Add course ticket'))
2734    def addCourseTicket(self, **data):
2735        # Safety belt
2736        if self.context.student.state != PAID:
2737            return
2738        course = data['course']
2739        success = addCourseTicket(self, course)
2740        if success:
2741            self.redirect(self.url(self.context, u'@@edit'))
2742        return
2743
2744class SetPasswordPage(KofaPage):
2745    grok.context(IKofaObject)
2746    grok.name('setpassword')
2747    grok.require('waeup.Anonymous')
2748    grok.template('setpassword')
2749    label = _('Set password for first-time login')
2750    ac_prefix = 'PWD'
2751    pnav = 0
2752    set_button = _('Set')
2753
2754    def update(self, SUBMIT=None):
2755        self.reg_number = self.request.form.get('reg_number', None)
2756        self.ac_series = self.request.form.get('ac_series', None)
2757        self.ac_number = self.request.form.get('ac_number', None)
2758
2759        if SUBMIT is None:
2760            return
2761        hitlist = search(query=self.reg_number,
2762            searchtype='reg_number', view=self)
2763        if not hitlist:
2764            self.flash(_('No student found.'), type="warning")
2765            return
2766        if len(hitlist) != 1:   # Cannot happen but anyway
2767            self.flash(_('More than one student found.'), type="warning")
2768            return
2769        student = hitlist[0].context
2770        self.student_id = student.student_id
2771        student_pw = student.password
2772        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2773        code = get_access_code(pin)
2774        if not code:
2775            self.flash(_('Access code is invalid.'), type="warning")
2776            return
2777        if student_pw and pin == student.adm_code:
2778            self.flash(_(
2779                'Password has already been set. Your Student Id is ${a}',
2780                mapping = {'a':self.student_id}))
2781            return
2782        elif student_pw:
2783            self.flash(
2784                _('Password has already been set. You are using the ' +
2785                'wrong Access Code.'), type="warning")
2786            return
2787        # Mark pin as used (this also fires a pin related transition)
2788        # and set student password
2789        if code.state == USED:
2790            self.flash(_('Access code has already been used.'), type="warning")
2791            return
2792        else:
2793            comment = _(u"invalidated")
2794            # Here we know that the ac is in state initialized so we do not
2795            # expect an exception
2796            invalidate_accesscode(pin,comment)
2797            IUserAccount(student).setPassword(self.ac_number)
2798            student.adm_code = pin
2799        self.flash(_('Password has been set. Your Student Id is ${a}',
2800            mapping = {'a':self.student_id}))
2801        return
2802
2803class StudentRequestPasswordPage(KofaAddFormPage):
2804    """Captcha'd request password page for students.
2805    """
2806    grok.name('requestpw')
2807    grok.require('waeup.Anonymous')
2808    grok.template('requestpw')
2809    form_fields = grok.AutoFields(IStudentRequestPW).select(
2810        'lastname','number','email')
2811    label = _('Request password for first-time login')
2812
2813    def update(self):
2814        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2815        if blocker:
2816            self.flash(_('The portal is in maintenance mode. '
2817                        'Password request forms are temporarily disabled.'),
2818                       type='warning')
2819            self.redirect(self.url(self.context))
2820            return
2821        # Handle captcha
2822        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2823        self.captcha_result = self.captcha.verify(self.request)
2824        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2825        return
2826
2827    def _redirect(self, email, password, student_id):
2828        # Forward only email to landing page in base package.
2829        self.redirect(self.url(self.context, 'requestpw_complete',
2830            data = dict(email=email)))
2831        return
2832
2833    def _pw_used(self):
2834        # XXX: False if password has not been used. We need an extra
2835        #      attribute which remembers if student logged in.
2836        return True
2837
2838    @action(_('Send login credentials to email address'), style='primary')
2839    def get_credentials(self, **data):
2840        if not self.captcha_result.is_valid:
2841            # Captcha will display error messages automatically.
2842            # No need to flash something.
2843            return
2844        number = data.get('number','')
2845        lastname = data.get('lastname','')
2846        cat = getUtility(ICatalog, name='students_catalog')
2847        results = list(
2848            cat.searchResults(reg_number=(number, number)))
2849        if not results:
2850            results = list(
2851                cat.searchResults(matric_number=(number, number)))
2852        if results:
2853            student = results[0]
2854            if getattr(student,'lastname',None) is None:
2855                self.flash(_('An error occurred.'), type="danger")
2856                return
2857            elif student.lastname.lower() != lastname.lower():
2858                # Don't tell the truth here. Anonymous must not
2859                # know that a record was found and only the lastname
2860                # verification failed.
2861                self.flash(_('No student record found.'), type="warning")
2862                return
2863            elif student.password is not None and self._pw_used:
2864                self.flash(_('Your password has already been set and used. '
2865                             'Please proceed to the login page.'),
2866                           type="warning")
2867                return
2868            # Store email address but nothing else.
2869            student.email = data['email']
2870            notify(grok.ObjectModifiedEvent(student))
2871        else:
2872            # No record found, this is the truth.
2873            self.flash(_('No student record found.'), type="warning")
2874            return
2875
2876        kofa_utils = getUtility(IKofaUtils)
2877        password = kofa_utils.genPassword()
2878        mandate = PasswordMandate()
2879        mandate.params['password'] = password
2880        mandate.params['user'] = student
2881        site = grok.getSite()
2882        site['mandates'].addMandate(mandate)
2883        # Send email with credentials
2884        args = {'mandate_id':mandate.mandate_id}
2885        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2886        url_info = u'Confirmation link: %s' % mandate_url
2887        msg = _('You have successfully requested a password for the')
2888        if kofa_utils.sendCredentials(IUserAccount(student),
2889            password, url_info, msg):
2890            email_sent = student.email
2891        else:
2892            email_sent = None
2893        self._redirect(email=email_sent, password=password,
2894            student_id=student.student_id)
2895        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2896        self.context.logger.info(
2897            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2898        return
2899
2900class StudentRequestPasswordEmailSent(KofaPage):
2901    """Landing page after successful password request.
2902
2903    """
2904    grok.name('requestpw_complete')
2905    grok.require('waeup.Public')
2906    grok.template('requestpwmailsent')
2907    label = _('Your password request was successful.')
2908
2909    def update(self, email=None, student_id=None, password=None):
2910        self.email = email
2911        self.password = password
2912        self.student_id = student_id
2913        return
2914
2915class FilterStudentsInDepartmentPage(KofaPage):
2916    """Page that filters and lists students.
2917    """
2918    grok.context(IDepartment)
2919    grok.require('waeup.showStudents')
2920    grok.name('students')
2921    grok.template('filterstudentspage')
2922    pnav = 1
2923    session_label = _('Current Session')
2924    level_label = _('Current Level')
2925
2926    def label(self):
2927        return 'Students in %s' % self.context.longtitle
2928
2929    def _set_session_values(self):
2930        vocab_terms = academic_sessions_vocab.by_value.values()
2931        self.sessions = sorted(
2932            [(x.title, x.token) for x in vocab_terms], reverse=True)
2933        self.sessions += [('All Sessions', 'all')]
2934        return
2935
2936    def _set_level_values(self):
2937        vocab_terms = course_levels.by_value.values()
2938        self.levels = sorted(
2939            [(x.title, x.token) for x in vocab_terms])
2940        self.levels += [('All Levels', 'all')]
2941        return
2942
2943    def _searchCatalog(self, session, level):
2944        if level not in (10, 999, None):
2945            start_level = 100 * (level // 100)
2946            end_level = start_level + 90
2947        else:
2948            start_level = end_level = level
2949        cat = queryUtility(ICatalog, name='students_catalog')
2950        students = cat.searchResults(
2951            current_session=(session, session),
2952            current_level=(start_level, end_level),
2953            depcode=(self.context.code, self.context.code)
2954            )
2955        hitlist = []
2956        for student in students:
2957            hitlist.append(StudentQueryResultItem(student, view=self))
2958        return hitlist
2959
2960    def update(self, SHOW=None, session=None, level=None):
2961        self.parent_url = self.url(self.context.__parent__)
2962        self._set_session_values()
2963        self._set_level_values()
2964        self.hitlist = []
2965        self.session_default = session
2966        self.level_default = level
2967        if SHOW is not None:
2968            if session != 'all':
2969                self.session = int(session)
2970                self.session_string = '%s %s/%s' % (
2971                    self.session_label, self.session, self.session+1)
2972            else:
2973                self.session = None
2974                self.session_string = _('in any session')
2975            if level != 'all':
2976                self.level = int(level)
2977                self.level_string = '%s %s' % (self.level_label, self.level)
2978            else:
2979                self.level = None
2980                self.level_string = _('at any level')
2981            self.hitlist = self._searchCatalog(self.session, self.level)
2982            if not self.hitlist:
2983                self.flash(_('No student found.'), type="warning")
2984        return
2985
2986class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2987    """Page that filters and lists students.
2988    """
2989    grok.context(ICertificate)
2990
2991    def label(self):
2992        return 'Students studying %s' % self.context.longtitle
2993
2994    def _searchCatalog(self, session, level):
2995        if level not in (10, 999, None):
2996            start_level = 100 * (level // 100)
2997            end_level = start_level + 90
2998        else:
2999            start_level = end_level = level
3000        cat = queryUtility(ICatalog, name='students_catalog')
3001        students = cat.searchResults(
3002            current_session=(session, session),
3003            current_level=(start_level, end_level),
3004            certcode=(self.context.code, self.context.code)
3005            )
3006        hitlist = []
3007        for student in students:
3008            hitlist.append(StudentQueryResultItem(student, view=self))
3009        return hitlist
3010
3011class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3012    """Page that filters and lists students.
3013    """
3014    grok.context(ICourse)
3015
3016    session_label = _('Session')
3017    level_label = _('Level')
3018
3019    def label(self):
3020        return 'Students registered for %s' % self.context.longtitle
3021
3022    def _searchCatalog(self, session, level):
3023        if level not in (10, 999, None):
3024            start_level = 100 * (level // 100)
3025            end_level = start_level + 90
3026        else:
3027            start_level = end_level = level
3028        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3029        coursetickets = cat.searchResults(
3030            session=(session, session),
3031            level=(start_level, end_level),
3032            code=(self.context.code, self.context.code)
3033            )
3034        hitlist = []
3035        for ticket in coursetickets:
3036            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3037        return list(set(hitlist))
3038
3039class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3040    """ Clear all students of a department in state 'clearance requested'.
3041    """
3042    grok.context(IDepartment)
3043    grok.name('clearallstudents')
3044    grok.require('waeup.clearAllStudents')
3045
3046    def update(self):
3047        cat = queryUtility(ICatalog, name='students_catalog')
3048        students = cat.searchResults(
3049            depcode=(self.context.code, self.context.code),
3050            state=(REQUESTED, REQUESTED)
3051            )
3052        num = 0
3053        for student in students:
3054            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3055                continue
3056            IWorkflowInfo(student).fireTransition('clear')
3057            num += 1
3058        self.flash(_('%d students have been cleared.' % num))
3059        self.redirect(self.url(self.context))
3060        return
3061
3062    def render(self):
3063        return
3064
3065
3066class EditScoresPage(KofaPage):
3067    """Page that filters and lists students.
3068    """
3069    grok.context(ICourse)
3070    grok.require('waeup.editScores')
3071    grok.name('edit_scores')
3072    grok.template('editscorespage')
3073    pnav = 1
3074
3075    def label(self):
3076        session = academic_sessions_vocab.getTerm(
3077            self.current_academic_session).title
3078        return '%s tickets in academic session %s' % (
3079            self.context.code, session)
3080
3081    def _searchCatalog(self, session):
3082        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3083        coursetickets = cat.searchResults(
3084            session=(session, session),
3085            code=(self.context.code, self.context.code)
3086            )
3087        return list(coursetickets)
3088
3089    def update(self,  *args, **kw):
3090        form = self.request.form
3091        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3092        self.current_academic_session = grok.getSite()[
3093            'configuration'].current_academic_session
3094        if self.context.__parent__.__parent__.score_editing_disabled:
3095            self.flash(_('Score editing disabled.'), type="warning")
3096            self.redirect(self.url(self.context))
3097            return
3098        if not self.current_academic_session:
3099            self.flash(_('Current academic session not set.'), type="warning")
3100            self.redirect(self.url(self.context))
3101            return
3102        self.tickets = self._searchCatalog(self.current_academic_session)
3103        editable_tickets = [
3104            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3105        if not self.tickets:
3106            self.flash(_('No student found.'), type="warning")
3107            self.redirect(self.url(self.context))
3108            return
3109        if 'UPDATE' in form:
3110            tno = 0
3111            error = ''
3112            if not editable_tickets:
3113                return
3114            scores = form['scores']
3115            if isinstance(scores, basestring):
3116                scores = [scores]
3117            for ticket in editable_tickets:
3118                score = ticket.score
3119                if scores[tno] == '':
3120                    score = None
3121                else:
3122                    try:
3123                        score = int(scores[tno])
3124                    except ValueError:
3125                        error += '%s, ' % ticket.student.display_fullname
3126                if ticket.score != score:
3127                    ticket.score = score
3128                    ticket.student.__parent__.logger.info(
3129                        '%s - %s %s/%s score updated (%s)' %
3130                        (ob_class, ticket.student.student_id,
3131                         ticket.level, ticket.code, score))
3132                    #notify(grok.ObjectModifiedEvent(ticket))
3133                tno += 1
3134            if error:
3135                self.flash(_('Error: Score(s) of %s have not be updated. '
3136                  'Only integers are allowed.' % error.strip(', ')),
3137                  type="danger")
3138        return
3139
3140class ExportJobContainerOverview(KofaPage):
3141    """Page that lists active student data export jobs and provides links
3142    to discard or download CSV files.
3143
3144    """
3145    grok.context(VirtualExportJobContainer)
3146    grok.require('waeup.showStudents')
3147    grok.name('index.html')
3148    grok.template('exportjobsindex')
3149    label = _('Student Data Exports')
3150    pnav = 1
3151    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3152
3153    def update(self, CREATE=None, DISCARD=None, job_id=None):
3154        if CREATE:
3155            self.redirect(self.url('@@exportconfig'))
3156            return
3157        if DISCARD and job_id:
3158            entry = self.context.entry_from_job_id(job_id)
3159            self.context.delete_export_entry(entry)
3160            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3161            self.context.logger.info(
3162                '%s - discarded: job_id=%s' % (ob_class, job_id))
3163            self.flash(_('Discarded export') + ' %s' % job_id)
3164        self.entries = doll_up(self, user=self.request.principal.id)
3165        return
3166
3167class ExportJobContainerJobConfig(KofaPage):
3168    """Page that configures a students export job.
3169
3170    This is a baseclass.
3171    """
3172    grok.baseclass()
3173    grok.name('exportconfig')
3174    grok.require('waeup.showStudents')
3175    grok.template('exportconfig')
3176    label = _('Configure student data export')
3177    pnav = 1
3178    redirect_target = ''
3179    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3180
3181    def _set_session_values(self):
3182        vocab_terms = academic_sessions_vocab.by_value.values()
3183        self.sessions = sorted(
3184            [(x.title, x.token) for x in vocab_terms], reverse=True)
3185        self.sessions += [(_('All Sessions'), 'all')]
3186        return
3187
3188    def _set_level_values(self):
3189        vocab_terms = course_levels.by_value.values()
3190        self.levels = sorted(
3191            [(x.title, x.token) for x in vocab_terms])
3192        self.levels += [(_('All Levels'), 'all')]
3193        return
3194
3195    def _set_mode_values(self):
3196        utils = getUtility(IKofaUtils)
3197        self.modes = sorted([(value, key) for key, value in
3198                      utils.STUDY_MODES_DICT.items()])
3199        self.modes +=[(_('All Modes'), 'all')]
3200        return
3201
3202    def _set_exporter_values(self):
3203        # We provide all student exporters, nothing else, yet.
3204        # Bursary or Department Officers don't have the general exportData
3205        # permission and are only allowed to export bursary or payments
3206        # overview data respectively. This is the only place where
3207        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3208        # are used.
3209        exporters = []
3210        if not checkPermission('waeup.exportData', self.context):
3211            if checkPermission('waeup.exportBursaryData', self.context):
3212                exporters += [('Bursary Data', 'bursary')]
3213            if checkPermission('waeup.exportPaymentsOverview', self.context):
3214                exporters += [('Student Payments Overview', 'paymentsoverview')]
3215            self.exporters = exporters
3216            return
3217        STUDENT_EXPORTER_NAMES = getUtility(
3218            IStudentsUtils).STUDENT_EXPORTER_NAMES
3219        for name in STUDENT_EXPORTER_NAMES:
3220            util = getUtility(ICSVExporter, name=name)
3221            exporters.append((util.title, name),)
3222        self.exporters = exporters
3223        return
3224
3225    @property
3226    def faccode(self):
3227        return None
3228
3229    @property
3230    def depcode(self):
3231        return None
3232
3233    @property
3234    def certcode(self):
3235        return None
3236
3237    def update(self, START=None, session=None, level=None, mode=None,
3238               payments_start=None, payments_end=None,
3239               exporter=None):
3240        self._set_session_values()
3241        self._set_level_values()
3242        self._set_mode_values()
3243        self._set_exporter_values()
3244        if START is None:
3245            return
3246        ena = exports_not_allowed(self)
3247        if ena:
3248            self.flash(ena, type='danger')
3249            return
3250        if payments_start or payments_end:
3251            date_format = '%d/%m/%Y'
3252            try:
3253                dummy = datetime.strptime(payments_start, date_format)
3254                dummy = datetime.strptime(payments_end, date_format)
3255            except ValueError:
3256                self.flash(_('Payment dates do not match format d/m/Y.'),
3257                           type="danger")
3258                return
3259        if session == 'all':
3260            session=None
3261        if level == 'all':
3262            level = None
3263        if mode == 'all':
3264            mode = None
3265        if payments_start == '':
3266            payments_start = None
3267        if payments_end == '':
3268            payments_end = None
3269        if (mode,
3270            level,
3271            session,
3272            self.faccode,
3273            self.depcode,
3274            self.certcode) == (None, None, None, None, None, None):
3275            # Export all students including those without certificate
3276            if payments_start:
3277                job_id = self.context.start_export_job(exporter,
3278                                              self.request.principal.id,
3279                                              payments_start = payments_start,
3280                                              payments_end = payments_end)
3281            else:
3282                job_id = self.context.start_export_job(exporter,
3283                                              self.request.principal.id)
3284        else:
3285            if payments_start:
3286                job_id = self.context.start_export_job(exporter,
3287                                              self.request.principal.id,
3288                                              current_session=session,
3289                                              current_level=level,
3290                                              current_mode=mode,
3291                                              faccode=self.faccode,
3292                                              depcode=self.depcode,
3293                                              certcode=self.certcode,
3294                                              payments_start = payments_start,
3295                                              payments_end = payments_end)
3296            else:
3297                job_id = self.context.start_export_job(exporter,
3298                                              self.request.principal.id,
3299                                              current_session=session,
3300                                              current_level=level,
3301                                              current_mode=mode,
3302                                              faccode=self.faccode,
3303                                              depcode=self.depcode,
3304                                              certcode=self.certcode)
3305        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3306        self.context.logger.info(
3307            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3308            % (ob_class, exporter, session, level, mode, self.faccode,
3309            self.depcode, self.certcode, payments_start, payments_end, job_id))
3310        self.flash(_('Export started for students with') +
3311                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3312                   session, level, mode))
3313        self.redirect(self.url(self.redirect_target))
3314        return
3315
3316class ExportJobContainerDownload(ExportCSVView):
3317    """Page that downloads a students export csv file.
3318
3319    """
3320    grok.context(VirtualExportJobContainer)
3321    grok.require('waeup.showStudents')
3322
3323class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3324    """Page that configures a students export job in datacenter.
3325
3326    """
3327    grok.context(IDataCenter)
3328    redirect_target = '@@export'
3329
3330class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3331    """Page that configures a students export job in datacenter.
3332
3333    """
3334    grok.name('exportselected')
3335    grok.context(IDataCenter)
3336    redirect_target = '@@export'
3337    grok.template('exportselected')
3338    label = _('Configure student data export')
3339
3340    def update(self, START=None, students=None, exporter=None):
3341        self._set_exporter_values()
3342        if START is None:
3343            return
3344        ena = exports_not_allowed(self)
3345        if ena:
3346            self.flash(ena, type='danger')
3347            return
3348        try:
3349            ids = students.replace(',', ' ').split()
3350        except:
3351            self.flash(sys.exc_info()[1])
3352            self.redirect(self.url(self.redirect_target))
3353            return
3354        job_id = self.context.start_export_job(
3355            exporter, self.request.principal.id, selected=ids)
3356        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3357        self.context.logger.info(
3358            '%s - selected students exported: %s, job_id=%s' %
3359            (ob_class, exporter, job_id))
3360        self.flash(_('Export of selected students started.'))
3361        self.redirect(self.url(self.redirect_target))
3362        return
3363
3364class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3365    """Page that configures a students export job in facultiescontainer.
3366
3367    """
3368    grok.context(VirtualFacultiesExportJobContainer)
3369
3370
3371class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3372    """Page that configures a students export job in faculties.
3373
3374    """
3375    grok.context(VirtualFacultyExportJobContainer)
3376
3377    @property
3378    def faccode(self):
3379        return self.context.__parent__.code
3380
3381class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3382    """Page that configures a students export job in departments.
3383
3384    """
3385    grok.context(VirtualDepartmentExportJobContainer)
3386
3387    @property
3388    def depcode(self):
3389        return self.context.__parent__.code
3390
3391class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3392    """Page that configures a students export job for certificates.
3393
3394    """
3395    grok.context(VirtualCertificateExportJobContainer)
3396    grok.template('exportconfig_certificate')
3397
3398    @property
3399    def certcode(self):
3400        return self.context.__parent__.code
3401
3402class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3403    """Page that configures a students export job for courses.
3404
3405    In contrast to department or certificate student data exports the
3406    coursetickets_catalog is searched here. Therefore the update
3407    method from the base class is customized.
3408    """
3409    grok.context(VirtualCourseExportJobContainer)
3410    grok.template('exportconfig_course')
3411
3412    def _set_exporter_values(self):
3413        # We provide only two exporters.
3414        exporters = []
3415        for name in ('students', 'coursetickets'):
3416            util = getUtility(ICSVExporter, name=name)
3417            exporters.append((util.title, name),)
3418        self.exporters = exporters
3419
3420    def update(self, START=None, session=None, level=None, mode=None,
3421               exporter=None):
3422        self._set_session_values()
3423        self._set_level_values()
3424        self._set_mode_values()
3425        self._set_exporter_values()
3426        if START is None:
3427            return
3428        ena = exports_not_allowed(self)
3429        if ena:
3430            self.flash(ena, type='danger')
3431            return
3432        if session == 'all':
3433            session = None
3434        if level == 'all':
3435            level = None
3436        job_id = self.context.start_export_job(exporter,
3437                                      self.request.principal.id,
3438                                      # Use a different catalog and
3439                                      # pass different keywords than
3440                                      # for the (default) students_catalog
3441                                      catalog='coursetickets',
3442                                      session=session,
3443                                      level=level,
3444                                      code=self.context.__parent__.code)
3445        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3446        self.context.logger.info(
3447            '%s - exported: %s (%s, %s, %s), job_id=%s'
3448            % (ob_class, exporter, session, level,
3449            self.context.__parent__.code, job_id))
3450        self.flash(_('Export started for course tickets with') +
3451                   ' level_session=%s, level=%s' % (
3452                   session, level))
3453        self.redirect(self.url(self.redirect_target))
3454        return
Note: See TracBrowser for help on using the repository browser.