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

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

Redirect to payment ticket page after ticket creation.

Remove 'Comment by Import Manager:' in emails.

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