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

Last change on this file since 17476 was 17458, checked in by Henrik Bettermann, 17 months ago

Don't led students selct blocked hostels on AccommodationManageFormPage.

Remove empty lines.

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