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

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

Add AccommodationDisplayFormPage

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