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

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

Configure Cc and Bcc properly.

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