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

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

Add semester filter to Fix CourseTicketExporter.

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