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

Last change on this file since 17740 was 17650, checked in by Henrik Bettermann, 13 months ago

Implement a QR Code view.

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