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

Last change on this file since 16120 was 16120, checked in by Henrik Bettermann, 4 years ago

Finetune transcript processing. Allow transcript officers to request a transcript.

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