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

Last change on this file since 16666 was 16609, checked in by Henrik Bettermann, 3 years ago

Enable students and officers to upload scanned signature in the same
way as passport pictures are handled. Some tabs and titles had to be
renamed. The placeholder file, which is still the portrait
placeholder, are no longer shown on base pages.

  • Property svn:keywords set to Id
File size: 172.0 KB
Line 
1## $Id: browser.py 16609 2021-09-08 07:15:07Z 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, KofaFormPage, 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, IFlashNotice)
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 passport picture by student. The class name
2927    is historical. The page is only used for the student's portrait.
2928    """
2929    grok.context(IStudent)
2930    grok.name('change_portrait')
2931    grok.require('waeup.uploadStudentFile')
2932    grok.template('filesuploadpage')
2933    label = _('Upload portrait')
2934    pnav = 4
2935
2936    def update(self):
2937        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2938        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2939            emit_lock_message(self,
2940                _('This form is locked. You are in the wrong state.'))
2941            return
2942        super(StudentFilesUploadPage, self).update()
2943        return
2944
2945class StudentSignatureUploadPage(KofaPage):
2946    """ View to upload scanned signature by student. Not active
2947    in base package.
2948    """
2949    grok.context(IStudent)
2950    grok.name('change_signature')
2951    grok.require('waeup.uploadStudentFile')
2952    grok.template('filesuploadpage')
2953    label = _('Upload signature')
2954    pnav = 4
2955
2956    def update(self):
2957        SIGNATURE_CHANGE_STATES = getUtility(IStudentsUtils).SIGNATURE_CHANGE_STATES
2958        if self.context.student.state not in SIGNATURE_CHANGE_STATES:
2959            emit_lock_message(self,
2960                _('This form is locked. You are in the wrong state.'))
2961            return
2962        super(StudentSignatureUploadPage, self).update()
2963        return
2964
2965class StartClearancePage(KofaPage):
2966    grok.context(IStudent)
2967    grok.name('start_clearance')
2968    grok.require('waeup.handleStudent')
2969    grok.template('enterpin')
2970    label = _('Start clearance')
2971    ac_prefix = 'CLR'
2972    notice = ''
2973    pnav = 4
2974    buttonname = _('Start clearance now')
2975    with_ac = True
2976
2977    @property
2978    def all_required_fields_filled(self):
2979        if not self.context.email:
2980            return _("Email address is missing."), 'edit_base'
2981        if not self.context.phone:
2982            return _("Phone number is missing."), 'edit_base'
2983        return
2984
2985    @property
2986    def portrait_uploaded(self):
2987        store = getUtility(IExtFileStore)
2988        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2989            return True
2990        return False
2991
2992    def update(self, SUBMIT=None):
2993        if not self.context.state == ADMITTED:
2994            self.flash(_("Wrong state"), type="warning")
2995            self.redirect(self.url(self.context))
2996            return
2997        if not self.portrait_uploaded:
2998            self.flash(_("No portrait uploaded."), type="warning")
2999            self.redirect(self.url(self.context, 'change_portrait'))
3000            return
3001        if self.all_required_fields_filled:
3002            arf_warning = self.all_required_fields_filled[0]
3003            arf_redirect = self.all_required_fields_filled[1]
3004            self.flash(arf_warning, type="warning")
3005            self.redirect(self.url(self.context, arf_redirect))
3006            return
3007        if self.with_ac:
3008            self.ac_series = self.request.form.get('ac_series', None)
3009            self.ac_number = self.request.form.get('ac_number', None)
3010        if SUBMIT is None:
3011            return
3012        if self.with_ac:
3013            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3014            code = get_access_code(pin)
3015            if not code:
3016                self.flash(_('Activation code is invalid.'), type="warning")
3017                return
3018            if code.state == USED:
3019                self.flash(_('Activation code has already been used.'),
3020                           type="warning")
3021                return
3022            # Mark pin as used (this also fires a pin related transition)
3023            # and fire transition start_clearance
3024            comment = _(u"invalidated")
3025            # Here we know that the ac is in state initialized so we do not
3026            # expect an exception, but the owner might be different
3027            if not invalidate_accesscode(pin, comment, self.context.student_id):
3028                self.flash(_('You are not the owner of this access code.'),
3029                           type="warning")
3030                return
3031            self.context.clr_code = pin
3032        IWorkflowInfo(self.context).fireTransition('start_clearance')
3033        self.flash(_('Clearance process has been started.'))
3034        self.redirect(self.url(self.context,'cedit'))
3035        return
3036
3037class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
3038    """ View to edit student clearance data by student
3039    """
3040    grok.context(IStudent)
3041    grok.name('cedit')
3042    grok.require('waeup.handleStudent')
3043    label = _('Edit clearance data')
3044
3045    @property
3046    def form_fields(self):
3047        if self.context.is_postgrad:
3048            form_fields = grok.AutoFields(IPGStudentClearance).omit(
3049                'clr_code', 'officer_comment')
3050        else:
3051            form_fields = grok.AutoFields(IUGStudentClearance).omit(
3052                'clr_code', 'officer_comment')
3053        return form_fields
3054
3055    def update(self):
3056        if self.context.clearance_locked:
3057            emit_lock_message(self)
3058            return
3059        return super(StudentClearanceEditFormPage, self).update()
3060
3061    @action(_('Save'), style='primary')
3062    def save(self, **data):
3063        self.applyData(self.context, **data)
3064        self.flash(_('Clearance form has been saved.'))
3065        return
3066
3067    def dataNotComplete(self):
3068        """To be implemented in the customization package.
3069        """
3070        return False
3071
3072    @action(_('Save and request clearance'), style='primary',
3073            warning=_('You can not edit your data after '
3074            'requesting clearance. You really want to request clearance now?'))
3075    def requestClearance(self, **data):
3076        self.applyData(self.context, **data)
3077        if self.dataNotComplete():
3078            self.flash(self.dataNotComplete(), type="warning")
3079            return
3080        self.flash(_('Clearance form has been saved.'))
3081        if self.context.clr_code:
3082            self.redirect(self.url(self.context, 'request_clearance'))
3083        else:
3084            # We bypass the request_clearance page if student
3085            # has been imported in state 'clearance started' and
3086            # no clr_code was entered before.
3087            state = IWorkflowState(self.context).getState()
3088            if state != CLEARANCE:
3089                # This shouldn't happen, but the application officer
3090                # might have forgotten to lock the form after changing the state
3091                self.flash(_('This form cannot be submitted. Wrong state!'),
3092                           type="danger")
3093                return
3094            IWorkflowInfo(self.context).fireTransition('request_clearance')
3095            self.flash(_('Clearance has been requested.'))
3096            self.redirect(self.url(self.context))
3097        return
3098
3099class RequestClearancePage(KofaPage):
3100    grok.context(IStudent)
3101    grok.name('request_clearance')
3102    grok.require('waeup.handleStudent')
3103    grok.template('enterpin')
3104    label = _('Request clearance')
3105    notice = _('Enter the CLR access code used for starting clearance.')
3106    ac_prefix = 'CLR'
3107    pnav = 4
3108    buttonname = _('Request clearance now')
3109    with_ac = True
3110
3111    def update(self, SUBMIT=None):
3112        if self.with_ac:
3113            self.ac_series = self.request.form.get('ac_series', None)
3114            self.ac_number = self.request.form.get('ac_number', None)
3115        if SUBMIT is None:
3116            return
3117        if self.with_ac:
3118            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3119            if self.context.clr_code and self.context.clr_code != pin:
3120                self.flash(_("This isn't your CLR access code."), type="danger")
3121                return
3122        state = IWorkflowState(self.context).getState()
3123        if state != CLEARANCE:
3124            # This shouldn't happen, but the application officer
3125            # might have forgotten to lock the form after changing the state
3126            self.flash(_('This form cannot be submitted. Wrong state!'),
3127                       type="danger")
3128            return
3129        IWorkflowInfo(self.context).fireTransition('request_clearance')
3130        self.flash(_('Clearance has been requested.'))
3131        self.redirect(self.url(self.context))
3132        return
3133
3134class StartSessionPage(KofaPage):
3135    grok.context(IStudentStudyCourse)
3136    grok.name('start_session')
3137    grok.require('waeup.handleStudent')
3138    grok.template('enterpin')
3139    label = _('Start session')
3140    ac_prefix = 'SFE'
3141    notice = ''
3142    pnav = 4
3143    buttonname = _('Start now')
3144    with_ac = True
3145
3146    def update(self, SUBMIT=None):
3147        if not self.context.is_current:
3148            emit_lock_message(self)
3149            return
3150        super(StartSessionPage, self).update()
3151        if not self.context.next_session_allowed:
3152            self.flash(_("You are not entitled to start session."),
3153                       type="warning")
3154            self.redirect(self.url(self.context))
3155            return
3156        if self.with_ac:
3157            self.ac_series = self.request.form.get('ac_series', None)
3158            self.ac_number = self.request.form.get('ac_number', None)
3159        if SUBMIT is None:
3160            return
3161        if self.with_ac:
3162            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3163            code = get_access_code(pin)
3164            if not code:
3165                self.flash(_('Activation code is invalid.'), type="warning")
3166                return
3167            # Mark pin as used (this also fires a pin related transition)
3168            if code.state == USED:
3169                self.flash(_('Activation code has already been used.'),
3170                           type="warning")
3171                return
3172            else:
3173                comment = _(u"invalidated")
3174                # Here we know that the ac is in state initialized so we do not
3175                # expect an error, but the owner might be different
3176                if not invalidate_accesscode(
3177                    pin,comment,self.context.student.student_id):
3178                    self.flash(_('You are not the owner of this access code.'),
3179                               type="warning")
3180                    return
3181        try:
3182            if self.context.student.state == CLEARED:
3183                IWorkflowInfo(self.context.student).fireTransition(
3184                    'pay_first_school_fee')
3185            elif self.context.student.state == RETURNING:
3186                IWorkflowInfo(self.context.student).fireTransition(
3187                    'pay_school_fee')
3188            elif self.context.student.state == PAID:
3189                IWorkflowInfo(self.context.student).fireTransition(
3190                    'pay_pg_fee')
3191        except ConstraintNotSatisfied:
3192            self.flash(_('An error occurred, please contact the system administrator.'),
3193                       type="danger")
3194            return
3195        self.flash(_('Session started.'))
3196        self.redirect(self.url(self.context))
3197        return
3198
3199class AddStudyLevelFormPage(KofaEditFormPage):
3200    """ Page for students to add current study levels
3201    """
3202    grok.context(IStudentStudyCourse)
3203    grok.name('add')
3204    grok.require('waeup.handleStudent')
3205    grok.template('studyleveladdpage')
3206    form_fields = grok.AutoFields(IStudentStudyCourse)
3207    pnav = 4
3208
3209    @property
3210    def label(self):
3211        studylevelsource = StudyLevelSource().factory
3212        code = self.context.current_level
3213        title = studylevelsource.getTitle(self.context, code)
3214        return _('Add current level ${a}', mapping = {'a':title})
3215
3216    def update(self):
3217        if not self.context.is_current \
3218            or self.context.student.studycourse_locked:
3219            emit_lock_message(self)
3220            return
3221        if self.context.student.state != PAID:
3222            emit_lock_message(self)
3223            return
3224        code = self.context.current_level
3225        if code is None:
3226            self.flash(_('Your data are incomplete'), type="danger")
3227            self.redirect(self.url(self.context))
3228            return
3229        super(AddStudyLevelFormPage, self).update()
3230        return
3231
3232    @action(_('Create course list now'), style='primary')
3233    def addStudyLevel(self, **data):
3234        studylevel = createObject(u'waeup.StudentStudyLevel')
3235        studylevel.level = self.context.current_level
3236        studylevel.level_session = self.context.current_session
3237        try:
3238            self.context.addStudentStudyLevel(
3239                self.context.certificate,studylevel)
3240        except KeyError:
3241            self.flash(_('This level exists.'), type="warning")
3242            self.redirect(self.url(self.context))
3243            return
3244        except RequiredMissing:
3245            self.flash(_('Your data are incomplete.'), type="danger")
3246            self.redirect(self.url(self.context))
3247            return
3248        self.flash(_('You successfully created a new course list.'))
3249        self.redirect(self.url(self.context, str(studylevel.level)))
3250        return
3251
3252class StudyLevelEditFormPage(KofaEditFormPage):
3253    """ Page to edit the student study level data by students
3254    """
3255    grok.context(IStudentStudyLevel)
3256    grok.name('edit')
3257    grok.require('waeup.editStudyLevel')
3258    grok.template('studyleveleditpage')
3259    pnav = 4
3260    placeholder = _('Enter valid course code')
3261
3262    def update(self, ADD=None, course=None):
3263        if not self.context.__parent__.is_current:
3264            emit_lock_message(self)
3265            return
3266        if self.context.student.state != PAID or \
3267            not self.context.is_current_level:
3268            emit_lock_message(self)
3269            return
3270        super(StudyLevelEditFormPage, self).update()
3271        if ADD is not None:
3272            if not course:
3273                self.flash(_('No valid course code entered.'), type="warning")
3274                return
3275            cat = queryUtility(ICatalog, name='courses_catalog')
3276            result = cat.searchResults(code=(course, course))
3277            if len(result) != 1:
3278                self.flash(_('Course not found.'), type="warning")
3279                return
3280            course = list(result)[0]
3281            if course.former_course:
3282                self.flash(_('Former courses can\'t be added.'), type="warning")
3283                return
3284            addCourseTicket(self, course)
3285        return
3286
3287    @property
3288    def label(self):
3289        # Here we know that the cookie has been set
3290        lang = self.request.cookies.get('kofa.language')
3291        level_title = translate(self.context.level_title, 'waeup.kofa',
3292            target_language=lang)
3293        return _('Edit course list of ${a}',
3294            mapping = {'a':level_title})
3295
3296    @property
3297    def translated_values(self):
3298        return translated_values(self)
3299
3300    def _delCourseTicket(self, **data):
3301        form = self.request.form
3302        if 'val_id' in form:
3303            child_id = form['val_id']
3304        else:
3305            self.flash(_('No ticket selected.'), type="warning")
3306            self.redirect(self.url(self.context, '@@edit'))
3307            return
3308        if not isinstance(child_id, list):
3309            child_id = [child_id]
3310        deleted = []
3311        for id in child_id:
3312            # Students are not allowed to remove core tickets
3313            if id in self.context and \
3314                self.context[id].removable_by_student:
3315                del self.context[id]
3316                deleted.append(id)
3317        if len(deleted):
3318            self.flash(_('Successfully removed: ${a}',
3319                mapping = {'a':', '.join(deleted)}))
3320            self.context.writeLogMessage(
3321                self,'removed: %s at %s' %
3322                (', '.join(deleted), self.context.level))
3323        self.redirect(self.url(self.context, u'@@edit'))
3324        return
3325
3326    @jsaction(_('Remove selected tickets'))
3327    def delCourseTicket(self, **data):
3328        self._delCourseTicket(**data)
3329        return
3330
3331    def _updateTickets(self, **data):
3332        cat = queryUtility(ICatalog, name='courses_catalog')
3333        invalidated = list()
3334        for value in self.context.values():
3335            result = cat.searchResults(code=(value.code, value.code))
3336            if len(result) != 1:
3337                course = None
3338            else:
3339                course = list(result)[0]
3340            invalid = self.context.updateCourseTicket(value, course)
3341            if invalid:
3342                invalidated.append(invalid)
3343        if invalidated:
3344            invalidated_string = ', '.join(invalidated)
3345            self.context.writeLogMessage(
3346                self, 'course tickets invalidated: %s' % invalidated_string)
3347        self.flash(_('All course tickets updated.'))
3348        return
3349
3350    @action(_('Update all tickets'),
3351        tooltip=_('Update all course parameters including course titles.'))
3352    def updateTickets(self, **data):
3353        self._updateTickets(**data)
3354        return
3355
3356    def _registerCourses(self, **data):
3357        if self.context.student.is_postgrad and \
3358            not self.context.student.is_special_postgrad:
3359            self.flash(_(
3360                "You are a postgraduate student, "
3361                "your course list can't bee registered."), type="warning")
3362            self.redirect(self.url(self.context))
3363            return
3364        students_utils = getUtility(IStudentsUtils)
3365        warning = students_utils.warnCreditsOOR(self.context)
3366        if warning:
3367            self.flash(warning, type="warning")
3368            return
3369        msg = self.context.course_registration_forbidden
3370        if msg:
3371            self.flash(msg, type="warning")
3372            return
3373        IWorkflowInfo(self.context.student).fireTransition(
3374            'register_courses')
3375        self.flash(_('Course list has been registered.'))
3376        self.redirect(self.url(self.context))
3377        return
3378
3379    @action(_('Register course list'), style='primary',
3380        warning=_('You can not edit your course list after registration.'
3381            ' You really want to register?'))
3382    def registerCourses(self, **data):
3383        self._registerCourses(**data)
3384        return
3385
3386class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3387    """Add a course ticket by student.
3388    """
3389    grok.name('ctadd')
3390    grok.require('waeup.handleStudent')
3391    form_fields = grok.AutoFields(ICourseTicketAdd)
3392
3393    def update(self):
3394        if self.context.student.state != PAID or \
3395            not self.context.is_current_level:
3396            emit_lock_message(self)
3397            return
3398        super(CourseTicketAddFormPage2, self).update()
3399        return
3400
3401    @action(_('Add course ticket'))
3402    def addCourseTicket(self, **data):
3403        # Safety belt
3404        if self.context.student.state != PAID:
3405            return
3406        course = data['course']
3407        if course.former_course:
3408            self.flash(_('Former courses can\'t be added.'), type="warning")
3409            return
3410        success = addCourseTicket(self, course)
3411        if success:
3412            self.redirect(self.url(self.context, u'@@edit'))
3413        return
3414
3415class SetPasswordPage(KofaPage):
3416    grok.context(IKofaObject)
3417    grok.name('setpassword')
3418    grok.require('waeup.Anonymous')
3419    grok.template('setpassword')
3420    label = _('Set password for first-time login')
3421    ac_prefix = 'PWD'
3422    pnav = 0
3423    set_button = _('Set')
3424
3425    def update(self, SUBMIT=None):
3426        self.reg_number = self.request.form.get('reg_number', None)
3427        self.ac_series = self.request.form.get('ac_series', None)
3428        self.ac_number = self.request.form.get('ac_number', None)
3429
3430        if SUBMIT is None:
3431            return
3432        hitlist = search(query=self.reg_number,
3433            searchtype='reg_number', view=self)
3434        if not hitlist:
3435            self.flash(_('No student found.'), type="warning")
3436            return
3437        if len(hitlist) != 1:   # Cannot happen but anyway
3438            self.flash(_('More than one student found.'), type="warning")
3439            return
3440        student = hitlist[0].context
3441        self.student_id = student.student_id
3442        student_pw = student.password
3443        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3444        code = get_access_code(pin)
3445        if not code:
3446            self.flash(_('Access code is invalid.'), type="warning")
3447            return
3448        if student_pw and pin == student.adm_code:
3449            self.flash(_(
3450                'Password has already been set. Your Student Id is ${a}',
3451                mapping = {'a':self.student_id}))
3452            return
3453        elif student_pw:
3454            self.flash(
3455                _('Password has already been set. You are using the ' +
3456                'wrong Access Code.'), type="warning")
3457            return
3458        # Mark pin as used (this also fires a pin related transition)
3459        # and set student password
3460        if code.state == USED:
3461            self.flash(_('Access code has already been used.'), type="warning")
3462            return
3463        else:
3464            comment = _(u"invalidated")
3465            # Here we know that the ac is in state initialized so we do not
3466            # expect an exception
3467            invalidate_accesscode(pin,comment)
3468            IUserAccount(student).setPassword(self.ac_number)
3469            student.adm_code = pin
3470        self.flash(_('Password has been set. Your Student Id is ${a}',
3471            mapping = {'a':self.student_id}))
3472        return
3473
3474class StudentRequestPasswordPage(KofaAddFormPage):
3475    """Captcha'd request password page for students.
3476    """
3477    grok.name('requestpw')
3478    grok.require('waeup.Anonymous')
3479    grok.template('requestpw')
3480    form_fields = grok.AutoFields(IStudentRequestPW).select(
3481        'lastname','number','email')
3482    label = _('Request password for first-time login')
3483
3484    def update(self):
3485        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3486        if blocker:
3487            self.flash(_('The portal is in maintenance mode. '
3488                        'Password request forms are temporarily disabled.'),
3489                       type='warning')
3490            self.redirect(self.url(self.context))
3491            return
3492        # Handle captcha
3493        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3494        self.captcha_result = self.captcha.verify(self.request)
3495        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3496        return
3497
3498    def _redirect(self, email, password, student_id):
3499        # Forward only email to landing page in base package.
3500        self.redirect(self.url(self.context, 'requestpw_complete',
3501            data = dict(email=email)))
3502        return
3503
3504    def _redirect_no_student(self):
3505        # No record found, this is the truth. We do not redirect here.
3506        # We are using this method in custom packages
3507        # for redirecting alumni to the application section.
3508        self.flash(_('No student record found.'), type="warning")
3509        return
3510
3511    def _pw_used(self):
3512        # XXX: False if password has not been used. We need an extra
3513        #      attribute which remembers if student logged in.
3514        return True
3515
3516    @action(_('Send login credentials to email address'), style='primary')
3517    def get_credentials(self, **data):
3518        if not self.captcha_result.is_valid:
3519            # Captcha will display error messages automatically.
3520            # No need to flash something.
3521            return
3522        number = data.get('number','')
3523        lastname = data.get('lastname','')
3524        cat = getUtility(ICatalog, name='students_catalog')
3525        results = list(
3526            cat.searchResults(reg_number=(number, number)))
3527        if not results:
3528            results = list(
3529                cat.searchResults(matric_number=(number, number)))
3530        if results:
3531            student = results[0]
3532            if getattr(student,'lastname',None) is None:
3533                self.flash(_('An error occurred.'), type="danger")
3534                return
3535            elif student.lastname.lower() != lastname.lower():
3536                # Don't tell the truth here. Anonymous must not
3537                # know that a record was found and only the lastname
3538                # verification failed.
3539                self.flash(_('No student record found.'), type="warning")
3540                return
3541            elif student.password is not None and self._pw_used:
3542                self.flash(_('Your password has already been set and used. '
3543                             'Please proceed to the login page.'),
3544                           type="warning")
3545                return
3546            # Store email address but nothing else.
3547            student.email = data['email']
3548            notify(grok.ObjectModifiedEvent(student))
3549        else:
3550            self._redirect_no_student()
3551            return
3552
3553        kofa_utils = getUtility(IKofaUtils)
3554        password = kofa_utils.genPassword()
3555        mandate = PasswordMandate()
3556        mandate.params['password'] = password
3557        mandate.params['user'] = student
3558        site = grok.getSite()
3559        site['mandates'].addMandate(mandate)
3560        # Send email with credentials
3561        args = {'mandate_id':mandate.mandate_id}
3562        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3563        url_info = u'Confirmation link: %s' % mandate_url
3564        msg = _('You have successfully requested a password for the')
3565        if kofa_utils.sendCredentials(IUserAccount(student),
3566            password, url_info, msg):
3567            email_sent = student.email
3568        else:
3569            email_sent = None
3570        self._redirect(email=email_sent, password=password,
3571            student_id=student.student_id)
3572        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3573        self.context.logger.info(
3574            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3575        return
3576
3577class ParentsUser:
3578    pass
3579
3580class RequestParentsPasswordPage(StudentRequestPasswordPage):
3581    """Captcha'd request password page for parents.
3582    """
3583    grok.name('requestppw')
3584    grok.template('requestppw')
3585    label = _('Request password for parents access')
3586
3587    def update(self):
3588        super(RequestParentsPasswordPage, self).update()
3589        kofa_utils = getUtility(IKofaUtils)
3590        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3591        return
3592
3593    @action(_('Send temporary login credentials to email address'), style='primary')
3594    def get_credentials(self, **data):
3595        if not self.captcha_result.is_valid:
3596            # Captcha will display error messages automatically.
3597            # No need to flash something.
3598            return
3599        number = data.get('number','')
3600        lastname = data.get('lastname','')
3601        email = data['email']
3602        cat = getUtility(ICatalog, name='students_catalog')
3603        results = list(
3604            cat.searchResults(reg_number=(number, number)))
3605        if not results:
3606            results = list(
3607                cat.searchResults(matric_number=(number, number)))
3608        if results:
3609            student = results[0]
3610            if getattr(student,'lastname',None) is None:
3611                self.flash(_('An error occurred.'), type="danger")
3612                return
3613            elif student.lastname.lower() != lastname.lower():
3614                # Don't tell the truth here. Anonymous must not
3615                # know that a record was found and only the lastname
3616                # verification failed.
3617                self.flash(_('No student record found.'), type="warning")
3618                return
3619            elif email != student.parents_email:
3620                self.flash(_('Wrong email address.'), type="warning")
3621                return
3622        else:
3623            self._redirect_no_student()
3624            return
3625        kofa_utils = getUtility(IKofaUtils)
3626        password = kofa_utils.genPassword()
3627        mandate = ParentsPasswordMandate()
3628        mandate.params['password'] = password
3629        mandate.params['student'] = student
3630        site = grok.getSite()
3631        site['mandates'].addMandate(mandate)
3632        # Send email with credentials
3633        args = {'mandate_id':mandate.mandate_id}
3634        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3635        url_info = u'Confirmation link: %s' % mandate_url
3636        msg = _('You have successfully requested a parents password for the')
3637        # Create a fake user
3638        user = ParentsUser()
3639        user.name = student.student_id
3640        user.title = "Parents of %s" % student.display_fullname
3641        user.email = student.parents_email
3642        if kofa_utils.sendCredentials(user, password, url_info, msg):
3643            email_sent = user.email
3644        else:
3645            email_sent = None
3646        self._redirect(email=email_sent, password=password,
3647            student_id=student.student_id)
3648        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3649        self.context.logger.info(
3650            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3651        return
3652
3653class StudentRequestPasswordEmailSent(KofaPage):
3654    """Landing page after successful password request.
3655
3656    """
3657    grok.name('requestpw_complete')
3658    grok.require('waeup.Public')
3659    grok.template('requestpwmailsent')
3660    label = _('Your password request was successful.')
3661
3662    def update(self, email=None, student_id=None, password=None):
3663        self.email = email
3664        self.password = password
3665        self.student_id = student_id
3666        return
3667
3668class FilterStudentsInDepartmentPage(KofaPage):
3669    """Page that filters and lists students.
3670    """
3671    grok.context(IDepartment)
3672    grok.require('waeup.showStudents')
3673    grok.name('students')
3674    grok.template('filterstudentspage')
3675    pnav = 1
3676    session_label = _('Current Session')
3677    level_label = _('Current Level')
3678
3679    def label(self):
3680        return 'Students in %s' % self.context.longtitle
3681
3682    def _set_session_values(self):
3683        vocab_terms = academic_sessions_vocab.by_value.values()
3684        self.sessions = sorted(
3685            [(x.title, x.token) for x in vocab_terms], reverse=True)
3686        self.sessions += [('All Sessions', 'all')]
3687        return
3688
3689    def _set_level_values(self):
3690        vocab_terms = course_levels.by_value.values()
3691        self.levels = sorted(
3692            [(x.title, x.token) for x in vocab_terms])
3693        self.levels += [('All Levels', 'all')]
3694        return
3695
3696    def _searchCatalog(self, session, level):
3697        if level not in (10, 999, None):
3698            start_level = 100 * (level // 100)
3699            end_level = start_level + 90
3700        else:
3701            start_level = end_level = level
3702        cat = queryUtility(ICatalog, name='students_catalog')
3703        students = cat.searchResults(
3704            current_session=(session, session),
3705            current_level=(start_level, end_level),
3706            depcode=(self.context.code, self.context.code)
3707            )
3708        hitlist = []
3709        for student in students:
3710            hitlist.append(StudentQueryResultItem(student, view=self))
3711        return hitlist
3712
3713    def update(self, SHOW=None, session=None, level=None):
3714        self.parent_url = self.url(self.context.__parent__)
3715        self._set_session_values()
3716        self._set_level_values()
3717        self.hitlist = []
3718        self.session_default = session
3719        self.level_default = level
3720        if SHOW is not None:
3721            if session != 'all':
3722                self.session = int(session)
3723                self.session_string = '%s %s/%s' % (
3724                    self.session_label, self.session, self.session+1)
3725            else:
3726                self.session = None
3727                self.session_string = _('in any session')
3728            if level != 'all':
3729                self.level = int(level)
3730                self.level_string = '%s %s' % (self.level_label, self.level)
3731            else:
3732                self.level = None
3733                self.level_string = _('at any level')
3734            self.hitlist = self._searchCatalog(self.session, self.level)
3735            if not self.hitlist:
3736                self.flash(_('No student found.'), type="warning")
3737        return
3738
3739class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3740    """Page that filters and lists students.
3741    """
3742    grok.context(ICertificate)
3743
3744    def label(self):
3745        return 'Students studying %s' % self.context.longtitle
3746
3747    def _searchCatalog(self, session, level):
3748        if level not in (10, 999, None):
3749            start_level = 100 * (level // 100)
3750            end_level = start_level + 90
3751        else:
3752            start_level = end_level = level
3753        cat = queryUtility(ICatalog, name='students_catalog')
3754        students = cat.searchResults(
3755            current_session=(session, session),
3756            current_level=(start_level, end_level),
3757            certcode=(self.context.code, self.context.code)
3758            )
3759        hitlist = []
3760        for student in students:
3761            hitlist.append(StudentQueryResultItem(student, view=self))
3762        return hitlist
3763
3764class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3765    """Page that filters and lists students.
3766    """
3767    grok.context(ICourse)
3768    grok.require('waeup.viewStudent')
3769
3770    session_label = _('Session')
3771    level_label = _('Level')
3772
3773    def label(self):
3774        return 'Students registered for %s' % self.context.longtitle
3775
3776    def _searchCatalog(self, session, level):
3777        if level not in (10, 999, None):
3778            start_level = 100 * (level // 100)
3779            end_level = start_level + 90
3780        else:
3781            start_level = end_level = level
3782        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3783        coursetickets = cat.searchResults(
3784            session=(session, session),
3785            level=(start_level, end_level),
3786            code=(self.context.code, self.context.code)
3787            )
3788        hitlist = []
3789        for ticket in coursetickets:
3790            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3791        return list(set(hitlist))
3792
3793class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3794    """ Clear all students of a department in state 'clearance requested'.
3795    """
3796    grok.context(IDepartment)
3797    grok.name('clearallstudents')
3798    grok.require('waeup.clearAllStudents')
3799
3800    def update(self):
3801        cat = queryUtility(ICatalog, name='students_catalog')
3802        students = cat.searchResults(
3803            depcode=(self.context.code, self.context.code),
3804            state=(REQUESTED, REQUESTED)
3805            )
3806        num = 0
3807        for student in students:
3808            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3809                continue
3810            IWorkflowInfo(student).fireTransition('clear')
3811            num += 1
3812        self.flash(_('%d students have been cleared.' % num))
3813        self.redirect(self.url(self.context))
3814        return
3815
3816    def render(self):
3817        return
3818
3819#class RemoveFlashNoticeAllStudentsInDepartmentView(UtilityView, grok.View):
3820#    """ Remove flash notices of all students in a department.
3821#    """
3822#    grok.context(IDepartment)
3823#    grok.name('removeflash')
3824#    grok.require('waeup.manageStudent')
3825
3826#    def update(self):
3827#        cat = queryUtility(ICatalog, name='students_catalog')
3828#        students = cat.searchResults(
3829#            depcode=(self.context.code, self.context.code),
3830#            )
3831#        num = 0
3832#        for student in students:
3833#            student.flash_notice = u''
3834#            num += 1
3835#        self.flash(_('%d flash notices have been removed.' % num))
3836#        self.redirect(self.url(self.context))
3837#        return
3838
3839#    def render(self):
3840#        return
3841
3842class EditFlashNoticesFormPage(KofaFormPage):
3843    """Edit all flash notices of students in a department.
3844    """
3845    grok.context(IDepartment)
3846    grok.name('edit_flash_notices')
3847    grok.template('editflashnotices')
3848    grok.require('waeup.manageStudent')
3849    form_fields = grok.AutoFields(IFlashNotice)
3850    pnav = 0
3851
3852    def label(self):
3853        return _(u'Set flash notices for all students in ${a}',
3854            mapping = {'a':self.context.longtitle})
3855
3856    @action('Save flash notices', style='primary')
3857    def save(self, *args, **data):
3858        cat = queryUtility(ICatalog, name='students_catalog')
3859        students = cat.searchResults(
3860            depcode=(self.context.code, self.context.code),
3861            )
3862        num = 0
3863        for student in students:
3864            student.flash_notice = data['flash_notice']
3865            num += 1
3866        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3867        grok.getSite().logger.info(
3868            '%s - %s - saved flash notice: %s'
3869            % (ob_class, self.context.__name__, data['flash_notice']))
3870        self.flash(_('%d flash notices have been edited.' % num))
3871        self.redirect(self.url(self.context))
3872        return
3873
3874    @action(_('Cancel'), validator=NullValidator)
3875    def cancel(self, **data):
3876        self.redirect(self.url(self.context))
3877
3878class EditScoresPage(KofaPage):
3879    """Page that allows to edit batches of scores.
3880    """
3881    grok.context(ICourse)
3882    grok.require('waeup.editScores')
3883    grok.name('edit_scores')
3884    grok.template('editscorespage')
3885    pnav = 1
3886    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3887
3888    def label(self):
3889        return '%s tickets in academic session %s' % (
3890            self.context.code, self.session_title)
3891
3892    def _searchCatalog(self, session):
3893        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3894        # Attention: Also tickets of previous studycourses are found
3895        coursetickets = cat.searchResults(
3896            session=(session, session),
3897            code=(self.context.code, self.context.code)
3898            )
3899        return list(coursetickets)
3900
3901    def _extract_uploadfile(self, uploadfile):
3902        """Get a mapping of student-ids to scores.
3903
3904        The mapping is constructed by reading contents from `uploadfile`.
3905
3906        We expect uploadfile to be a regular CSV file with columns
3907        ``student_id`` and ``score`` (other cols are ignored).
3908        """
3909        result = dict()
3910        data = StringIO(uploadfile.read())  # ensure we have something seekable
3911        reader = csv.DictReader(data)
3912        for row in reader:
3913            if not 'student_id' in row or not 'score' in row:
3914                continue
3915            result[row['student_id']] = row['score']
3916        return result
3917
3918    def _update_scores(self, form):
3919        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3920        error = ''
3921        if 'UPDATE_FILE' in form:
3922            if form['uploadfile']:
3923                try:
3924                    formvals = self._extract_uploadfile(form['uploadfile'])
3925                except:
3926                    self.flash(
3927                        _('Uploaded file contains illegal data. Ignored'),
3928                        type="danger")
3929                    return False
3930            else:
3931                self.flash(
3932                    _('No file provided.'), type="danger")
3933                return False
3934        else:
3935            formvals = dict(zip(form['sids'], form['scores']))
3936        for ticket in self.editable_tickets:
3937            score = ticket.score
3938            sid = ticket.student.student_id
3939            if sid not in formvals:
3940                continue
3941            if formvals[sid] == '':
3942                score = None
3943            else:
3944                try:
3945                    score = int(formvals[sid])
3946                except ValueError:
3947                    error += '%s, ' % ticket.student.display_fullname
3948            if ticket.score != score:
3949                ticket.score = score
3950                ticket.student.__parent__.logger.info(
3951                    '%s - %s %s/%s score updated (%s)' % (
3952                        ob_class, ticket.student.student_id,
3953                        ticket.level, ticket.code, score)
3954                    )
3955        if error:
3956            self.flash(
3957                _('Error: Score(s) of following students have not been '
3958                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3959                type="danger")
3960        return True
3961
3962    def _validate_results(self, form):
3963        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3964        user = get_current_principal()
3965        if user is None:
3966            usertitle = 'system'
3967        else:
3968            usertitle = getattr(user, 'public_name', None)
3969            if not usertitle:
3970                usertitle = user.title
3971        self.context.results_validated_by = usertitle
3972        self.context.results_validation_date = datetime.utcnow()
3973        self.context.results_validation_session = self.current_academic_session
3974        return
3975
3976    def _results_editable(self, results_validation_session,
3977                         current_academic_session):
3978        user = get_current_principal()
3979        prm = IPrincipalRoleManager(self.context)
3980        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3981        if 'waeup.local.LocalStudentsManager' in roles:
3982            return True
3983        if results_validation_session \
3984            and results_validation_session >= current_academic_session:
3985            return False
3986        return True
3987
3988    def update(self,  *args, **kw):
3989        form = self.request.form
3990        self.current_academic_session = grok.getSite()[
3991            'configuration'].current_academic_session
3992        if self.context.__parent__.__parent__.score_editing_disabled \
3993            or self.context.score_editing_disabled:
3994            self.flash(_('Score editing disabled.'), type="warning")
3995            self.redirect(self.url(self.context))
3996            return
3997        if not self.current_academic_session:
3998            self.flash(_('Current academic session not set.'), type="warning")
3999            self.redirect(self.url(self.context))
4000            return
4001        vs = self.context.results_validation_session
4002        if not self._results_editable(vs, self.current_academic_session):
4003            self.flash(
4004                _('Course results have already been '
4005                  'validated and can no longer be changed.'),
4006                type="danger")
4007            self.redirect(self.url(self.context))
4008            return
4009        self.session_title = academic_sessions_vocab.getTerm(
4010            self.current_academic_session).title
4011        self.tickets = self._searchCatalog(self.current_academic_session)
4012        if not self.tickets:
4013            self.flash(_('No student found.'), type="warning")
4014            self.redirect(self.url(self.context))
4015            return
4016        self.editable_tickets = [
4017            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
4018        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
4019            and not 'VALIDATE_RESULTS' in form:
4020            return
4021        if 'VALIDATE_RESULTS' in form:
4022            if vs and vs >= self.current_academic_session:
4023                self.flash(
4024                    _('Course results have already been validated.'),
4025                    type="danger")
4026            for ticket in self.tickets:
4027                if ticket.total_score is not None:
4028                    break
4029                self.flash(
4030                    _('No score has been entered.'),
4031                    type="danger")
4032                return
4033            self._validate_results(form)
4034            self.flash(_('You successfully validated the course results.'))
4035            self.redirect(self.url(self.context))
4036            return
4037        if not self.editable_tickets:
4038            return
4039        success = self._update_scores(form)
4040        if success:
4041            self.flash(_('You successfully updated course results.'))
4042        return
4043
4044class DownloadScoresView(UtilityView, grok.View):
4045    """View that exports scores.
4046    """
4047    grok.context(ICourse)
4048    grok.require('waeup.editScores')
4049    grok.name('download_scores')
4050
4051    def _results_editable(self, results_validation_session,
4052                         current_academic_session):
4053        user = get_current_principal()
4054        prm = IPrincipalRoleManager(self.context)
4055        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
4056        if 'waeup.local.LocalStudentsManager' in roles:
4057            return True
4058        if results_validation_session \
4059            and results_validation_session >= current_academic_session:
4060            return False
4061        return True
4062
4063    def update(self):
4064        self.current_academic_session = grok.getSite()[
4065            'configuration'].current_academic_session
4066        if self.context.__parent__.__parent__.score_editing_disabled \
4067            or self.context.score_editing_disabled:
4068            self.flash(_('Score editing disabled.'), type="warning")
4069            self.redirect(self.url(self.context))
4070            return
4071        if not self.current_academic_session:
4072            self.flash(_('Current academic session not set.'), type="warning")
4073            self.redirect(self.url(self.context))
4074            return
4075        vs = self.context.results_validation_session
4076        if not self._results_editable(vs, self.current_academic_session):
4077            self.flash(
4078                _('Course results have already been '
4079                  'validated and can no longer be changed.'),
4080                type="danger")
4081            self.redirect(self.url(self.context))
4082            return
4083        site = grok.getSite()
4084        exporter = getUtility(ICSVExporter, name='lecturer')
4085        self.csv = exporter.export_filtered(site, filepath=None,
4086                                 catalog='coursetickets',
4087                                 session=self.current_academic_session,
4088                                 level=None,
4089                                 code=self.context.code)
4090        return
4091
4092    def render(self):
4093        filename = 'results_%s_%s.csv' % (
4094            self.context.code, self.current_academic_session)
4095        self.response.setHeader(
4096            'Content-Type', 'text/csv; charset=UTF-8')
4097        self.response.setHeader(
4098            'Content-Disposition:', 'attachment; filename="%s' % filename)
4099        return self.csv
4100
4101class ExportPDFScoresSlip(UtilityView, grok.View,
4102    LocalRoleAssignmentUtilityView):
4103    """Deliver a PDF slip of course tickets for a lecturer.
4104    """
4105    grok.context(ICourse)
4106    grok.name('coursetickets.pdf')
4107    grok.require('waeup.showStudents')
4108
4109    def update(self):
4110        self.current_academic_session = grok.getSite()[
4111            'configuration'].current_academic_session
4112        if not self.current_academic_session:
4113            self.flash(_('Current academic session not set.'), type="danger")
4114            self.redirect(self.url(self.context))
4115            return
4116
4117    @property
4118    def note(self):
4119        return
4120
4121    def data(self, session):
4122        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4123        # Attention: Also tickets of previous studycourses are found
4124        coursetickets = cat.searchResults(
4125            session=(session, session),
4126            code=(self.context.code, self.context.code)
4127            )
4128        header = [[_('S/N'),
4129                   _('Matric No.'),
4130                   _('Reg. No.'),
4131                   _('Fullname'),
4132                   _('Status'),
4133                   _('Course of Studies'),
4134                   _('Level'),
4135                   _('Score') ],]
4136        tickets = []
4137        for ticket in list(coursetickets):
4138            row = [ticket.student.matric_number,
4139                  ticket.student.reg_number,
4140                  ticket.student.display_fullname,
4141                  ticket.student.translated_state,
4142                  ticket.student.certcode,
4143                  ticket.level,
4144                  ticket.score,
4145                  ticket.student.lastname # for sorting only
4146                  ]
4147            tickets.append(row)
4148        data = sorted(tickets, key=lambda value: value[7])
4149        sn = 1
4150        for d in data:
4151            d.pop(7)
4152            d.insert(0, sn)
4153            sn += 1
4154        return header + data, None
4155
4156    def render(self):
4157        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4158                     if i['local_role'] == 'waeup.local.Lecturer']
4159        lecturers = sorted(lecturers)
4160        lecturers =  ', '.join(lecturers)
4161        students_utils = getUtility(IStudentsUtils)
4162        return students_utils.renderPDFCourseticketsOverview(
4163            self, 'coursetickets', self.current_academic_session,
4164            self.data(self.current_academic_session), lecturers,
4165            'landscape', 90, self.note)
4166
4167class ExportAttendanceSlip(UtilityView, grok.View,
4168    LocalRoleAssignmentUtilityView):
4169    """Deliver a PDF slip of course tickets in attendance sheet format.
4170    """
4171    grok.context(ICourse)
4172    grok.name('attendance.pdf')
4173    grok.require('waeup.showStudents')
4174
4175    def update(self):
4176        self.current_academic_session = grok.getSite()[
4177            'configuration'].current_academic_session
4178        if not self.current_academic_session:
4179            self.flash(_('Current academic session not set.'), type="danger")
4180            self.redirect(self.url(self.context))
4181            return
4182
4183    @property
4184    def note(self):
4185        return
4186
4187    def data(self, session):
4188        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4189        # Attention: Also tickets of previous studycourses are found
4190        coursetickets = cat.searchResults(
4191            session=(session, session),
4192            code=(self.context.code, self.context.code)
4193            )
4194        header = [[_('S/N'),
4195                   _('Matric No.'),
4196                   _('Name'),
4197                   _('Level'),
4198                   _('Course of\nStudies'),
4199                   _('Booklet No.'),
4200                   _('Signature'),
4201                   ],]
4202        tickets = []
4203        sn = 1
4204        ctlist = sorted(list(coursetickets),
4205                        key=lambda value: str(value.student.faccode) +
4206                                          str(value.student.depcode) +
4207                                          str(value.student.certcode) +
4208                                          str(value.student.matric_number))
4209        # In AAUE only editable appear on the attendance sheet. Hopefully
4210        # this holds for other universities too.
4211        editable_tickets = [ticket for ticket in ctlist
4212            if ticket.editable_by_lecturer]
4213        for ticket in editable_tickets:
4214            name = textwrap.fill(ticket.student.display_fullname, 20)
4215            row = [sn,
4216                  ticket.student.matric_number,
4217                  name,
4218                  ticket.level,
4219                  ticket.student.certcode,
4220                  20 * ' ',
4221                  27 * ' ',
4222                  ]
4223            tickets.append(row)
4224            sn += 1
4225        return header + tickets, None
4226
4227    def render(self):
4228        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4229                     if i['local_role'] == 'waeup.local.Lecturer']
4230        lecturers =  ', '.join(lecturers)
4231        students_utils = getUtility(IStudentsUtils)
4232        return students_utils.renderPDFCourseticketsOverview(
4233            self, 'attendance', self.current_academic_session,
4234            self.data(self.current_academic_session),
4235            lecturers, '', 65, self.note)
4236
4237class ExportJobContainerOverview(KofaPage):
4238    """Page that lists active student data export jobs and provides links
4239    to discard or download CSV files.
4240
4241    """
4242    grok.context(VirtualExportJobContainer)
4243    grok.require('waeup.showStudents')
4244    grok.name('index.html')
4245    grok.template('exportjobsindex')
4246    label = _('Student Data Exports')
4247    pnav = 1
4248    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4249
4250    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
4251        if CREATE1:
4252            self.redirect(self.url('@@exportconfig'))
4253            return
4254        if CREATE2:
4255            self.redirect(self.url('@@exportselected'))
4256            return
4257        if DISCARD and job_id:
4258            entry = self.context.entry_from_job_id(job_id)
4259            self.context.delete_export_entry(entry)
4260            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4261            self.context.logger.info(
4262                '%s - discarded: job_id=%s' % (ob_class, job_id))
4263            self.flash(_('Discarded export') + ' %s' % job_id)
4264        self.entries = doll_up(self, user=self.request.principal.id)
4265        return
4266
4267class ExportJobContainerJobConfig(KofaPage):
4268    """Page that configures a students export job.
4269
4270    This is a baseclass.
4271    """
4272    grok.baseclass()
4273    grok.require('waeup.showStudents')
4274    grok.template('exportconfig')
4275    label = _('Configure student data export')
4276    pnav = 1
4277    redirect_target = ''
4278    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4279
4280    def _set_session_values(self):
4281        vocab_terms = academic_sessions_vocab.by_value.values()
4282        self.sessions = [(_('All Sessions'), 'all')]
4283        self.sessions += sorted(
4284            [(x.title, x.token) for x in vocab_terms], reverse=True)
4285        return
4286
4287    def _set_level_values(self):
4288        vocab_terms = course_levels.by_value.values()
4289        self.levels = [(_('All Levels'), 'all')]
4290        self.levels += sorted(
4291            [(x.title, x.token) for x in vocab_terms])
4292        return
4293
4294    def _set_semesters_values(self):
4295        utils = getUtility(IKofaUtils)
4296        self.semesters =[(_('All Semesters'), 'all')]
4297        self.semesters += sorted([(value, key) for key, value in
4298                      utils.SEMESTER_DICT.items()])
4299        return
4300
4301    def _set_mode_values(self):
4302        utils = getUtility(IKofaUtils)
4303        self.modes =[(_('All Modes'), 'all')]
4304        self.modes += sorted([(value, key) for key, value in
4305                      utils.STUDY_MODES_DICT.items()])
4306        return
4307
4308    def _set_paycat_values(self):
4309        utils = getUtility(IKofaUtils)
4310        self.paycats =[(_('All Payment Categories'), 'all')]
4311        self.paycats += sorted([(value, key) for key, value in
4312                      utils.PAYMENT_CATEGORIES.items()])
4313        return
4314
4315    def _set_exporter_values(self):
4316        # We provide all student exporters, nothing else, yet.
4317        # Bursary, Department or Accommodation Officers don't
4318        # have the general exportData
4319        # permission and are only allowed to export bursary, payments
4320        # overview or accommodation data respectively.
4321        # This is the only place where waeup.exportAccommodationData,
4322        # waeup.exportBursaryData and waeup.exportPaymentsOverview
4323        # are used.
4324        exporters = []
4325        if not checkPermission('waeup.exportData', self.context):
4326            if checkPermission('waeup.exportBursaryData', self.context):
4327                exporters += [('Bursary Data', 'bursary')]
4328            if checkPermission('waeup.exportPaymentsOverview', self.context):
4329                exporters += [('School Fee Payments Overview',
4330                               'sfpaymentsoverview'),
4331                              ('Session Payments Overview',
4332                               'sessionpaymentsoverview')]
4333            if checkPermission('waeup.exportAccommodationData', self.context):
4334                exporters += [('Bed Tickets', 'bedtickets'),
4335                              ('Accommodation Payments',
4336                               'accommodationpayments')]
4337            self.exporters = exporters
4338            return
4339        STUDENT_EXPORTER_NAMES = getUtility(
4340            IStudentsUtils).STUDENT_EXPORTER_NAMES
4341        for name in STUDENT_EXPORTER_NAMES:
4342            util = getUtility(ICSVExporter, name=name)
4343            exporters.append((util.title, name),)
4344        self.exporters = exporters
4345        return
4346
4347    @property
4348    def faccode(self):
4349        return None
4350
4351    @property
4352    def depcode(self):
4353        return None
4354
4355    @property
4356    def certcode(self):
4357        return None
4358
4359    def update(self, START=None, session=None, level=None, mode=None,
4360               payments_start=None, payments_end=None, ct_level=None,
4361               ct_session=None, ct_semester=None, paycat=None,
4362               paysession=None, level_session=None, exporter=None):
4363        self._set_session_values()
4364        self._set_level_values()
4365        self._set_mode_values()
4366        self._set_paycat_values()
4367        self._set_exporter_values()
4368        self._set_semesters_values()
4369        if START is None:
4370            return
4371        ena = exports_not_allowed(self)
4372        if ena:
4373            self.flash(ena, type='danger')
4374            return
4375        if payments_start or payments_end:
4376            date_format = '%d/%m/%Y'
4377            try:
4378                datetime.strptime(payments_start, date_format)
4379                datetime.strptime(payments_end, date_format)
4380            except ValueError:
4381                self.flash(_('Payment dates do not match format d/m/Y.'),
4382                           type="danger")
4383                return
4384        if session == 'all':
4385            session=None
4386        if level == 'all':
4387            level = None
4388        if mode == 'all':
4389            mode = None
4390        if (mode,
4391            level,
4392            session,
4393            self.faccode,
4394            self.depcode,
4395            self.certcode) == (None, None, None, None, None, None):
4396            # Export all students including those without certificate
4397            job_id = self.context.start_export_job(exporter,
4398                                          self.request.principal.id,
4399                                          payments_start = payments_start,
4400                                          payments_end = payments_end,
4401                                          paycat=paycat,
4402                                          paysession=paysession,
4403                                          ct_level = ct_level,
4404                                          ct_session = ct_session,
4405                                          ct_semester = ct_semester,
4406                                          level_session=level_session,
4407                                          )
4408        else:
4409            job_id = self.context.start_export_job(exporter,
4410                                          self.request.principal.id,
4411                                          current_session=session,
4412                                          current_level=level,
4413                                          current_mode=mode,
4414                                          faccode=self.faccode,
4415                                          depcode=self.depcode,
4416                                          certcode=self.certcode,
4417                                          payments_start = payments_start,
4418                                          payments_end = payments_end,
4419                                          paycat=paycat,
4420                                          paysession=paysession,
4421                                          ct_level = ct_level,
4422                                          ct_session = ct_session,
4423                                          ct_semester = ct_semester,
4424                                          level_session=level_session,)
4425        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4426        self.context.logger.info(
4427            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4428            % (ob_class, exporter, session, level, mode, self.faccode,
4429            self.depcode, self.certcode, payments_start, payments_end,
4430            ct_level, ct_session, paycat, paysession, level_session, job_id))
4431        self.flash(_('Export started for students with') +
4432                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4433                   session, level, mode))
4434        self.redirect(self.url(self.redirect_target))
4435        return
4436
4437class ExportJobContainerDownload(ExportCSVView):
4438    """Page that downloads a students export csv file.
4439
4440    """
4441    grok.context(VirtualExportJobContainer)
4442    grok.require('waeup.showStudents')
4443
4444class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4445    """Page that configures a students export job in datacenter.
4446
4447    """
4448    grok.name('exportconfig')
4449    grok.context(IDataCenter)
4450    redirect_target = '@@export'
4451
4452class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4453    """Page that configures a students export job in datacenter.
4454
4455    """
4456    grok.name('exportselected')
4457    grok.context(IDataCenter)
4458    redirect_target = '@@export'
4459    grok.template('exportselected')
4460
4461    def update(self, START=None, students=None, exporter=None):
4462        self._set_exporter_values()
4463        if START is None:
4464            return
4465        ena = exports_not_allowed(self)
4466        if ena:
4467            self.flash(ena, type='danger')
4468            return
4469        try:
4470            ids = students.replace(',', ' ').split()
4471        except:
4472            self.flash(sys.exc_info()[1])
4473            self.redirect(self.url(self.redirect_target))
4474            return
4475        job_id = self.context.start_export_job(
4476            exporter, self.request.principal.id, selected=ids)
4477        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4478        self.context.logger.info(
4479            '%s - selected students exported: %s, job_id=%s' %
4480            (ob_class, exporter, job_id))
4481        self.flash(_('Export of selected students started.'))
4482        self.redirect(self.url(self.redirect_target))
4483        return
4484
4485class FacultiesExportJobContainerJobConfig(
4486    DatacenterExportJobContainerJobConfig):
4487    """Page that configures a students export job in facultiescontainer.
4488
4489    """
4490    grok.context(VirtualFacultiesExportJobContainer)
4491    redirect_target = ''
4492
4493class FacultiesExportJobContainerSelectStudents(
4494    DatacenterExportJobContainerSelectStudents):
4495    """Page that configures a students export job in facultiescontainer.
4496
4497    """
4498    grok.context(VirtualFacultiesExportJobContainer)
4499    redirect_target = ''
4500
4501class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4502    """Page that configures a students export job in faculties.
4503
4504    """
4505    grok.context(VirtualFacultyExportJobContainer)
4506    redirect_target = ''
4507
4508    @property
4509    def faccode(self):
4510        return self.context.__parent__.code
4511
4512class DepartmentExportJobContainerJobConfig(
4513    DatacenterExportJobContainerJobConfig):
4514    """Page that configures a students export job in departments.
4515
4516    """
4517    grok.context(VirtualDepartmentExportJobContainer)
4518    redirect_target = ''
4519
4520    @property
4521    def depcode(self):
4522        return self.context.__parent__.code
4523
4524class CertificateExportJobContainerJobConfig(
4525    DatacenterExportJobContainerJobConfig):
4526    """Page that configures a students export job for certificates.
4527
4528    """
4529    grok.context(VirtualCertificateExportJobContainer)
4530    grok.template('exportconfig_certificate')
4531    redirect_target = ''
4532
4533    @property
4534    def certcode(self):
4535        return self.context.__parent__.code
4536
4537class CourseExportJobContainerJobConfig(
4538    DatacenterExportJobContainerJobConfig):
4539    """Page that configures a students export job for courses.
4540
4541    In contrast to department or certificate student data exports the
4542    coursetickets_catalog is searched here. Therefore the update
4543    method from the base class is customized.
4544    """
4545    grok.context(VirtualCourseExportJobContainer)
4546    grok.template('exportconfig_course')
4547    redirect_target = ''
4548
4549    def _set_exporter_values(self):
4550        # We provide only the 'coursetickets' and 'lecturer' exporter
4551        # but can add more.
4552        exporters = []
4553        for name in ('coursetickets', 'lecturer'):
4554            util = getUtility(ICSVExporter, name=name)
4555            exporters.append((util.title, name),)
4556        self.exporters = exporters
4557        return
4558
4559    # Disabled on 10/03/21
4560    #def _set_session_values(self):
4561    #    # We allow only current academic session
4562    #    academic_session = grok.getSite()['configuration'].current_academic_session
4563    #    if not academic_session:
4564    #        self.sessions = []
4565    #        return
4566    #    x = academic_sessions_vocab.getTerm(academic_session)
4567    #    self.sessions = [(x.title, x.token)]
4568    #    return
4569
4570    def _set_session_values(self):
4571        vocab_terms = academic_sessions_vocab.by_value.values()
4572        self.sessions = sorted(
4573            [(x.title, x.token) for x in vocab_terms], reverse=True)
4574        self.sessions += [('All Sessions', 'all')]
4575        return
4576
4577    def update(self, START=None, session=None, level=None, mode=None,
4578               exporter=None):
4579        if not checkPermission('waeup.exportData', self.context):
4580            self.flash(_('Not permitted.'), type='danger')
4581            self.redirect(self.url(self.context))
4582            return
4583        self._set_session_values()
4584        self._set_level_values()
4585        self._set_mode_values()
4586        self._set_exporter_values()
4587        # Disabled on 10/03/21
4588        #if not self.sessions:
4589        #    self.flash(
4590        #        _('Academic session not set. '
4591        #          'Please contact the administrator.'),
4592        #        type='danger')
4593        #    self.redirect(self.url(self.context))
4594        #    return
4595        if START is None:
4596            return
4597        ena = exports_not_allowed(self)
4598        if ena:
4599            self.flash(ena, type='danger')
4600            return
4601        if session == 'all':
4602            session = None
4603        if level == 'all':
4604            level = None
4605        job_id = self.context.start_export_job(exporter,
4606                                      self.request.principal.id,
4607                                      # Use a different catalog and
4608                                      # pass different keywords than
4609                                      # for the (default) students_catalog
4610                                      catalog='coursetickets',
4611                                      session=session,
4612                                      level=level,
4613                                      code=self.context.__parent__.code)
4614        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4615        self.context.logger.info(
4616            '%s - exported: %s (%s, %s, %s), job_id=%s'
4617            % (ob_class, exporter, session, level,
4618            self.context.__parent__.code, job_id))
4619        self.flash(_('Export started for course tickets with') +
4620                   ' level_session=%s, level=%s' % (
4621                   session, level))
4622        self.redirect(self.url(self.redirect_target))
4623        return
Note: See TracBrowser for help on using the repository browser.