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

Last change on this file since 15204 was 15203, checked in by Henrik Bettermann, 6 years ago

Implement study level 0 (Level Zero) option for storing for
orphaned course tickets (tickets without level information).
Add ticket_session field to ICourseTicket.

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