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

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

Add LetterPDFCreator utility and use this pdf creator
for admission slips. letterhead_admission.jpg file
must exist in students/static.

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