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

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

Sort lecturers on scores slip.

  • Property svn:keywords set to Id
File size: 156.9 KB
Line 
1## $Id: browser.py 15865 2019-12-03 08:29:23Z 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')
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 _signatures(self):
1588        return ()
1589
1590    def _sigsInFooter(self):
1591        return ()
1592
1593    def render(self):
1594        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1595        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1596        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1597        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1598        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1599        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1600        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1601        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1602        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1603        studentview = StudentBasePDFFormPage(self.context.student,
1604            self.request, self.omit_fields)
1605        students_utils = getUtility(IStudentsUtils)
1606
1607        tabledata = []
1608        tableheader = []
1609        for i in range(1,7):
1610            tabledata.append(sorted(
1611                [value for value in self.context.values() if value.semester == i],
1612                key=lambda value: str(value.semester) + value.code))
1613            tableheader.append([(Code,'code', 2.5),
1614                             (Title,'title', 5),
1615                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1616                             (Cred, 'credits', 1.5),
1617                             #(Mand, 'mandatory', 1.5),
1618                             (Score, 'score', 1.5),
1619                             (Grade, 'grade', 1.5),
1620                             #('Auto', 'automatic', 1.5)
1621                             ])
1622        return students_utils.renderPDF(
1623            self, 'course_registration_slip.pdf',
1624            self.context.student, studentview,
1625            tableheader=tableheader,
1626            tabledata=tabledata,
1627            omit_fields=self.omit_fields,
1628            signatures=self._signatures(),
1629            sigs_in_footer=self._sigsInFooter(),
1630            )
1631
1632class StudyLevelManageFormPage(KofaEditFormPage):
1633    """ Page to edit the student study level data
1634    """
1635    grok.context(IStudentStudyLevel)
1636    grok.name('manage')
1637    grok.require('waeup.manageStudent')
1638    grok.template('studylevelmanagepage')
1639    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1640        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1641    pnav = 4
1642    taboneactions = [_('Save'),_('Cancel')]
1643    tabtwoactions = [_('Add course ticket'),
1644        _('Remove selected tickets'),_('Cancel')]
1645    placeholder = _('Enter valid course code')
1646
1647    def update(self, ADD=None, course=None):
1648        if not self.context.__parent__.is_current \
1649            or self.context.student.studycourse_locked:
1650            emit_lock_message(self)
1651            return
1652        super(StudyLevelManageFormPage, self).update()
1653        if ADD is not None:
1654            if not course:
1655                self.flash(_('No valid course code entered.'), type="warning")
1656                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1657                return
1658            cat = queryUtility(ICatalog, name='courses_catalog')
1659            result = cat.searchResults(code=(course, course))
1660            if len(result) != 1:
1661                self.flash(_('Course not found.'), type="warning")
1662            else:
1663                course = list(result)[0]
1664                addCourseTicket(self, course)
1665            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1666        return
1667
1668    @property
1669    def translated_values(self):
1670        return translated_values(self)
1671
1672    @property
1673    def label(self):
1674        # Here we know that the cookie has been set
1675        lang = self.request.cookies.get('kofa.language')
1676        level_title = translate(self.context.level_title, 'waeup.kofa',
1677            target_language=lang)
1678        return _('Manage ${a}',
1679            mapping = {'a':level_title})
1680
1681    @action(_('Save'), style='primary')
1682    def save(self, **data):
1683        msave(self, **data)
1684        return
1685
1686    @jsaction(_('Remove selected tickets'))
1687    def delCourseTicket(self, **data):
1688        form = self.request.form
1689        if 'val_id' in form:
1690            child_id = form['val_id']
1691        else:
1692            self.flash(_('No ticket selected.'), type="warning")
1693            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1694            return
1695        if not isinstance(child_id, list):
1696            child_id = [child_id]
1697        deleted = []
1698        for id in child_id:
1699            del self.context[id]
1700            deleted.append(id)
1701        if len(deleted):
1702            self.flash(_('Successfully removed: ${a}',
1703                mapping = {'a':', '.join(deleted)}))
1704            self.context.writeLogMessage(
1705                self,'removed: %s at %s' %
1706                (', '.join(deleted), self.context.level))
1707        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1708        return
1709
1710class StudyLevelRemarkFormPage(KofaEditFormPage):
1711    """ Page to edit the student study level transcript remark only
1712    """
1713    grok.context(IStudentStudyLevel)
1714    grok.name('remark')
1715    grok.require('waeup.processTranscript')
1716    grok.template('studylevelremarkpage')
1717    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1718    form_fields['level_session'].for_display = True
1719    form_fields['level_verdict'].for_display = True
1720    form_fields['validation_date'].for_display = True
1721    form_fields['validated_by'].for_display = True
1722
1723    def update(self, ADD=None, course=None):
1724        if self.context.student.studycourse_locked:
1725            emit_lock_message(self)
1726            return
1727        super(StudyLevelRemarkFormPage, self).update()
1728
1729    @property
1730    def label(self):
1731        lang = self.request.cookies.get('kofa.language')
1732        level_title = translate(self.context.level_title, 'waeup.kofa',
1733            target_language=lang)
1734        return _(
1735            'Edit transcript remark of level ${a}', mapping = {'a':level_title})
1736
1737    @property
1738    def translated_values(self):
1739        return translated_values(self)
1740
1741    @action(_('Save remark and go and back to transcript validation page'),
1742        style='primary')
1743    def save(self, **data):
1744        msave(self, **data)
1745        self.redirect(self.url(self.context.student)
1746            + '/studycourse/validate_transcript#tab4')
1747        return
1748
1749class ValidateCoursesView(UtilityView, grok.View):
1750    """ Validate course list by course adviser
1751    """
1752    grok.context(IStudentStudyLevel)
1753    grok.name('validate_courses')
1754    grok.require('waeup.validateStudent')
1755
1756    def update(self):
1757        if not self.context.__parent__.is_current:
1758            emit_lock_message(self)
1759            return
1760        if str(self.context.student.current_level) != self.context.__name__:
1761            self.flash(_('This is not the student\'s current level.'),
1762                       type="danger")
1763        elif self.context.student.state == REGISTERED:
1764            IWorkflowInfo(self.context.student).fireTransition(
1765                'validate_courses')
1766            self.flash(_('Course list has been validated.'))
1767        else:
1768            self.flash(_('Student is in the wrong state.'), type="warning")
1769        self.redirect(self.url(self.context))
1770        return
1771
1772    def render(self):
1773        return
1774
1775class RejectCoursesView(UtilityView, grok.View):
1776    """ Reject course list by course adviser
1777    """
1778    grok.context(IStudentStudyLevel)
1779    grok.name('reject_courses')
1780    grok.require('waeup.validateStudent')
1781
1782    def update(self):
1783        if not self.context.__parent__.is_current:
1784            emit_lock_message(self)
1785            return
1786        if str(self.context.__parent__.current_level) != self.context.__name__:
1787            self.flash(_('This is not the student\'s current level.'),
1788                       type="danger")
1789            self.redirect(self.url(self.context))
1790            return
1791        elif self.context.student.state == VALIDATED:
1792            IWorkflowInfo(self.context.student).fireTransition('reset8')
1793            message = _('Course list request has been annulled.')
1794            self.flash(message)
1795        elif self.context.student.state == REGISTERED:
1796            IWorkflowInfo(self.context.student).fireTransition('reset7')
1797            message = _('Course list has been unregistered.')
1798            self.flash(message)
1799        else:
1800            self.flash(_('Student is in the wrong state.'), type="warning")
1801            self.redirect(self.url(self.context))
1802            return
1803        args = {'subject':message}
1804        self.redirect(self.url(self.context.student) +
1805            '/contactstudent?%s' % urlencode(args))
1806        return
1807
1808    def render(self):
1809        return
1810
1811class UnregisterCoursesView(UtilityView, grok.View):
1812    """Unregister course list by student
1813    """
1814    grok.context(IStudentStudyLevel)
1815    grok.name('unregister_courses')
1816    grok.require('waeup.handleStudent')
1817
1818    def update(self):
1819        if not self.context.__parent__.is_current:
1820            emit_lock_message(self)
1821            return
1822        try:
1823            deadline = grok.getSite()['configuration'][
1824                str(self.context.level_session)].coursereg_deadline
1825        except (TypeError, KeyError):
1826            deadline = None
1827        if deadline and deadline < datetime.now(pytz.utc):
1828            self.flash(_(
1829                "Course registration has ended. "
1830                "Unregistration is disabled."), type="warning")
1831        elif str(self.context.__parent__.current_level) != self.context.__name__:
1832            self.flash(_('This is not your current level.'), type="danger")
1833        elif self.context.student.state == REGISTERED:
1834            IWorkflowInfo(self.context.student).fireTransition('reset7')
1835            message = _('Course list has been unregistered.')
1836            self.flash(message)
1837        else:
1838            self.flash(_('You are in the wrong state.'), type="warning")
1839        self.redirect(self.url(self.context))
1840        return
1841
1842    def render(self):
1843        return
1844
1845class CourseTicketAddFormPage(KofaAddFormPage):
1846    """Add a course ticket.
1847    """
1848    grok.context(IStudentStudyLevel)
1849    grok.name('add')
1850    grok.require('waeup.manageStudent')
1851    label = _('Add course ticket')
1852    form_fields = grok.AutoFields(ICourseTicketAdd)
1853    pnav = 4
1854
1855    def update(self):
1856        if not self.context.__parent__.is_current \
1857            or self.context.student.studycourse_locked:
1858            emit_lock_message(self)
1859            return
1860        super(CourseTicketAddFormPage, self).update()
1861        return
1862
1863    @action(_('Add course ticket'), style='primary')
1864    def addCourseTicket(self, **data):
1865        course = data['course']
1866        success = addCourseTicket(self, course)
1867        if success:
1868            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1869        return
1870
1871    @action(_('Cancel'), validator=NullValidator)
1872    def cancel(self, **data):
1873        self.redirect(self.url(self.context))
1874
1875class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1876    """ Page to display course tickets
1877    """
1878    grok.context(ICourseTicket)
1879    grok.name('index')
1880    grok.require('waeup.viewStudent')
1881    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
1882        'ticket_session')
1883    grok.template('courseticketpage')
1884    pnav = 4
1885
1886    @property
1887    def label(self):
1888        return _('${a}: Course Ticket ${b}', mapping = {
1889            'a':self.context.student.display_fullname,
1890            'b':self.context.code})
1891
1892class CourseTicketManageFormPage(KofaEditFormPage):
1893    """ Page to manage course tickets
1894    """
1895    grok.context(ICourseTicket)
1896    grok.name('manage')
1897    grok.require('waeup.manageStudent')
1898    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1899    form_fields['title'].for_display = True
1900    form_fields['fcode'].for_display = True
1901    form_fields['dcode'].for_display = True
1902    form_fields['semester'].for_display = True
1903    form_fields['passmark'].for_display = True
1904    form_fields['credits'].for_display = True
1905    form_fields['mandatory'].for_display = False
1906    form_fields['automatic'].for_display = True
1907    form_fields['carry_over'].for_display = True
1908    form_fields['ticket_session'].for_display = True
1909    pnav = 4
1910    grok.template('courseticketmanagepage')
1911
1912    def update(self):
1913        if not self.context.__parent__.__parent__.is_current \
1914            or self.context.student.studycourse_locked:
1915            emit_lock_message(self)
1916            return
1917        super(CourseTicketManageFormPage, self).update()
1918        return
1919
1920    @property
1921    def label(self):
1922        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1923
1924    @action('Save', style='primary')
1925    def save(self, **data):
1926        msave(self, **data)
1927        return
1928
1929class PaymentsManageFormPage(KofaEditFormPage):
1930    """ Page to manage the student payments
1931
1932    This manage form page is for both students and students officers.
1933    """
1934    grok.context(IStudentPaymentsContainer)
1935    grok.name('index')
1936    grok.require('waeup.viewStudent')
1937    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1938    grok.template('paymentsmanagepage')
1939    pnav = 4
1940
1941    @property
1942    def manage_payments_allowed(self):
1943        return checkPermission('waeup.payStudent', self.context)
1944
1945    def unremovable(self, ticket):
1946        usertype = getattr(self.request.principal, 'user_type', None)
1947        if not usertype:
1948            return False
1949        if not self.manage_payments_allowed:
1950            return True
1951        return (self.request.principal.user_type == 'student' and ticket.r_code)
1952
1953    @property
1954    def label(self):
1955        return _('${a}: Payments',
1956            mapping = {'a':self.context.__parent__.display_fullname})
1957
1958    @jsaction(_('Remove selected tickets'))
1959    def delPaymentTicket(self, **data):
1960        form = self.request.form
1961        if 'val_id' in form:
1962            child_id = form['val_id']
1963        else:
1964            self.flash(_('No payment selected.'), type="warning")
1965            self.redirect(self.url(self.context))
1966            return
1967        if not isinstance(child_id, list):
1968            child_id = [child_id]
1969        deleted = []
1970        for id in child_id:
1971            # Students are not allowed to remove used payment tickets
1972            ticket = self.context.get(id, None)
1973            if ticket is not None and not self.unremovable(ticket):
1974                del self.context[id]
1975                deleted.append(id)
1976        if len(deleted):
1977            self.flash(_('Successfully removed: ${a}',
1978                mapping = {'a': ', '.join(deleted)}))
1979            self.context.writeLogMessage(
1980                self,'removed: %s' % ', '.join(deleted))
1981        self.redirect(self.url(self.context))
1982        return
1983
1984    #@action(_('Add online payment ticket'))
1985    #def addPaymentTicket(self, **data):
1986    #    self.redirect(self.url(self.context, '@@addop'))
1987
1988class OnlinePaymentAddFormPage(KofaAddFormPage):
1989    """ Page to add an online payment ticket
1990    """
1991    grok.context(IStudentPaymentsContainer)
1992    grok.name('addop')
1993    grok.template('onlinepaymentaddform')
1994    grok.require('waeup.payStudent')
1995    form_fields = grok.AutoFields(IStudentOnlinePayment).select('p_combi')
1996    label = _('Add online payment')
1997    pnav = 4
1998
1999    @property
2000    def selectable_categories(self):
2001        student = self.context.__parent__
2002        categories = getUtility(
2003            IKofaUtils).selectable_payment_categories(student)
2004        return sorted(categories.items(), key=lambda value: value[1])
2005
2006    @action(_('Create ticket'), style='primary')
2007    def createTicket(self, **data):
2008        form = self.request.form
2009        p_category = form.get('form.p_category', None)
2010        p_combi = form.get('form.p_combi', [])
2011        if isinstance(form.get('form.p_combi', None), unicode):
2012            p_combi = [p_combi,]
2013        student = self.context.__parent__
2014        # The hostel_application payment category is temporarily used
2015        # by Uniben.
2016        if p_category in ('bed_allocation', 'hostel_application') and student[
2017            'studycourse'].current_session != grok.getSite()[
2018            'hostels'].accommodation_session:
2019                self.flash(
2020                    _('Your current session does not match ' + \
2021                    'accommodation session.'), type="danger")
2022                return
2023        if 'maintenance' in p_category:
2024            current_session = str(student['studycourse'].current_session)
2025            if not current_session in student['accommodation']:
2026                self.flash(_('You have not yet booked accommodation.'),
2027                           type="warning")
2028                return
2029        students_utils = getUtility(IStudentsUtils)
2030        error, payment = students_utils.setPaymentDetails(
2031            p_category, student, None, None, p_combi)
2032        if error is not None:
2033            self.flash(error, type="danger")
2034            return
2035        if p_category == 'transfer':
2036            payment.p_item = form['new_programme']
2037        self.context[payment.p_id] = payment
2038        self.flash(_('Payment ticket created.'))
2039        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2040        self.redirect(self.url(self.context))
2041        return
2042
2043    @action(_('Cancel'), validator=NullValidator)
2044    def cancel(self, **data):
2045        self.redirect(self.url(self.context))
2046
2047class PreviousPaymentAddFormPage(KofaAddFormPage):
2048    """ Page to add an online payment ticket for previous sessions.
2049    """
2050    grok.context(IStudentPaymentsContainer)
2051    grok.name('addpp')
2052    grok.require('waeup.payStudent')
2053    form_fields = grok.AutoFields(IStudentPreviousPayment)
2054    label = _('Add previous session online payment')
2055    pnav = 4
2056
2057    def update(self):
2058        if self.context.student.before_payment:
2059            self.flash(_("No previous payment to be made."), type="warning")
2060            self.redirect(self.url(self.context))
2061        super(PreviousPaymentAddFormPage, self).update()
2062        return
2063
2064    @action(_('Create ticket'), style='primary')
2065    def createTicket(self, **data):
2066        p_category = data['p_category']
2067        previous_session = data.get('p_session', None)
2068        previous_level = data.get('p_level', None)
2069        student = self.context.__parent__
2070        students_utils = getUtility(IStudentsUtils)
2071        error, payment = students_utils.setPaymentDetails(
2072            p_category, student, previous_session, previous_level, None)
2073        if error is not None:
2074            self.flash(error, type="danger")
2075            return
2076        self.context[payment.p_id] = payment
2077        self.flash(_('Payment ticket created.'))
2078        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2079        self.redirect(self.url(self.context))
2080        return
2081
2082    @action(_('Cancel'), validator=NullValidator)
2083    def cancel(self, **data):
2084        self.redirect(self.url(self.context))
2085
2086class BalancePaymentAddFormPage(KofaAddFormPage):
2087    """ Page to add an online payment which can balance s previous session
2088    payment.
2089    """
2090    grok.context(IStudentPaymentsContainer)
2091    grok.name('addbp')
2092    grok.require('waeup.manageStudent')
2093    form_fields = grok.AutoFields(IStudentBalancePayment)
2094    label = _('Add balance')
2095    pnav = 4
2096
2097    @action(_('Create ticket'), style='primary')
2098    def createTicket(self, **data):
2099        p_category = data['p_category']
2100        balance_session = data.get('balance_session', None)
2101        balance_level = data.get('balance_level', None)
2102        balance_amount = data.get('balance_amount', None)
2103        student = self.context.__parent__
2104        students_utils = getUtility(IStudentsUtils)
2105        error, payment = students_utils.setBalanceDetails(
2106            p_category, student, balance_session,
2107            balance_level, balance_amount)
2108        if error is not None:
2109            self.flash(error, type="danger")
2110            return
2111        self.context[payment.p_id] = payment
2112        self.flash(_('Payment ticket created.'))
2113        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2114        self.redirect(self.url(self.context))
2115        return
2116
2117    @action(_('Cancel'), validator=NullValidator)
2118    def cancel(self, **data):
2119        self.redirect(self.url(self.context))
2120
2121class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2122    """ Page to view an online payment ticket
2123    """
2124    grok.context(IStudentOnlinePayment)
2125    grok.name('index')
2126    grok.require('waeup.viewStudent')
2127    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2128        'p_item', 'p_combi')
2129    form_fields[
2130        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2131    form_fields[
2132        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2133    pnav = 4
2134
2135    @property
2136    def label(self):
2137        return _('${a}: Online Payment Ticket ${b}', mapping = {
2138            'a':self.context.student.display_fullname,
2139            'b':self.context.p_id})
2140
2141class OnlinePaymentApproveView(UtilityView, grok.View):
2142    """ Callback view
2143    """
2144    grok.context(IStudentOnlinePayment)
2145    grok.name('approve')
2146    grok.require('waeup.managePortal')
2147
2148    def update(self):
2149        flashtype, msg, log = self.context.approveStudentPayment()
2150        if log is not None:
2151            # Add log message to students.log
2152            self.context.writeLogMessage(self,log)
2153            # Add log message to payments.log
2154            self.context.logger.info(
2155                '%s,%s,%s,%s,%s,,,,,,' % (
2156                self.context.student.student_id,
2157                self.context.p_id, self.context.p_category,
2158                self.context.amount_auth, self.context.r_code))
2159        self.flash(msg, type=flashtype)
2160        return
2161
2162    def render(self):
2163        self.redirect(self.url(self.context, '@@index'))
2164        return
2165
2166class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2167    """ Approval view for students.
2168
2169    This view is used for browser tests only and
2170    must be neutralized in custom pages!
2171    """
2172    grok.name('fake_approve')
2173    grok.require('waeup.payStudent')
2174
2175class ExportPDFPaymentSlip(UtilityView, grok.View):
2176    """Deliver a PDF slip of the context.
2177    """
2178    grok.context(IStudentOnlinePayment)
2179    grok.name('payment_slip.pdf')
2180    grok.require('waeup.viewStudent')
2181    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2182        'p_item', 'p_combi')
2183    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2184    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2185    prefix = 'form'
2186    note = None
2187    omit_fields = (
2188        'password', 'suspended', 'phone', 'date_of_birth',
2189        'adm_code', 'sex', 'suspended_comment', 'current_level',
2190        'flash_notice')
2191
2192    @property
2193    def title(self):
2194        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2195        return translate(_('Payment Data'), 'waeup.kofa',
2196            target_language=portal_language)
2197
2198    @property
2199    def label(self):
2200        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2201        return translate(_('Online Payment Slip'),
2202            'waeup.kofa', target_language=portal_language) \
2203            + ' %s' % self.context.p_id
2204
2205    def render(self):
2206        #if self.context.p_state != 'paid':
2207        #    self.flash('Ticket not yet paid.')
2208        #    self.redirect(self.url(self.context))
2209        #    return
2210        studentview = StudentBasePDFFormPage(self.context.student,
2211            self.request, self.omit_fields)
2212        students_utils = getUtility(IStudentsUtils)
2213        return students_utils.renderPDF(self, 'payment_slip.pdf',
2214            self.context.student, studentview, note=self.note,
2215            omit_fields=self.omit_fields)
2216
2217
2218class AccommodationManageFormPage(KofaEditFormPage):
2219    """ Page to manage bed tickets.
2220
2221    This manage form page is for both students and students officers.
2222    """
2223    grok.context(IStudentAccommodation)
2224    grok.name('index')
2225    grok.require('waeup.handleAccommodation')
2226    form_fields = grok.AutoFields(IStudentAccommodation)
2227    grok.template('accommodationmanagepage')
2228    pnav = 4
2229    with_hostel_selection = True
2230
2231    @property
2232    def booking_allowed(self):
2233        students_utils = getUtility(IStudentsUtils)
2234        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2235        error_message = students_utils.checkAccommodationRequirements(
2236            self.context.student, acc_details)
2237        if error_message:
2238            return False
2239        return True
2240
2241    @property
2242    def actionsgroup1(self):
2243        if not self.booking_allowed:
2244            return []
2245        if not self.with_hostel_selection:
2246            return []
2247        return [_('Save')]
2248
2249    @property
2250    def actionsgroup2(self):
2251        if getattr(self.request.principal, 'user_type', None) == 'student':
2252            ## Book button can be disabled in custom packages by
2253            ## uncommenting the following lines.
2254            #if not self.booking_allowed:
2255            #    return []
2256            return [_('Book accommodation')]
2257        return [_('Book accommodation'), _('Remove selected')]
2258
2259    @property
2260    def label(self):
2261        return _('${a}: Accommodation',
2262            mapping = {'a':self.context.__parent__.display_fullname})
2263
2264    @property
2265    def desired_hostel(self):
2266        if self.context.desired_hostel == 'no':
2267            return _('No favoured hostel')
2268        if self.context.desired_hostel:
2269            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2270            if hostel is not None:
2271                return hostel.hostel_name
2272        return
2273
2274    def getHostels(self):
2275        """Get a list of all stored hostels.
2276        """
2277        yield(dict(name=None, title='--', selected=''))
2278        selected = ''
2279        if self.context.desired_hostel == 'no':
2280          selected = 'selected'
2281        yield(dict(name='no', title=_('No favoured hostel'), selected=selected))
2282        for val in grok.getSite()['hostels'].values():
2283            selected = ''
2284            if val.hostel_id == self.context.desired_hostel:
2285                selected = 'selected'
2286            yield(dict(name=val.hostel_id, title=val.hostel_name,
2287                       selected=selected))
2288
2289    @action(_('Save'), style='primary')
2290    def save(self):
2291        hostel = self.request.form.get('hostel', None)
2292        self.context.desired_hostel = hostel
2293        self.flash(_('Your selection has been saved.'))
2294        return
2295
2296    @action(_('Book accommodation'), style='primary')
2297    def bookAccommodation(self, **data):
2298        self.redirect(self.url(self.context, 'add'))
2299        return
2300
2301    @jsaction(_('Remove selected'))
2302    def delBedTickets(self, **data):
2303        if getattr(self.request.principal, 'user_type', None) == 'student':
2304            self.flash(_('You are not allowed to remove bed tickets.'),
2305                       type="warning")
2306            self.redirect(self.url(self.context))
2307            return
2308        form = self.request.form
2309        if 'val_id' in form:
2310            child_id = form['val_id']
2311        else:
2312            self.flash(_('No bed ticket selected.'), type="warning")
2313            self.redirect(self.url(self.context))
2314            return
2315        if not isinstance(child_id, list):
2316            child_id = [child_id]
2317        deleted = []
2318        for id in child_id:
2319            del self.context[id]
2320            deleted.append(id)
2321        if len(deleted):
2322            self.flash(_('Successfully removed: ${a}',
2323                mapping = {'a':', '.join(deleted)}))
2324            self.context.writeLogMessage(
2325                self,'removed: % s' % ', '.join(deleted))
2326        self.redirect(self.url(self.context))
2327        return
2328
2329class BedTicketAddPage(KofaPage):
2330    """ Page to add a bed ticket
2331    """
2332    grok.context(IStudentAccommodation)
2333    grok.name('add')
2334    grok.require('waeup.handleAccommodation')
2335    #grok.template('enterpin')
2336    ac_prefix = 'HOS'
2337    label = _('Add bed ticket')
2338    pnav = 4
2339    buttonname = _('Create bed ticket')
2340    notice = ''
2341    with_ac = True
2342    with_bedselection = True
2343
2344    @property
2345    def getAvailableBeds(self):
2346        """Get a list of all available beds.
2347        """
2348        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2349        entries = cat.searchResults(
2350            bed_type=(self.acc_details['bt'],self.acc_details['bt']))
2351        available_beds = [
2352            entry for entry in entries if entry.owner == NOT_OCCUPIED]
2353        desired_hostel = self.context.desired_hostel
2354        # Filter desired hostel beds
2355        if desired_hostel and desired_hostel != 'no':
2356            filtered_beds = [bed for bed in available_beds
2357                             if bed.bed_id.startswith(desired_hostel)]
2358            available_beds = filtered_beds
2359        # Add legible bed coordinates
2360        for bed in available_beds:
2361            hall_title = bed.__parent__.hostel_name
2362            coordinates = bed.coordinates[1:]
2363            block, room_nr, bed_nr = coordinates
2364            bed.temp_bed_coordinates = _(
2365                '${a}, Block ${b}, Room ${c}, Bed ${d}', mapping = {
2366                'a':hall_title, 'b':block,
2367                'c':room_nr, 'd':bed_nr})
2368        return available_beds
2369
2370    def update(self, SUBMIT=None):
2371        student = self.context.student
2372        students_utils = getUtility(IStudentsUtils)
2373        self.acc_details  = students_utils.getAccommodationDetails(student)
2374        error_message = students_utils.checkAccommodationRequirements(
2375            student, self.acc_details)
2376        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2377        entries = cat.searchResults(
2378            owner=(student.student_id,student.student_id))
2379        self.show_available_beds = False
2380        if error_message:
2381            self.flash(error_message, type="warning")
2382            self.redirect(self.url(self.context))
2383            return
2384        if self.with_ac:
2385            self.ac_series = self.request.form.get('ac_series', None)
2386            self.ac_number = self.request.form.get('ac_number', None)
2387        available_beds = self.getAvailableBeds
2388        if SUBMIT is None:
2389            if self.with_bedselection and available_beds and not len(entries):
2390                self.show_available_beds = True
2391            return
2392        if self.with_ac:
2393            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2394            code = get_access_code(pin)
2395            if not code:
2396                self.flash(_('Activation code is invalid.'), type="warning")
2397                return
2398        # Search and book bed
2399        if len(entries):
2400            # If bed space has been manually allocated use this bed ...
2401            manual = True
2402            bed = list(entries)[0]
2403        else:
2404            # ... else search for available beds
2405            manual = False
2406            selected_bed = self.request.form.get('bed', None)
2407            if selected_bed:
2408                # Use selected bed
2409                beds = cat.searchResults(
2410                    bed_id=(selected_bed,selected_bed))
2411                bed = list(beds)[0]
2412                bed.bookBed(student.student_id)
2413            elif available_beds:
2414                # Select bed according to selectBed method
2415                students_utils = getUtility(IStudentsUtils)
2416                bed = students_utils.selectBed(available_beds)
2417                bed.bookBed(student.student_id)
2418            else:
2419                self.flash(_('There is no free bed in your category ${a}.',
2420                    mapping = {'a':self.acc_details['bt']}), type="warning")
2421                self.redirect(self.url(self.context))
2422                return
2423        if self.with_ac:
2424            # Mark pin as used (this also fires a pin related transition)
2425            if code.state == USED:
2426                self.flash(_('Activation code has already been used.'),
2427                           type="warning")
2428                if not manual:
2429                    # Release the previously booked bed
2430                    bed.owner = NOT_OCCUPIED
2431                    # Catalog must be informed
2432                    notify(grok.ObjectModifiedEvent(bed))
2433                return
2434            else:
2435                comment = _(u'invalidated')
2436                # Here we know that the ac is in state initialized so we do not
2437                # expect an exception, but the owner might be different
2438                success = invalidate_accesscode(
2439                    pin, comment, self.context.student.student_id)
2440                if not success:
2441                    self.flash(_('You are not the owner of this access code.'),
2442                               type="warning")
2443                    if not manual:
2444                        # Release the previously booked bed
2445                        bed.owner = NOT_OCCUPIED
2446                        # Catalog must be informed
2447                        notify(grok.ObjectModifiedEvent(bed))
2448                    return
2449        # Create bed ticket
2450        bedticket = createObject(u'waeup.BedTicket')
2451        if self.with_ac:
2452            bedticket.booking_code = pin
2453        bedticket.booking_session = self.acc_details['booking_session']
2454        bedticket.bed_type = self.acc_details['bt']
2455        bedticket.bed = bed
2456        hall_title = bed.__parent__.hostel_name
2457        coordinates = bed.coordinates[1:]
2458        block, room_nr, bed_nr = coordinates
2459        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2460            'a':hall_title, 'b':block,
2461            'c':room_nr, 'd':bed_nr,
2462            'e':bed.bed_type})
2463        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2464        bedticket.bed_coordinates = translate(
2465            bc, 'waeup.kofa',target_language=portal_language)
2466        self.context.addBedTicket(bedticket)
2467        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2468        self.flash(_('Bed ticket created and bed booked: ${a}',
2469            mapping = {'a':bedticket.display_coordinates}))
2470        self.redirect(self.url(self.context))
2471        return
2472
2473class BedTicketDisplayFormPage(KofaDisplayFormPage):
2474    """ Page to display bed tickets
2475    """
2476    grok.context(IBedTicket)
2477    grok.name('index')
2478    grok.require('waeup.handleAccommodation')
2479    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2480    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2481    pnav = 4
2482
2483    @property
2484    def label(self):
2485        return _('Bed Ticket for Session ${a}',
2486            mapping = {'a':self.context.getSessionString()})
2487
2488class ExportPDFBedTicketSlip(UtilityView, grok.View):
2489    """Deliver a PDF slip of the context.
2490    """
2491    grok.context(IBedTicket)
2492    grok.name('bed_allocation_slip.pdf')
2493    grok.require('waeup.handleAccommodation')
2494    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2495    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2496    prefix = 'form'
2497    omit_fields = (
2498        'password', 'suspended', 'phone', 'adm_code',
2499        'suspended_comment', 'date_of_birth', 'current_level',
2500        'flash_notice')
2501
2502    @property
2503    def title(self):
2504        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2505        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2506            target_language=portal_language)
2507
2508    @property
2509    def label(self):
2510        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2511        #return translate(_('Bed Allocation: '),
2512        #    'waeup.kofa', target_language=portal_language) \
2513        #    + ' %s' % self.context.bed_coordinates
2514        return translate(_('Bed Allocation Slip'),
2515            'waeup.kofa', target_language=portal_language) \
2516            + ' %s' % self.context.getSessionString()
2517
2518    def render(self):
2519        studentview = StudentBasePDFFormPage(self.context.student,
2520            self.request, self.omit_fields)
2521        students_utils = getUtility(IStudentsUtils)
2522        note = None
2523        n = grok.getSite()['hostels'].allocation_expiration
2524        if n:
2525            note = _("""
2526<br /><br /><br /><br /><br /><font size="12">
2527Please endeavour to pay your hostel maintenance charge within ${a} days
2528 of being allocated a space or else you are deemed to have
2529 voluntarily forfeited it and it goes back into circulation to be
2530 available for booking afresh!</font>)
2531""")
2532            note = _(note, mapping={'a': n})
2533            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2534            note = translate(
2535                note, 'waeup.kofa', target_language=portal_language)
2536        return students_utils.renderPDF(
2537            self, 'bed_allocation_slip.pdf',
2538            self.context.student, studentview,
2539            omit_fields=self.omit_fields,
2540            note=note)
2541
2542class BedTicketRelocationView(UtilityView, grok.View):
2543    """ Callback view
2544    """
2545    grok.context(IBedTicket)
2546    grok.name('relocate')
2547    grok.require('waeup.manageHostels')
2548
2549    # Relocate student if student parameters have changed or the bed_type
2550    # of the bed has changed
2551    def update(self):
2552        success, msg = self.context.relocateStudent()
2553        if not success:
2554            self.flash(msg, type="warning")
2555        else:
2556            self.flash(msg)
2557        self.redirect(self.url(self.context))
2558        return
2559
2560    def render(self):
2561        return
2562
2563class StudentHistoryPage(KofaPage):
2564    """ Page to display student history
2565    """
2566    grok.context(IStudent)
2567    grok.name('history')
2568    grok.require('waeup.viewStudent')
2569    grok.template('studenthistory')
2570    pnav = 4
2571
2572    @property
2573    def label(self):
2574        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2575
2576# Pages for students only
2577
2578class StudentBaseEditFormPage(KofaEditFormPage):
2579    """ View to edit student base data
2580    """
2581    grok.context(IStudent)
2582    grok.name('edit_base')
2583    grok.require('waeup.handleStudent')
2584    form_fields = grok.AutoFields(IStudentBase).select(
2585        'email', 'phone', 'parents_email')
2586    label = _('Edit base data')
2587    pnav = 4
2588
2589    @action(_('Save'), style='primary')
2590    def save(self, **data):
2591        msave(self, **data)
2592        return
2593
2594class StudentChangePasswordPage(KofaEditFormPage):
2595    """ View to edit student passwords
2596    """
2597    grok.context(IStudent)
2598    grok.name('change_password')
2599    grok.require('waeup.handleStudent')
2600    grok.template('change_password')
2601    label = _('Change password')
2602    pnav = 4
2603
2604    @action(_('Save'), style='primary')
2605    def save(self, **data):
2606        form = self.request.form
2607        password = form.get('change_password', None)
2608        password_ctl = form.get('change_password_repeat', None)
2609        if password:
2610            validator = getUtility(IPasswordValidator)
2611            errors = validator.validate_password(password, password_ctl)
2612            if not errors:
2613                IUserAccount(self.context).setPassword(password)
2614                # Unset temporary password
2615                self.context.temp_password = None
2616                self.context.writeLogMessage(self, 'saved: password')
2617                self.flash(_('Password changed.'))
2618            else:
2619                self.flash( ' '.join(errors), type="warning")
2620        return
2621
2622class StudentFilesUploadPage(KofaPage):
2623    """ View to upload files by student
2624    """
2625    grok.context(IStudent)
2626    grok.name('change_portrait')
2627    grok.require('waeup.uploadStudentFile')
2628    grok.template('filesuploadpage')
2629    label = _('Upload portrait')
2630    pnav = 4
2631
2632    def update(self):
2633        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2634        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2635            emit_lock_message(self)
2636            return
2637        super(StudentFilesUploadPage, self).update()
2638        return
2639
2640class StartClearancePage(KofaPage):
2641    grok.context(IStudent)
2642    grok.name('start_clearance')
2643    grok.require('waeup.handleStudent')
2644    grok.template('enterpin')
2645    label = _('Start clearance')
2646    ac_prefix = 'CLR'
2647    notice = ''
2648    pnav = 4
2649    buttonname = _('Start clearance now')
2650    with_ac = True
2651
2652    @property
2653    def all_required_fields_filled(self):
2654        if not self.context.email:
2655            return _("Email address is missing."), 'edit_base'
2656        if not self.context.phone:
2657            return _("Phone number is missing."), 'edit_base'
2658        return
2659
2660    @property
2661    def portrait_uploaded(self):
2662        store = getUtility(IExtFileStore)
2663        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2664            return True
2665        return False
2666
2667    def update(self, SUBMIT=None):
2668        if not self.context.state == ADMITTED:
2669            self.flash(_("Wrong state"), type="warning")
2670            self.redirect(self.url(self.context))
2671            return
2672        if not self.portrait_uploaded:
2673            self.flash(_("No portrait uploaded."), type="warning")
2674            self.redirect(self.url(self.context, 'change_portrait'))
2675            return
2676        if self.all_required_fields_filled:
2677            arf_warning = self.all_required_fields_filled[0]
2678            arf_redirect = self.all_required_fields_filled[1]
2679            self.flash(arf_warning, type="warning")
2680            self.redirect(self.url(self.context, arf_redirect))
2681            return
2682        if self.with_ac:
2683            self.ac_series = self.request.form.get('ac_series', None)
2684            self.ac_number = self.request.form.get('ac_number', None)
2685        if SUBMIT is None:
2686            return
2687        if self.with_ac:
2688            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2689            code = get_access_code(pin)
2690            if not code:
2691                self.flash(_('Activation code is invalid.'), type="warning")
2692                return
2693            if code.state == USED:
2694                self.flash(_('Activation code has already been used.'),
2695                           type="warning")
2696                return
2697            # Mark pin as used (this also fires a pin related transition)
2698            # and fire transition start_clearance
2699            comment = _(u"invalidated")
2700            # Here we know that the ac is in state initialized so we do not
2701            # expect an exception, but the owner might be different
2702            if not invalidate_accesscode(pin, comment, self.context.student_id):
2703                self.flash(_('You are not the owner of this access code.'),
2704                           type="warning")
2705                return
2706            self.context.clr_code = pin
2707        IWorkflowInfo(self.context).fireTransition('start_clearance')
2708        self.flash(_('Clearance process has been started.'))
2709        self.redirect(self.url(self.context,'cedit'))
2710        return
2711
2712class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2713    """ View to edit student clearance data by student
2714    """
2715    grok.context(IStudent)
2716    grok.name('cedit')
2717    grok.require('waeup.handleStudent')
2718    label = _('Edit clearance data')
2719
2720    @property
2721    def form_fields(self):
2722        if self.context.is_postgrad:
2723            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2724                'clr_code', 'officer_comment')
2725        else:
2726            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2727                'clr_code', 'officer_comment')
2728        return form_fields
2729
2730    def update(self):
2731        if self.context.clearance_locked:
2732            emit_lock_message(self)
2733            return
2734        return super(StudentClearanceEditFormPage, self).update()
2735
2736    @action(_('Save'), style='primary')
2737    def save(self, **data):
2738        self.applyData(self.context, **data)
2739        self.flash(_('Clearance form has been saved.'))
2740        return
2741
2742    def dataNotComplete(self):
2743        """To be implemented in the customization package.
2744        """
2745        return False
2746
2747    @action(_('Save and request clearance'), style='primary',
2748            warning=_('You can not edit your data after '
2749            'requesting clearance. You really want to request clearance now?'))
2750    def requestClearance(self, **data):
2751        self.applyData(self.context, **data)
2752        if self.dataNotComplete():
2753            self.flash(self.dataNotComplete(), type="warning")
2754            return
2755        self.flash(_('Clearance form has been saved.'))
2756        if self.context.clr_code:
2757            self.redirect(self.url(self.context, 'request_clearance'))
2758        else:
2759            # We bypass the request_clearance page if student
2760            # has been imported in state 'clearance started' and
2761            # no clr_code was entered before.
2762            state = IWorkflowState(self.context).getState()
2763            if state != CLEARANCE:
2764                # This shouldn't happen, but the application officer
2765                # might have forgotten to lock the form after changing the state
2766                self.flash(_('This form cannot be submitted. Wrong state!'),
2767                           type="danger")
2768                return
2769            IWorkflowInfo(self.context).fireTransition('request_clearance')
2770            self.flash(_('Clearance has been requested.'))
2771            self.redirect(self.url(self.context))
2772        return
2773
2774class RequestClearancePage(KofaPage):
2775    grok.context(IStudent)
2776    grok.name('request_clearance')
2777    grok.require('waeup.handleStudent')
2778    grok.template('enterpin')
2779    label = _('Request clearance')
2780    notice = _('Enter the CLR access code used for starting clearance.')
2781    ac_prefix = 'CLR'
2782    pnav = 4
2783    buttonname = _('Request clearance now')
2784    with_ac = True
2785
2786    def update(self, SUBMIT=None):
2787        if self.with_ac:
2788            self.ac_series = self.request.form.get('ac_series', None)
2789            self.ac_number = self.request.form.get('ac_number', None)
2790        if SUBMIT is None:
2791            return
2792        if self.with_ac:
2793            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2794            if self.context.clr_code and self.context.clr_code != pin:
2795                self.flash(_("This isn't your CLR access code."), type="danger")
2796                return
2797        state = IWorkflowState(self.context).getState()
2798        if state != CLEARANCE:
2799            # This shouldn't happen, but the application officer
2800            # might have forgotten to lock the form after changing the state
2801            self.flash(_('This form cannot be submitted. Wrong state!'),
2802                       type="danger")
2803            return
2804        IWorkflowInfo(self.context).fireTransition('request_clearance')
2805        self.flash(_('Clearance has been requested.'))
2806        self.redirect(self.url(self.context))
2807        return
2808
2809class StartSessionPage(KofaPage):
2810    grok.context(IStudentStudyCourse)
2811    grok.name('start_session')
2812    grok.require('waeup.handleStudent')
2813    grok.template('enterpin')
2814    label = _('Start session')
2815    ac_prefix = 'SFE'
2816    notice = ''
2817    pnav = 4
2818    buttonname = _('Start now')
2819    with_ac = True
2820
2821    def update(self, SUBMIT=None):
2822        if not self.context.is_current:
2823            emit_lock_message(self)
2824            return
2825        super(StartSessionPage, self).update()
2826        if not self.context.next_session_allowed:
2827            self.flash(_("You are not entitled to start session."),
2828                       type="warning")
2829            self.redirect(self.url(self.context))
2830            return
2831        if self.with_ac:
2832            self.ac_series = self.request.form.get('ac_series', None)
2833            self.ac_number = self.request.form.get('ac_number', None)
2834        if SUBMIT is None:
2835            return
2836        if self.with_ac:
2837            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2838            code = get_access_code(pin)
2839            if not code:
2840                self.flash(_('Activation code is invalid.'), type="warning")
2841                return
2842            # Mark pin as used (this also fires a pin related transition)
2843            if code.state == USED:
2844                self.flash(_('Activation code has already been used.'),
2845                           type="warning")
2846                return
2847            else:
2848                comment = _(u"invalidated")
2849                # Here we know that the ac is in state initialized so we do not
2850                # expect an error, but the owner might be different
2851                if not invalidate_accesscode(
2852                    pin,comment,self.context.student.student_id):
2853                    self.flash(_('You are not the owner of this access code.'),
2854                               type="warning")
2855                    return
2856        try:
2857            if self.context.student.state == CLEARED:
2858                IWorkflowInfo(self.context.student).fireTransition(
2859                    'pay_first_school_fee')
2860            elif self.context.student.state == RETURNING:
2861                IWorkflowInfo(self.context.student).fireTransition(
2862                    'pay_school_fee')
2863            elif self.context.student.state == PAID:
2864                IWorkflowInfo(self.context.student).fireTransition(
2865                    'pay_pg_fee')
2866        except ConstraintNotSatisfied:
2867            self.flash(_('An error occurred, please contact the system administrator.'),
2868                       type="danger")
2869            return
2870        self.flash(_('Session started.'))
2871        self.redirect(self.url(self.context))
2872        return
2873
2874class AddStudyLevelFormPage(KofaEditFormPage):
2875    """ Page for students to add current study levels
2876    """
2877    grok.context(IStudentStudyCourse)
2878    grok.name('add')
2879    grok.require('waeup.handleStudent')
2880    grok.template('studyleveladdpage')
2881    form_fields = grok.AutoFields(IStudentStudyCourse)
2882    pnav = 4
2883
2884    @property
2885    def label(self):
2886        studylevelsource = StudyLevelSource().factory
2887        code = self.context.current_level
2888        title = studylevelsource.getTitle(self.context, code)
2889        return _('Add current level ${a}', mapping = {'a':title})
2890
2891    def update(self):
2892        if not self.context.is_current \
2893            or self.context.student.studycourse_locked:
2894            emit_lock_message(self)
2895            return
2896        if self.context.student.state != PAID:
2897            emit_lock_message(self)
2898            return
2899        code = self.context.current_level
2900        if code is None:
2901            self.flash(_('Your data are incomplete'), type="danger")
2902            self.redirect(self.url(self.context))
2903            return
2904        super(AddStudyLevelFormPage, self).update()
2905        return
2906
2907    @action(_('Create course list now'), style='primary')
2908    def addStudyLevel(self, **data):
2909        studylevel = createObject(u'waeup.StudentStudyLevel')
2910        studylevel.level = self.context.current_level
2911        studylevel.level_session = self.context.current_session
2912        try:
2913            self.context.addStudentStudyLevel(
2914                self.context.certificate,studylevel)
2915        except KeyError:
2916            self.flash(_('This level exists.'), type="warning")
2917            self.redirect(self.url(self.context))
2918            return
2919        except RequiredMissing:
2920            self.flash(_('Your data are incomplete.'), type="danger")
2921            self.redirect(self.url(self.context))
2922            return
2923        self.flash(_('You successfully created a new course list.'))
2924        self.redirect(self.url(self.context, str(studylevel.level)))
2925        return
2926
2927class StudyLevelEditFormPage(KofaEditFormPage):
2928    """ Page to edit the student study level data by students
2929    """
2930    grok.context(IStudentStudyLevel)
2931    grok.name('edit')
2932    grok.require('waeup.editStudyLevel')
2933    grok.template('studyleveleditpage')
2934    pnav = 4
2935    placeholder = _('Enter valid course code')
2936
2937    def update(self, ADD=None, course=None):
2938        if not self.context.__parent__.is_current:
2939            emit_lock_message(self)
2940            return
2941        if self.context.student.state != PAID or \
2942            not self.context.is_current_level:
2943            emit_lock_message(self)
2944            return
2945        super(StudyLevelEditFormPage, self).update()
2946        if ADD is not None:
2947            if not course:
2948                self.flash(_('No valid course code entered.'), type="warning")
2949                return
2950            cat = queryUtility(ICatalog, name='courses_catalog')
2951            result = cat.searchResults(code=(course, course))
2952            if len(result) != 1:
2953                self.flash(_('Course not found.'), type="warning")
2954                return
2955            course = list(result)[0]
2956            addCourseTicket(self, course)
2957        return
2958
2959    @property
2960    def label(self):
2961        # Here we know that the cookie has been set
2962        lang = self.request.cookies.get('kofa.language')
2963        level_title = translate(self.context.level_title, 'waeup.kofa',
2964            target_language=lang)
2965        return _('Edit course list of ${a}',
2966            mapping = {'a':level_title})
2967
2968    @property
2969    def translated_values(self):
2970        return translated_values(self)
2971
2972    def _delCourseTicket(self, **data):
2973        form = self.request.form
2974        if 'val_id' in form:
2975            child_id = form['val_id']
2976        else:
2977            self.flash(_('No ticket selected.'), type="warning")
2978            self.redirect(self.url(self.context, '@@edit'))
2979            return
2980        if not isinstance(child_id, list):
2981            child_id = [child_id]
2982        deleted = []
2983        for id in child_id:
2984            # Students are not allowed to remove core tickets
2985            if id in self.context and \
2986                self.context[id].removable_by_student:
2987                del self.context[id]
2988                deleted.append(id)
2989        if len(deleted):
2990            self.flash(_('Successfully removed: ${a}',
2991                mapping = {'a':', '.join(deleted)}))
2992            self.context.writeLogMessage(
2993                self,'removed: %s at %s' %
2994                (', '.join(deleted), self.context.level))
2995        self.redirect(self.url(self.context, u'@@edit'))
2996        return
2997
2998    @jsaction(_('Remove selected tickets'))
2999    def delCourseTicket(self, **data):
3000        self._delCourseTicket(**data)
3001        return
3002
3003    def _updateTickets(self, **data):
3004        cat = queryUtility(ICatalog, name='courses_catalog')
3005        invalidated = list()
3006        for value in self.context.values():
3007            result = cat.searchResults(code=(value.code, value.code))
3008            if len(result) != 1:
3009                course = None
3010            else:
3011                course = list(result)[0]
3012            invalid = self.context.updateCourseTicket(value, course)
3013            if invalid:
3014                invalidated.append(invalid)
3015        if invalidated:
3016            invalidated_string = ', '.join(invalidated)
3017            self.context.writeLogMessage(
3018                self, 'course tickets invalidated: %s' % invalidated_string)
3019        self.flash(_('All course tickets updated.'))
3020        return
3021
3022    @action(_('Update all tickets'),
3023        tooltip=_('Update all course parameters including course titles.'))
3024    def updateTickets(self, **data):
3025        self._updateTickets(**data)
3026        return
3027
3028    def _registerCourses(self, **data):
3029        if self.context.student.is_postgrad and \
3030            not self.context.student.is_special_postgrad:
3031            self.flash(_(
3032                "You are a postgraduate student, "
3033                "your course list can't bee registered."), type="warning")
3034            self.redirect(self.url(self.context))
3035            return
3036        students_utils = getUtility(IStudentsUtils)
3037        warning = students_utils.warnCreditsOOR(self.context)
3038        if warning:
3039            self.flash(warning, type="warning")
3040            return
3041        msg = self.context.course_registration_forbidden
3042        if msg:
3043            self.flash(msg, type="warning")
3044            return
3045        IWorkflowInfo(self.context.student).fireTransition(
3046            'register_courses')
3047        self.flash(_('Course list has been registered.'))
3048        self.redirect(self.url(self.context))
3049        return
3050
3051    @action(_('Register course list'), style='primary',
3052        warning=_('You can not edit your course list after registration.'
3053            ' You really want to register?'))
3054    def registerCourses(self, **data):
3055        self._registerCourses(**data)
3056        return
3057
3058class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3059    """Add a course ticket by student.
3060    """
3061    grok.name('ctadd')
3062    grok.require('waeup.handleStudent')
3063    form_fields = grok.AutoFields(ICourseTicketAdd)
3064
3065    def update(self):
3066        if self.context.student.state != PAID or \
3067            not self.context.is_current_level:
3068            emit_lock_message(self)
3069            return
3070        super(CourseTicketAddFormPage2, self).update()
3071        return
3072
3073    @action(_('Add course ticket'))
3074    def addCourseTicket(self, **data):
3075        # Safety belt
3076        if self.context.student.state != PAID:
3077            return
3078        course = data['course']
3079        success = addCourseTicket(self, course)
3080        if success:
3081            self.redirect(self.url(self.context, u'@@edit'))
3082        return
3083
3084class SetPasswordPage(KofaPage):
3085    grok.context(IKofaObject)
3086    grok.name('setpassword')
3087    grok.require('waeup.Anonymous')
3088    grok.template('setpassword')
3089    label = _('Set password for first-time login')
3090    ac_prefix = 'PWD'
3091    pnav = 0
3092    set_button = _('Set')
3093
3094    def update(self, SUBMIT=None):
3095        self.reg_number = self.request.form.get('reg_number', None)
3096        self.ac_series = self.request.form.get('ac_series', None)
3097        self.ac_number = self.request.form.get('ac_number', None)
3098
3099        if SUBMIT is None:
3100            return
3101        hitlist = search(query=self.reg_number,
3102            searchtype='reg_number', view=self)
3103        if not hitlist:
3104            self.flash(_('No student found.'), type="warning")
3105            return
3106        if len(hitlist) != 1:   # Cannot happen but anyway
3107            self.flash(_('More than one student found.'), type="warning")
3108            return
3109        student = hitlist[0].context
3110        self.student_id = student.student_id
3111        student_pw = student.password
3112        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3113        code = get_access_code(pin)
3114        if not code:
3115            self.flash(_('Access code is invalid.'), type="warning")
3116            return
3117        if student_pw and pin == student.adm_code:
3118            self.flash(_(
3119                'Password has already been set. Your Student Id is ${a}',
3120                mapping = {'a':self.student_id}))
3121            return
3122        elif student_pw:
3123            self.flash(
3124                _('Password has already been set. You are using the ' +
3125                'wrong Access Code.'), type="warning")
3126            return
3127        # Mark pin as used (this also fires a pin related transition)
3128        # and set student password
3129        if code.state == USED:
3130            self.flash(_('Access code has already been used.'), type="warning")
3131            return
3132        else:
3133            comment = _(u"invalidated")
3134            # Here we know that the ac is in state initialized so we do not
3135            # expect an exception
3136            invalidate_accesscode(pin,comment)
3137            IUserAccount(student).setPassword(self.ac_number)
3138            student.adm_code = pin
3139        self.flash(_('Password has been set. Your Student Id is ${a}',
3140            mapping = {'a':self.student_id}))
3141        return
3142
3143class StudentRequestPasswordPage(KofaAddFormPage):
3144    """Captcha'd request password page for students.
3145    """
3146    grok.name('requestpw')
3147    grok.require('waeup.Anonymous')
3148    grok.template('requestpw')
3149    form_fields = grok.AutoFields(IStudentRequestPW).select(
3150        'lastname','number','email')
3151    label = _('Request password for first-time login')
3152
3153    def update(self):
3154        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3155        if blocker:
3156            self.flash(_('The portal is in maintenance mode. '
3157                        'Password request forms are temporarily disabled.'),
3158                       type='warning')
3159            self.redirect(self.url(self.context))
3160            return
3161        # Handle captcha
3162        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3163        self.captcha_result = self.captcha.verify(self.request)
3164        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3165        return
3166
3167    def _redirect(self, email, password, student_id):
3168        # Forward only email to landing page in base package.
3169        self.redirect(self.url(self.context, 'requestpw_complete',
3170            data = dict(email=email)))
3171        return
3172
3173    def _redirect_no_student(self):
3174        # No record found, this is the truth. We do not redirect here.
3175        # We are using this method in custom packages
3176        # for redirecting alumni to the application section.
3177        self.flash(_('No student record found.'), type="warning")
3178        return
3179
3180    def _pw_used(self):
3181        # XXX: False if password has not been used. We need an extra
3182        #      attribute which remembers if student logged in.
3183        return True
3184
3185    @action(_('Send login credentials to email address'), style='primary')
3186    def get_credentials(self, **data):
3187        if not self.captcha_result.is_valid:
3188            # Captcha will display error messages automatically.
3189            # No need to flash something.
3190            return
3191        number = data.get('number','')
3192        lastname = data.get('lastname','')
3193        cat = getUtility(ICatalog, name='students_catalog')
3194        results = list(
3195            cat.searchResults(reg_number=(number, number)))
3196        if not results:
3197            results = list(
3198                cat.searchResults(matric_number=(number, number)))
3199        if results:
3200            student = results[0]
3201            if getattr(student,'lastname',None) is None:
3202                self.flash(_('An error occurred.'), type="danger")
3203                return
3204            elif student.lastname.lower() != lastname.lower():
3205                # Don't tell the truth here. Anonymous must not
3206                # know that a record was found and only the lastname
3207                # verification failed.
3208                self.flash(_('No student record found.'), type="warning")
3209                return
3210            elif student.password is not None and self._pw_used:
3211                self.flash(_('Your password has already been set and used. '
3212                             'Please proceed to the login page.'),
3213                           type="warning")
3214                return
3215            # Store email address but nothing else.
3216            student.email = data['email']
3217            notify(grok.ObjectModifiedEvent(student))
3218        else:
3219            self._redirect_no_student()
3220            return
3221
3222        kofa_utils = getUtility(IKofaUtils)
3223        password = kofa_utils.genPassword()
3224        mandate = PasswordMandate()
3225        mandate.params['password'] = password
3226        mandate.params['user'] = student
3227        site = grok.getSite()
3228        site['mandates'].addMandate(mandate)
3229        # Send email with credentials
3230        args = {'mandate_id':mandate.mandate_id}
3231        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3232        url_info = u'Confirmation link: %s' % mandate_url
3233        msg = _('You have successfully requested a password for the')
3234        if kofa_utils.sendCredentials(IUserAccount(student),
3235            password, url_info, msg):
3236            email_sent = student.email
3237        else:
3238            email_sent = None
3239        self._redirect(email=email_sent, password=password,
3240            student_id=student.student_id)
3241        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3242        self.context.logger.info(
3243            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3244        return
3245
3246class ParentsUser:
3247    pass
3248
3249class RequestParentsPasswordPage(StudentRequestPasswordPage):
3250    """Captcha'd request password page for parents.
3251    """
3252    grok.name('requestppw')
3253    grok.template('requestppw')
3254    label = _('Request password for parents access')
3255
3256    def update(self):
3257        super(RequestParentsPasswordPage, self).update()
3258        kofa_utils = getUtility(IKofaUtils)
3259        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3260        return
3261
3262    @action(_('Send temporary login credentials to email address'), style='primary')
3263    def get_credentials(self, **data):
3264        if not self.captcha_result.is_valid:
3265            # Captcha will display error messages automatically.
3266            # No need to flash something.
3267            return
3268        number = data.get('number','')
3269        lastname = data.get('lastname','')
3270        email = data['email']
3271        cat = getUtility(ICatalog, name='students_catalog')
3272        results = list(
3273            cat.searchResults(reg_number=(number, number)))
3274        if not results:
3275            results = list(
3276                cat.searchResults(matric_number=(number, number)))
3277        if results:
3278            student = results[0]
3279            if getattr(student,'lastname',None) is None:
3280                self.flash(_('An error occurred.'), type="danger")
3281                return
3282            elif student.lastname.lower() != lastname.lower():
3283                # Don't tell the truth here. Anonymous must not
3284                # know that a record was found and only the lastname
3285                # verification failed.
3286                self.flash(_('No student record found.'), type="warning")
3287                return
3288            elif email != student.parents_email:
3289                self.flash(_('Wrong email address.'), type="warning")
3290                return
3291        else:
3292            self._redirect_no_student()
3293            return
3294        kofa_utils = getUtility(IKofaUtils)
3295        password = kofa_utils.genPassword()
3296        mandate = ParentsPasswordMandate()
3297        mandate.params['password'] = password
3298        mandate.params['student'] = student
3299        site = grok.getSite()
3300        site['mandates'].addMandate(mandate)
3301        # Send email with credentials
3302        args = {'mandate_id':mandate.mandate_id}
3303        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3304        url_info = u'Confirmation link: %s' % mandate_url
3305        msg = _('You have successfully requested a parents password for the')
3306        # Create a fake user
3307        user = ParentsUser()
3308        user.name = student.student_id
3309        user.title = "Parents of %s" % student.display_fullname
3310        user.email = student.parents_email
3311        if kofa_utils.sendCredentials(user, password, url_info, msg):
3312            email_sent = user.email
3313        else:
3314            email_sent = None
3315        self._redirect(email=email_sent, password=password,
3316            student_id=student.student_id)
3317        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3318        self.context.logger.info(
3319            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3320        return
3321
3322class StudentRequestPasswordEmailSent(KofaPage):
3323    """Landing page after successful password request.
3324
3325    """
3326    grok.name('requestpw_complete')
3327    grok.require('waeup.Public')
3328    grok.template('requestpwmailsent')
3329    label = _('Your password request was successful.')
3330
3331    def update(self, email=None, student_id=None, password=None):
3332        self.email = email
3333        self.password = password
3334        self.student_id = student_id
3335        return
3336
3337class FilterStudentsInDepartmentPage(KofaPage):
3338    """Page that filters and lists students.
3339    """
3340    grok.context(IDepartment)
3341    grok.require('waeup.showStudents')
3342    grok.name('students')
3343    grok.template('filterstudentspage')
3344    pnav = 1
3345    session_label = _('Current Session')
3346    level_label = _('Current Level')
3347
3348    def label(self):
3349        return 'Students in %s' % self.context.longtitle
3350
3351    def _set_session_values(self):
3352        vocab_terms = academic_sessions_vocab.by_value.values()
3353        self.sessions = sorted(
3354            [(x.title, x.token) for x in vocab_terms], reverse=True)
3355        self.sessions += [('All Sessions', 'all')]
3356        return
3357
3358    def _set_level_values(self):
3359        vocab_terms = course_levels.by_value.values()
3360        self.levels = sorted(
3361            [(x.title, x.token) for x in vocab_terms])
3362        self.levels += [('All Levels', 'all')]
3363        return
3364
3365    def _searchCatalog(self, session, level):
3366        if level not in (10, 999, None):
3367            start_level = 100 * (level // 100)
3368            end_level = start_level + 90
3369        else:
3370            start_level = end_level = level
3371        cat = queryUtility(ICatalog, name='students_catalog')
3372        students = cat.searchResults(
3373            current_session=(session, session),
3374            current_level=(start_level, end_level),
3375            depcode=(self.context.code, self.context.code)
3376            )
3377        hitlist = []
3378        for student in students:
3379            hitlist.append(StudentQueryResultItem(student, view=self))
3380        return hitlist
3381
3382    def update(self, SHOW=None, session=None, level=None):
3383        self.parent_url = self.url(self.context.__parent__)
3384        self._set_session_values()
3385        self._set_level_values()
3386        self.hitlist = []
3387        self.session_default = session
3388        self.level_default = level
3389        if SHOW is not None:
3390            if session != 'all':
3391                self.session = int(session)
3392                self.session_string = '%s %s/%s' % (
3393                    self.session_label, self.session, self.session+1)
3394            else:
3395                self.session = None
3396                self.session_string = _('in any session')
3397            if level != 'all':
3398                self.level = int(level)
3399                self.level_string = '%s %s' % (self.level_label, self.level)
3400            else:
3401                self.level = None
3402                self.level_string = _('at any level')
3403            self.hitlist = self._searchCatalog(self.session, self.level)
3404            if not self.hitlist:
3405                self.flash(_('No student found.'), type="warning")
3406        return
3407
3408class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3409    """Page that filters and lists students.
3410    """
3411    grok.context(ICertificate)
3412
3413    def label(self):
3414        return 'Students studying %s' % self.context.longtitle
3415
3416    def _searchCatalog(self, session, level):
3417        if level not in (10, 999, None):
3418            start_level = 100 * (level // 100)
3419            end_level = start_level + 90
3420        else:
3421            start_level = end_level = level
3422        cat = queryUtility(ICatalog, name='students_catalog')
3423        students = cat.searchResults(
3424            current_session=(session, session),
3425            current_level=(start_level, end_level),
3426            certcode=(self.context.code, self.context.code)
3427            )
3428        hitlist = []
3429        for student in students:
3430            hitlist.append(StudentQueryResultItem(student, view=self))
3431        return hitlist
3432
3433class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3434    """Page that filters and lists students.
3435    """
3436    grok.context(ICourse)
3437    grok.require('waeup.viewStudent')
3438
3439    session_label = _('Session')
3440    level_label = _('Level')
3441
3442    def label(self):
3443        return 'Students registered for %s' % self.context.longtitle
3444
3445    def _searchCatalog(self, session, level):
3446        if level not in (10, 999, None):
3447            start_level = 100 * (level // 100)
3448            end_level = start_level + 90
3449        else:
3450            start_level = end_level = level
3451        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3452        coursetickets = cat.searchResults(
3453            session=(session, session),
3454            level=(start_level, end_level),
3455            code=(self.context.code, self.context.code)
3456            )
3457        hitlist = []
3458        for ticket in coursetickets:
3459            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3460        return list(set(hitlist))
3461
3462class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3463    """ Clear all students of a department in state 'clearance requested'.
3464    """
3465    grok.context(IDepartment)
3466    grok.name('clearallstudents')
3467    grok.require('waeup.clearAllStudents')
3468
3469    def update(self):
3470        cat = queryUtility(ICatalog, name='students_catalog')
3471        students = cat.searchResults(
3472            depcode=(self.context.code, self.context.code),
3473            state=(REQUESTED, REQUESTED)
3474            )
3475        num = 0
3476        for student in students:
3477            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3478                continue
3479            IWorkflowInfo(student).fireTransition('clear')
3480            num += 1
3481        self.flash(_('%d students have been cleared.' % num))
3482        self.redirect(self.url(self.context))
3483        return
3484
3485    def render(self):
3486        return
3487
3488class EditScoresPage(KofaPage):
3489    """Page that allows to edit batches of scores.
3490    """
3491    grok.context(ICourse)
3492    grok.require('waeup.editScores')
3493    grok.name('edit_scores')
3494    grok.template('editscorespage')
3495    pnav = 1
3496    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3497
3498    def label(self):
3499        return '%s tickets in academic session %s' % (
3500            self.context.code, self.session_title)
3501
3502    def _searchCatalog(self, session):
3503        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3504        # Attention: Also tickets of previous studycourses are found
3505        coursetickets = cat.searchResults(
3506            session=(session, session),
3507            code=(self.context.code, self.context.code)
3508            )
3509        return list(coursetickets)
3510
3511    def _extract_uploadfile(self, uploadfile):
3512        """Get a mapping of student-ids to scores.
3513
3514        The mapping is constructed by reading contents from `uploadfile`.
3515
3516        We expect uploadfile to be a regular CSV file with columns
3517        ``student_id`` and ``score`` (other cols are ignored).
3518        """
3519        result = dict()
3520        data = StringIO(uploadfile.read())  # ensure we have something seekable
3521        reader = csv.DictReader(data)
3522        for row in reader:
3523            if not 'student_id' in row or not 'score' in row:
3524                continue
3525            result[row['student_id']] = row['score']
3526        return result
3527
3528    def _update_scores(self, form):
3529        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3530        error = ''
3531        if 'UPDATE_FILE' in form:
3532            if form['uploadfile']:
3533                try:
3534                    formvals = self._extract_uploadfile(form['uploadfile'])
3535                except:
3536                    self.flash(
3537                        _('Uploaded file contains illegal data. Ignored'),
3538                        type="danger")
3539                    return False
3540            else:
3541                self.flash(
3542                    _('No file provided.'), type="danger")
3543                return False
3544        else:
3545            formvals = dict(zip(form['sids'], form['scores']))
3546        for ticket in self.editable_tickets:
3547            score = ticket.score
3548            sid = ticket.student.student_id
3549            if sid not in formvals:
3550                continue
3551            if formvals[sid] == '':
3552                score = None
3553            else:
3554                try:
3555                    score = int(formvals[sid])
3556                except ValueError:
3557                    error += '%s, ' % ticket.student.display_fullname
3558            if ticket.score != score:
3559                ticket.score = score
3560                ticket.student.__parent__.logger.info(
3561                    '%s - %s %s/%s score updated (%s)' % (
3562                        ob_class, ticket.student.student_id,
3563                        ticket.level, ticket.code, score)
3564                    )
3565        if error:
3566            self.flash(
3567                _('Error: Score(s) of following students have not been '
3568                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3569                type="danger")
3570        return True
3571
3572    def _validate_results(self, form):
3573        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3574        user = get_current_principal()
3575        if user is None:
3576            usertitle = 'system'
3577        else:
3578            usertitle = getattr(user, 'public_name', None)
3579            if not usertitle:
3580                usertitle = user.title
3581        self.context.results_validated_by = usertitle
3582        self.context.results_validation_date = datetime.utcnow()
3583        self.context.results_validation_session = self.current_academic_session
3584        return
3585
3586    def _results_editable(self, results_validation_session,
3587                         current_academic_session):
3588        user = get_current_principal()
3589        prm = IPrincipalRoleManager(self.context)
3590        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3591        if 'waeup.local.LocalStudentsManager' in roles:
3592            return True
3593        if results_validation_session \
3594            and results_validation_session >= current_academic_session:
3595            return False
3596        return True
3597
3598    def update(self,  *args, **kw):
3599        form = self.request.form
3600        self.current_academic_session = grok.getSite()[
3601            'configuration'].current_academic_session
3602        if self.context.__parent__.__parent__.score_editing_disabled \
3603            or self.context.score_editing_disabled:
3604            self.flash(_('Score editing disabled.'), type="warning")
3605            self.redirect(self.url(self.context))
3606            return
3607        if not self.current_academic_session:
3608            self.flash(_('Current academic session not set.'), type="warning")
3609            self.redirect(self.url(self.context))
3610            return
3611        vs = self.context.results_validation_session
3612        if not self._results_editable(vs, self.current_academic_session):
3613            self.flash(
3614                _('Course results have already been '
3615                  'validated and can no longer be changed.'),
3616                type="danger")
3617            self.redirect(self.url(self.context))
3618            return
3619        self.session_title = academic_sessions_vocab.getTerm(
3620            self.current_academic_session).title
3621        self.tickets = self._searchCatalog(self.current_academic_session)
3622        if not self.tickets:
3623            self.flash(_('No student found.'), type="warning")
3624            self.redirect(self.url(self.context))
3625            return
3626        self.editable_tickets = [
3627            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3628        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
3629            and not 'VALIDATE_RESULTS' in form:
3630            return
3631        if 'VALIDATE_RESULTS' in form:
3632            if vs and vs >= self.current_academic_session:
3633                self.flash(
3634                    _('Course results have already been validated.'),
3635                    type="danger")
3636                return
3637            self._validate_results(form)
3638            self.flash(_('You successfully validated the course results.'))
3639            self.redirect(self.url(self.context))
3640            return
3641        if not self.editable_tickets:
3642            return
3643        success = self._update_scores(form)
3644        if success:
3645            self.flash(_('You successfully updated course results.'))
3646        return
3647
3648class DownloadScoresView(UtilityView, grok.View):
3649    """View that exports scores.
3650    """
3651    grok.context(ICourse)
3652    grok.require('waeup.editScores')
3653    grok.name('download_scores')
3654
3655    def _results_editable(self, results_validation_session,
3656                         current_academic_session):
3657        user = get_current_principal()
3658        prm = IPrincipalRoleManager(self.context)
3659        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3660        if 'waeup.local.LocalStudentsManager' in roles:
3661            return True
3662        if results_validation_session \
3663            and results_validation_session >= current_academic_session:
3664            return False
3665        return True
3666
3667    def update(self):
3668        self.current_academic_session = grok.getSite()[
3669            'configuration'].current_academic_session
3670        if self.context.__parent__.__parent__.score_editing_disabled \
3671            or self.context.score_editing_disabled:
3672            self.flash(_('Score editing disabled.'), type="warning")
3673            self.redirect(self.url(self.context))
3674            return
3675        if not self.current_academic_session:
3676            self.flash(_('Current academic session not set.'), type="warning")
3677            self.redirect(self.url(self.context))
3678            return
3679        vs = self.context.results_validation_session
3680        if not self._results_editable(vs, self.current_academic_session):
3681            self.flash(
3682                _('Course results have already been '
3683                  'validated and can no longer be changed.'),
3684                type="danger")
3685            self.redirect(self.url(self.context))
3686            return
3687        site = grok.getSite()
3688        exporter = getUtility(ICSVExporter, name='lecturer')
3689        self.csv = exporter.export_filtered(site, filepath=None,
3690                                 catalog='coursetickets',
3691                                 session=self.current_academic_session,
3692                                 level=None,
3693                                 code=self.context.code)
3694        return
3695
3696    def render(self):
3697        filename = 'results_%s_%s.csv' % (
3698            self.context.code, self.current_academic_session)
3699        self.response.setHeader(
3700            'Content-Type', 'text/csv; charset=UTF-8')
3701        self.response.setHeader(
3702            'Content-Disposition:', 'attachment; filename="%s' % filename)
3703        return self.csv
3704
3705class ExportPDFScoresSlip(UtilityView, grok.View,
3706    LocalRoleAssignmentUtilityView):
3707    """Deliver a PDF slip of course tickets for a lecturer.
3708    """
3709    grok.context(ICourse)
3710    grok.name('coursetickets.pdf')
3711    grok.require('waeup.showStudents')
3712
3713    def update(self):
3714        self.current_academic_session = grok.getSite()[
3715            'configuration'].current_academic_session
3716        if not self.current_academic_session:
3717            self.flash(_('Current academic session not set.'), type="danger")
3718            self.redirect(self.url(self.context))
3719            return
3720
3721    @property
3722    def note(self):
3723        return
3724
3725    def data(self, session):
3726        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3727        # Attention: Also tickets of previous studycourses are found
3728        coursetickets = cat.searchResults(
3729            session=(session, session),
3730            code=(self.context.code, self.context.code)
3731            )
3732        header = [[_('Matric No.'),
3733                   _('Reg. No.'),
3734                   _('Fullname'),
3735                   _('Status'),
3736                   _('Course of Studies'),
3737                   _('Level'),
3738                   _('Score') ],]
3739        tickets = []
3740        for ticket in list(coursetickets):
3741            row = [ticket.student.matric_number,
3742                  ticket.student.reg_number,
3743                  ticket.student.display_fullname,
3744                  ticket.student.translated_state,
3745                  ticket.student.certcode,
3746                  ticket.level,
3747                  ticket.score]
3748            tickets.append(row)
3749        return header + sorted(tickets, key=lambda value: value[0]), None
3750
3751    def render(self):
3752        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3753                     if i['local_role'] == 'waeup.local.Lecturer']
3754        lecturers = sorted(lecturers)
3755        lecturers =  ', '.join(lecturers)
3756        students_utils = getUtility(IStudentsUtils)
3757        return students_utils.renderPDFCourseticketsOverview(
3758            self, 'coursetickets', self.current_academic_session,
3759            self.data(self.current_academic_session), lecturers,
3760            'landscape', 90, self.note)
3761
3762class ExportAttendanceSlip(UtilityView, grok.View,
3763    LocalRoleAssignmentUtilityView):
3764    """Deliver a PDF slip of course tickets in attendance sheet format.
3765    """
3766    grok.context(ICourse)
3767    grok.name('attendance.pdf')
3768    grok.require('waeup.showStudents')
3769
3770    def update(self):
3771        self.current_academic_session = grok.getSite()[
3772            'configuration'].current_academic_session
3773        if not self.current_academic_session:
3774            self.flash(_('Current academic session not set.'), type="danger")
3775            self.redirect(self.url(self.context))
3776            return
3777
3778    @property
3779    def note(self):
3780        return
3781
3782    def data(self, session):
3783        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3784        # Attention: Also tickets of previous studycourses are found
3785        coursetickets = cat.searchResults(
3786            session=(session, session),
3787            code=(self.context.code, self.context.code)
3788            )
3789        header = [[_('S/N'),
3790                   _('Matric No.'),
3791                   _('Name'),
3792                   _('Level'),
3793                   _('Course of\nStudies'),
3794                   _('Booklet No.'),
3795                   _('Signature'),
3796                   ],]
3797        tickets = []
3798        sn = 1
3799        ctlist = sorted(list(coursetickets),
3800                        key=lambda value: str(value.student.certcode) +
3801                                          str(value.student.matric_number))
3802        # In AAUE only editable appear on the attendance sheet. Hopefully
3803        # this holds for other universities too.
3804        editable_tickets = [ticket for ticket in ctlist
3805            if ticket.editable_by_lecturer]
3806        for ticket in editable_tickets:
3807            name = textwrap.fill(ticket.student.display_fullname, 20)
3808            row = [sn,
3809                  ticket.student.matric_number,
3810                  name,
3811                  ticket.level,
3812                  ticket.student.certcode,
3813                  20 * ' ',
3814                  27 * ' ',
3815                  ]
3816            tickets.append(row)
3817            sn += 1
3818        return header + tickets, None
3819
3820    def render(self):
3821        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3822                     if i['local_role'] == 'waeup.local.Lecturer']
3823        lecturers =  ', '.join(lecturers)
3824        students_utils = getUtility(IStudentsUtils)
3825        return students_utils.renderPDFCourseticketsOverview(
3826            self, 'attendance', self.current_academic_session,
3827            self.data(self.current_academic_session),
3828            lecturers, '', 65, self.note)
3829
3830class ExportJobContainerOverview(KofaPage):
3831    """Page that lists active student data export jobs and provides links
3832    to discard or download CSV files.
3833
3834    """
3835    grok.context(VirtualExportJobContainer)
3836    grok.require('waeup.showStudents')
3837    grok.name('index.html')
3838    grok.template('exportjobsindex')
3839    label = _('Student Data Exports')
3840    pnav = 1
3841    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3842
3843    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
3844        if CREATE1:
3845            self.redirect(self.url('@@exportconfig'))
3846            return
3847        if CREATE2:
3848            self.redirect(self.url('@@exportselected'))
3849            return
3850        if DISCARD and job_id:
3851            entry = self.context.entry_from_job_id(job_id)
3852            self.context.delete_export_entry(entry)
3853            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3854            self.context.logger.info(
3855                '%s - discarded: job_id=%s' % (ob_class, job_id))
3856            self.flash(_('Discarded export') + ' %s' % job_id)
3857        self.entries = doll_up(self, user=self.request.principal.id)
3858        return
3859
3860class ExportJobContainerJobConfig(KofaPage):
3861    """Page that configures a students export job.
3862
3863    This is a baseclass.
3864    """
3865    grok.baseclass()
3866    grok.require('waeup.showStudents')
3867    grok.template('exportconfig')
3868    label = _('Configure student data export')
3869    pnav = 1
3870    redirect_target = ''
3871    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3872
3873    def _set_session_values(self):
3874        vocab_terms = academic_sessions_vocab.by_value.values()
3875        self.sessions = [(_('All Sessions'), 'all')]
3876        self.sessions += sorted(
3877            [(x.title, x.token) for x in vocab_terms], reverse=True)
3878        return
3879
3880    def _set_level_values(self):
3881        vocab_terms = course_levels.by_value.values()
3882        self.levels = [(_('All Levels'), 'all')]
3883        self.levels += sorted(
3884            [(x.title, x.token) for x in vocab_terms])
3885        return
3886
3887    def _set_semesters_values(self):
3888        utils = getUtility(IKofaUtils)
3889        self.semesters =[(_('All Semesters'), 'all')]
3890        self.semesters += sorted([(value, key) for key, value in
3891                      utils.SEMESTER_DICT.items()])
3892        return
3893
3894    def _set_mode_values(self):
3895        utils = getUtility(IKofaUtils)
3896        self.modes =[(_('All Modes'), 'all')]
3897        self.modes += sorted([(value, key) for key, value in
3898                      utils.STUDY_MODES_DICT.items()])
3899        return
3900
3901    def _set_paycat_values(self):
3902        utils = getUtility(IKofaUtils)
3903        self.paycats =[(_('All Payment Categories'), 'all')]
3904        self.paycats += sorted([(value, key) for key, value in
3905                      utils.PAYMENT_CATEGORIES.items()])
3906        return
3907
3908    def _set_exporter_values(self):
3909        # We provide all student exporters, nothing else, yet.
3910        # Bursary, Department or Accommodation Officers don't
3911        # have the general exportData
3912        # permission and are only allowed to export bursary, payments
3913        # overview or accommodation data respectively.
3914        # This is the only place where waeup.exportAccommodationData,
3915        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3916        # are used.
3917        exporters = []
3918        if not checkPermission('waeup.exportData', self.context):
3919            if checkPermission('waeup.exportBursaryData', self.context):
3920                exporters += [('Bursary Data', 'bursary')]
3921            if checkPermission('waeup.exportPaymentsOverview', self.context):
3922                exporters += [('School Fee Payments Overview',
3923                               'sfpaymentsoverview'),
3924                              ('Session Payments Overview',
3925                               'sessionpaymentsoverview')]
3926            if checkPermission('waeup.exportAccommodationData', self.context):
3927                exporters += [('Bed Tickets', 'bedtickets'),
3928                              ('Accommodation Payments',
3929                               'accommodationpayments')]
3930            self.exporters = exporters
3931            return
3932        STUDENT_EXPORTER_NAMES = getUtility(
3933            IStudentsUtils).STUDENT_EXPORTER_NAMES
3934        for name in STUDENT_EXPORTER_NAMES:
3935            util = getUtility(ICSVExporter, name=name)
3936            exporters.append((util.title, name),)
3937        self.exporters = exporters
3938        return
3939
3940    @property
3941    def faccode(self):
3942        return None
3943
3944    @property
3945    def depcode(self):
3946        return None
3947
3948    @property
3949    def certcode(self):
3950        return None
3951
3952    def update(self, START=None, session=None, level=None, mode=None,
3953               payments_start=None, payments_end=None, ct_level=None,
3954               ct_session=None, ct_semester=None, paycat=None,
3955               paysession=None, exporter=None):
3956        self._set_session_values()
3957        self._set_level_values()
3958        self._set_mode_values()
3959        self._set_paycat_values()
3960        self._set_exporter_values()
3961        self._set_semesters_values()
3962        if START is None:
3963            return
3964        ena = exports_not_allowed(self)
3965        if ena:
3966            self.flash(ena, type='danger')
3967            return
3968        if payments_start or payments_end:
3969            date_format = '%d/%m/%Y'
3970            try:
3971                datetime.strptime(payments_start, date_format)
3972                datetime.strptime(payments_end, date_format)
3973            except ValueError:
3974                self.flash(_('Payment dates do not match format d/m/Y.'),
3975                           type="danger")
3976                return
3977        if session == 'all':
3978            session=None
3979        if level == 'all':
3980            level = None
3981        if mode == 'all':
3982            mode = None
3983        if (mode,
3984            level,
3985            session,
3986            self.faccode,
3987            self.depcode,
3988            self.certcode) == (None, None, None, None, None, None):
3989            # Export all students including those without certificate
3990            job_id = self.context.start_export_job(exporter,
3991                                          self.request.principal.id,
3992                                          payments_start = payments_start,
3993                                          payments_end = payments_end,
3994                                          paycat=paycat,
3995                                          paysession=paysession,
3996                                          ct_level = ct_level,
3997                                          ct_session = ct_session,
3998                                          ct_semester = ct_semester,
3999                                          )
4000        else:
4001            job_id = self.context.start_export_job(exporter,
4002                                          self.request.principal.id,
4003                                          current_session=session,
4004                                          current_level=level,
4005                                          current_mode=mode,
4006                                          faccode=self.faccode,
4007                                          depcode=self.depcode,
4008                                          certcode=self.certcode,
4009                                          payments_start = payments_start,
4010                                          payments_end = payments_end,
4011                                          paycat=paycat,
4012                                          paysession=paysession,
4013                                          ct_level = ct_level,
4014                                          ct_session = ct_session,
4015                                          ct_semester = ct_semester,)
4016        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4017        self.context.logger.info(
4018            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4019            % (ob_class, exporter, session, level, mode, self.faccode,
4020            self.depcode, self.certcode, payments_start, payments_end,
4021            ct_level, ct_session, paycat, paysession, job_id))
4022        self.flash(_('Export started for students with') +
4023                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4024                   session, level, mode))
4025        self.redirect(self.url(self.redirect_target))
4026        return
4027
4028class ExportJobContainerDownload(ExportCSVView):
4029    """Page that downloads a students export csv file.
4030
4031    """
4032    grok.context(VirtualExportJobContainer)
4033    grok.require('waeup.showStudents')
4034
4035class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4036    """Page that configures a students export job in datacenter.
4037
4038    """
4039    grok.name('exportconfig')
4040    grok.context(IDataCenter)
4041    redirect_target = '@@export'
4042
4043class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4044    """Page that configures a students export job in datacenter.
4045
4046    """
4047    grok.name('exportselected')
4048    grok.context(IDataCenter)
4049    redirect_target = '@@export'
4050    grok.template('exportselected')
4051
4052    def update(self, START=None, students=None, exporter=None):
4053        self._set_exporter_values()
4054        if START is None:
4055            return
4056        ena = exports_not_allowed(self)
4057        if ena:
4058            self.flash(ena, type='danger')
4059            return
4060        try:
4061            ids = students.replace(',', ' ').split()
4062        except:
4063            self.flash(sys.exc_info()[1])
4064            self.redirect(self.url(self.redirect_target))
4065            return
4066        job_id = self.context.start_export_job(
4067            exporter, self.request.principal.id, selected=ids)
4068        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4069        self.context.logger.info(
4070            '%s - selected students exported: %s, job_id=%s' %
4071            (ob_class, exporter, job_id))
4072        self.flash(_('Export of selected students started.'))
4073        self.redirect(self.url(self.redirect_target))
4074        return
4075
4076class FacultiesExportJobContainerJobConfig(
4077    DatacenterExportJobContainerJobConfig):
4078    """Page that configures a students export job in facultiescontainer.
4079
4080    """
4081    grok.context(VirtualFacultiesExportJobContainer)
4082    redirect_target = ''
4083
4084class FacultiesExportJobContainerSelectStudents(
4085    DatacenterExportJobContainerSelectStudents):
4086    """Page that configures a students export job in facultiescontainer.
4087
4088    """
4089    grok.context(VirtualFacultiesExportJobContainer)
4090    redirect_target = ''
4091
4092class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4093    """Page that configures a students export job in faculties.
4094
4095    """
4096    grok.context(VirtualFacultyExportJobContainer)
4097    redirect_target = ''
4098
4099    @property
4100    def faccode(self):
4101        return self.context.__parent__.code
4102
4103class DepartmentExportJobContainerJobConfig(
4104    DatacenterExportJobContainerJobConfig):
4105    """Page that configures a students export job in departments.
4106
4107    """
4108    grok.context(VirtualDepartmentExportJobContainer)
4109    redirect_target = ''
4110
4111    @property
4112    def depcode(self):
4113        return self.context.__parent__.code
4114
4115class CertificateExportJobContainerJobConfig(
4116    DatacenterExportJobContainerJobConfig):
4117    """Page that configures a students export job for certificates.
4118
4119    """
4120    grok.context(VirtualCertificateExportJobContainer)
4121    grok.template('exportconfig_certificate')
4122    redirect_target = ''
4123
4124    @property
4125    def certcode(self):
4126        return self.context.__parent__.code
4127
4128class CourseExportJobContainerJobConfig(
4129    DatacenterExportJobContainerJobConfig):
4130    """Page that configures a students export job for courses.
4131
4132    In contrast to department or certificate student data exports the
4133    coursetickets_catalog is searched here. Therefore the update
4134    method from the base class is customized.
4135    """
4136    grok.context(VirtualCourseExportJobContainer)
4137    grok.template('exportconfig_course')
4138    redirect_target = ''
4139
4140    def _set_exporter_values(self):
4141        # We provide only the 'coursetickets' and 'lecturer' exporter
4142        # but can add more.
4143        exporters = []
4144        for name in ('coursetickets', 'lecturer'):
4145            util = getUtility(ICSVExporter, name=name)
4146            exporters.append((util.title, name),)
4147        self.exporters = exporters
4148        return
4149
4150    def _set_session_values(self):
4151        # We allow only current academic session
4152        academic_session = grok.getSite()['configuration'].current_academic_session
4153        if not academic_session:
4154            self.sessions = []
4155            return
4156        x = academic_sessions_vocab.getTerm(academic_session)
4157        self.sessions = [(x.title, x.token)]
4158        return
4159
4160    def update(self, START=None, session=None, level=None, mode=None,
4161               exporter=None):
4162        if not checkPermission('waeup.exportData', self.context):
4163            self.flash(_('Not permitted.'), type='danger')
4164            self.redirect(self.url(self.context))
4165            return
4166        self._set_session_values()
4167        self._set_level_values()
4168        self._set_mode_values()
4169        self._set_exporter_values()
4170        if not self.sessions:
4171            self.flash(
4172                _('Academic session not set. '
4173                  'Please contact the administrator.'),
4174                type='danger')
4175            self.redirect(self.url(self.context))
4176            return
4177        if START is None:
4178            return
4179        ena = exports_not_allowed(self)
4180        if ena:
4181            self.flash(ena, type='danger')
4182            return
4183        if session == 'all':
4184            session = None
4185        if level == 'all':
4186            level = None
4187        job_id = self.context.start_export_job(exporter,
4188                                      self.request.principal.id,
4189                                      # Use a different catalog and
4190                                      # pass different keywords than
4191                                      # for the (default) students_catalog
4192                                      catalog='coursetickets',
4193                                      session=session,
4194                                      level=level,
4195                                      code=self.context.__parent__.code)
4196        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4197        self.context.logger.info(
4198            '%s - exported: %s (%s, %s, %s), job_id=%s'
4199            % (ob_class, exporter, session, level,
4200            self.context.__parent__.code, job_id))
4201        self.flash(_('Export started for course tickets with') +
4202                   ' level_session=%s, level=%s' % (
4203                   session, level))
4204        self.redirect(self.url(self.redirect_target))
4205        return
Note: See TracBrowser for help on using the repository browser.