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

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

Allow customization of lock message.

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