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

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

Enable managers to edit/remove all flash notices of students in a department.

  • Property svn:keywords set to Id
File size: 170.9 KB
Line 
1## $Id: browser.py 16465 2021-04-16 15:12:37Z 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 files by student
2927    """
2928    grok.context(IStudent)
2929    grok.name('change_portrait')
2930    grok.require('waeup.uploadStudentFile')
2931    grok.template('filesuploadpage')
2932    label = _('Upload portrait')
2933    pnav = 4
2934
2935    def update(self):
2936        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2937        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
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 StartClearancePage(KofaPage):
2945    grok.context(IStudent)
2946    grok.name('start_clearance')
2947    grok.require('waeup.handleStudent')
2948    grok.template('enterpin')
2949    label = _('Start clearance')
2950    ac_prefix = 'CLR'
2951    notice = ''
2952    pnav = 4
2953    buttonname = _('Start clearance now')
2954    with_ac = True
2955
2956    @property
2957    def all_required_fields_filled(self):
2958        if not self.context.email:
2959            return _("Email address is missing."), 'edit_base'
2960        if not self.context.phone:
2961            return _("Phone number is missing."), 'edit_base'
2962        return
2963
2964    @property
2965    def portrait_uploaded(self):
2966        store = getUtility(IExtFileStore)
2967        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2968            return True
2969        return False
2970
2971    def update(self, SUBMIT=None):
2972        if not self.context.state == ADMITTED:
2973            self.flash(_("Wrong state"), type="warning")
2974            self.redirect(self.url(self.context))
2975            return
2976        if not self.portrait_uploaded:
2977            self.flash(_("No portrait uploaded."), type="warning")
2978            self.redirect(self.url(self.context, 'change_portrait'))
2979            return
2980        if self.all_required_fields_filled:
2981            arf_warning = self.all_required_fields_filled[0]
2982            arf_redirect = self.all_required_fields_filled[1]
2983            self.flash(arf_warning, type="warning")
2984            self.redirect(self.url(self.context, arf_redirect))
2985            return
2986        if self.with_ac:
2987            self.ac_series = self.request.form.get('ac_series', None)
2988            self.ac_number = self.request.form.get('ac_number', None)
2989        if SUBMIT is None:
2990            return
2991        if self.with_ac:
2992            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2993            code = get_access_code(pin)
2994            if not code:
2995                self.flash(_('Activation code is invalid.'), type="warning")
2996                return
2997            if code.state == USED:
2998                self.flash(_('Activation code has already been used.'),
2999                           type="warning")
3000                return
3001            # Mark pin as used (this also fires a pin related transition)
3002            # and fire transition start_clearance
3003            comment = _(u"invalidated")
3004            # Here we know that the ac is in state initialized so we do not
3005            # expect an exception, but the owner might be different
3006            if not invalidate_accesscode(pin, comment, self.context.student_id):
3007                self.flash(_('You are not the owner of this access code.'),
3008                           type="warning")
3009                return
3010            self.context.clr_code = pin
3011        IWorkflowInfo(self.context).fireTransition('start_clearance')
3012        self.flash(_('Clearance process has been started.'))
3013        self.redirect(self.url(self.context,'cedit'))
3014        return
3015
3016class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
3017    """ View to edit student clearance data by student
3018    """
3019    grok.context(IStudent)
3020    grok.name('cedit')
3021    grok.require('waeup.handleStudent')
3022    label = _('Edit clearance data')
3023
3024    @property
3025    def form_fields(self):
3026        if self.context.is_postgrad:
3027            form_fields = grok.AutoFields(IPGStudentClearance).omit(
3028                'clr_code', 'officer_comment')
3029        else:
3030            form_fields = grok.AutoFields(IUGStudentClearance).omit(
3031                'clr_code', 'officer_comment')
3032        return form_fields
3033
3034    def update(self):
3035        if self.context.clearance_locked:
3036            emit_lock_message(self)
3037            return
3038        return super(StudentClearanceEditFormPage, self).update()
3039
3040    @action(_('Save'), style='primary')
3041    def save(self, **data):
3042        self.applyData(self.context, **data)
3043        self.flash(_('Clearance form has been saved.'))
3044        return
3045
3046    def dataNotComplete(self):
3047        """To be implemented in the customization package.
3048        """
3049        return False
3050
3051    @action(_('Save and request clearance'), style='primary',
3052            warning=_('You can not edit your data after '
3053            'requesting clearance. You really want to request clearance now?'))
3054    def requestClearance(self, **data):
3055        self.applyData(self.context, **data)
3056        if self.dataNotComplete():
3057            self.flash(self.dataNotComplete(), type="warning")
3058            return
3059        self.flash(_('Clearance form has been saved.'))
3060        if self.context.clr_code:
3061            self.redirect(self.url(self.context, 'request_clearance'))
3062        else:
3063            # We bypass the request_clearance page if student
3064            # has been imported in state 'clearance started' and
3065            # no clr_code was entered before.
3066            state = IWorkflowState(self.context).getState()
3067            if state != CLEARANCE:
3068                # This shouldn't happen, but the application officer
3069                # might have forgotten to lock the form after changing the state
3070                self.flash(_('This form cannot be submitted. Wrong state!'),
3071                           type="danger")
3072                return
3073            IWorkflowInfo(self.context).fireTransition('request_clearance')
3074            self.flash(_('Clearance has been requested.'))
3075            self.redirect(self.url(self.context))
3076        return
3077
3078class RequestClearancePage(KofaPage):
3079    grok.context(IStudent)
3080    grok.name('request_clearance')
3081    grok.require('waeup.handleStudent')
3082    grok.template('enterpin')
3083    label = _('Request clearance')
3084    notice = _('Enter the CLR access code used for starting clearance.')
3085    ac_prefix = 'CLR'
3086    pnav = 4
3087    buttonname = _('Request clearance now')
3088    with_ac = True
3089
3090    def update(self, SUBMIT=None):
3091        if self.with_ac:
3092            self.ac_series = self.request.form.get('ac_series', None)
3093            self.ac_number = self.request.form.get('ac_number', None)
3094        if SUBMIT is None:
3095            return
3096        if self.with_ac:
3097            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3098            if self.context.clr_code and self.context.clr_code != pin:
3099                self.flash(_("This isn't your CLR access code."), type="danger")
3100                return
3101        state = IWorkflowState(self.context).getState()
3102        if state != CLEARANCE:
3103            # This shouldn't happen, but the application officer
3104            # might have forgotten to lock the form after changing the state
3105            self.flash(_('This form cannot be submitted. Wrong state!'),
3106                       type="danger")
3107            return
3108        IWorkflowInfo(self.context).fireTransition('request_clearance')
3109        self.flash(_('Clearance has been requested.'))
3110        self.redirect(self.url(self.context))
3111        return
3112
3113class StartSessionPage(KofaPage):
3114    grok.context(IStudentStudyCourse)
3115    grok.name('start_session')
3116    grok.require('waeup.handleStudent')
3117    grok.template('enterpin')
3118    label = _('Start session')
3119    ac_prefix = 'SFE'
3120    notice = ''
3121    pnav = 4
3122    buttonname = _('Start now')
3123    with_ac = True
3124
3125    def update(self, SUBMIT=None):
3126        if not self.context.is_current:
3127            emit_lock_message(self)
3128            return
3129        super(StartSessionPage, self).update()
3130        if not self.context.next_session_allowed:
3131            self.flash(_("You are not entitled to start session."),
3132                       type="warning")
3133            self.redirect(self.url(self.context))
3134            return
3135        if self.with_ac:
3136            self.ac_series = self.request.form.get('ac_series', None)
3137            self.ac_number = self.request.form.get('ac_number', None)
3138        if SUBMIT is None:
3139            return
3140        if self.with_ac:
3141            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3142            code = get_access_code(pin)
3143            if not code:
3144                self.flash(_('Activation code is invalid.'), type="warning")
3145                return
3146            # Mark pin as used (this also fires a pin related transition)
3147            if code.state == USED:
3148                self.flash(_('Activation code has already been used.'),
3149                           type="warning")
3150                return
3151            else:
3152                comment = _(u"invalidated")
3153                # Here we know that the ac is in state initialized so we do not
3154                # expect an error, but the owner might be different
3155                if not invalidate_accesscode(
3156                    pin,comment,self.context.student.student_id):
3157                    self.flash(_('You are not the owner of this access code.'),
3158                               type="warning")
3159                    return
3160        try:
3161            if self.context.student.state == CLEARED:
3162                IWorkflowInfo(self.context.student).fireTransition(
3163                    'pay_first_school_fee')
3164            elif self.context.student.state == RETURNING:
3165                IWorkflowInfo(self.context.student).fireTransition(
3166                    'pay_school_fee')
3167            elif self.context.student.state == PAID:
3168                IWorkflowInfo(self.context.student).fireTransition(
3169                    'pay_pg_fee')
3170        except ConstraintNotSatisfied:
3171            self.flash(_('An error occurred, please contact the system administrator.'),
3172                       type="danger")
3173            return
3174        self.flash(_('Session started.'))
3175        self.redirect(self.url(self.context))
3176        return
3177
3178class AddStudyLevelFormPage(KofaEditFormPage):
3179    """ Page for students to add current study levels
3180    """
3181    grok.context(IStudentStudyCourse)
3182    grok.name('add')
3183    grok.require('waeup.handleStudent')
3184    grok.template('studyleveladdpage')
3185    form_fields = grok.AutoFields(IStudentStudyCourse)
3186    pnav = 4
3187
3188    @property
3189    def label(self):
3190        studylevelsource = StudyLevelSource().factory
3191        code = self.context.current_level
3192        title = studylevelsource.getTitle(self.context, code)
3193        return _('Add current level ${a}', mapping = {'a':title})
3194
3195    def update(self):
3196        if not self.context.is_current \
3197            or self.context.student.studycourse_locked:
3198            emit_lock_message(self)
3199            return
3200        if self.context.student.state != PAID:
3201            emit_lock_message(self)
3202            return
3203        code = self.context.current_level
3204        if code is None:
3205            self.flash(_('Your data are incomplete'), type="danger")
3206            self.redirect(self.url(self.context))
3207            return
3208        super(AddStudyLevelFormPage, self).update()
3209        return
3210
3211    @action(_('Create course list now'), style='primary')
3212    def addStudyLevel(self, **data):
3213        studylevel = createObject(u'waeup.StudentStudyLevel')
3214        studylevel.level = self.context.current_level
3215        studylevel.level_session = self.context.current_session
3216        try:
3217            self.context.addStudentStudyLevel(
3218                self.context.certificate,studylevel)
3219        except KeyError:
3220            self.flash(_('This level exists.'), type="warning")
3221            self.redirect(self.url(self.context))
3222            return
3223        except RequiredMissing:
3224            self.flash(_('Your data are incomplete.'), type="danger")
3225            self.redirect(self.url(self.context))
3226            return
3227        self.flash(_('You successfully created a new course list.'))
3228        self.redirect(self.url(self.context, str(studylevel.level)))
3229        return
3230
3231class StudyLevelEditFormPage(KofaEditFormPage):
3232    """ Page to edit the student study level data by students
3233    """
3234    grok.context(IStudentStudyLevel)
3235    grok.name('edit')
3236    grok.require('waeup.editStudyLevel')
3237    grok.template('studyleveleditpage')
3238    pnav = 4
3239    placeholder = _('Enter valid course code')
3240
3241    def update(self, ADD=None, course=None):
3242        if not self.context.__parent__.is_current:
3243            emit_lock_message(self)
3244            return
3245        if self.context.student.state != PAID or \
3246            not self.context.is_current_level:
3247            emit_lock_message(self)
3248            return
3249        super(StudyLevelEditFormPage, self).update()
3250        if ADD is not None:
3251            if not course:
3252                self.flash(_('No valid course code entered.'), type="warning")
3253                return
3254            cat = queryUtility(ICatalog, name='courses_catalog')
3255            result = cat.searchResults(code=(course, course))
3256            if len(result) != 1:
3257                self.flash(_('Course not found.'), type="warning")
3258                return
3259            course = list(result)[0]
3260            if course.former_course:
3261                self.flash(_('Former courses can\'t be added.'), type="warning")
3262                return
3263            addCourseTicket(self, course)
3264        return
3265
3266    @property
3267    def label(self):
3268        # Here we know that the cookie has been set
3269        lang = self.request.cookies.get('kofa.language')
3270        level_title = translate(self.context.level_title, 'waeup.kofa',
3271            target_language=lang)
3272        return _('Edit course list of ${a}',
3273            mapping = {'a':level_title})
3274
3275    @property
3276    def translated_values(self):
3277        return translated_values(self)
3278
3279    def _delCourseTicket(self, **data):
3280        form = self.request.form
3281        if 'val_id' in form:
3282            child_id = form['val_id']
3283        else:
3284            self.flash(_('No ticket selected.'), type="warning")
3285            self.redirect(self.url(self.context, '@@edit'))
3286            return
3287        if not isinstance(child_id, list):
3288            child_id = [child_id]
3289        deleted = []
3290        for id in child_id:
3291            # Students are not allowed to remove core tickets
3292            if id in self.context and \
3293                self.context[id].removable_by_student:
3294                del self.context[id]
3295                deleted.append(id)
3296        if len(deleted):
3297            self.flash(_('Successfully removed: ${a}',
3298                mapping = {'a':', '.join(deleted)}))
3299            self.context.writeLogMessage(
3300                self,'removed: %s at %s' %
3301                (', '.join(deleted), self.context.level))
3302        self.redirect(self.url(self.context, u'@@edit'))
3303        return
3304
3305    @jsaction(_('Remove selected tickets'))
3306    def delCourseTicket(self, **data):
3307        self._delCourseTicket(**data)
3308        return
3309
3310    def _updateTickets(self, **data):
3311        cat = queryUtility(ICatalog, name='courses_catalog')
3312        invalidated = list()
3313        for value in self.context.values():
3314            result = cat.searchResults(code=(value.code, value.code))
3315            if len(result) != 1:
3316                course = None
3317            else:
3318                course = list(result)[0]
3319            invalid = self.context.updateCourseTicket(value, course)
3320            if invalid:
3321                invalidated.append(invalid)
3322        if invalidated:
3323            invalidated_string = ', '.join(invalidated)
3324            self.context.writeLogMessage(
3325                self, 'course tickets invalidated: %s' % invalidated_string)
3326        self.flash(_('All course tickets updated.'))
3327        return
3328
3329    @action(_('Update all tickets'),
3330        tooltip=_('Update all course parameters including course titles.'))
3331    def updateTickets(self, **data):
3332        self._updateTickets(**data)
3333        return
3334
3335    def _registerCourses(self, **data):
3336        if self.context.student.is_postgrad and \
3337            not self.context.student.is_special_postgrad:
3338            self.flash(_(
3339                "You are a postgraduate student, "
3340                "your course list can't bee registered."), type="warning")
3341            self.redirect(self.url(self.context))
3342            return
3343        students_utils = getUtility(IStudentsUtils)
3344        warning = students_utils.warnCreditsOOR(self.context)
3345        if warning:
3346            self.flash(warning, type="warning")
3347            return
3348        msg = self.context.course_registration_forbidden
3349        if msg:
3350            self.flash(msg, type="warning")
3351            return
3352        IWorkflowInfo(self.context.student).fireTransition(
3353            'register_courses')
3354        self.flash(_('Course list has been registered.'))
3355        self.redirect(self.url(self.context))
3356        return
3357
3358    @action(_('Register course list'), style='primary',
3359        warning=_('You can not edit your course list after registration.'
3360            ' You really want to register?'))
3361    def registerCourses(self, **data):
3362        self._registerCourses(**data)
3363        return
3364
3365class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3366    """Add a course ticket by student.
3367    """
3368    grok.name('ctadd')
3369    grok.require('waeup.handleStudent')
3370    form_fields = grok.AutoFields(ICourseTicketAdd)
3371
3372    def update(self):
3373        if self.context.student.state != PAID or \
3374            not self.context.is_current_level:
3375            emit_lock_message(self)
3376            return
3377        super(CourseTicketAddFormPage2, self).update()
3378        return
3379
3380    @action(_('Add course ticket'))
3381    def addCourseTicket(self, **data):
3382        # Safety belt
3383        if self.context.student.state != PAID:
3384            return
3385        course = data['course']
3386        if course.former_course:
3387            self.flash(_('Former courses can\'t be added.'), type="warning")
3388            return
3389        success = addCourseTicket(self, course)
3390        if success:
3391            self.redirect(self.url(self.context, u'@@edit'))
3392        return
3393
3394class SetPasswordPage(KofaPage):
3395    grok.context(IKofaObject)
3396    grok.name('setpassword')
3397    grok.require('waeup.Anonymous')
3398    grok.template('setpassword')
3399    label = _('Set password for first-time login')
3400    ac_prefix = 'PWD'
3401    pnav = 0
3402    set_button = _('Set')
3403
3404    def update(self, SUBMIT=None):
3405        self.reg_number = self.request.form.get('reg_number', None)
3406        self.ac_series = self.request.form.get('ac_series', None)
3407        self.ac_number = self.request.form.get('ac_number', None)
3408
3409        if SUBMIT is None:
3410            return
3411        hitlist = search(query=self.reg_number,
3412            searchtype='reg_number', view=self)
3413        if not hitlist:
3414            self.flash(_('No student found.'), type="warning")
3415            return
3416        if len(hitlist) != 1:   # Cannot happen but anyway
3417            self.flash(_('More than one student found.'), type="warning")
3418            return
3419        student = hitlist[0].context
3420        self.student_id = student.student_id
3421        student_pw = student.password
3422        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3423        code = get_access_code(pin)
3424        if not code:
3425            self.flash(_('Access code is invalid.'), type="warning")
3426            return
3427        if student_pw and pin == student.adm_code:
3428            self.flash(_(
3429                'Password has already been set. Your Student Id is ${a}',
3430                mapping = {'a':self.student_id}))
3431            return
3432        elif student_pw:
3433            self.flash(
3434                _('Password has already been set. You are using the ' +
3435                'wrong Access Code.'), type="warning")
3436            return
3437        # Mark pin as used (this also fires a pin related transition)
3438        # and set student password
3439        if code.state == USED:
3440            self.flash(_('Access code has already been used.'), type="warning")
3441            return
3442        else:
3443            comment = _(u"invalidated")
3444            # Here we know that the ac is in state initialized so we do not
3445            # expect an exception
3446            invalidate_accesscode(pin,comment)
3447            IUserAccount(student).setPassword(self.ac_number)
3448            student.adm_code = pin
3449        self.flash(_('Password has been set. Your Student Id is ${a}',
3450            mapping = {'a':self.student_id}))
3451        return
3452
3453class StudentRequestPasswordPage(KofaAddFormPage):
3454    """Captcha'd request password page for students.
3455    """
3456    grok.name('requestpw')
3457    grok.require('waeup.Anonymous')
3458    grok.template('requestpw')
3459    form_fields = grok.AutoFields(IStudentRequestPW).select(
3460        'lastname','number','email')
3461    label = _('Request password for first-time login')
3462
3463    def update(self):
3464        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3465        if blocker:
3466            self.flash(_('The portal is in maintenance mode. '
3467                        'Password request forms are temporarily disabled.'),
3468                       type='warning')
3469            self.redirect(self.url(self.context))
3470            return
3471        # Handle captcha
3472        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3473        self.captcha_result = self.captcha.verify(self.request)
3474        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3475        return
3476
3477    def _redirect(self, email, password, student_id):
3478        # Forward only email to landing page in base package.
3479        self.redirect(self.url(self.context, 'requestpw_complete',
3480            data = dict(email=email)))
3481        return
3482
3483    def _redirect_no_student(self):
3484        # No record found, this is the truth. We do not redirect here.
3485        # We are using this method in custom packages
3486        # for redirecting alumni to the application section.
3487        self.flash(_('No student record found.'), type="warning")
3488        return
3489
3490    def _pw_used(self):
3491        # XXX: False if password has not been used. We need an extra
3492        #      attribute which remembers if student logged in.
3493        return True
3494
3495    @action(_('Send login credentials to email address'), style='primary')
3496    def get_credentials(self, **data):
3497        if not self.captcha_result.is_valid:
3498            # Captcha will display error messages automatically.
3499            # No need to flash something.
3500            return
3501        number = data.get('number','')
3502        lastname = data.get('lastname','')
3503        cat = getUtility(ICatalog, name='students_catalog')
3504        results = list(
3505            cat.searchResults(reg_number=(number, number)))
3506        if not results:
3507            results = list(
3508                cat.searchResults(matric_number=(number, number)))
3509        if results:
3510            student = results[0]
3511            if getattr(student,'lastname',None) is None:
3512                self.flash(_('An error occurred.'), type="danger")
3513                return
3514            elif student.lastname.lower() != lastname.lower():
3515                # Don't tell the truth here. Anonymous must not
3516                # know that a record was found and only the lastname
3517                # verification failed.
3518                self.flash(_('No student record found.'), type="warning")
3519                return
3520            elif student.password is not None and self._pw_used:
3521                self.flash(_('Your password has already been set and used. '
3522                             'Please proceed to the login page.'),
3523                           type="warning")
3524                return
3525            # Store email address but nothing else.
3526            student.email = data['email']
3527            notify(grok.ObjectModifiedEvent(student))
3528        else:
3529            self._redirect_no_student()
3530            return
3531
3532        kofa_utils = getUtility(IKofaUtils)
3533        password = kofa_utils.genPassword()
3534        mandate = PasswordMandate()
3535        mandate.params['password'] = password
3536        mandate.params['user'] = student
3537        site = grok.getSite()
3538        site['mandates'].addMandate(mandate)
3539        # Send email with credentials
3540        args = {'mandate_id':mandate.mandate_id}
3541        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3542        url_info = u'Confirmation link: %s' % mandate_url
3543        msg = _('You have successfully requested a password for the')
3544        if kofa_utils.sendCredentials(IUserAccount(student),
3545            password, url_info, msg):
3546            email_sent = student.email
3547        else:
3548            email_sent = None
3549        self._redirect(email=email_sent, password=password,
3550            student_id=student.student_id)
3551        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3552        self.context.logger.info(
3553            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3554        return
3555
3556class ParentsUser:
3557    pass
3558
3559class RequestParentsPasswordPage(StudentRequestPasswordPage):
3560    """Captcha'd request password page for parents.
3561    """
3562    grok.name('requestppw')
3563    grok.template('requestppw')
3564    label = _('Request password for parents access')
3565
3566    def update(self):
3567        super(RequestParentsPasswordPage, self).update()
3568        kofa_utils = getUtility(IKofaUtils)
3569        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3570        return
3571
3572    @action(_('Send temporary login credentials to email address'), style='primary')
3573    def get_credentials(self, **data):
3574        if not self.captcha_result.is_valid:
3575            # Captcha will display error messages automatically.
3576            # No need to flash something.
3577            return
3578        number = data.get('number','')
3579        lastname = data.get('lastname','')
3580        email = data['email']
3581        cat = getUtility(ICatalog, name='students_catalog')
3582        results = list(
3583            cat.searchResults(reg_number=(number, number)))
3584        if not results:
3585            results = list(
3586                cat.searchResults(matric_number=(number, number)))
3587        if results:
3588            student = results[0]
3589            if getattr(student,'lastname',None) is None:
3590                self.flash(_('An error occurred.'), type="danger")
3591                return
3592            elif student.lastname.lower() != lastname.lower():
3593                # Don't tell the truth here. Anonymous must not
3594                # know that a record was found and only the lastname
3595                # verification failed.
3596                self.flash(_('No student record found.'), type="warning")
3597                return
3598            elif email != student.parents_email:
3599                self.flash(_('Wrong email address.'), type="warning")
3600                return
3601        else:
3602            self._redirect_no_student()
3603            return
3604        kofa_utils = getUtility(IKofaUtils)
3605        password = kofa_utils.genPassword()
3606        mandate = ParentsPasswordMandate()
3607        mandate.params['password'] = password
3608        mandate.params['student'] = student
3609        site = grok.getSite()
3610        site['mandates'].addMandate(mandate)
3611        # Send email with credentials
3612        args = {'mandate_id':mandate.mandate_id}
3613        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3614        url_info = u'Confirmation link: %s' % mandate_url
3615        msg = _('You have successfully requested a parents password for the')
3616        # Create a fake user
3617        user = ParentsUser()
3618        user.name = student.student_id
3619        user.title = "Parents of %s" % student.display_fullname
3620        user.email = student.parents_email
3621        if kofa_utils.sendCredentials(user, password, url_info, msg):
3622            email_sent = user.email
3623        else:
3624            email_sent = None
3625        self._redirect(email=email_sent, password=password,
3626            student_id=student.student_id)
3627        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3628        self.context.logger.info(
3629            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3630        return
3631
3632class StudentRequestPasswordEmailSent(KofaPage):
3633    """Landing page after successful password request.
3634
3635    """
3636    grok.name('requestpw_complete')
3637    grok.require('waeup.Public')
3638    grok.template('requestpwmailsent')
3639    label = _('Your password request was successful.')
3640
3641    def update(self, email=None, student_id=None, password=None):
3642        self.email = email
3643        self.password = password
3644        self.student_id = student_id
3645        return
3646
3647class FilterStudentsInDepartmentPage(KofaPage):
3648    """Page that filters and lists students.
3649    """
3650    grok.context(IDepartment)
3651    grok.require('waeup.showStudents')
3652    grok.name('students')
3653    grok.template('filterstudentspage')
3654    pnav = 1
3655    session_label = _('Current Session')
3656    level_label = _('Current Level')
3657
3658    def label(self):
3659        return 'Students in %s' % self.context.longtitle
3660
3661    def _set_session_values(self):
3662        vocab_terms = academic_sessions_vocab.by_value.values()
3663        self.sessions = sorted(
3664            [(x.title, x.token) for x in vocab_terms], reverse=True)
3665        self.sessions += [('All Sessions', 'all')]
3666        return
3667
3668    def _set_level_values(self):
3669        vocab_terms = course_levels.by_value.values()
3670        self.levels = sorted(
3671            [(x.title, x.token) for x in vocab_terms])
3672        self.levels += [('All Levels', 'all')]
3673        return
3674
3675    def _searchCatalog(self, session, level):
3676        if level not in (10, 999, None):
3677            start_level = 100 * (level // 100)
3678            end_level = start_level + 90
3679        else:
3680            start_level = end_level = level
3681        cat = queryUtility(ICatalog, name='students_catalog')
3682        students = cat.searchResults(
3683            current_session=(session, session),
3684            current_level=(start_level, end_level),
3685            depcode=(self.context.code, self.context.code)
3686            )
3687        hitlist = []
3688        for student in students:
3689            hitlist.append(StudentQueryResultItem(student, view=self))
3690        return hitlist
3691
3692    def update(self, SHOW=None, session=None, level=None):
3693        self.parent_url = self.url(self.context.__parent__)
3694        self._set_session_values()
3695        self._set_level_values()
3696        self.hitlist = []
3697        self.session_default = session
3698        self.level_default = level
3699        if SHOW is not None:
3700            if session != 'all':
3701                self.session = int(session)
3702                self.session_string = '%s %s/%s' % (
3703                    self.session_label, self.session, self.session+1)
3704            else:
3705                self.session = None
3706                self.session_string = _('in any session')
3707            if level != 'all':
3708                self.level = int(level)
3709                self.level_string = '%s %s' % (self.level_label, self.level)
3710            else:
3711                self.level = None
3712                self.level_string = _('at any level')
3713            self.hitlist = self._searchCatalog(self.session, self.level)
3714            if not self.hitlist:
3715                self.flash(_('No student found.'), type="warning")
3716        return
3717
3718class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3719    """Page that filters and lists students.
3720    """
3721    grok.context(ICertificate)
3722
3723    def label(self):
3724        return 'Students studying %s' % self.context.longtitle
3725
3726    def _searchCatalog(self, session, level):
3727        if level not in (10, 999, None):
3728            start_level = 100 * (level // 100)
3729            end_level = start_level + 90
3730        else:
3731            start_level = end_level = level
3732        cat = queryUtility(ICatalog, name='students_catalog')
3733        students = cat.searchResults(
3734            current_session=(session, session),
3735            current_level=(start_level, end_level),
3736            certcode=(self.context.code, self.context.code)
3737            )
3738        hitlist = []
3739        for student in students:
3740            hitlist.append(StudentQueryResultItem(student, view=self))
3741        return hitlist
3742
3743class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3744    """Page that filters and lists students.
3745    """
3746    grok.context(ICourse)
3747    grok.require('waeup.viewStudent')
3748
3749    session_label = _('Session')
3750    level_label = _('Level')
3751
3752    def label(self):
3753        return 'Students registered for %s' % self.context.longtitle
3754
3755    def _searchCatalog(self, session, level):
3756        if level not in (10, 999, None):
3757            start_level = 100 * (level // 100)
3758            end_level = start_level + 90
3759        else:
3760            start_level = end_level = level
3761        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3762        coursetickets = cat.searchResults(
3763            session=(session, session),
3764            level=(start_level, end_level),
3765            code=(self.context.code, self.context.code)
3766            )
3767        hitlist = []
3768        for ticket in coursetickets:
3769            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3770        return list(set(hitlist))
3771
3772class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3773    """ Clear all students of a department in state 'clearance requested'.
3774    """
3775    grok.context(IDepartment)
3776    grok.name('clearallstudents')
3777    grok.require('waeup.clearAllStudents')
3778
3779    def update(self):
3780        cat = queryUtility(ICatalog, name='students_catalog')
3781        students = cat.searchResults(
3782            depcode=(self.context.code, self.context.code),
3783            state=(REQUESTED, REQUESTED)
3784            )
3785        num = 0
3786        for student in students:
3787            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3788                continue
3789            IWorkflowInfo(student).fireTransition('clear')
3790            num += 1
3791        self.flash(_('%d students have been cleared.' % num))
3792        self.redirect(self.url(self.context))
3793        return
3794
3795    def render(self):
3796        return
3797
3798#class RemoveFlashNoticeAllStudentsInDepartmentView(UtilityView, grok.View):
3799#    """ Remove flash notices of all students in a department.
3800#    """
3801#    grok.context(IDepartment)
3802#    grok.name('removeflash')
3803#    grok.require('waeup.manageStudent')
3804
3805#    def update(self):
3806#        cat = queryUtility(ICatalog, name='students_catalog')
3807#        students = cat.searchResults(
3808#            depcode=(self.context.code, self.context.code),
3809#            )
3810#        num = 0
3811#        for student in students:
3812#            student.flash_notice = u''
3813#            num += 1
3814#        self.flash(_('%d flash notices have been removed.' % num))
3815#        self.redirect(self.url(self.context))
3816#        return
3817
3818#    def render(self):
3819#        return
3820
3821class EditFlashNoticesFormPage(KofaFormPage):
3822    """Edit all flash notices of students in a department.
3823    """
3824    grok.context(IDepartment)
3825    grok.name('edit_flash_notices')
3826    grok.template('editflashnotices')
3827    grok.require('waeup.manageStudent')
3828    form_fields = grok.AutoFields(IFlashNotice)
3829    pnav = 0
3830
3831    def label(self):
3832        return _(u'Set flash notices for all students in ${a}',
3833            mapping = {'a':self.context.longtitle})
3834
3835    @action('Save flash notices', style='primary')
3836    def save(self, *args, **data):
3837        cat = queryUtility(ICatalog, name='students_catalog')
3838        students = cat.searchResults(
3839            depcode=(self.context.code, self.context.code),
3840            )
3841        num = 0
3842        for student in students:
3843            student.flash_notice = data['flash_notice']
3844            num += 1
3845        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3846        grok.getSite().logger.info(
3847            '%s - %s - saved flash notice: %s'
3848            % (ob_class, self.context.__name__, data['flash_notice']))
3849        self.flash(_('%d flash notices have been edited.' % num))
3850        self.redirect(self.url(self.context))
3851        return
3852
3853    @action(_('Cancel'), validator=NullValidator)
3854    def cancel(self, **data):
3855        self.redirect(self.url(self.context))
3856
3857class EditScoresPage(KofaPage):
3858    """Page that allows to edit batches of scores.
3859    """
3860    grok.context(ICourse)
3861    grok.require('waeup.editScores')
3862    grok.name('edit_scores')
3863    grok.template('editscorespage')
3864    pnav = 1
3865    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3866
3867    def label(self):
3868        return '%s tickets in academic session %s' % (
3869            self.context.code, self.session_title)
3870
3871    def _searchCatalog(self, session):
3872        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3873        # Attention: Also tickets of previous studycourses are found
3874        coursetickets = cat.searchResults(
3875            session=(session, session),
3876            code=(self.context.code, self.context.code)
3877            )
3878        return list(coursetickets)
3879
3880    def _extract_uploadfile(self, uploadfile):
3881        """Get a mapping of student-ids to scores.
3882
3883        The mapping is constructed by reading contents from `uploadfile`.
3884
3885        We expect uploadfile to be a regular CSV file with columns
3886        ``student_id`` and ``score`` (other cols are ignored).
3887        """
3888        result = dict()
3889        data = StringIO(uploadfile.read())  # ensure we have something seekable
3890        reader = csv.DictReader(data)
3891        for row in reader:
3892            if not 'student_id' in row or not 'score' in row:
3893                continue
3894            result[row['student_id']] = row['score']
3895        return result
3896
3897    def _update_scores(self, form):
3898        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3899        error = ''
3900        if 'UPDATE_FILE' in form:
3901            if form['uploadfile']:
3902                try:
3903                    formvals = self._extract_uploadfile(form['uploadfile'])
3904                except:
3905                    self.flash(
3906                        _('Uploaded file contains illegal data. Ignored'),
3907                        type="danger")
3908                    return False
3909            else:
3910                self.flash(
3911                    _('No file provided.'), type="danger")
3912                return False
3913        else:
3914            formvals = dict(zip(form['sids'], form['scores']))
3915        for ticket in self.editable_tickets:
3916            score = ticket.score
3917            sid = ticket.student.student_id
3918            if sid not in formvals:
3919                continue
3920            if formvals[sid] == '':
3921                score = None
3922            else:
3923                try:
3924                    score = int(formvals[sid])
3925                except ValueError:
3926                    error += '%s, ' % ticket.student.display_fullname
3927            if ticket.score != score:
3928                ticket.score = score
3929                ticket.student.__parent__.logger.info(
3930                    '%s - %s %s/%s score updated (%s)' % (
3931                        ob_class, ticket.student.student_id,
3932                        ticket.level, ticket.code, score)
3933                    )
3934        if error:
3935            self.flash(
3936                _('Error: Score(s) of following students have not been '
3937                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3938                type="danger")
3939        return True
3940
3941    def _validate_results(self, form):
3942        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3943        user = get_current_principal()
3944        if user is None:
3945            usertitle = 'system'
3946        else:
3947            usertitle = getattr(user, 'public_name', None)
3948            if not usertitle:
3949                usertitle = user.title
3950        self.context.results_validated_by = usertitle
3951        self.context.results_validation_date = datetime.utcnow()
3952        self.context.results_validation_session = self.current_academic_session
3953        return
3954
3955    def _results_editable(self, results_validation_session,
3956                         current_academic_session):
3957        user = get_current_principal()
3958        prm = IPrincipalRoleManager(self.context)
3959        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3960        if 'waeup.local.LocalStudentsManager' in roles:
3961            return True
3962        if results_validation_session \
3963            and results_validation_session >= current_academic_session:
3964            return False
3965        return True
3966
3967    def update(self,  *args, **kw):
3968        form = self.request.form
3969        self.current_academic_session = grok.getSite()[
3970            'configuration'].current_academic_session
3971        if self.context.__parent__.__parent__.score_editing_disabled \
3972            or self.context.score_editing_disabled:
3973            self.flash(_('Score editing disabled.'), type="warning")
3974            self.redirect(self.url(self.context))
3975            return
3976        if not self.current_academic_session:
3977            self.flash(_('Current academic session not set.'), type="warning")
3978            self.redirect(self.url(self.context))
3979            return
3980        vs = self.context.results_validation_session
3981        if not self._results_editable(vs, self.current_academic_session):
3982            self.flash(
3983                _('Course results have already been '
3984                  'validated and can no longer be changed.'),
3985                type="danger")
3986            self.redirect(self.url(self.context))
3987            return
3988        self.session_title = academic_sessions_vocab.getTerm(
3989            self.current_academic_session).title
3990        self.tickets = self._searchCatalog(self.current_academic_session)
3991        if not self.tickets:
3992            self.flash(_('No student found.'), type="warning")
3993            self.redirect(self.url(self.context))
3994            return
3995        self.editable_tickets = [
3996            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3997        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
3998            and not 'VALIDATE_RESULTS' in form:
3999            return
4000        if 'VALIDATE_RESULTS' in form:
4001            if vs and vs >= self.current_academic_session:
4002                self.flash(
4003                    _('Course results have already been validated.'),
4004                    type="danger")
4005            for ticket in self.tickets:
4006                if ticket.total_score is not None:
4007                    break
4008                self.flash(
4009                    _('No score has been entered.'),
4010                    type="danger")
4011                return
4012            self._validate_results(form)
4013            self.flash(_('You successfully validated the course results.'))
4014            self.redirect(self.url(self.context))
4015            return
4016        if not self.editable_tickets:
4017            return
4018        success = self._update_scores(form)
4019        if success:
4020            self.flash(_('You successfully updated course results.'))
4021        return
4022
4023class DownloadScoresView(UtilityView, grok.View):
4024    """View that exports scores.
4025    """
4026    grok.context(ICourse)
4027    grok.require('waeup.editScores')
4028    grok.name('download_scores')
4029
4030    def _results_editable(self, results_validation_session,
4031                         current_academic_session):
4032        user = get_current_principal()
4033        prm = IPrincipalRoleManager(self.context)
4034        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
4035        if 'waeup.local.LocalStudentsManager' in roles:
4036            return True
4037        if results_validation_session \
4038            and results_validation_session >= current_academic_session:
4039            return False
4040        return True
4041
4042    def update(self):
4043        self.current_academic_session = grok.getSite()[
4044            'configuration'].current_academic_session
4045        if self.context.__parent__.__parent__.score_editing_disabled \
4046            or self.context.score_editing_disabled:
4047            self.flash(_('Score editing disabled.'), type="warning")
4048            self.redirect(self.url(self.context))
4049            return
4050        if not self.current_academic_session:
4051            self.flash(_('Current academic session not set.'), type="warning")
4052            self.redirect(self.url(self.context))
4053            return
4054        vs = self.context.results_validation_session
4055        if not self._results_editable(vs, self.current_academic_session):
4056            self.flash(
4057                _('Course results have already been '
4058                  'validated and can no longer be changed.'),
4059                type="danger")
4060            self.redirect(self.url(self.context))
4061            return
4062        site = grok.getSite()
4063        exporter = getUtility(ICSVExporter, name='lecturer')
4064        self.csv = exporter.export_filtered(site, filepath=None,
4065                                 catalog='coursetickets',
4066                                 session=self.current_academic_session,
4067                                 level=None,
4068                                 code=self.context.code)
4069        return
4070
4071    def render(self):
4072        filename = 'results_%s_%s.csv' % (
4073            self.context.code, self.current_academic_session)
4074        self.response.setHeader(
4075            'Content-Type', 'text/csv; charset=UTF-8')
4076        self.response.setHeader(
4077            'Content-Disposition:', 'attachment; filename="%s' % filename)
4078        return self.csv
4079
4080class ExportPDFScoresSlip(UtilityView, grok.View,
4081    LocalRoleAssignmentUtilityView):
4082    """Deliver a PDF slip of course tickets for a lecturer.
4083    """
4084    grok.context(ICourse)
4085    grok.name('coursetickets.pdf')
4086    grok.require('waeup.showStudents')
4087
4088    def update(self):
4089        self.current_academic_session = grok.getSite()[
4090            'configuration'].current_academic_session
4091        if not self.current_academic_session:
4092            self.flash(_('Current academic session not set.'), type="danger")
4093            self.redirect(self.url(self.context))
4094            return
4095
4096    @property
4097    def note(self):
4098        return
4099
4100    def data(self, session):
4101        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4102        # Attention: Also tickets of previous studycourses are found
4103        coursetickets = cat.searchResults(
4104            session=(session, session),
4105            code=(self.context.code, self.context.code)
4106            )
4107        header = [[_('Matric No.'),
4108                   _('Reg. No.'),
4109                   _('Fullname'),
4110                   _('Status'),
4111                   _('Course of Studies'),
4112                   _('Level'),
4113                   _('Score') ],]
4114        tickets = []
4115        for ticket in list(coursetickets):
4116            row = [ticket.student.matric_number,
4117                  ticket.student.reg_number,
4118                  ticket.student.display_fullname,
4119                  ticket.student.translated_state,
4120                  ticket.student.certcode,
4121                  ticket.level,
4122                  ticket.score]
4123            tickets.append(row)
4124        return header + sorted(tickets, key=lambda value: value[0]), None
4125
4126    def render(self):
4127        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4128                     if i['local_role'] == 'waeup.local.Lecturer']
4129        lecturers = sorted(lecturers)
4130        lecturers =  ', '.join(lecturers)
4131        students_utils = getUtility(IStudentsUtils)
4132        return students_utils.renderPDFCourseticketsOverview(
4133            self, 'coursetickets', self.current_academic_session,
4134            self.data(self.current_academic_session), lecturers,
4135            'landscape', 90, self.note)
4136
4137class ExportAttendanceSlip(UtilityView, grok.View,
4138    LocalRoleAssignmentUtilityView):
4139    """Deliver a PDF slip of course tickets in attendance sheet format.
4140    """
4141    grok.context(ICourse)
4142    grok.name('attendance.pdf')
4143    grok.require('waeup.showStudents')
4144
4145    def update(self):
4146        self.current_academic_session = grok.getSite()[
4147            'configuration'].current_academic_session
4148        if not self.current_academic_session:
4149            self.flash(_('Current academic session not set.'), type="danger")
4150            self.redirect(self.url(self.context))
4151            return
4152
4153    @property
4154    def note(self):
4155        return
4156
4157    def data(self, session):
4158        cat = queryUtility(ICatalog, name='coursetickets_catalog')
4159        # Attention: Also tickets of previous studycourses are found
4160        coursetickets = cat.searchResults(
4161            session=(session, session),
4162            code=(self.context.code, self.context.code)
4163            )
4164        header = [[_('S/N'),
4165                   _('Matric No.'),
4166                   _('Name'),
4167                   _('Level'),
4168                   _('Course of\nStudies'),
4169                   _('Booklet No.'),
4170                   _('Signature'),
4171                   ],]
4172        tickets = []
4173        sn = 1
4174        ctlist = sorted(list(coursetickets),
4175                        key=lambda value: str(value.student.certcode) +
4176                                          str(value.student.matric_number))
4177        # In AAUE only editable appear on the attendance sheet. Hopefully
4178        # this holds for other universities too.
4179        editable_tickets = [ticket for ticket in ctlist
4180            if ticket.editable_by_lecturer]
4181        for ticket in editable_tickets:
4182            name = textwrap.fill(ticket.student.display_fullname, 20)
4183            row = [sn,
4184                  ticket.student.matric_number,
4185                  name,
4186                  ticket.level,
4187                  ticket.student.certcode,
4188                  20 * ' ',
4189                  27 * ' ',
4190                  ]
4191            tickets.append(row)
4192            sn += 1
4193        return header + tickets, None
4194
4195    def render(self):
4196        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
4197                     if i['local_role'] == 'waeup.local.Lecturer']
4198        lecturers =  ', '.join(lecturers)
4199        students_utils = getUtility(IStudentsUtils)
4200        return students_utils.renderPDFCourseticketsOverview(
4201            self, 'attendance', self.current_academic_session,
4202            self.data(self.current_academic_session),
4203            lecturers, '', 65, self.note)
4204
4205class ExportJobContainerOverview(KofaPage):
4206    """Page that lists active student data export jobs and provides links
4207    to discard or download CSV files.
4208
4209    """
4210    grok.context(VirtualExportJobContainer)
4211    grok.require('waeup.showStudents')
4212    grok.name('index.html')
4213    grok.template('exportjobsindex')
4214    label = _('Student Data Exports')
4215    pnav = 1
4216    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4217
4218    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
4219        if CREATE1:
4220            self.redirect(self.url('@@exportconfig'))
4221            return
4222        if CREATE2:
4223            self.redirect(self.url('@@exportselected'))
4224            return
4225        if DISCARD and job_id:
4226            entry = self.context.entry_from_job_id(job_id)
4227            self.context.delete_export_entry(entry)
4228            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4229            self.context.logger.info(
4230                '%s - discarded: job_id=%s' % (ob_class, job_id))
4231            self.flash(_('Discarded export') + ' %s' % job_id)
4232        self.entries = doll_up(self, user=self.request.principal.id)
4233        return
4234
4235class ExportJobContainerJobConfig(KofaPage):
4236    """Page that configures a students export job.
4237
4238    This is a baseclass.
4239    """
4240    grok.baseclass()
4241    grok.require('waeup.showStudents')
4242    grok.template('exportconfig')
4243    label = _('Configure student data export')
4244    pnav = 1
4245    redirect_target = ''
4246    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
4247
4248    def _set_session_values(self):
4249        vocab_terms = academic_sessions_vocab.by_value.values()
4250        self.sessions = [(_('All Sessions'), 'all')]
4251        self.sessions += sorted(
4252            [(x.title, x.token) for x in vocab_terms], reverse=True)
4253        return
4254
4255    def _set_level_values(self):
4256        vocab_terms = course_levels.by_value.values()
4257        self.levels = [(_('All Levels'), 'all')]
4258        self.levels += sorted(
4259            [(x.title, x.token) for x in vocab_terms])
4260        return
4261
4262    def _set_semesters_values(self):
4263        utils = getUtility(IKofaUtils)
4264        self.semesters =[(_('All Semesters'), 'all')]
4265        self.semesters += sorted([(value, key) for key, value in
4266                      utils.SEMESTER_DICT.items()])
4267        return
4268
4269    def _set_mode_values(self):
4270        utils = getUtility(IKofaUtils)
4271        self.modes =[(_('All Modes'), 'all')]
4272        self.modes += sorted([(value, key) for key, value in
4273                      utils.STUDY_MODES_DICT.items()])
4274        return
4275
4276    def _set_paycat_values(self):
4277        utils = getUtility(IKofaUtils)
4278        self.paycats =[(_('All Payment Categories'), 'all')]
4279        self.paycats += sorted([(value, key) for key, value in
4280                      utils.PAYMENT_CATEGORIES.items()])
4281        return
4282
4283    def _set_exporter_values(self):
4284        # We provide all student exporters, nothing else, yet.
4285        # Bursary, Department or Accommodation Officers don't
4286        # have the general exportData
4287        # permission and are only allowed to export bursary, payments
4288        # overview or accommodation data respectively.
4289        # This is the only place where waeup.exportAccommodationData,
4290        # waeup.exportBursaryData and waeup.exportPaymentsOverview
4291        # are used.
4292        exporters = []
4293        if not checkPermission('waeup.exportData', self.context):
4294            if checkPermission('waeup.exportBursaryData', self.context):
4295                exporters += [('Bursary Data', 'bursary')]
4296            if checkPermission('waeup.exportPaymentsOverview', self.context):
4297                exporters += [('School Fee Payments Overview',
4298                               'sfpaymentsoverview'),
4299                              ('Session Payments Overview',
4300                               'sessionpaymentsoverview')]
4301            if checkPermission('waeup.exportAccommodationData', self.context):
4302                exporters += [('Bed Tickets', 'bedtickets'),
4303                              ('Accommodation Payments',
4304                               'accommodationpayments')]
4305            self.exporters = exporters
4306            return
4307        STUDENT_EXPORTER_NAMES = getUtility(
4308            IStudentsUtils).STUDENT_EXPORTER_NAMES
4309        for name in STUDENT_EXPORTER_NAMES:
4310            util = getUtility(ICSVExporter, name=name)
4311            exporters.append((util.title, name),)
4312        self.exporters = exporters
4313        return
4314
4315    @property
4316    def faccode(self):
4317        return None
4318
4319    @property
4320    def depcode(self):
4321        return None
4322
4323    @property
4324    def certcode(self):
4325        return None
4326
4327    def update(self, START=None, session=None, level=None, mode=None,
4328               payments_start=None, payments_end=None, ct_level=None,
4329               ct_session=None, ct_semester=None, paycat=None,
4330               paysession=None, level_session=None, exporter=None):
4331        self._set_session_values()
4332        self._set_level_values()
4333        self._set_mode_values()
4334        self._set_paycat_values()
4335        self._set_exporter_values()
4336        self._set_semesters_values()
4337        if START is None:
4338            return
4339        ena = exports_not_allowed(self)
4340        if ena:
4341            self.flash(ena, type='danger')
4342            return
4343        if payments_start or payments_end:
4344            date_format = '%d/%m/%Y'
4345            try:
4346                datetime.strptime(payments_start, date_format)
4347                datetime.strptime(payments_end, date_format)
4348            except ValueError:
4349                self.flash(_('Payment dates do not match format d/m/Y.'),
4350                           type="danger")
4351                return
4352        if session == 'all':
4353            session=None
4354        if level == 'all':
4355            level = None
4356        if mode == 'all':
4357            mode = None
4358        if (mode,
4359            level,
4360            session,
4361            self.faccode,
4362            self.depcode,
4363            self.certcode) == (None, None, None, None, None, None):
4364            # Export all students including those without certificate
4365            job_id = self.context.start_export_job(exporter,
4366                                          self.request.principal.id,
4367                                          payments_start = payments_start,
4368                                          payments_end = payments_end,
4369                                          paycat=paycat,
4370                                          paysession=paysession,
4371                                          ct_level = ct_level,
4372                                          ct_session = ct_session,
4373                                          ct_semester = ct_semester,
4374                                          level_session=level_session,
4375                                          )
4376        else:
4377            job_id = self.context.start_export_job(exporter,
4378                                          self.request.principal.id,
4379                                          current_session=session,
4380                                          current_level=level,
4381                                          current_mode=mode,
4382                                          faccode=self.faccode,
4383                                          depcode=self.depcode,
4384                                          certcode=self.certcode,
4385                                          payments_start = payments_start,
4386                                          payments_end = payments_end,
4387                                          paycat=paycat,
4388                                          paysession=paysession,
4389                                          ct_level = ct_level,
4390                                          ct_session = ct_session,
4391                                          ct_semester = ct_semester,
4392                                          level_session=level_session,)
4393        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4394        self.context.logger.info(
4395            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4396            % (ob_class, exporter, session, level, mode, self.faccode,
4397            self.depcode, self.certcode, payments_start, payments_end,
4398            ct_level, ct_session, paycat, paysession, level_session, job_id))
4399        self.flash(_('Export started for students with') +
4400                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4401                   session, level, mode))
4402        self.redirect(self.url(self.redirect_target))
4403        return
4404
4405class ExportJobContainerDownload(ExportCSVView):
4406    """Page that downloads a students export csv file.
4407
4408    """
4409    grok.context(VirtualExportJobContainer)
4410    grok.require('waeup.showStudents')
4411
4412class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4413    """Page that configures a students export job in datacenter.
4414
4415    """
4416    grok.name('exportconfig')
4417    grok.context(IDataCenter)
4418    redirect_target = '@@export'
4419
4420class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4421    """Page that configures a students export job in datacenter.
4422
4423    """
4424    grok.name('exportselected')
4425    grok.context(IDataCenter)
4426    redirect_target = '@@export'
4427    grok.template('exportselected')
4428
4429    def update(self, START=None, students=None, exporter=None):
4430        self._set_exporter_values()
4431        if START is None:
4432            return
4433        ena = exports_not_allowed(self)
4434        if ena:
4435            self.flash(ena, type='danger')
4436            return
4437        try:
4438            ids = students.replace(',', ' ').split()
4439        except:
4440            self.flash(sys.exc_info()[1])
4441            self.redirect(self.url(self.redirect_target))
4442            return
4443        job_id = self.context.start_export_job(
4444            exporter, self.request.principal.id, selected=ids)
4445        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4446        self.context.logger.info(
4447            '%s - selected students exported: %s, job_id=%s' %
4448            (ob_class, exporter, job_id))
4449        self.flash(_('Export of selected students started.'))
4450        self.redirect(self.url(self.redirect_target))
4451        return
4452
4453class FacultiesExportJobContainerJobConfig(
4454    DatacenterExportJobContainerJobConfig):
4455    """Page that configures a students export job in facultiescontainer.
4456
4457    """
4458    grok.context(VirtualFacultiesExportJobContainer)
4459    redirect_target = ''
4460
4461class FacultiesExportJobContainerSelectStudents(
4462    DatacenterExportJobContainerSelectStudents):
4463    """Page that configures a students export job in facultiescontainer.
4464
4465    """
4466    grok.context(VirtualFacultiesExportJobContainer)
4467    redirect_target = ''
4468
4469class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4470    """Page that configures a students export job in faculties.
4471
4472    """
4473    grok.context(VirtualFacultyExportJobContainer)
4474    redirect_target = ''
4475
4476    @property
4477    def faccode(self):
4478        return self.context.__parent__.code
4479
4480class DepartmentExportJobContainerJobConfig(
4481    DatacenterExportJobContainerJobConfig):
4482    """Page that configures a students export job in departments.
4483
4484    """
4485    grok.context(VirtualDepartmentExportJobContainer)
4486    redirect_target = ''
4487
4488    @property
4489    def depcode(self):
4490        return self.context.__parent__.code
4491
4492class CertificateExportJobContainerJobConfig(
4493    DatacenterExportJobContainerJobConfig):
4494    """Page that configures a students export job for certificates.
4495
4496    """
4497    grok.context(VirtualCertificateExportJobContainer)
4498    grok.template('exportconfig_certificate')
4499    redirect_target = ''
4500
4501    @property
4502    def certcode(self):
4503        return self.context.__parent__.code
4504
4505class CourseExportJobContainerJobConfig(
4506    DatacenterExportJobContainerJobConfig):
4507    """Page that configures a students export job for courses.
4508
4509    In contrast to department or certificate student data exports the
4510    coursetickets_catalog is searched here. Therefore the update
4511    method from the base class is customized.
4512    """
4513    grok.context(VirtualCourseExportJobContainer)
4514    grok.template('exportconfig_course')
4515    redirect_target = ''
4516
4517    def _set_exporter_values(self):
4518        # We provide only the 'coursetickets' and 'lecturer' exporter
4519        # but can add more.
4520        exporters = []
4521        for name in ('coursetickets', 'lecturer'):
4522            util = getUtility(ICSVExporter, name=name)
4523            exporters.append((util.title, name),)
4524        self.exporters = exporters
4525        return
4526
4527    # Disabled on 10/03/21
4528    #def _set_session_values(self):
4529    #    # We allow only current academic session
4530    #    academic_session = grok.getSite()['configuration'].current_academic_session
4531    #    if not academic_session:
4532    #        self.sessions = []
4533    #        return
4534    #    x = academic_sessions_vocab.getTerm(academic_session)
4535    #    self.sessions = [(x.title, x.token)]
4536    #    return
4537
4538    def _set_session_values(self):
4539        vocab_terms = academic_sessions_vocab.by_value.values()
4540        self.sessions = sorted(
4541            [(x.title, x.token) for x in vocab_terms], reverse=True)
4542        self.sessions += [('All Sessions', 'all')]
4543        return
4544
4545    def update(self, START=None, session=None, level=None, mode=None,
4546               exporter=None):
4547        if not checkPermission('waeup.exportData', self.context):
4548            self.flash(_('Not permitted.'), type='danger')
4549            self.redirect(self.url(self.context))
4550            return
4551        self._set_session_values()
4552        self._set_level_values()
4553        self._set_mode_values()
4554        self._set_exporter_values()
4555        # Disabled on 10/03/21
4556        #if not self.sessions:
4557        #    self.flash(
4558        #        _('Academic session not set. '
4559        #          'Please contact the administrator.'),
4560        #        type='danger')
4561        #    self.redirect(self.url(self.context))
4562        #    return
4563        if START is None:
4564            return
4565        ena = exports_not_allowed(self)
4566        if ena:
4567            self.flash(ena, type='danger')
4568            return
4569        if session == 'all':
4570            session = None
4571        if level == 'all':
4572            level = None
4573        job_id = self.context.start_export_job(exporter,
4574                                      self.request.principal.id,
4575                                      # Use a different catalog and
4576                                      # pass different keywords than
4577                                      # for the (default) students_catalog
4578                                      catalog='coursetickets',
4579                                      session=session,
4580                                      level=level,
4581                                      code=self.context.__parent__.code)
4582        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4583        self.context.logger.info(
4584            '%s - exported: %s (%s, %s, %s), job_id=%s'
4585            % (ob_class, exporter, session, level,
4586            self.context.__parent__.code, job_id))
4587        self.flash(_('Export started for course tickets with') +
4588                   ' level_session=%s, level=%s' % (
4589                   session, level))
4590        self.redirect(self.url(self.redirect_target))
4591        return
Note: See TracBrowser for help on using the repository browser.