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

Last change on this file since 17877 was 17867, checked in by Henrik Bettermann, 3 months ago

Implement Final Year Clearance Routing Slip upload (not active in base package).

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