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

Last change on this file since 16700 was 16671, checked in by Henrik Bettermann, 3 years ago

Ease customization of portrait upload conditions.

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