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

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

Add payment option (p_option) field to payment tickets and add
select box on online payment add form pages. Disable this feature
in the base package.

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