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

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

Set maximum number of bulk emails.

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