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

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

Disable clearance request button if not in state 'clearance started'.

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