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

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

Don't allow students to add former courses in course lists.

  • Property svn:keywords set to Id
File size: 157.6 KB
Line 
1## $Id: browser.py 15970 2020-01-31 12:12:17Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import csv
21import grok
22import pytz
23import sys
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 on 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            if course.former_course:
2963                self.flash(_('Former courses can\'t be added.'), type="warning")
2964                return
2965            addCourseTicket(self, course)
2966        return
2967
2968    @property
2969    def label(self):
2970        # Here we know that the cookie has been set
2971        lang = self.request.cookies.get('kofa.language')
2972        level_title = translate(self.context.level_title, 'waeup.kofa',
2973            target_language=lang)
2974        return _('Edit course list of ${a}',
2975            mapping = {'a':level_title})
2976
2977    @property
2978    def translated_values(self):
2979        return translated_values(self)
2980
2981    def _delCourseTicket(self, **data):
2982        form = self.request.form
2983        if 'val_id' in form:
2984            child_id = form['val_id']
2985        else:
2986            self.flash(_('No ticket selected.'), type="warning")
2987            self.redirect(self.url(self.context, '@@edit'))
2988            return
2989        if not isinstance(child_id, list):
2990            child_id = [child_id]
2991        deleted = []
2992        for id in child_id:
2993            # Students are not allowed to remove core tickets
2994            if id in self.context and \
2995                self.context[id].removable_by_student:
2996                del self.context[id]
2997                deleted.append(id)
2998        if len(deleted):
2999            self.flash(_('Successfully removed: ${a}',
3000                mapping = {'a':', '.join(deleted)}))
3001            self.context.writeLogMessage(
3002                self,'removed: %s at %s' %
3003                (', '.join(deleted), self.context.level))
3004        self.redirect(self.url(self.context, u'@@edit'))
3005        return
3006
3007    @jsaction(_('Remove selected tickets'))
3008    def delCourseTicket(self, **data):
3009        self._delCourseTicket(**data)
3010        return
3011
3012    def _updateTickets(self, **data):
3013        cat = queryUtility(ICatalog, name='courses_catalog')
3014        invalidated = list()
3015        for value in self.context.values():
3016            result = cat.searchResults(code=(value.code, value.code))
3017            if len(result) != 1:
3018                course = None
3019            else:
3020                course = list(result)[0]
3021            invalid = self.context.updateCourseTicket(value, course)
3022            if invalid:
3023                invalidated.append(invalid)
3024        if invalidated:
3025            invalidated_string = ', '.join(invalidated)
3026            self.context.writeLogMessage(
3027                self, 'course tickets invalidated: %s' % invalidated_string)
3028        self.flash(_('All course tickets updated.'))
3029        return
3030
3031    @action(_('Update all tickets'),
3032        tooltip=_('Update all course parameters including course titles.'))
3033    def updateTickets(self, **data):
3034        self._updateTickets(**data)
3035        return
3036
3037    def _registerCourses(self, **data):
3038        if self.context.student.is_postgrad and \
3039            not self.context.student.is_special_postgrad:
3040            self.flash(_(
3041                "You are a postgraduate student, "
3042                "your course list can't bee registered."), type="warning")
3043            self.redirect(self.url(self.context))
3044            return
3045        students_utils = getUtility(IStudentsUtils)
3046        warning = students_utils.warnCreditsOOR(self.context)
3047        if warning:
3048            self.flash(warning, type="warning")
3049            return
3050        msg = self.context.course_registration_forbidden
3051        if msg:
3052            self.flash(msg, type="warning")
3053            return
3054        IWorkflowInfo(self.context.student).fireTransition(
3055            'register_courses')
3056        self.flash(_('Course list has been registered.'))
3057        self.redirect(self.url(self.context))
3058        return
3059
3060    @action(_('Register course list'), style='primary',
3061        warning=_('You can not edit your course list after registration.'
3062            ' You really want to register?'))
3063    def registerCourses(self, **data):
3064        self._registerCourses(**data)
3065        return
3066
3067class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3068    """Add a course ticket by student.
3069    """
3070    grok.name('ctadd')
3071    grok.require('waeup.handleStudent')
3072    form_fields = grok.AutoFields(ICourseTicketAdd)
3073
3074    def update(self):
3075        if self.context.student.state != PAID or \
3076            not self.context.is_current_level:
3077            emit_lock_message(self)
3078            return
3079        super(CourseTicketAddFormPage2, self).update()
3080        return
3081
3082    @action(_('Add course ticket'))
3083    def addCourseTicket(self, **data):
3084        # Safety belt
3085        if self.context.student.state != PAID:
3086            return
3087        course = data['course']
3088        if course.former_course:
3089            self.flash(_('Former courses can\'t be added.'), type="warning")
3090            return
3091        success = addCourseTicket(self, course)
3092        if success:
3093            self.redirect(self.url(self.context, u'@@edit'))
3094        return
3095
3096class SetPasswordPage(KofaPage):
3097    grok.context(IKofaObject)
3098    grok.name('setpassword')
3099    grok.require('waeup.Anonymous')
3100    grok.template('setpassword')
3101    label = _('Set password for first-time login')
3102    ac_prefix = 'PWD'
3103    pnav = 0
3104    set_button = _('Set')
3105
3106    def update(self, SUBMIT=None):
3107        self.reg_number = self.request.form.get('reg_number', None)
3108        self.ac_series = self.request.form.get('ac_series', None)
3109        self.ac_number = self.request.form.get('ac_number', None)
3110
3111        if SUBMIT is None:
3112            return
3113        hitlist = search(query=self.reg_number,
3114            searchtype='reg_number', view=self)
3115        if not hitlist:
3116            self.flash(_('No student found.'), type="warning")
3117            return
3118        if len(hitlist) != 1:   # Cannot happen but anyway
3119            self.flash(_('More than one student found.'), type="warning")
3120            return
3121        student = hitlist[0].context
3122        self.student_id = student.student_id
3123        student_pw = student.password
3124        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3125        code = get_access_code(pin)
3126        if not code:
3127            self.flash(_('Access code is invalid.'), type="warning")
3128            return
3129        if student_pw and pin == student.adm_code:
3130            self.flash(_(
3131                'Password has already been set. Your Student Id is ${a}',
3132                mapping = {'a':self.student_id}))
3133            return
3134        elif student_pw:
3135            self.flash(
3136                _('Password has already been set. You are using the ' +
3137                'wrong Access Code.'), type="warning")
3138            return
3139        # Mark pin as used (this also fires a pin related transition)
3140        # and set student password
3141        if code.state == USED:
3142            self.flash(_('Access code has already been used.'), type="warning")
3143            return
3144        else:
3145            comment = _(u"invalidated")
3146            # Here we know that the ac is in state initialized so we do not
3147            # expect an exception
3148            invalidate_accesscode(pin,comment)
3149            IUserAccount(student).setPassword(self.ac_number)
3150            student.adm_code = pin
3151        self.flash(_('Password has been set. Your Student Id is ${a}',
3152            mapping = {'a':self.student_id}))
3153        return
3154
3155class StudentRequestPasswordPage(KofaAddFormPage):
3156    """Captcha'd request password page for students.
3157    """
3158    grok.name('requestpw')
3159    grok.require('waeup.Anonymous')
3160    grok.template('requestpw')
3161    form_fields = grok.AutoFields(IStudentRequestPW).select(
3162        'lastname','number','email')
3163    label = _('Request password for first-time login')
3164
3165    def update(self):
3166        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3167        if blocker:
3168            self.flash(_('The portal is in maintenance mode. '
3169                        'Password request forms are temporarily disabled.'),
3170                       type='warning')
3171            self.redirect(self.url(self.context))
3172            return
3173        # Handle captcha
3174        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3175        self.captcha_result = self.captcha.verify(self.request)
3176        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3177        return
3178
3179    def _redirect(self, email, password, student_id):
3180        # Forward only email to landing page in base package.
3181        self.redirect(self.url(self.context, 'requestpw_complete',
3182            data = dict(email=email)))
3183        return
3184
3185    def _redirect_no_student(self):
3186        # No record found, this is the truth. We do not redirect here.
3187        # We are using this method in custom packages
3188        # for redirecting alumni to the application section.
3189        self.flash(_('No student record found.'), type="warning")
3190        return
3191
3192    def _pw_used(self):
3193        # XXX: False if password has not been used. We need an extra
3194        #      attribute which remembers if student logged in.
3195        return True
3196
3197    @action(_('Send login credentials to email address'), style='primary')
3198    def get_credentials(self, **data):
3199        if not self.captcha_result.is_valid:
3200            # Captcha will display error messages automatically.
3201            # No need to flash something.
3202            return
3203        number = data.get('number','')
3204        lastname = data.get('lastname','')
3205        cat = getUtility(ICatalog, name='students_catalog')
3206        results = list(
3207            cat.searchResults(reg_number=(number, number)))
3208        if not results:
3209            results = list(
3210                cat.searchResults(matric_number=(number, number)))
3211        if results:
3212            student = results[0]
3213            if getattr(student,'lastname',None) is None:
3214                self.flash(_('An error occurred.'), type="danger")
3215                return
3216            elif student.lastname.lower() != lastname.lower():
3217                # Don't tell the truth here. Anonymous must not
3218                # know that a record was found and only the lastname
3219                # verification failed.
3220                self.flash(_('No student record found.'), type="warning")
3221                return
3222            elif student.password is not None and self._pw_used:
3223                self.flash(_('Your password has already been set and used. '
3224                             'Please proceed to the login page.'),
3225                           type="warning")
3226                return
3227            # Store email address but nothing else.
3228            student.email = data['email']
3229            notify(grok.ObjectModifiedEvent(student))
3230        else:
3231            self._redirect_no_student()
3232            return
3233
3234        kofa_utils = getUtility(IKofaUtils)
3235        password = kofa_utils.genPassword()
3236        mandate = PasswordMandate()
3237        mandate.params['password'] = password
3238        mandate.params['user'] = student
3239        site = grok.getSite()
3240        site['mandates'].addMandate(mandate)
3241        # Send email with credentials
3242        args = {'mandate_id':mandate.mandate_id}
3243        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3244        url_info = u'Confirmation link: %s' % mandate_url
3245        msg = _('You have successfully requested a password for the')
3246        if kofa_utils.sendCredentials(IUserAccount(student),
3247            password, url_info, msg):
3248            email_sent = student.email
3249        else:
3250            email_sent = None
3251        self._redirect(email=email_sent, password=password,
3252            student_id=student.student_id)
3253        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3254        self.context.logger.info(
3255            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3256        return
3257
3258class ParentsUser:
3259    pass
3260
3261class RequestParentsPasswordPage(StudentRequestPasswordPage):
3262    """Captcha'd request password page for parents.
3263    """
3264    grok.name('requestppw')
3265    grok.template('requestppw')
3266    label = _('Request password for parents access')
3267
3268    def update(self):
3269        super(RequestParentsPasswordPage, self).update()
3270        kofa_utils = getUtility(IKofaUtils)
3271        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3272        return
3273
3274    @action(_('Send temporary login credentials to email address'), style='primary')
3275    def get_credentials(self, **data):
3276        if not self.captcha_result.is_valid:
3277            # Captcha will display error messages automatically.
3278            # No need to flash something.
3279            return
3280        number = data.get('number','')
3281        lastname = data.get('lastname','')
3282        email = data['email']
3283        cat = getUtility(ICatalog, name='students_catalog')
3284        results = list(
3285            cat.searchResults(reg_number=(number, number)))
3286        if not results:
3287            results = list(
3288                cat.searchResults(matric_number=(number, number)))
3289        if results:
3290            student = results[0]
3291            if getattr(student,'lastname',None) is None:
3292                self.flash(_('An error occurred.'), type="danger")
3293                return
3294            elif student.lastname.lower() != lastname.lower():
3295                # Don't tell the truth here. Anonymous must not
3296                # know that a record was found and only the lastname
3297                # verification failed.
3298                self.flash(_('No student record found.'), type="warning")
3299                return
3300            elif email != student.parents_email:
3301                self.flash(_('Wrong email address.'), type="warning")
3302                return
3303        else:
3304            self._redirect_no_student()
3305            return
3306        kofa_utils = getUtility(IKofaUtils)
3307        password = kofa_utils.genPassword()
3308        mandate = ParentsPasswordMandate()
3309        mandate.params['password'] = password
3310        mandate.params['student'] = student
3311        site = grok.getSite()
3312        site['mandates'].addMandate(mandate)
3313        # Send email with credentials
3314        args = {'mandate_id':mandate.mandate_id}
3315        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3316        url_info = u'Confirmation link: %s' % mandate_url
3317        msg = _('You have successfully requested a parents password for the')
3318        # Create a fake user
3319        user = ParentsUser()
3320        user.name = student.student_id
3321        user.title = "Parents of %s" % student.display_fullname
3322        user.email = student.parents_email
3323        if kofa_utils.sendCredentials(user, password, url_info, msg):
3324            email_sent = user.email
3325        else:
3326            email_sent = None
3327        self._redirect(email=email_sent, password=password,
3328            student_id=student.student_id)
3329        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3330        self.context.logger.info(
3331            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3332        return
3333
3334class StudentRequestPasswordEmailSent(KofaPage):
3335    """Landing page after successful password request.
3336
3337    """
3338    grok.name('requestpw_complete')
3339    grok.require('waeup.Public')
3340    grok.template('requestpwmailsent')
3341    label = _('Your password request was successful.')
3342
3343    def update(self, email=None, student_id=None, password=None):
3344        self.email = email
3345        self.password = password
3346        self.student_id = student_id
3347        return
3348
3349class FilterStudentsInDepartmentPage(KofaPage):
3350    """Page that filters and lists students.
3351    """
3352    grok.context(IDepartment)
3353    grok.require('waeup.showStudents')
3354    grok.name('students')
3355    grok.template('filterstudentspage')
3356    pnav = 1
3357    session_label = _('Current Session')
3358    level_label = _('Current Level')
3359
3360    def label(self):
3361        return 'Students in %s' % self.context.longtitle
3362
3363    def _set_session_values(self):
3364        vocab_terms = academic_sessions_vocab.by_value.values()
3365        self.sessions = sorted(
3366            [(x.title, x.token) for x in vocab_terms], reverse=True)
3367        self.sessions += [('All Sessions', 'all')]
3368        return
3369
3370    def _set_level_values(self):
3371        vocab_terms = course_levels.by_value.values()
3372        self.levels = sorted(
3373            [(x.title, x.token) for x in vocab_terms])
3374        self.levels += [('All Levels', 'all')]
3375        return
3376
3377    def _searchCatalog(self, session, level):
3378        if level not in (10, 999, None):
3379            start_level = 100 * (level // 100)
3380            end_level = start_level + 90
3381        else:
3382            start_level = end_level = level
3383        cat = queryUtility(ICatalog, name='students_catalog')
3384        students = cat.searchResults(
3385            current_session=(session, session),
3386            current_level=(start_level, end_level),
3387            depcode=(self.context.code, self.context.code)
3388            )
3389        hitlist = []
3390        for student in students:
3391            hitlist.append(StudentQueryResultItem(student, view=self))
3392        return hitlist
3393
3394    def update(self, SHOW=None, session=None, level=None):
3395        self.parent_url = self.url(self.context.__parent__)
3396        self._set_session_values()
3397        self._set_level_values()
3398        self.hitlist = []
3399        self.session_default = session
3400        self.level_default = level
3401        if SHOW is not None:
3402            if session != 'all':
3403                self.session = int(session)
3404                self.session_string = '%s %s/%s' % (
3405                    self.session_label, self.session, self.session+1)
3406            else:
3407                self.session = None
3408                self.session_string = _('in any session')
3409            if level != 'all':
3410                self.level = int(level)
3411                self.level_string = '%s %s' % (self.level_label, self.level)
3412            else:
3413                self.level = None
3414                self.level_string = _('at any level')
3415            self.hitlist = self._searchCatalog(self.session, self.level)
3416            if not self.hitlist:
3417                self.flash(_('No student found.'), type="warning")
3418        return
3419
3420class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3421    """Page that filters and lists students.
3422    """
3423    grok.context(ICertificate)
3424
3425    def label(self):
3426        return 'Students studying %s' % self.context.longtitle
3427
3428    def _searchCatalog(self, session, level):
3429        if level not in (10, 999, None):
3430            start_level = 100 * (level // 100)
3431            end_level = start_level + 90
3432        else:
3433            start_level = end_level = level
3434        cat = queryUtility(ICatalog, name='students_catalog')
3435        students = cat.searchResults(
3436            current_session=(session, session),
3437            current_level=(start_level, end_level),
3438            certcode=(self.context.code, self.context.code)
3439            )
3440        hitlist = []
3441        for student in students:
3442            hitlist.append(StudentQueryResultItem(student, view=self))
3443        return hitlist
3444
3445class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3446    """Page that filters and lists students.
3447    """
3448    grok.context(ICourse)
3449    grok.require('waeup.viewStudent')
3450
3451    session_label = _('Session')
3452    level_label = _('Level')
3453
3454    def label(self):
3455        return 'Students registered for %s' % self.context.longtitle
3456
3457    def _searchCatalog(self, session, level):
3458        if level not in (10, 999, None):
3459            start_level = 100 * (level // 100)
3460            end_level = start_level + 90
3461        else:
3462            start_level = end_level = level
3463        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3464        coursetickets = cat.searchResults(
3465            session=(session, session),
3466            level=(start_level, end_level),
3467            code=(self.context.code, self.context.code)
3468            )
3469        hitlist = []
3470        for ticket in coursetickets:
3471            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3472        return list(set(hitlist))
3473
3474class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3475    """ Clear all students of a department in state 'clearance requested'.
3476    """
3477    grok.context(IDepartment)
3478    grok.name('clearallstudents')
3479    grok.require('waeup.clearAllStudents')
3480
3481    def update(self):
3482        cat = queryUtility(ICatalog, name='students_catalog')
3483        students = cat.searchResults(
3484            depcode=(self.context.code, self.context.code),
3485            state=(REQUESTED, REQUESTED)
3486            )
3487        num = 0
3488        for student in students:
3489            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3490                continue
3491            IWorkflowInfo(student).fireTransition('clear')
3492            num += 1
3493        self.flash(_('%d students have been cleared.' % num))
3494        self.redirect(self.url(self.context))
3495        return
3496
3497    def render(self):
3498        return
3499
3500class EditScoresPage(KofaPage):
3501    """Page that allows to edit batches of scores.
3502    """
3503    grok.context(ICourse)
3504    grok.require('waeup.editScores')
3505    grok.name('edit_scores')
3506    grok.template('editscorespage')
3507    pnav = 1
3508    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3509
3510    def label(self):
3511        return '%s tickets in academic session %s' % (
3512            self.context.code, self.session_title)
3513
3514    def _searchCatalog(self, session):
3515        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3516        # Attention: Also tickets of previous studycourses are found
3517        coursetickets = cat.searchResults(
3518            session=(session, session),
3519            code=(self.context.code, self.context.code)
3520            )
3521        return list(coursetickets)
3522
3523    def _extract_uploadfile(self, uploadfile):
3524        """Get a mapping of student-ids to scores.
3525
3526        The mapping is constructed by reading contents from `uploadfile`.
3527
3528        We expect uploadfile to be a regular CSV file with columns
3529        ``student_id`` and ``score`` (other cols are ignored).
3530        """
3531        result = dict()
3532        data = StringIO(uploadfile.read())  # ensure we have something seekable
3533        reader = csv.DictReader(data)
3534        for row in reader:
3535            if not 'student_id' in row or not 'score' in row:
3536                continue
3537            result[row['student_id']] = row['score']
3538        return result
3539
3540    def _update_scores(self, form):
3541        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3542        error = ''
3543        if 'UPDATE_FILE' in form:
3544            if form['uploadfile']:
3545                try:
3546                    formvals = self._extract_uploadfile(form['uploadfile'])
3547                except:
3548                    self.flash(
3549                        _('Uploaded file contains illegal data. Ignored'),
3550                        type="danger")
3551                    return False
3552            else:
3553                self.flash(
3554                    _('No file provided.'), type="danger")
3555                return False
3556        else:
3557            formvals = dict(zip(form['sids'], form['scores']))
3558        for ticket in self.editable_tickets:
3559            score = ticket.score
3560            sid = ticket.student.student_id
3561            if sid not in formvals:
3562                continue
3563            if formvals[sid] == '':
3564                score = None
3565            else:
3566                try:
3567                    score = int(formvals[sid])
3568                except ValueError:
3569                    error += '%s, ' % ticket.student.display_fullname
3570            if ticket.score != score:
3571                ticket.score = score
3572                ticket.student.__parent__.logger.info(
3573                    '%s - %s %s/%s score updated (%s)' % (
3574                        ob_class, ticket.student.student_id,
3575                        ticket.level, ticket.code, score)
3576                    )
3577        if error:
3578            self.flash(
3579                _('Error: Score(s) of following students have not been '
3580                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3581                type="danger")
3582        return True
3583
3584    def _validate_results(self, form):
3585        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3586        user = get_current_principal()
3587        if user is None:
3588            usertitle = 'system'
3589        else:
3590            usertitle = getattr(user, 'public_name', None)
3591            if not usertitle:
3592                usertitle = user.title
3593        self.context.results_validated_by = usertitle
3594        self.context.results_validation_date = datetime.utcnow()
3595        self.context.results_validation_session = self.current_academic_session
3596        return
3597
3598    def _results_editable(self, results_validation_session,
3599                         current_academic_session):
3600        user = get_current_principal()
3601        prm = IPrincipalRoleManager(self.context)
3602        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3603        if 'waeup.local.LocalStudentsManager' in roles:
3604            return True
3605        if results_validation_session \
3606            and results_validation_session >= current_academic_session:
3607            return False
3608        return True
3609
3610    def update(self,  *args, **kw):
3611        form = self.request.form
3612        self.current_academic_session = grok.getSite()[
3613            'configuration'].current_academic_session
3614        if self.context.__parent__.__parent__.score_editing_disabled \
3615            or self.context.score_editing_disabled:
3616            self.flash(_('Score editing disabled.'), type="warning")
3617            self.redirect(self.url(self.context))
3618            return
3619        if not self.current_academic_session:
3620            self.flash(_('Current academic session not set.'), type="warning")
3621            self.redirect(self.url(self.context))
3622            return
3623        vs = self.context.results_validation_session
3624        if not self._results_editable(vs, self.current_academic_session):
3625            self.flash(
3626                _('Course results have already been '
3627                  'validated and can no longer be changed.'),
3628                type="danger")
3629            self.redirect(self.url(self.context))
3630            return
3631        self.session_title = academic_sessions_vocab.getTerm(
3632            self.current_academic_session).title
3633        self.tickets = self._searchCatalog(self.current_academic_session)
3634        if not self.tickets:
3635            self.flash(_('No student found.'), type="warning")
3636            self.redirect(self.url(self.context))
3637            return
3638        self.editable_tickets = [
3639            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3640        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
3641            and not 'VALIDATE_RESULTS' in form:
3642            return
3643        if 'VALIDATE_RESULTS' in form:
3644            if vs and vs >= self.current_academic_session:
3645                self.flash(
3646                    _('Course results have already been validated.'),
3647                    type="danger")
3648                return
3649            self._validate_results(form)
3650            self.flash(_('You successfully validated the course results.'))
3651            self.redirect(self.url(self.context))
3652            return
3653        if not self.editable_tickets:
3654            return
3655        success = self._update_scores(form)
3656        if success:
3657            self.flash(_('You successfully updated course results.'))
3658        return
3659
3660class DownloadScoresView(UtilityView, grok.View):
3661    """View that exports scores.
3662    """
3663    grok.context(ICourse)
3664    grok.require('waeup.editScores')
3665    grok.name('download_scores')
3666
3667    def _results_editable(self, results_validation_session,
3668                         current_academic_session):
3669        user = get_current_principal()
3670        prm = IPrincipalRoleManager(self.context)
3671        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3672        if 'waeup.local.LocalStudentsManager' in roles:
3673            return True
3674        if results_validation_session \
3675            and results_validation_session >= current_academic_session:
3676            return False
3677        return True
3678
3679    def update(self):
3680        self.current_academic_session = grok.getSite()[
3681            'configuration'].current_academic_session
3682        if self.context.__parent__.__parent__.score_editing_disabled \
3683            or self.context.score_editing_disabled:
3684            self.flash(_('Score editing disabled.'), type="warning")
3685            self.redirect(self.url(self.context))
3686            return
3687        if not self.current_academic_session:
3688            self.flash(_('Current academic session not set.'), type="warning")
3689            self.redirect(self.url(self.context))
3690            return
3691        vs = self.context.results_validation_session
3692        if not self._results_editable(vs, self.current_academic_session):
3693            self.flash(
3694                _('Course results have already been '
3695                  'validated and can no longer be changed.'),
3696                type="danger")
3697            self.redirect(self.url(self.context))
3698            return
3699        site = grok.getSite()
3700        exporter = getUtility(ICSVExporter, name='lecturer')
3701        self.csv = exporter.export_filtered(site, filepath=None,
3702                                 catalog='coursetickets',
3703                                 session=self.current_academic_session,
3704                                 level=None,
3705                                 code=self.context.code)
3706        return
3707
3708    def render(self):
3709        filename = 'results_%s_%s.csv' % (
3710            self.context.code, self.current_academic_session)
3711        self.response.setHeader(
3712            'Content-Type', 'text/csv; charset=UTF-8')
3713        self.response.setHeader(
3714            'Content-Disposition:', 'attachment; filename="%s' % filename)
3715        return self.csv
3716
3717class ExportPDFScoresSlip(UtilityView, grok.View,
3718    LocalRoleAssignmentUtilityView):
3719    """Deliver a PDF slip of course tickets for a lecturer.
3720    """
3721    grok.context(ICourse)
3722    grok.name('coursetickets.pdf')
3723    grok.require('waeup.showStudents')
3724
3725    def update(self):
3726        self.current_academic_session = grok.getSite()[
3727            'configuration'].current_academic_session
3728        if not self.current_academic_session:
3729            self.flash(_('Current academic session not set.'), type="danger")
3730            self.redirect(self.url(self.context))
3731            return
3732
3733    @property
3734    def note(self):
3735        return
3736
3737    def data(self, session):
3738        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3739        # Attention: Also tickets of previous studycourses are found
3740        coursetickets = cat.searchResults(
3741            session=(session, session),
3742            code=(self.context.code, self.context.code)
3743            )
3744        header = [[_('Matric No.'),
3745                   _('Reg. No.'),
3746                   _('Fullname'),
3747                   _('Status'),
3748                   _('Course of Studies'),
3749                   _('Level'),
3750                   _('Score') ],]
3751        tickets = []
3752        for ticket in list(coursetickets):
3753            row = [ticket.student.matric_number,
3754                  ticket.student.reg_number,
3755                  ticket.student.display_fullname,
3756                  ticket.student.translated_state,
3757                  ticket.student.certcode,
3758                  ticket.level,
3759                  ticket.score]
3760            tickets.append(row)
3761        return header + sorted(tickets, key=lambda value: value[0]), None
3762
3763    def render(self):
3764        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3765                     if i['local_role'] == 'waeup.local.Lecturer']
3766        lecturers = sorted(lecturers)
3767        lecturers =  ', '.join(lecturers)
3768        students_utils = getUtility(IStudentsUtils)
3769        return students_utils.renderPDFCourseticketsOverview(
3770            self, 'coursetickets', self.current_academic_session,
3771            self.data(self.current_academic_session), lecturers,
3772            'landscape', 90, self.note)
3773
3774class ExportAttendanceSlip(UtilityView, grok.View,
3775    LocalRoleAssignmentUtilityView):
3776    """Deliver a PDF slip of course tickets in attendance sheet format.
3777    """
3778    grok.context(ICourse)
3779    grok.name('attendance.pdf')
3780    grok.require('waeup.showStudents')
3781
3782    def update(self):
3783        self.current_academic_session = grok.getSite()[
3784            'configuration'].current_academic_session
3785        if not self.current_academic_session:
3786            self.flash(_('Current academic session not set.'), type="danger")
3787            self.redirect(self.url(self.context))
3788            return
3789
3790    @property
3791    def note(self):
3792        return
3793
3794    def data(self, session):
3795        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3796        # Attention: Also tickets of previous studycourses are found
3797        coursetickets = cat.searchResults(
3798            session=(session, session),
3799            code=(self.context.code, self.context.code)
3800            )
3801        header = [[_('S/N'),
3802                   _('Matric No.'),
3803                   _('Name'),
3804                   _('Level'),
3805                   _('Course of\nStudies'),
3806                   _('Booklet No.'),
3807                   _('Signature'),
3808                   ],]
3809        tickets = []
3810        sn = 1
3811        ctlist = sorted(list(coursetickets),
3812                        key=lambda value: str(value.student.certcode) +
3813                                          str(value.student.matric_number))
3814        # In AAUE only editable appear on the attendance sheet. Hopefully
3815        # this holds for other universities too.
3816        editable_tickets = [ticket for ticket in ctlist
3817            if ticket.editable_by_lecturer]
3818        for ticket in editable_tickets:
3819            name = textwrap.fill(ticket.student.display_fullname, 20)
3820            row = [sn,
3821                  ticket.student.matric_number,
3822                  name,
3823                  ticket.level,
3824                  ticket.student.certcode,
3825                  20 * ' ',
3826                  27 * ' ',
3827                  ]
3828            tickets.append(row)
3829            sn += 1
3830        return header + tickets, None
3831
3832    def render(self):
3833        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3834                     if i['local_role'] == 'waeup.local.Lecturer']
3835        lecturers =  ', '.join(lecturers)
3836        students_utils = getUtility(IStudentsUtils)
3837        return students_utils.renderPDFCourseticketsOverview(
3838            self, 'attendance', self.current_academic_session,
3839            self.data(self.current_academic_session),
3840            lecturers, '', 65, self.note)
3841
3842class ExportJobContainerOverview(KofaPage):
3843    """Page that lists active student data export jobs and provides links
3844    to discard or download CSV files.
3845
3846    """
3847    grok.context(VirtualExportJobContainer)
3848    grok.require('waeup.showStudents')
3849    grok.name('index.html')
3850    grok.template('exportjobsindex')
3851    label = _('Student Data Exports')
3852    pnav = 1
3853    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3854
3855    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
3856        if CREATE1:
3857            self.redirect(self.url('@@exportconfig'))
3858            return
3859        if CREATE2:
3860            self.redirect(self.url('@@exportselected'))
3861            return
3862        if DISCARD and job_id:
3863            entry = self.context.entry_from_job_id(job_id)
3864            self.context.delete_export_entry(entry)
3865            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3866            self.context.logger.info(
3867                '%s - discarded: job_id=%s' % (ob_class, job_id))
3868            self.flash(_('Discarded export') + ' %s' % job_id)
3869        self.entries = doll_up(self, user=self.request.principal.id)
3870        return
3871
3872class ExportJobContainerJobConfig(KofaPage):
3873    """Page that configures a students export job.
3874
3875    This is a baseclass.
3876    """
3877    grok.baseclass()
3878    grok.require('waeup.showStudents')
3879    grok.template('exportconfig')
3880    label = _('Configure student data export')
3881    pnav = 1
3882    redirect_target = ''
3883    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3884
3885    def _set_session_values(self):
3886        vocab_terms = academic_sessions_vocab.by_value.values()
3887        self.sessions = [(_('All Sessions'), 'all')]
3888        self.sessions += sorted(
3889            [(x.title, x.token) for x in vocab_terms], reverse=True)
3890        return
3891
3892    def _set_level_values(self):
3893        vocab_terms = course_levels.by_value.values()
3894        self.levels = [(_('All Levels'), 'all')]
3895        self.levels += sorted(
3896            [(x.title, x.token) for x in vocab_terms])
3897        return
3898
3899    def _set_semesters_values(self):
3900        utils = getUtility(IKofaUtils)
3901        self.semesters =[(_('All Semesters'), 'all')]
3902        self.semesters += sorted([(value, key) for key, value in
3903                      utils.SEMESTER_DICT.items()])
3904        return
3905
3906    def _set_mode_values(self):
3907        utils = getUtility(IKofaUtils)
3908        self.modes =[(_('All Modes'), 'all')]
3909        self.modes += sorted([(value, key) for key, value in
3910                      utils.STUDY_MODES_DICT.items()])
3911        return
3912
3913    def _set_paycat_values(self):
3914        utils = getUtility(IKofaUtils)
3915        self.paycats =[(_('All Payment Categories'), 'all')]
3916        self.paycats += sorted([(value, key) for key, value in
3917                      utils.PAYMENT_CATEGORIES.items()])
3918        return
3919
3920    def _set_exporter_values(self):
3921        # We provide all student exporters, nothing else, yet.
3922        # Bursary, Department or Accommodation Officers don't
3923        # have the general exportData
3924        # permission and are only allowed to export bursary, payments
3925        # overview or accommodation data respectively.
3926        # This is the only place where waeup.exportAccommodationData,
3927        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3928        # are used.
3929        exporters = []
3930        if not checkPermission('waeup.exportData', self.context):
3931            if checkPermission('waeup.exportBursaryData', self.context):
3932                exporters += [('Bursary Data', 'bursary')]
3933            if checkPermission('waeup.exportPaymentsOverview', self.context):
3934                exporters += [('School Fee Payments Overview',
3935                               'sfpaymentsoverview'),
3936                              ('Session Payments Overview',
3937                               'sessionpaymentsoverview')]
3938            if checkPermission('waeup.exportAccommodationData', self.context):
3939                exporters += [('Bed Tickets', 'bedtickets'),
3940                              ('Accommodation Payments',
3941                               'accommodationpayments')]
3942            self.exporters = exporters
3943            return
3944        STUDENT_EXPORTER_NAMES = getUtility(
3945            IStudentsUtils).STUDENT_EXPORTER_NAMES
3946        for name in STUDENT_EXPORTER_NAMES:
3947            util = getUtility(ICSVExporter, name=name)
3948            exporters.append((util.title, name),)
3949        self.exporters = exporters
3950        return
3951
3952    @property
3953    def faccode(self):
3954        return None
3955
3956    @property
3957    def depcode(self):
3958        return None
3959
3960    @property
3961    def certcode(self):
3962        return None
3963
3964    def update(self, START=None, session=None, level=None, mode=None,
3965               payments_start=None, payments_end=None, ct_level=None,
3966               ct_session=None, ct_semester=None, paycat=None,
3967               paysession=None, level_session=None, exporter=None):
3968        self._set_session_values()
3969        self._set_level_values()
3970        self._set_mode_values()
3971        self._set_paycat_values()
3972        self._set_exporter_values()
3973        self._set_semesters_values()
3974        if START is None:
3975            return
3976        ena = exports_not_allowed(self)
3977        if ena:
3978            self.flash(ena, type='danger')
3979            return
3980        if payments_start or payments_end:
3981            date_format = '%d/%m/%Y'
3982            try:
3983                datetime.strptime(payments_start, date_format)
3984                datetime.strptime(payments_end, date_format)
3985            except ValueError:
3986                self.flash(_('Payment dates do not match format d/m/Y.'),
3987                           type="danger")
3988                return
3989        if session == 'all':
3990            session=None
3991        if level == 'all':
3992            level = None
3993        if mode == 'all':
3994            mode = None
3995        if (mode,
3996            level,
3997            session,
3998            self.faccode,
3999            self.depcode,
4000            self.certcode) == (None, None, None, None, None, None):
4001            # Export all students including those without certificate
4002            job_id = self.context.start_export_job(exporter,
4003                                          self.request.principal.id,
4004                                          payments_start = payments_start,
4005                                          payments_end = payments_end,
4006                                          paycat=paycat,
4007                                          paysession=paysession,
4008                                          ct_level = ct_level,
4009                                          ct_session = ct_session,
4010                                          ct_semester = ct_semester,
4011                                          level_session=level_session,
4012                                          )
4013        else:
4014            job_id = self.context.start_export_job(exporter,
4015                                          self.request.principal.id,
4016                                          current_session=session,
4017                                          current_level=level,
4018                                          current_mode=mode,
4019                                          faccode=self.faccode,
4020                                          depcode=self.depcode,
4021                                          certcode=self.certcode,
4022                                          payments_start = payments_start,
4023                                          payments_end = payments_end,
4024                                          paycat=paycat,
4025                                          paysession=paysession,
4026                                          ct_level = ct_level,
4027                                          ct_session = ct_session,
4028                                          ct_semester = ct_semester,
4029                                          level_session=level_session,)
4030        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4031        self.context.logger.info(
4032            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4033            % (ob_class, exporter, session, level, mode, self.faccode,
4034            self.depcode, self.certcode, payments_start, payments_end,
4035            ct_level, ct_session, paycat, paysession, level_session, job_id))
4036        self.flash(_('Export started for students with') +
4037                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4038                   session, level, mode))
4039        self.redirect(self.url(self.redirect_target))
4040        return
4041
4042class ExportJobContainerDownload(ExportCSVView):
4043    """Page that downloads a students export csv file.
4044
4045    """
4046    grok.context(VirtualExportJobContainer)
4047    grok.require('waeup.showStudents')
4048
4049class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4050    """Page that configures a students export job in datacenter.
4051
4052    """
4053    grok.name('exportconfig')
4054    grok.context(IDataCenter)
4055    redirect_target = '@@export'
4056
4057class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4058    """Page that configures a students export job in datacenter.
4059
4060    """
4061    grok.name('exportselected')
4062    grok.context(IDataCenter)
4063    redirect_target = '@@export'
4064    grok.template('exportselected')
4065
4066    def update(self, START=None, students=None, exporter=None):
4067        self._set_exporter_values()
4068        if START is None:
4069            return
4070        ena = exports_not_allowed(self)
4071        if ena:
4072            self.flash(ena, type='danger')
4073            return
4074        try:
4075            ids = students.replace(',', ' ').split()
4076        except:
4077            self.flash(sys.exc_info()[1])
4078            self.redirect(self.url(self.redirect_target))
4079            return
4080        job_id = self.context.start_export_job(
4081            exporter, self.request.principal.id, selected=ids)
4082        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4083        self.context.logger.info(
4084            '%s - selected students exported: %s, job_id=%s' %
4085            (ob_class, exporter, job_id))
4086        self.flash(_('Export of selected students started.'))
4087        self.redirect(self.url(self.redirect_target))
4088        return
4089
4090class FacultiesExportJobContainerJobConfig(
4091    DatacenterExportJobContainerJobConfig):
4092    """Page that configures a students export job in facultiescontainer.
4093
4094    """
4095    grok.context(VirtualFacultiesExportJobContainer)
4096    redirect_target = ''
4097
4098class FacultiesExportJobContainerSelectStudents(
4099    DatacenterExportJobContainerSelectStudents):
4100    """Page that configures a students export job in facultiescontainer.
4101
4102    """
4103    grok.context(VirtualFacultiesExportJobContainer)
4104    redirect_target = ''
4105
4106class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4107    """Page that configures a students export job in faculties.
4108
4109    """
4110    grok.context(VirtualFacultyExportJobContainer)
4111    redirect_target = ''
4112
4113    @property
4114    def faccode(self):
4115        return self.context.__parent__.code
4116
4117class DepartmentExportJobContainerJobConfig(
4118    DatacenterExportJobContainerJobConfig):
4119    """Page that configures a students export job in departments.
4120
4121    """
4122    grok.context(VirtualDepartmentExportJobContainer)
4123    redirect_target = ''
4124
4125    @property
4126    def depcode(self):
4127        return self.context.__parent__.code
4128
4129class CertificateExportJobContainerJobConfig(
4130    DatacenterExportJobContainerJobConfig):
4131    """Page that configures a students export job for certificates.
4132
4133    """
4134    grok.context(VirtualCertificateExportJobContainer)
4135    grok.template('exportconfig_certificate')
4136    redirect_target = ''
4137
4138    @property
4139    def certcode(self):
4140        return self.context.__parent__.code
4141
4142class CourseExportJobContainerJobConfig(
4143    DatacenterExportJobContainerJobConfig):
4144    """Page that configures a students export job for courses.
4145
4146    In contrast to department or certificate student data exports the
4147    coursetickets_catalog is searched here. Therefore the update
4148    method from the base class is customized.
4149    """
4150    grok.context(VirtualCourseExportJobContainer)
4151    grok.template('exportconfig_course')
4152    redirect_target = ''
4153
4154    def _set_exporter_values(self):
4155        # We provide only the 'coursetickets' and 'lecturer' exporter
4156        # but can add more.
4157        exporters = []
4158        for name in ('coursetickets', 'lecturer'):
4159            util = getUtility(ICSVExporter, name=name)
4160            exporters.append((util.title, name),)
4161        self.exporters = exporters
4162        return
4163
4164    def _set_session_values(self):
4165        # We allow only current academic session
4166        academic_session = grok.getSite()['configuration'].current_academic_session
4167        if not academic_session:
4168            self.sessions = []
4169            return
4170        x = academic_sessions_vocab.getTerm(academic_session)
4171        self.sessions = [(x.title, x.token)]
4172        return
4173
4174    def update(self, START=None, session=None, level=None, mode=None,
4175               exporter=None):
4176        if not checkPermission('waeup.exportData', self.context):
4177            self.flash(_('Not permitted.'), type='danger')
4178            self.redirect(self.url(self.context))
4179            return
4180        self._set_session_values()
4181        self._set_level_values()
4182        self._set_mode_values()
4183        self._set_exporter_values()
4184        if not self.sessions:
4185            self.flash(
4186                _('Academic session not set. '
4187                  'Please contact the administrator.'),
4188                type='danger')
4189            self.redirect(self.url(self.context))
4190            return
4191        if START is None:
4192            return
4193        ena = exports_not_allowed(self)
4194        if ena:
4195            self.flash(ena, type='danger')
4196            return
4197        if session == 'all':
4198            session = None
4199        if level == 'all':
4200            level = None
4201        job_id = self.context.start_export_job(exporter,
4202                                      self.request.principal.id,
4203                                      # Use a different catalog and
4204                                      # pass different keywords than
4205                                      # for the (default) students_catalog
4206                                      catalog='coursetickets',
4207                                      session=session,
4208                                      level=level,
4209                                      code=self.context.__parent__.code)
4210        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4211        self.context.logger.info(
4212            '%s - exported: %s (%s, %s, %s), job_id=%s'
4213            % (ob_class, exporter, session, level,
4214            self.context.__parent__.code, job_id))
4215        self.flash(_('Export started for course tickets with') +
4216                   ' level_session=%s, level=%s' % (
4217                   session, level))
4218        self.redirect(self.url(self.redirect_target))
4219        return
Note: See TracBrowser for help on using the repository browser.