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

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

Add 'No favoured hostel' option.

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