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

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

ivama: The lecturers, agreed collectively that we should allow KOFA to fetch name into the name columns.

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