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

Last change on this file since 15694 was 15694, checked in by Henrik Bettermann, 5 years ago

Enable signatures on course registration slip.

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