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

Last change on this file since 14891 was 14891, checked in by Henrik Bettermann, 7 years ago

Old returning students get a discount of N30000 (science) and N28000 (non-science) respectively. we use the custom_textline_1 attribute of certificates to distinguish science and non-science students.

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