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

Last change on this file since 17327 was 17245, checked in by Henrik Bettermann, 2 years ago

Save p_option.

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