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

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

Add graduated students filter.

Inform students_catalog after setting workflow state.

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