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

Last change on this file since 17176 was 17176, checked in by Henrik Bettermann, 22 months ago

Allow students to book accommodation also if they are in previous sessions (not activated in base package).

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