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

Last change on this file since 16329 was 16329, checked in by Henrik Bettermann, 4 years ago

Add field to bursary data exporter.

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