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

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

Implement combi payments (tests will follow).

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