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

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

Add further permissions to the local ApplicationsManager? role and
make it assignable at applicants container level.

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