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

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

Implement bulk emailing.

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