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

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

Implement 2-step validation process.

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