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

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

Add tests and adjust the documentation.

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