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

Last change on this file since 17628 was 17497, checked in by Henrik Bettermann, 18 months ago

Remove redundant ‚key‘ field from exports.
Adjust tests and code.
Add columns to transcript officers landing page.

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