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

Last change on this file since 10248 was 10248, checked in by Henrik Bettermann, 11 years ago

Bursary Officers are only allowed to export bursary data at all levels in academics.

  • Property svn:keywords set to Id
File size: 109.6 KB
Line 
1## $Id: browser.py 10248 2013-05-29 06:46:54Z 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 sys
21import grok
22from urllib import urlencode
23from datetime import datetime
24from zope.event import notify
25from zope.i18n import translate
26from zope.catalog.interfaces import ICatalog
27from zope.component import queryUtility, getUtility, createObject
28from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
29from zope.formlib.textwidgets import BytesDisplayWidget
30from zope.security import checkPermission
31from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
32from waeup.kofa.accesscodes import (
33    invalidate_accesscode, get_access_code)
34from waeup.kofa.accesscodes.workflow import USED
35from waeup.kofa.browser.layout import (
36    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
37    KofaForm, NullValidator)
38from waeup.kofa.browser.breadcrumbs import Breadcrumb
39from waeup.kofa.browser.pages import ContactAdminForm, ExportCSVView, doll_up
40from waeup.kofa.browser.resources import (
41    datepicker, datatable, tabs, warning, toggleall)
42from waeup.kofa.browser.layout import jsaction, action, UtilityView
43from waeup.kofa.browser.interfaces import ICaptchaManager
44from waeup.kofa.hostels.hostel import NOT_OCCUPIED
45from waeup.kofa.interfaces import (
46    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
47    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
48    academic_sessions_vocab, IJobManager, IDataCenter)
49from waeup.kofa.interfaces import MessageFactory as _
50from waeup.kofa.widgets.datewidget import (
51    FriendlyDateWidget, FriendlyDateDisplayWidget,
52    FriendlyDatetimeDisplayWidget)
53from waeup.kofa.mandates.mandate import PasswordMandate
54from waeup.kofa.university.interfaces import (
55    IDepartment, ICertificate, ICourse)
56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer,)
58from waeup.kofa.university.facultiescontainer import (
59    VirtualFacultiesExportJobContainer, FacultiesContainer)
60from waeup.kofa.university.certificate import (
61    VirtualCertificateExportJobContainer,)
62from waeup.kofa.university.course import (
63    VirtualCourseExportJobContainer,)
64from waeup.kofa.university.vocabularies import course_levels
65from waeup.kofa.utils.batching import VirtualExportJobContainer
66from waeup.kofa.utils.helpers import get_current_principal, to_timezone
67from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
68from waeup.kofa.students.interfaces import (
69    IStudentsContainer, IStudent,
70    IUGStudentClearance,IPGStudentClearance,
71    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
72    IStudentStudyCourseTransfer,
73    IStudentAccommodation, IStudentStudyLevel,
74    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
75    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
76    IBedTicket, IStudentsUtils, IStudentRequestPW
77    )
78from waeup.kofa.students.catalog import search, StudentQueryResultItem
79from waeup.kofa.students.export import EXPORTER_NAMES
80from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
81from waeup.kofa.students.vocabularies import StudyLevelSource
82from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
83    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
84    FORBIDDEN_POSTGRAD_TRANS)
85
86
87grok.context(IKofaObject) # Make IKofaObject the default context
88
89# Save function used for save methods in pages
90def msave(view, **data):
91    changed_fields = view.applyData(view.context, **data)
92    # Turn list of lists into single list
93    if changed_fields:
94        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
95    # Inform catalog if certificate has changed
96    # (applyData does this only for the context)
97    if 'certificate' in changed_fields:
98        notify(grok.ObjectModifiedEvent(view.context.student))
99    fields_string = ' + '.join(changed_fields)
100    view.flash(_('Form has been saved.'))
101    if fields_string:
102        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
103    return
104
105def emit_lock_message(view):
106    """Flash a lock message.
107    """
108    view.flash(_('The requested form is locked (read-only).'))
109    view.redirect(view.url(view.context))
110    return
111
112def translated_values(view):
113    """Translate course ticket attribute values to be displayed on
114    studylevel pages.
115    """
116    lang = view.request.cookies.get('kofa.language')
117    for value in view.context.values():
118        # We have to unghostify (according to Tres Seaver) the __dict__
119        # by activating the object, otherwise value_dict will be empty
120        # when calling the first time.
121        value._p_activate()
122        value_dict = dict([i for i in value.__dict__.items()])
123        value_dict['removable_by_student'] = value.removable_by_student
124        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
125            target_language=lang)
126        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
127            target_language=lang)
128        value_dict['automatic'] = translate(str(value.automatic), 'zope',
129            target_language=lang)
130        value_dict['grade'] = value.grade
131        value_dict['weight'] = value.weight
132        yield value_dict
133
134def clearance_disabled_message(student):
135    try:
136        session_config = grok.getSite()[
137            'configuration'][str(student.current_session)]
138    except KeyError:
139        return _('Session configuration object is not available.')
140    if not session_config.clearance_enabled:
141        return _('Clearance is disabled for this session.')
142    return None
143
144
145def addCourseTicket(view, course=None):
146    students_utils = getUtility(IStudentsUtils)
147    ticket = createObject(u'waeup.CourseTicket')
148    ticket.automatic = False
149    ticket.carry_over = False
150    max_credits = students_utils.maxCreditsExceeded(view.context, course)
151    if max_credits:
152        view.flash(_(
153            'Total credits exceed ${a}.',
154            mapping = {'a': max_credits}))
155        return False
156    try:
157        view.context.addCourseTicket(ticket, course)
158    except KeyError:
159        view.flash(_('The ticket exists.'))
160        return False
161    view.flash(_('Successfully added ${a}.',
162        mapping = {'a':ticket.code}))
163    view.context.writeLogMessage(
164        view,'added: %s|%s|%s' % (
165        ticket.code, ticket.level, ticket.level_session))
166    return True
167
168class StudentsBreadcrumb(Breadcrumb):
169    """A breadcrumb for the students container.
170    """
171    grok.context(IStudentsContainer)
172    title = _('Students')
173
174    @property
175    def target(self):
176        user = get_current_principal()
177        if getattr(user, 'user_type', None) == 'student':
178            return None
179        return self.viewname
180
181class StudentBreadcrumb(Breadcrumb):
182    """A breadcrumb for the student container.
183    """
184    grok.context(IStudent)
185
186    def title(self):
187        return self.context.display_fullname
188
189class SudyCourseBreadcrumb(Breadcrumb):
190    """A breadcrumb for the student study course.
191    """
192    grok.context(IStudentStudyCourse)
193
194    def title(self):
195        if self.context.is_current:
196            return _('Study Course')
197        else:
198            return _('Previous Study Course')
199
200class PaymentsBreadcrumb(Breadcrumb):
201    """A breadcrumb for the student payments folder.
202    """
203    grok.context(IStudentPaymentsContainer)
204    title = _('Payments')
205
206class OnlinePaymentBreadcrumb(Breadcrumb):
207    """A breadcrumb for payments.
208    """
209    grok.context(IStudentOnlinePayment)
210
211    @property
212    def title(self):
213        return self.context.p_id
214
215class AccommodationBreadcrumb(Breadcrumb):
216    """A breadcrumb for the student accommodation folder.
217    """
218    grok.context(IStudentAccommodation)
219    title = _('Accommodation')
220
221class BedTicketBreadcrumb(Breadcrumb):
222    """A breadcrumb for bed tickets.
223    """
224    grok.context(IBedTicket)
225
226    @property
227    def title(self):
228        return _('Bed Ticket ${a}',
229            mapping = {'a':self.context.getSessionString()})
230
231class StudyLevelBreadcrumb(Breadcrumb):
232    """A breadcrumb for course lists.
233    """
234    grok.context(IStudentStudyLevel)
235
236    @property
237    def title(self):
238        return self.context.level_title
239
240class StudentsContainerPage(KofaPage):
241    """The standard view for student containers.
242    """
243    grok.context(IStudentsContainer)
244    grok.name('index')
245    grok.require('waeup.viewStudentsContainer')
246    grok.template('containerpage')
247    label = _('Student Section')
248    search_button = _('Search')
249    pnav = 4
250
251    def update(self, *args, **kw):
252        datatable.need()
253        form = self.request.form
254        self.hitlist = []
255        if form.get('searchtype', None) == 'suspended':
256            self.searchtype = form['searchtype']
257            self.searchterm = None
258        elif 'searchterm' in form and form['searchterm']:
259            self.searchterm = form['searchterm']
260            self.searchtype = form['searchtype']
261        elif 'old_searchterm' in form:
262            self.searchterm = form['old_searchterm']
263            self.searchtype = form['old_searchtype']
264        else:
265            if 'search' in form:
266                self.flash(_('Empty search string'))
267            return
268        if self.searchtype == 'current_session':
269            try:
270                self.searchterm = int(self.searchterm)
271            except ValueError:
272                self.flash(_('Only year dates allowed (e.g. 2011).'))
273                return
274        self.hitlist = search(query=self.searchterm,
275            searchtype=self.searchtype, view=self)
276        if not self.hitlist:
277            self.flash(_('No student found.'))
278        return
279
280class StudentsContainerManagePage(KofaPage):
281    """The manage page for student containers.
282    """
283    grok.context(IStudentsContainer)
284    grok.name('manage')
285    grok.require('waeup.manageStudent')
286    grok.template('containermanagepage')
287    pnav = 4
288    label = _('Manage student section')
289    search_button = _('Search')
290    remove_button = _('Remove selected')
291
292    def update(self, *args, **kw):
293        datatable.need()
294        toggleall.need()
295        warning.need()
296        form = self.request.form
297        self.hitlist = []
298        if form.get('searchtype', None) == 'suspended':
299            self.searchtype = form['searchtype']
300            self.searchterm = None
301        elif 'searchterm' in form and form['searchterm']:
302            self.searchterm = form['searchterm']
303            self.searchtype = form['searchtype']
304        elif 'old_searchterm' in form:
305            self.searchterm = form['old_searchterm']
306            self.searchtype = form['old_searchtype']
307        else:
308            if 'search' in form:
309                self.flash(_('Empty search string'))
310            return
311        if self.searchtype == 'current_session':
312            try:
313                self.searchterm = int(self.searchterm)
314            except ValueError:
315                self.flash('Only year dates allowed (e.g. 2011).')
316                return
317        if not 'entries' in form:
318            self.hitlist = search(query=self.searchterm,
319                searchtype=self.searchtype, view=self)
320            if not self.hitlist:
321                self.flash(_('No student found.'))
322            if 'remove' in form:
323                self.flash(_('No item selected.'))
324            return
325        entries = form['entries']
326        if isinstance(entries, basestring):
327            entries = [entries]
328        deleted = []
329        for entry in entries:
330            if 'remove' in form:
331                del self.context[entry]
332                deleted.append(entry)
333        self.hitlist = search(query=self.searchterm,
334            searchtype=self.searchtype, view=self)
335        if len(deleted):
336            self.flash(_('Successfully removed: ${a}',
337                mapping = {'a':', '.join(deleted)}))
338        return
339
340class StudentAddFormPage(KofaAddFormPage):
341    """Add-form to add a student.
342    """
343    grok.context(IStudentsContainer)
344    grok.require('waeup.manageStudent')
345    grok.name('addstudent')
346    form_fields = grok.AutoFields(IStudent).select(
347        'firstname', 'middlename', 'lastname', 'reg_number')
348    label = _('Add student')
349    pnav = 4
350
351    @action(_('Create student record'), style='primary')
352    def addStudent(self, **data):
353        student = createObject(u'waeup.Student')
354        self.applyData(student, **data)
355        self.context.addStudent(student)
356        self.flash(_('Student record created.'))
357        self.redirect(self.url(self.context[student.student_id], 'index'))
358        return
359
360class LoginAsStudentStep1(KofaEditFormPage):
361    """ View to temporarily set a student password.
362    """
363    grok.context(IStudent)
364    grok.name('loginasstep1')
365    grok.require('waeup.loginAsStudent')
366    grok.template('loginasstep1')
367    pnav = 4
368
369    def label(self):
370        return _(u'Set temporary password for ${a}',
371            mapping = {'a':self.context.display_fullname})
372
373    @action('Set password now', style='primary')
374    def setPassword(self, *args, **data):
375        kofa_utils = getUtility(IKofaUtils)
376        password = kofa_utils.genPassword()
377        self.context.setTempPassword(self.request.principal.id, password)
378        self.context.writeLogMessage(
379            self, 'temp_password generated: %s' % password)
380        args = {'password':password}
381        self.redirect(self.url(self.context) +
382            '/loginasstep2?%s' % urlencode(args))
383        return
384
385class LoginAsStudentStep2(KofaPage):
386    """ View to temporarily login as student with a temporary password.
387    """
388    grok.context(IStudent)
389    grok.name('loginasstep2')
390    grok.require('waeup.Public')
391    grok.template('loginasstep2')
392    login_button = _('Login now')
393    pnav = 4
394
395    def label(self):
396        return _(u'Login as ${a}',
397            mapping = {'a':self.context.student_id})
398
399    def update(self, SUBMIT=None, password=None):
400        self.password = password
401        if SUBMIT is not None:
402            self.flash(_('You successfully logged in as student.'))
403            self.redirect(self.url(self.context))
404        return
405
406class StudentBaseDisplayFormPage(KofaDisplayFormPage):
407    """ Page to display student base data
408    """
409    grok.context(IStudent)
410    grok.name('index')
411    grok.require('waeup.viewStudent')
412    grok.template('basepage')
413    form_fields = grok.AutoFields(IStudentBase).omit(
414        'password', 'suspended', 'suspended_comment')
415    pnav = 4
416
417    @property
418    def label(self):
419        if self.context.suspended:
420            return _('${a}: Base Data (account deactivated)',
421                mapping = {'a':self.context.display_fullname})
422        return  _('${a}: Base Data',
423            mapping = {'a':self.context.display_fullname})
424
425    @property
426    def hasPassword(self):
427        if self.context.password:
428            return _('set')
429        return _('unset')
430
431class StudentBasePDFFormPage(KofaDisplayFormPage):
432    """ Page to display student base data in pdf files.
433    """
434
435    def __init__(self, context, request, omit_fields):
436        self.omit_fields = omit_fields
437        super(StudentBasePDFFormPage, self).__init__(context, request)
438
439    @property
440    def form_fields(self):
441        form_fields = grok.AutoFields(IStudentBase)
442        for field in self.omit_fields:
443            form_fields = form_fields.omit(field)
444        return form_fields
445
446class ContactStudentForm(ContactAdminForm):
447    grok.context(IStudent)
448    grok.name('contactstudent')
449    grok.require('waeup.viewStudent')
450    pnav = 4
451    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
452
453    def update(self, subject=u'', body=u''):
454        super(ContactStudentForm, self).update()
455        self.form_fields.get('subject').field.default = subject
456        self.form_fields.get('body').field.default = body
457        return
458
459    def label(self):
460        return _(u'Send message to ${a}',
461            mapping = {'a':self.context.display_fullname})
462
463    @action('Send message now', style='primary')
464    def send(self, *args, **data):
465        try:
466            email = self.request.principal.email
467        except AttributeError:
468            email = self.config.email_admin
469        usertype = getattr(self.request.principal,
470                           'user_type', 'system').title()
471        kofa_utils = getUtility(IKofaUtils)
472        success = kofa_utils.sendContactForm(
473                self.request.principal.title,email,
474                self.context.display_fullname,self.context.email,
475                self.request.principal.id,usertype,
476                self.config.name,
477                data['body'],data['subject'])
478        if success:
479            self.flash(_('Your message has been sent.'))
480        else:
481            self.flash(_('An smtp server error occurred.'))
482        return
483
484class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
485    """Deliver a PDF Admission slip.
486    """
487    grok.context(IStudent)
488    grok.name('admission_slip.pdf')
489    grok.require('waeup.viewStudent')
490    prefix = 'form'
491
492    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
493
494    @property
495    def label(self):
496        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
497        return translate(_('Admission Letter of'),
498            'waeup.kofa', target_language=portal_language) \
499            + ' %s' % self.context.display_fullname
500
501    def render(self):
502        students_utils = getUtility(IStudentsUtils)
503        return students_utils.renderPDFAdmissionLetter(self,
504            self.context.student)
505
506class StudentBaseManageFormPage(KofaEditFormPage):
507    """ View to manage student base data
508    """
509    grok.context(IStudent)
510    grok.name('manage_base')
511    grok.require('waeup.manageStudent')
512    form_fields = grok.AutoFields(IStudentBase).omit(
513        'student_id', 'adm_code', 'suspended')
514    grok.template('basemanagepage')
515    label = _('Manage base data')
516    pnav = 4
517
518    def update(self):
519        datepicker.need() # Enable jQuery datepicker in date fields.
520        tabs.need()
521        self.tab1 = self.tab2 = ''
522        qs = self.request.get('QUERY_STRING', '')
523        if not qs:
524            qs = 'tab1'
525        setattr(self, qs, 'active')
526        super(StudentBaseManageFormPage, self).update()
527        self.wf_info = IWorkflowInfo(self.context)
528        return
529
530    @action(_('Save'), style='primary')
531    def save(self, **data):
532        form = self.request.form
533        password = form.get('password', None)
534        password_ctl = form.get('control_password', None)
535        if password:
536            validator = getUtility(IPasswordValidator)
537            errors = validator.validate_password(password, password_ctl)
538            if errors:
539                self.flash( ' '.join(errors))
540                return
541        changed_fields = self.applyData(self.context, **data)
542        # Turn list of lists into single list
543        if changed_fields:
544            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
545        else:
546            changed_fields = []
547        if password:
548            # Now we know that the form has no errors and can set password
549            IUserAccount(self.context).setPassword(password)
550            changed_fields.append('password')
551        fields_string = ' + '.join(changed_fields)
552        self.flash(_('Form has been saved.'))
553        if fields_string:
554            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
555        return
556
557class StudentTriggerTransitionFormPage(KofaEditFormPage):
558    """ View to manage student base data
559    """
560    grok.context(IStudent)
561    grok.name('trigtrans')
562    grok.require('waeup.triggerTransition')
563    grok.template('trigtrans')
564    label = _('Trigger registration transition')
565    pnav = 4
566
567    def getTransitions(self):
568        """Return a list of dicts of allowed transition ids and titles.
569
570        Each list entry provides keys ``name`` and ``title`` for
571        internal name and (human readable) title of a single
572        transition.
573        """
574        wf_info = IWorkflowInfo(self.context)
575        allowed_transitions = [t for t in wf_info.getManualTransitions()
576            if not t[0].startswith('pay')]
577        if self.context.is_postgrad and not self.context.is_special_postgrad:
578            allowed_transitions = [t for t in allowed_transitions
579                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
580        return [dict(name='', title=_('No transition'))] +[
581            dict(name=x, title=y) for x, y in allowed_transitions]
582
583    @action(_('Save'), style='primary')
584    def save(self, **data):
585        form = self.request.form
586        if 'transition' in form and form['transition']:
587            transition_id = form['transition']
588            wf_info = IWorkflowInfo(self.context)
589            wf_info.fireTransition(transition_id)
590        return
591
592class StudentActivatePage(UtilityView, grok.View):
593    """ Activate student account
594    """
595    grok.context(IStudent)
596    grok.name('activate')
597    grok.require('waeup.manageStudent')
598
599    def update(self):
600        self.context.suspended = False
601        self.context.writeLogMessage(self, 'account activated')
602        history = IObjectHistory(self.context)
603        history.addMessage('Student account activated')
604        self.flash(_('Student account has been activated.'))
605        self.redirect(self.url(self.context))
606        return
607
608    def render(self):
609        return
610
611class StudentDeactivatePage(UtilityView, grok.View):
612    """ Deactivate student account
613    """
614    grok.context(IStudent)
615    grok.name('deactivate')
616    grok.require('waeup.manageStudent')
617
618    def update(self):
619        self.context.suspended = True
620        self.context.writeLogMessage(self, 'account deactivated')
621        history = IObjectHistory(self.context)
622        history.addMessage('Student account deactivated')
623        self.flash(_('Student account has been deactivated.'))
624        self.redirect(self.url(self.context))
625        return
626
627    def render(self):
628        return
629
630class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
631    """ Page to display student clearance data
632    """
633    grok.context(IStudent)
634    grok.name('view_clearance')
635    grok.require('waeup.viewStudent')
636    pnav = 4
637
638    @property
639    def separators(self):
640        return getUtility(IStudentsUtils).SEPARATORS_DICT
641
642    @property
643    def form_fields(self):
644        if self.context.is_postgrad:
645            form_fields = grok.AutoFields(
646                IPGStudentClearance).omit('clearance_locked')
647        else:
648            form_fields = grok.AutoFields(
649                IUGStudentClearance).omit('clearance_locked')
650        if not getattr(self.context, 'officer_comment'):
651            form_fields = form_fields.omit('officer_comment')
652        else:
653            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
654        return form_fields
655
656    @property
657    def label(self):
658        return _('${a}: Clearance Data',
659            mapping = {'a':self.context.display_fullname})
660
661class ExportPDFClearanceSlipPage(grok.View):
662    """Deliver a PDF slip of the context.
663    """
664    grok.context(IStudent)
665    grok.name('clearance_slip.pdf')
666    grok.require('waeup.viewStudent')
667    prefix = 'form'
668    omit_fields = (
669        'password', 'suspended', 'phone',
670        'adm_code', 'suspended_comment')
671
672    @property
673    def form_fields(self):
674        if self.context.is_postgrad:
675            form_fields = grok.AutoFields(
676                IPGStudentClearance).omit('clearance_locked')
677        else:
678            form_fields = grok.AutoFields(
679                IUGStudentClearance).omit('clearance_locked')
680        if not getattr(self.context, 'officer_comment'):
681            form_fields = form_fields.omit('officer_comment')
682        return form_fields
683
684    @property
685    def title(self):
686        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
687        return translate(_('Clearance Data'), 'waeup.kofa',
688            target_language=portal_language)
689
690    @property
691    def label(self):
692        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
693        return translate(_('Clearance Slip of'),
694            'waeup.kofa', target_language=portal_language) \
695            + ' %s' % self.context.display_fullname
696
697    # XXX: not used in waeup.kofa and thus not tested
698    def _signatures(self):
699        isStudent = getattr(
700            self.request.principal, 'user_type', None) == 'student'
701        if not isStudent and self.context.state in (CLEARED, ):
702            return ([_('Student Signature')],
703                    [_('Clearance Officer Signature')])
704        return
705
706    def _sigsInFooter(self):
707        isStudent = getattr(
708            self.request.principal, 'user_type', None) == 'student'
709        if not isStudent and self.context.state in (CLEARED, ):
710            return (_('Date, Student Signature'),
711                    _('Date, Clearance Officer Signature'),
712                    )
713        return ()
714
715    def render(self):
716        studentview = StudentBasePDFFormPage(self.context.student,
717            self.request, self.omit_fields)
718        students_utils = getUtility(IStudentsUtils)
719        return students_utils.renderPDF(
720            self, 'clearance_slip.pdf',
721            self.context.student, studentview, signatures=self._signatures(),
722            sigs_in_footer=self._sigsInFooter())
723
724class StudentClearanceManageFormPage(KofaEditFormPage):
725    """ Page to manage student clearance data
726    """
727    grok.context(IStudent)
728    grok.name('manage_clearance')
729    grok.require('waeup.manageStudent')
730    grok.template('clearanceeditpage')
731    label = _('Manage clearance data')
732    pnav = 4
733
734    @property
735    def separators(self):
736        return getUtility(IStudentsUtils).SEPARATORS_DICT
737
738    @property
739    def form_fields(self):
740        if self.context.is_postgrad:
741            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
742        else:
743            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
744        return form_fields
745
746    def update(self):
747        datepicker.need() # Enable jQuery datepicker in date fields.
748        tabs.need()
749        self.tab1 = self.tab2 = ''
750        qs = self.request.get('QUERY_STRING', '')
751        if not qs:
752            qs = 'tab1'
753        setattr(self, qs, 'active')
754        return super(StudentClearanceManageFormPage, self).update()
755
756    @action(_('Save'), style='primary')
757    def save(self, **data):
758        msave(self, **data)
759        return
760
761class StudentClearPage(UtilityView, grok.View):
762    """ Clear student by clearance officer
763    """
764    grok.context(IStudent)
765    grok.name('clear')
766    grok.require('waeup.clearStudent')
767
768    def update(self):
769        if clearance_disabled_message(self.context):
770            self.flash(clearance_disabled_message(self.context))
771            self.redirect(self.url(self.context,'view_clearance'))
772            return
773        if self.context.state == REQUESTED:
774            IWorkflowInfo(self.context).fireTransition('clear')
775            self.flash(_('Student has been cleared.'))
776        else:
777            self.flash(_('Student is in wrong state.'))
778        self.redirect(self.url(self.context,'view_clearance'))
779        return
780
781    def render(self):
782        return
783
784class StudentRejectClearancePage(KofaEditFormPage):
785    """ Reject clearance by clearance officers
786    """
787    grok.context(IStudent)
788    grok.name('reject_clearance')
789    label = _('Reject clearance')
790    grok.require('waeup.clearStudent')
791    form_fields = grok.AutoFields(
792        IUGStudentClearance).select('officer_comment')
793
794    def update(self):
795        if clearance_disabled_message(self.context):
796            self.flash(clearance_disabled_message(self.context))
797            self.redirect(self.url(self.context,'view_clearance'))
798            return
799        return super(StudentRejectClearancePage, self).update()
800
801    @action(_('Save comment and reject clearance now'), style='primary')
802    def reject(self, **data):
803        if self.context.state == CLEARED:
804            IWorkflowInfo(self.context).fireTransition('reset4')
805            message = _('Clearance has been annulled.')
806            self.flash(message)
807        elif self.context.state == REQUESTED:
808            IWorkflowInfo(self.context).fireTransition('reset3')
809            message = _('Clearance request has been rejected.')
810            self.flash(message)
811        else:
812            self.flash(_('Student is in wrong state.'))
813            self.redirect(self.url(self.context,'view_clearance'))
814            return
815        self.applyData(self.context, **data)
816        comment = data['officer_comment']
817        if comment:
818            self.context.writeLogMessage(
819                self, 'comment: %s' % comment.replace('\n', '<br>'))
820            args = {'subject':message, 'body':comment}
821        else:
822            args = {'subject':message,}
823        self.redirect(self.url(self.context) +
824            '/contactstudent?%s' % urlencode(args))
825        return
826
827
828class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
829    """ Page to display student personal data
830    """
831    grok.context(IStudent)
832    grok.name('view_personal')
833    grok.require('waeup.viewStudent')
834    form_fields = grok.AutoFields(IStudentPersonal)
835    form_fields['perm_address'].custom_widget = BytesDisplayWidget
836    form_fields[
837        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
838    pnav = 4
839
840    @property
841    def label(self):
842        return _('${a}: Personal Data',
843            mapping = {'a':self.context.display_fullname})
844
845class StudentPersonalManageFormPage(KofaEditFormPage):
846    """ Page to manage personal data
847    """
848    grok.context(IStudent)
849    grok.name('manage_personal')
850    grok.require('waeup.manageStudent')
851    form_fields = grok.AutoFields(IStudentPersonal)
852    form_fields['personal_updated'].for_display = True
853    form_fields[
854        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
855    label = _('Manage personal data')
856    pnav = 4
857
858    @action(_('Save'), style='primary')
859    def save(self, **data):
860        msave(self, **data)
861        return
862
863class StudentPersonalEditFormPage(KofaEditFormPage):
864    """ Page to edit personal data
865    """
866    grok.context(IStudent)
867    grok.name('edit_personal')
868    grok.require('waeup.handleStudent')
869    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
870    label = _('Edit personal data')
871    pnav = 4
872
873    @action(_('Save/Confirm'), style='primary')
874    def save(self, **data):
875        msave(self, **data)
876        self.context.personal_updated = datetime.utcnow()
877        return
878
879class StudyCourseDisplayFormPage(KofaDisplayFormPage):
880    """ Page to display the student study course data
881    """
882    grok.context(IStudentStudyCourse)
883    grok.name('index')
884    grok.require('waeup.viewStudent')
885    grok.template('studycoursepage')
886    pnav = 4
887
888    @property
889    def form_fields(self):
890        if self.context.is_postgrad:
891            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
892                'previous_verdict')
893        else:
894            form_fields = grok.AutoFields(IStudentStudyCourse)
895        return form_fields
896
897    @property
898    def label(self):
899        if self.context.is_current:
900            return _('${a}: Study Course',
901                mapping = {'a':self.context.__parent__.display_fullname})
902        else:
903            return _('${a}: Previous Study Course',
904                mapping = {'a':self.context.__parent__.display_fullname})
905
906    @property
907    def current_mode(self):
908        if self.context.certificate is not None:
909            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
910            return studymodes_dict[self.context.certificate.study_mode]
911        return
912
913    @property
914    def department(self):
915        if self.context.certificate is not None:
916            return self.context.certificate.__parent__.__parent__
917        return
918
919    @property
920    def faculty(self):
921        if self.context.certificate is not None:
922            return self.context.certificate.__parent__.__parent__.__parent__
923        return
924
925    @property
926    def prev_studycourses(self):
927        if self.context.is_current:
928            if self.context.__parent__.get('studycourse_2', None) is not None:
929                return (
930                        {'href':self.url(self.context.student) + '/studycourse_1',
931                        'title':_('First Study Course, ')},
932                        {'href':self.url(self.context.student) + '/studycourse_2',
933                        'title':_('Second Study Course')}
934                        )
935            if self.context.__parent__.get('studycourse_1', None) is not None:
936                return (
937                        {'href':self.url(self.context.student) + '/studycourse_1',
938                        'title':_('First Study Course')},
939                        )
940        return
941
942class StudyCourseManageFormPage(KofaEditFormPage):
943    """ Page to edit the student study course data
944    """
945    grok.context(IStudentStudyCourse)
946    grok.name('manage')
947    grok.require('waeup.manageStudent')
948    grok.template('studycoursemanagepage')
949    label = _('Manage study course')
950    pnav = 4
951    taboneactions = [_('Save'),_('Cancel')]
952    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
953    tabthreeactions = [_('Add study level')]
954
955    @property
956    def form_fields(self):
957        if self.context.is_postgrad:
958            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
959                'previous_verdict')
960        else:
961            form_fields = grok.AutoFields(IStudentStudyCourse)
962        return form_fields
963
964    def update(self):
965        if not self.context.is_current:
966            emit_lock_message(self)
967            return
968        super(StudyCourseManageFormPage, self).update()
969        tabs.need()
970        self.tab1 = self.tab2 = ''
971        qs = self.request.get('QUERY_STRING', '')
972        if not qs:
973            qs = 'tab1'
974        setattr(self, qs, 'active')
975        warning.need()
976        datatable.need()
977        return
978
979    @action(_('Save'), style='primary')
980    def save(self, **data):
981        try:
982            msave(self, **data)
983        except ConstraintNotSatisfied:
984            # The selected level might not exist in certificate
985            self.flash(_('Current level not available for certificate.'))
986            return
987        notify(grok.ObjectModifiedEvent(self.context.__parent__))
988        return
989
990    @property
991    def level_dict(self):
992        studylevelsource = StudyLevelSource().factory
993        for code in studylevelsource.getValues(self.context):
994            title = studylevelsource.getTitle(self.context, code)
995            yield(dict(code=code, title=title))
996
997    @property
998    def session_dict(self):
999        yield(dict(code='', title='--'))
1000        for item in academic_sessions():
1001            code = item[1]
1002            title = item[0]
1003            yield(dict(code=code, title=title))
1004
1005    @action(_('Add study level'))
1006    def addStudyLevel(self, **data):
1007        level_code = self.request.form.get('addlevel', None)
1008        level_session = self.request.form.get('level_session', None)
1009        if not level_session:
1010            self.flash(_('You must select a session for the level.'))
1011            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1012            return
1013        studylevel = createObject(u'waeup.StudentStudyLevel')
1014        studylevel.level = int(level_code)
1015        studylevel.level_session = int(level_session)
1016        try:
1017            self.context.addStudentStudyLevel(
1018                self.context.certificate,studylevel)
1019            self.flash(_('Study level has been added.'))
1020        except KeyError:
1021            self.flash(_('This level exists.'))
1022        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1023        return
1024
1025    @jsaction(_('Remove selected levels'))
1026    def delStudyLevels(self, **data):
1027        form = self.request.form
1028        if 'val_id' in form:
1029            child_id = form['val_id']
1030        else:
1031            self.flash(_('No study level selected.'))
1032            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1033            return
1034        if not isinstance(child_id, list):
1035            child_id = [child_id]
1036        deleted = []
1037        for id in child_id:
1038            del self.context[id]
1039            deleted.append(id)
1040        if len(deleted):
1041            self.flash(_('Successfully removed: ${a}',
1042                mapping = {'a':', '.join(deleted)}))
1043            self.context.writeLogMessage(
1044                self,'removed: %s' % ', '.join(deleted))
1045        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1046        return
1047
1048class StudyCourseTranscriptPage(KofaDisplayFormPage):
1049    """ Page to display the student's transcript.
1050    """
1051    grok.context(IStudentStudyCourse)
1052    grok.name('transcript')
1053    grok.require('waeup.viewStudent')
1054    grok.template('transcript')
1055    pnav = 4
1056
1057    def update(self):
1058        super(StudyCourseTranscriptPage, self).update()
1059        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1060        self.session_dict = dict(
1061            [(item[1], item[0]) for item in academic_sessions()])
1062        self.course_levels = course_levels
1063        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1064        return
1065
1066    @property
1067    def label(self):
1068        # Here we know that the cookie has been set
1069        lang = self.request.cookies.get('kofa.language')
1070        return _('${a}: Transcript Data', mapping = {
1071            'a':self.context.student.display_fullname})
1072
1073class StudentTransferFormPage(KofaAddFormPage):
1074    """Page to transfer the student.
1075    """
1076    grok.context(IStudent)
1077    grok.name('transfer')
1078    grok.require('waeup.manageStudent')
1079    label = _('Transfer student')
1080    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1081        'entry_mode', 'entry_session')
1082    pnav = 4
1083
1084    def update(self):
1085        super(StudentTransferFormPage, self).update()
1086        warning.need()
1087        return
1088
1089    @jsaction(_('Transfer'))
1090    def transferStudent(self, **data):
1091        error = self.context.transfer(**data)
1092        if error == -1:
1093            self.flash(_('Current level does not match certificate levels.'))
1094        elif error == -2:
1095            self.flash(_('Former study course record incomplete.'))
1096        elif error == -3:
1097            self.flash(_('Maximum number of transfers exceeded.'))
1098        else:
1099            self.flash(_('Successfully transferred.'))
1100        return
1101
1102class RevertTransferFormPage(KofaEditFormPage):
1103    """View that reverts the previous transfer.
1104    """
1105    grok.context(IStudent)
1106    grok.name('revert_transfer')
1107    grok.require('waeup.manageStudent')
1108    grok.template('reverttransfer')
1109    label = _('Revert previous transfer')
1110
1111    def update(self):
1112        warning.need()
1113        if not self.context.has_key('studycourse_1'):
1114            self.flash(_('No previous transfer.'))
1115            self.redirect(self.url(self.context))
1116            return
1117        return
1118
1119    @jsaction(_('Revert now'))
1120    def transferStudent(self, **data):
1121        self.context.revert_transfer()
1122        self.flash(_('Previous transfer reverted.'))
1123        self.redirect(self.url(self.context, 'studycourse'))
1124        return
1125
1126class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1127    """ Page to display student study levels
1128    """
1129    grok.context(IStudentStudyLevel)
1130    grok.name('index')
1131    grok.require('waeup.viewStudent')
1132    form_fields = grok.AutoFields(IStudentStudyLevel)
1133    form_fields[
1134        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1135    grok.template('studylevelpage')
1136    pnav = 4
1137
1138    def update(self):
1139        super(StudyLevelDisplayFormPage, self).update()
1140        datatable.need()
1141        return
1142
1143    @property
1144    def translated_values(self):
1145        return translated_values(self)
1146
1147    @property
1148    def label(self):
1149        # Here we know that the cookie has been set
1150        lang = self.request.cookies.get('kofa.language')
1151        level_title = translate(self.context.level_title, 'waeup.kofa',
1152            target_language=lang)
1153        return _('${a}: Study Level ${b}', mapping = {
1154            'a':self.context.student.display_fullname,
1155            'b':level_title})
1156
1157class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1158    """Deliver a PDF slip of the context.
1159    """
1160    grok.context(IStudentStudyLevel)
1161    grok.name('course_registration_slip.pdf')
1162    grok.require('waeup.viewStudent')
1163    form_fields = grok.AutoFields(IStudentStudyLevel)
1164    form_fields[
1165        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1166    prefix = 'form'
1167    omit_fields = (
1168        'password', 'suspended', 'phone',
1169        'adm_code', 'sex', 'suspended_comment')
1170
1171    @property
1172    def title(self):
1173        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1174        return translate(_('Level Data'), 'waeup.kofa',
1175            target_language=portal_language)
1176
1177    @property
1178    def content_title_1(self):
1179        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1180        return translate(_('1st Semester Courses'), 'waeup.kofa',
1181            target_language=portal_language)
1182
1183    @property
1184    def content_title_2(self):
1185        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1186        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1187            target_language=portal_language)
1188
1189    @property
1190    def content_title_3(self):
1191        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1192        return translate(_('Level Courses'), 'waeup.kofa',
1193            target_language=portal_language)
1194
1195    @property
1196    def label(self):
1197        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1198        lang = self.request.cookies.get('kofa.language', portal_language)
1199        level_title = translate(self.context.level_title, 'waeup.kofa',
1200            target_language=lang)
1201        return translate(_('Course Registration Slip'),
1202            'waeup.kofa', target_language=portal_language) \
1203            + ' %s' % level_title
1204
1205    def render(self):
1206        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1207        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1208        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1209        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1210        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1211        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1212        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1213        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1214        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1215        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1216        studentview = StudentBasePDFFormPage(self.context.student,
1217            self.request, self.omit_fields)
1218        students_utils = getUtility(IStudentsUtils)
1219        tabledata_1 = sorted(
1220            [value for value in self.context.values() if value.semester == 1],
1221            key=lambda value: str(value.semester) + value.code)
1222        tabledata_2 = sorted(
1223            [value for value in self.context.values() if value.semester == 2],
1224            key=lambda value: str(value.semester) + value.code)
1225        tabledata_3 = sorted(
1226            [value for value in self.context.values() if value.semester == 3],
1227            key=lambda value: str(value.semester) + value.code)
1228        tableheader = [(Code,'code', 2.5),
1229                         (Title,'title', 5),
1230                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1231                         (Cred, 'credits', 1.5),
1232                         #(Mand, 'mandatory', 1.5),
1233                         (Score, 'score', 1.5),
1234                         (Grade, 'grade', 1.5),
1235                         #('Auto', 'automatic', 1.5)
1236                         ]
1237        return students_utils.renderPDF(
1238            self, 'course_registration_slip.pdf',
1239            self.context.student, studentview,
1240            tableheader_1=tableheader,
1241            tabledata_1=tabledata_1,
1242            tableheader_2=tableheader,
1243            tabledata_2=tabledata_2,
1244            tableheader_3=tableheader,
1245            tabledata_3=tabledata_3
1246            )
1247
1248class StudyLevelManageFormPage(KofaEditFormPage):
1249    """ Page to edit the student study level data
1250    """
1251    grok.context(IStudentStudyLevel)
1252    grok.name('manage')
1253    grok.require('waeup.manageStudent')
1254    grok.template('studylevelmanagepage')
1255    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1256        'validation_date', 'validated_by', 'total_credits', 'gpa')
1257    pnav = 4
1258    taboneactions = [_('Save'),_('Cancel')]
1259    tabtwoactions = [_('Add course ticket'),
1260        _('Remove selected tickets'),_('Cancel')]
1261
1262    def update(self, ADD=None, course=None):
1263        if not self.context.__parent__.is_current:
1264            emit_lock_message(self)
1265            return
1266        super(StudyLevelManageFormPage, self).update()
1267        tabs.need()
1268        self.tab1 = self.tab2 = ''
1269        qs = self.request.get('QUERY_STRING', '')
1270        if not qs:
1271            qs = 'tab1'
1272        setattr(self, qs, 'active')
1273        warning.need()
1274        datatable.need()
1275        if ADD is not None:
1276            if not course:
1277                self.flash(_('No valid course code entered.'))
1278                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1279                return
1280            cat = queryUtility(ICatalog, name='courses_catalog')
1281            result = cat.searchResults(code=(course, course))
1282            if len(result) != 1:
1283                self.flash(_('Course not found.'))
1284            else:
1285                course = list(result)[0]
1286                addCourseTicket(self, course)
1287            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1288        return
1289
1290    @property
1291    def translated_values(self):
1292        return translated_values(self)
1293
1294    @property
1295    def label(self):
1296        # Here we know that the cookie has been set
1297        lang = self.request.cookies.get('kofa.language')
1298        level_title = translate(self.context.level_title, 'waeup.kofa',
1299            target_language=lang)
1300        return _('Manage study level ${a}',
1301            mapping = {'a':level_title})
1302
1303    @action(_('Save'), style='primary')
1304    def save(self, **data):
1305        msave(self, **data)
1306        return
1307
1308    @jsaction(_('Remove selected tickets'))
1309    def delCourseTicket(self, **data):
1310        form = self.request.form
1311        if 'val_id' in form:
1312            child_id = form['val_id']
1313        else:
1314            self.flash(_('No ticket selected.'))
1315            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1316            return
1317        if not isinstance(child_id, list):
1318            child_id = [child_id]
1319        deleted = []
1320        for id in child_id:
1321            del self.context[id]
1322            deleted.append(id)
1323        if len(deleted):
1324            self.flash(_('Successfully removed: ${a}',
1325                mapping = {'a':', '.join(deleted)}))
1326            self.context.writeLogMessage(
1327                self,'removed: %s at %s' %
1328                (', '.join(deleted), self.context.level))
1329        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1330        return
1331
1332class ValidateCoursesPage(UtilityView, grok.View):
1333    """ Validate course list by course adviser
1334    """
1335    grok.context(IStudentStudyLevel)
1336    grok.name('validate_courses')
1337    grok.require('waeup.validateStudent')
1338
1339    def update(self):
1340        if not self.context.__parent__.is_current:
1341            emit_lock_message(self)
1342            return
1343        if str(self.context.__parent__.current_level) != self.context.__name__:
1344            self.flash(_('This level does not correspond current level.'))
1345        elif self.context.student.state == REGISTERED:
1346            IWorkflowInfo(self.context.student).fireTransition(
1347                'validate_courses')
1348            self.flash(_('Course list has been validated.'))
1349        else:
1350            self.flash(_('Student is in the wrong state.'))
1351        self.redirect(self.url(self.context))
1352        return
1353
1354    def render(self):
1355        return
1356
1357class RejectCoursesPage(UtilityView, grok.View):
1358    """ Reject course list by course adviser
1359    """
1360    grok.context(IStudentStudyLevel)
1361    grok.name('reject_courses')
1362    grok.require('waeup.validateStudent')
1363
1364    def update(self):
1365        if not self.context.__parent__.is_current:
1366            emit_lock_message(self)
1367            return
1368        if str(self.context.__parent__.current_level) != self.context.__name__:
1369            self.flash(_('This level does not correspond current level.'))
1370            self.redirect(self.url(self.context))
1371            return
1372        elif self.context.student.state == VALIDATED:
1373            IWorkflowInfo(self.context.student).fireTransition('reset8')
1374            message = _('Course list request has been annulled.')
1375            self.flash(message)
1376        elif self.context.student.state == REGISTERED:
1377            IWorkflowInfo(self.context.student).fireTransition('reset7')
1378            message = _('Course list request has been rejected:')
1379            self.flash(message)
1380        else:
1381            self.flash(_('Student is in the wrong state.'))
1382            self.redirect(self.url(self.context))
1383            return
1384        args = {'subject':message}
1385        self.redirect(self.url(self.context.student) +
1386            '/contactstudent?%s' % urlencode(args))
1387        return
1388
1389    def render(self):
1390        return
1391
1392class CourseTicketAddFormPage(KofaAddFormPage):
1393    """Add a course ticket.
1394    """
1395    grok.context(IStudentStudyLevel)
1396    grok.name('add')
1397    grok.require('waeup.manageStudent')
1398    label = _('Add course ticket')
1399    form_fields = grok.AutoFields(ICourseTicketAdd)
1400    pnav = 4
1401
1402    def update(self):
1403        if not self.context.__parent__.is_current:
1404            emit_lock_message(self)
1405            return
1406        super(CourseTicketAddFormPage, self).update()
1407        return
1408
1409    @action(_('Add course ticket'))
1410    def addCourseTicket(self, **data):
1411        course = data['course']
1412        success = addCourseTicket(self, course)
1413        if success:
1414            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1415        return
1416
1417    @action(_('Cancel'), validator=NullValidator)
1418    def cancel(self, **data):
1419        self.redirect(self.url(self.context))
1420
1421class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1422    """ Page to display course tickets
1423    """
1424    grok.context(ICourseTicket)
1425    grok.name('index')
1426    grok.require('waeup.viewStudent')
1427    form_fields = grok.AutoFields(ICourseTicket)
1428    grok.template('courseticketpage')
1429    pnav = 4
1430
1431    @property
1432    def label(self):
1433        return _('${a}: Course Ticket ${b}', mapping = {
1434            'a':self.context.student.display_fullname,
1435            'b':self.context.code})
1436
1437class CourseTicketManageFormPage(KofaEditFormPage):
1438    """ Page to manage course tickets
1439    """
1440    grok.context(ICourseTicket)
1441    grok.name('manage')
1442    grok.require('waeup.manageStudent')
1443    form_fields = grok.AutoFields(ICourseTicket)
1444    form_fields['title'].for_display = True
1445    form_fields['fcode'].for_display = True
1446    form_fields['dcode'].for_display = True
1447    form_fields['semester'].for_display = True
1448    form_fields['passmark'].for_display = True
1449    form_fields['credits'].for_display = True
1450    form_fields['mandatory'].for_display = False
1451    form_fields['automatic'].for_display = True
1452    form_fields['carry_over'].for_display = True
1453    pnav = 4
1454    grok.template('courseticketmanagepage')
1455
1456    @property
1457    def label(self):
1458        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1459
1460    @action('Save', style='primary')
1461    def save(self, **data):
1462        msave(self, **data)
1463        return
1464
1465class PaymentsManageFormPage(KofaEditFormPage):
1466    """ Page to manage the student payments
1467
1468    This manage form page is for both students and students officers.
1469    """
1470    grok.context(IStudentPaymentsContainer)
1471    grok.name('index')
1472    grok.require('waeup.viewStudent')
1473    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1474    grok.template('paymentsmanagepage')
1475    pnav = 4
1476
1477    @property
1478    def manage_payments_allowed(self):
1479        return checkPermission('waeup.payStudent', self.context)
1480
1481    def unremovable(self, ticket):
1482        usertype = getattr(self.request.principal, 'user_type', None)
1483        if not usertype:
1484            return False
1485        if not self.manage_payments_allowed:
1486            return True
1487        return (self.request.principal.user_type == 'student' and ticket.r_code)
1488
1489    @property
1490    def label(self):
1491        return _('${a}: Payments',
1492            mapping = {'a':self.context.__parent__.display_fullname})
1493
1494    def update(self):
1495        super(PaymentsManageFormPage, self).update()
1496        datatable.need()
1497        warning.need()
1498        return
1499
1500    @jsaction(_('Remove selected tickets'))
1501    def delPaymentTicket(self, **data):
1502        form = self.request.form
1503        if 'val_id' in form:
1504            child_id = form['val_id']
1505        else:
1506            self.flash(_('No payment selected.'))
1507            self.redirect(self.url(self.context))
1508            return
1509        if not isinstance(child_id, list):
1510            child_id = [child_id]
1511        deleted = []
1512        for id in child_id:
1513            # Students are not allowed to remove used payment tickets
1514            ticket = self.context.get(id, None)
1515            if ticket is not None and not self.unremovable(ticket):
1516                del self.context[id]
1517                deleted.append(id)
1518        if len(deleted):
1519            self.flash(_('Successfully removed: ${a}',
1520                mapping = {'a': ', '.join(deleted)}))
1521            self.context.writeLogMessage(
1522                self,'removed: %s' % ', '.join(deleted))
1523        self.redirect(self.url(self.context))
1524        return
1525
1526    #@action(_('Add online payment ticket'))
1527    #def addPaymentTicket(self, **data):
1528    #    self.redirect(self.url(self.context, '@@addop'))
1529
1530class OnlinePaymentAddFormPage(KofaAddFormPage):
1531    """ Page to add an online payment ticket
1532    """
1533    grok.context(IStudentPaymentsContainer)
1534    grok.name('addop')
1535    grok.template('onlinepaymentaddform')
1536    grok.require('waeup.payStudent')
1537    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1538        'p_category')
1539    label = _('Add online payment')
1540    pnav = 4
1541
1542    @property
1543    def selectable_categories(self):
1544        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1545        return sorted(categories.items())
1546
1547    @action(_('Create ticket'), style='primary')
1548    def createTicket(self, **data):
1549        p_category = data['p_category']
1550        previous_session = data.get('p_session', None)
1551        previous_level = data.get('p_level', None)
1552        student = self.context.__parent__
1553        if p_category == 'bed_allocation' and student[
1554            'studycourse'].current_session != grok.getSite()[
1555            'hostels'].accommodation_session:
1556                self.flash(
1557                    _('Your current session does not match ' + \
1558                    'accommodation session.'))
1559                return
1560        if 'maintenance' in p_category:
1561            current_session = str(student['studycourse'].current_session)
1562            if not current_session in student['accommodation']:
1563                self.flash(_('You have not yet booked accommodation.'))
1564                return
1565        students_utils = getUtility(IStudentsUtils)
1566        error, payment = students_utils.setPaymentDetails(
1567            p_category, student, previous_session, previous_level)
1568        if error is not None:
1569            self.flash(error)
1570            return
1571        self.context[payment.p_id] = payment
1572        self.flash(_('Payment ticket created.'))
1573        self.redirect(self.url(self.context))
1574        return
1575
1576    @action(_('Cancel'), validator=NullValidator)
1577    def cancel(self, **data):
1578        self.redirect(self.url(self.context))
1579
1580class PreviousPaymentAddFormPage(KofaAddFormPage):
1581    """ Page to add an online payment ticket for previous sessions
1582    """
1583    grok.context(IStudentPaymentsContainer)
1584    grok.name('addpp')
1585    grok.require('waeup.payStudent')
1586    form_fields = grok.AutoFields(IStudentPreviousPayment)
1587    label = _('Add previous session online payment')
1588    pnav = 4
1589
1590    def update(self):
1591        if self.context.student.before_payment:
1592            self.flash(_("No previous payment to be made."))
1593            self.redirect(self.url(self.context))
1594        super(PreviousPaymentAddFormPage, self).update()
1595        return
1596
1597    @action(_('Create ticket'), style='primary')
1598    def createTicket(self, **data):
1599        p_category = data['p_category']
1600        previous_session = data.get('p_session', None)
1601        previous_level = data.get('p_level', None)
1602        student = self.context.__parent__
1603        students_utils = getUtility(IStudentsUtils)
1604        error, payment = students_utils.setPaymentDetails(
1605            p_category, student, previous_session, previous_level)
1606        if error is not None:
1607            self.flash(error)
1608            return
1609        self.context[payment.p_id] = payment
1610        self.flash(_('Payment ticket created.'))
1611        self.redirect(self.url(self.context))
1612        return
1613
1614    @action(_('Cancel'), validator=NullValidator)
1615    def cancel(self, **data):
1616        self.redirect(self.url(self.context))
1617
1618class BalancePaymentAddFormPage(KofaAddFormPage):
1619    """ Page to add an online payment ticket for balance sessions
1620    """
1621    grok.context(IStudentPaymentsContainer)
1622    grok.name('addbp')
1623    grok.require('waeup.manageStudent')
1624    form_fields = grok.AutoFields(IStudentBalancePayment)
1625    label = _('Add balance')
1626    pnav = 4
1627
1628    @action(_('Create ticket'), style='primary')
1629    def createTicket(self, **data):
1630        p_category = data['p_category']
1631        balance_session = data.get('balance_session', None)
1632        balance_level = data.get('balance_level', None)
1633        balance_amount = data.get('balance_amount', None)
1634        student = self.context.__parent__
1635        students_utils = getUtility(IStudentsUtils)
1636        error, payment = students_utils.setBalanceDetails(
1637            p_category, student, balance_session,
1638            balance_level, balance_amount)
1639        if error is not None:
1640            self.flash(error)
1641            return
1642        self.context[payment.p_id] = payment
1643        self.flash(_('Payment ticket created.'))
1644        self.redirect(self.url(self.context))
1645        return
1646
1647    @action(_('Cancel'), validator=NullValidator)
1648    def cancel(self, **data):
1649        self.redirect(self.url(self.context))
1650
1651class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1652    """ Page to view an online payment ticket
1653    """
1654    grok.context(IStudentOnlinePayment)
1655    grok.name('index')
1656    grok.require('waeup.viewStudent')
1657    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1658    form_fields[
1659        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1660    form_fields[
1661        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1662    pnav = 4
1663
1664    @property
1665    def label(self):
1666        return _('${a}: Online Payment Ticket ${b}', mapping = {
1667            'a':self.context.student.display_fullname,
1668            'b':self.context.p_id})
1669
1670class OnlinePaymentApprovePage(UtilityView, grok.View):
1671    """ Callback view
1672    """
1673    grok.context(IStudentOnlinePayment)
1674    grok.name('approve')
1675    grok.require('waeup.managePortal')
1676
1677    def update(self):
1678        success, msg, log = self.context.approveStudentPayment()
1679        if log is not None:
1680            # Add log message to students.log
1681            self.context.writeLogMessage(self,log)
1682            # Add log message to payments.log
1683            self.context.logger.info(
1684                '%s,%s,%s,%s,%s,,,,,,' % (
1685                self.context.student.student_id,
1686                self.context.p_id, self.context.p_category,
1687                self.context.amount_auth, self.context.r_code))
1688        self.flash(msg)
1689        return
1690
1691    def render(self):
1692        self.redirect(self.url(self.context, '@@index'))
1693        return
1694
1695class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1696    """ Approval view for students.
1697
1698    This view is used for browser tests only and
1699    must be neutralized in custom pages!
1700    """
1701
1702    grok.name('fake_approve')
1703    grok.require('waeup.payStudent')
1704
1705class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1706    """Deliver a PDF slip of the context.
1707    """
1708    grok.context(IStudentOnlinePayment)
1709    grok.name('payment_slip.pdf')
1710    grok.require('waeup.viewStudent')
1711    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1712    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1713    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1714    prefix = 'form'
1715    note = None
1716    omit_fields = (
1717        'password', 'suspended', 'phone',
1718        'adm_code', 'sex', 'suspended_comment')
1719
1720    @property
1721    def title(self):
1722        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1723        return translate(_('Payment Data'), 'waeup.kofa',
1724            target_language=portal_language)
1725
1726    @property
1727    def label(self):
1728        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1729        return translate(_('Online Payment Slip'),
1730            'waeup.kofa', target_language=portal_language) \
1731            + ' %s' % self.context.p_id
1732
1733    def render(self):
1734        #if self.context.p_state != 'paid':
1735        #    self.flash('Ticket not yet paid.')
1736        #    self.redirect(self.url(self.context))
1737        #    return
1738        studentview = StudentBasePDFFormPage(self.context.student,
1739            self.request, self.omit_fields)
1740        students_utils = getUtility(IStudentsUtils)
1741        return students_utils.renderPDF(self, 'payment_slip.pdf',
1742            self.context.student, studentview, note=self.note)
1743
1744
1745class AccommodationManageFormPage(KofaEditFormPage):
1746    """ Page to manage bed tickets.
1747
1748    This manage form page is for both students and students officers.
1749    """
1750    grok.context(IStudentAccommodation)
1751    grok.name('index')
1752    grok.require('waeup.handleAccommodation')
1753    form_fields = grok.AutoFields(IStudentAccommodation)
1754    grok.template('accommodationmanagepage')
1755    pnav = 4
1756    officers_only_actions = [_('Remove selected')]
1757
1758    @property
1759    def label(self):
1760        return _('${a}: Accommodation',
1761            mapping = {'a':self.context.__parent__.display_fullname})
1762
1763    def update(self):
1764        super(AccommodationManageFormPage, self).update()
1765        datatable.need()
1766        warning.need()
1767        return
1768
1769    @jsaction(_('Remove selected'))
1770    def delBedTickets(self, **data):
1771        if getattr(self.request.principal, 'user_type', None) == 'student':
1772            self.flash(_('You are not allowed to remove bed tickets.'))
1773            self.redirect(self.url(self.context))
1774            return
1775        form = self.request.form
1776        if 'val_id' in form:
1777            child_id = form['val_id']
1778        else:
1779            self.flash(_('No bed ticket selected.'))
1780            self.redirect(self.url(self.context))
1781            return
1782        if not isinstance(child_id, list):
1783            child_id = [child_id]
1784        deleted = []
1785        for id in child_id:
1786            del self.context[id]
1787            deleted.append(id)
1788        if len(deleted):
1789            self.flash(_('Successfully removed: ${a}',
1790                mapping = {'a':', '.join(deleted)}))
1791            self.context.writeLogMessage(
1792                self,'removed: % s' % ', '.join(deleted))
1793        self.redirect(self.url(self.context))
1794        return
1795
1796    @property
1797    def selected_actions(self):
1798        if getattr(self.request.principal, 'user_type', None) == 'student':
1799            return [action for action in self.actions
1800                    if not action.label in self.officers_only_actions]
1801        return self.actions
1802
1803class BedTicketAddPage(KofaPage):
1804    """ Page to add an online payment ticket
1805    """
1806    grok.context(IStudentAccommodation)
1807    grok.name('add')
1808    grok.require('waeup.handleAccommodation')
1809    grok.template('enterpin')
1810    ac_prefix = 'HOS'
1811    label = _('Add bed ticket')
1812    pnav = 4
1813    buttonname = _('Create bed ticket')
1814    notice = ''
1815    with_ac = True
1816
1817    def update(self, SUBMIT=None):
1818        student = self.context.student
1819        students_utils = getUtility(IStudentsUtils)
1820        acc_details  = students_utils.getAccommodationDetails(student)
1821        if acc_details.get('expired', False):
1822            startdate = acc_details.get('startdate')
1823            enddate = acc_details.get('enddate')
1824            if startdate and enddate:
1825                tz = getUtility(IKofaUtils).tzinfo
1826                startdate = to_timezone(
1827                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1828                enddate = to_timezone(
1829                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1830                self.flash(_("Outside booking period: ${a} - ${b}",
1831                    mapping = {'a': startdate, 'b': enddate}))
1832            else:
1833                self.flash(_("Outside booking period."))
1834            self.redirect(self.url(self.context))
1835            return
1836        if not acc_details:
1837            self.flash(_("Your data are incomplete."))
1838            self.redirect(self.url(self.context))
1839            return
1840        if not student.state in acc_details['allowed_states']:
1841            self.flash(_("You are in the wrong registration state."))
1842            self.redirect(self.url(self.context))
1843            return
1844        if student['studycourse'].current_session != acc_details[
1845            'booking_session']:
1846            self.flash(
1847                _('Your current session does not match accommodation session.'))
1848            self.redirect(self.url(self.context))
1849            return
1850        if str(acc_details['booking_session']) in self.context.keys():
1851            self.flash(
1852                _('You already booked a bed space in current ' \
1853                    + 'accommodation session.'))
1854            self.redirect(self.url(self.context))
1855            return
1856        if self.with_ac:
1857            self.ac_series = self.request.form.get('ac_series', None)
1858            self.ac_number = self.request.form.get('ac_number', None)
1859        if SUBMIT is None:
1860            return
1861        if self.with_ac:
1862            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1863            code = get_access_code(pin)
1864            if not code:
1865                self.flash(_('Activation code is invalid.'))
1866                return
1867        # Search and book bed
1868        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1869        entries = cat.searchResults(
1870            owner=(student.student_id,student.student_id))
1871        if len(entries):
1872            # If bed space has been manually allocated use this bed
1873            bed = [entry for entry in entries][0]
1874            # Safety belt for paranoids: Does this bed really exist on portal?
1875            # XXX: Can be remove if nobody complains.
1876            if bed.__parent__.__parent__ is None:
1877                self.flash(_('System error: Please contact the adminsitrator.'))
1878                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1879                return
1880        else:
1881            # else search for other available beds
1882            entries = cat.searchResults(
1883                bed_type=(acc_details['bt'],acc_details['bt']))
1884            available_beds = [
1885                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1886            if available_beds:
1887                students_utils = getUtility(IStudentsUtils)
1888                bed = students_utils.selectBed(available_beds)
1889                # Safety belt for paranoids: Does this bed really exist in portal?
1890                # XXX: Can be remove if nobody complains.
1891                if bed.__parent__.__parent__ is None:
1892                    self.flash(_('System error: Please contact the adminsitrator.'))
1893                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1894                    return
1895                bed.bookBed(student.student_id)
1896            else:
1897                self.flash(_('There is no free bed in your category ${a}.',
1898                    mapping = {'a':acc_details['bt']}))
1899                return
1900        if self.with_ac:
1901            # Mark pin as used (this also fires a pin related transition)
1902            if code.state == USED:
1903                self.flash(_('Activation code has already been used.'))
1904                return
1905            else:
1906                comment = _(u'invalidated')
1907                # Here we know that the ac is in state initialized so we do not
1908                # expect an exception, but the owner might be different
1909                if not invalidate_accesscode(
1910                    pin,comment,self.context.student.student_id):
1911                    self.flash(_('You are not the owner of this access code.'))
1912                    return
1913        # Create bed ticket
1914        bedticket = createObject(u'waeup.BedTicket')
1915        if self.with_ac:
1916            bedticket.booking_code = pin
1917        bedticket.booking_session = acc_details['booking_session']
1918        bedticket.bed_type = acc_details['bt']
1919        bedticket.bed = bed
1920        hall_title = bed.__parent__.hostel_name
1921        coordinates = bed.coordinates[1:]
1922        block, room_nr, bed_nr = coordinates
1923        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1924            'a':hall_title, 'b':block,
1925            'c':room_nr, 'd':bed_nr,
1926            'e':bed.bed_type})
1927        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1928        bedticket.bed_coordinates = translate(
1929            bc, 'waeup.kofa',target_language=portal_language)
1930        self.context.addBedTicket(bedticket)
1931        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1932        self.flash(_('Bed ticket created and bed booked: ${a}',
1933            mapping = {'a':bedticket.display_coordinates}))
1934        self.redirect(self.url(self.context))
1935        return
1936
1937class BedTicketDisplayFormPage(KofaDisplayFormPage):
1938    """ Page to display bed tickets
1939    """
1940    grok.context(IBedTicket)
1941    grok.name('index')
1942    grok.require('waeup.handleAccommodation')
1943    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
1944    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1945    pnav = 4
1946
1947    @property
1948    def label(self):
1949        return _('Bed Ticket for Session ${a}',
1950            mapping = {'a':self.context.getSessionString()})
1951
1952class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1953    """Deliver a PDF slip of the context.
1954    """
1955    grok.context(IBedTicket)
1956    grok.name('bed_allocation_slip.pdf')
1957    grok.require('waeup.handleAccommodation')
1958    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
1959    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1960    prefix = 'form'
1961    omit_fields = (
1962        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
1963
1964    @property
1965    def title(self):
1966        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1967        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1968            target_language=portal_language)
1969
1970    @property
1971    def label(self):
1972        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1973        #return translate(_('Bed Allocation: '),
1974        #    'waeup.kofa', target_language=portal_language) \
1975        #    + ' %s' % self.context.bed_coordinates
1976        return translate(_('Bed Allocation Slip'),
1977            'waeup.kofa', target_language=portal_language) \
1978            + ' %s' % self.context.getSessionString()
1979
1980    def render(self):
1981        studentview = StudentBasePDFFormPage(self.context.student,
1982            self.request, self.omit_fields)
1983        students_utils = getUtility(IStudentsUtils)
1984        return students_utils.renderPDF(
1985            self, 'bed_allocation_slip.pdf',
1986            self.context.student, studentview)
1987
1988class BedTicketRelocationPage(UtilityView, grok.View):
1989    """ Callback view
1990    """
1991    grok.context(IBedTicket)
1992    grok.name('relocate')
1993    grok.require('waeup.manageHostels')
1994
1995    # Relocate student if student parameters have changed or the bed_type
1996    # of the bed has changed
1997    def update(self):
1998        student = self.context.student
1999        students_utils = getUtility(IStudentsUtils)
2000        acc_details  = students_utils.getAccommodationDetails(student)
2001        if self.context.bed != None and \
2002              'reserved' in self.context.bed.bed_type:
2003            self.flash(_("Students in reserved beds can't be relocated."))
2004            self.redirect(self.url(self.context))
2005            return
2006        if acc_details['bt'] == self.context.bed_type and \
2007                self.context.bed != None and \
2008                self.context.bed.bed_type == self.context.bed_type:
2009            self.flash(_("Student can't be relocated."))
2010            self.redirect(self.url(self.context))
2011            return
2012        # Search a bed
2013        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2014        entries = cat.searchResults(
2015            owner=(student.student_id,student.student_id))
2016        if len(entries) and self.context.bed == None:
2017            # If booking has been cancelled but other bed space has been
2018            # manually allocated after cancellation use this bed
2019            new_bed = [entry for entry in entries][0]
2020        else:
2021            # Search for other available beds
2022            entries = cat.searchResults(
2023                bed_type=(acc_details['bt'],acc_details['bt']))
2024            available_beds = [
2025                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2026            if available_beds:
2027                students_utils = getUtility(IStudentsUtils)
2028                new_bed = students_utils.selectBed(available_beds)
2029                new_bed.bookBed(student.student_id)
2030            else:
2031                self.flash(_('There is no free bed in your category ${a}.',
2032                    mapping = {'a':acc_details['bt']}))
2033                self.redirect(self.url(self.context))
2034                return
2035        # Release old bed if exists
2036        if self.context.bed != None:
2037            self.context.bed.owner = NOT_OCCUPIED
2038            notify(grok.ObjectModifiedEvent(self.context.bed))
2039        # Alocate new bed
2040        self.context.bed_type = acc_details['bt']
2041        self.context.bed = new_bed
2042        hall_title = new_bed.__parent__.hostel_name
2043        coordinates = new_bed.coordinates[1:]
2044        block, room_nr, bed_nr = coordinates
2045        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2046            'a':hall_title, 'b':block,
2047            'c':room_nr, 'd':bed_nr,
2048            'e':new_bed.bed_type})
2049        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2050        self.context.bed_coordinates = translate(
2051            bc, 'waeup.kofa',target_language=portal_language)
2052        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2053        self.flash(_('Student relocated: ${a}',
2054            mapping = {'a':self.context.display_coordinates}))
2055        self.redirect(self.url(self.context))
2056        return
2057
2058    def render(self):
2059        return
2060
2061class StudentHistoryPage(KofaPage):
2062    """ Page to display student clearance data
2063    """
2064    grok.context(IStudent)
2065    grok.name('history')
2066    grok.require('waeup.viewStudent')
2067    grok.template('studenthistory')
2068    pnav = 4
2069
2070    @property
2071    def label(self):
2072        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2073
2074# Pages for students only
2075
2076class StudentBaseEditFormPage(KofaEditFormPage):
2077    """ View to edit student base data
2078    """
2079    grok.context(IStudent)
2080    grok.name('edit_base')
2081    grok.require('waeup.handleStudent')
2082    form_fields = grok.AutoFields(IStudentBase).select(
2083        'email', 'phone')
2084    label = _('Edit base data')
2085    pnav = 4
2086
2087    @action(_('Save'), style='primary')
2088    def save(self, **data):
2089        msave(self, **data)
2090        return
2091
2092class StudentChangePasswordPage(KofaEditFormPage):
2093    """ View to manage student base data
2094    """
2095    grok.context(IStudent)
2096    grok.name('change_password')
2097    grok.require('waeup.handleStudent')
2098    grok.template('change_password')
2099    label = _('Change password')
2100    pnav = 4
2101
2102    @action(_('Save'), style='primary')
2103    def save(self, **data):
2104        form = self.request.form
2105        password = form.get('change_password', None)
2106        password_ctl = form.get('change_password_repeat', None)
2107        if password:
2108            validator = getUtility(IPasswordValidator)
2109            errors = validator.validate_password(password, password_ctl)
2110            if not errors:
2111                IUserAccount(self.context).setPassword(password)
2112                self.context.writeLogMessage(self, 'saved: password')
2113                self.flash(_('Password changed.'))
2114            else:
2115                self.flash( ' '.join(errors))
2116        return
2117
2118class StudentFilesUploadPage(KofaPage):
2119    """ View to upload files by student
2120    """
2121    grok.context(IStudent)
2122    grok.name('change_portrait')
2123    grok.require('waeup.uploadStudentFile')
2124    grok.template('filesuploadpage')
2125    label = _('Upload portrait')
2126    pnav = 4
2127
2128    def update(self):
2129        if self.context.student.state != ADMITTED:
2130            emit_lock_message(self)
2131            return
2132        super(StudentFilesUploadPage, self).update()
2133        return
2134
2135class StartClearancePage(KofaPage):
2136    grok.context(IStudent)
2137    grok.name('start_clearance')
2138    grok.require('waeup.handleStudent')
2139    grok.template('enterpin')
2140    label = _('Start clearance')
2141    ac_prefix = 'CLR'
2142    notice = ''
2143    pnav = 4
2144    buttonname = _('Start clearance now')
2145    with_ac = True
2146
2147    @property
2148    def all_required_fields_filled(self):
2149        if self.context.email and self.context.phone:
2150            return True
2151        return False
2152
2153    @property
2154    def portrait_uploaded(self):
2155        store = getUtility(IExtFileStore)
2156        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2157            return True
2158        return False
2159
2160    def update(self, SUBMIT=None):
2161        if not self.context.state == ADMITTED:
2162            self.flash(_("Wrong state"))
2163            self.redirect(self.url(self.context))
2164            return
2165        if not self.portrait_uploaded:
2166            self.flash(_("No portrait uploaded."))
2167            self.redirect(self.url(self.context, 'change_portrait'))
2168            return
2169        if not self.all_required_fields_filled:
2170            self.flash(_("Not all required fields filled."))
2171            self.redirect(self.url(self.context, 'edit_base'))
2172            return
2173        if self.with_ac:
2174            self.ac_series = self.request.form.get('ac_series', None)
2175            self.ac_number = self.request.form.get('ac_number', None)
2176        if SUBMIT is None:
2177            return
2178        if self.with_ac:
2179            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2180            code = get_access_code(pin)
2181            if not code:
2182                self.flash(_('Activation code is invalid.'))
2183                return
2184            if code.state == USED:
2185                self.flash(_('Activation code has already been used.'))
2186                return
2187            # Mark pin as used (this also fires a pin related transition)
2188            # and fire transition start_clearance
2189            comment = _(u"invalidated")
2190            # Here we know that the ac is in state initialized so we do not
2191            # expect an exception, but the owner might be different
2192            if not invalidate_accesscode(pin, comment, self.context.student_id):
2193                self.flash(_('You are not the owner of this access code.'))
2194                return
2195            self.context.clr_code = pin
2196        IWorkflowInfo(self.context).fireTransition('start_clearance')
2197        self.flash(_('Clearance process has been started.'))
2198        self.redirect(self.url(self.context,'cedit'))
2199        return
2200
2201class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2202    """ View to edit student clearance data by student
2203    """
2204    grok.context(IStudent)
2205    grok.name('cedit')
2206    grok.require('waeup.handleStudent')
2207    label = _('Edit clearance data')
2208
2209    @property
2210    def form_fields(self):
2211        if self.context.is_postgrad:
2212            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2213                'clearance_locked', 'clr_code', 'officer_comment')
2214        else:
2215            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2216                'clearance_locked', 'clr_code', 'officer_comment')
2217        return form_fields
2218
2219    def update(self):
2220        if self.context.clearance_locked:
2221            emit_lock_message(self)
2222            return
2223        return super(StudentClearanceEditFormPage, self).update()
2224
2225    @action(_('Save'), style='primary')
2226    def save(self, **data):
2227        self.applyData(self.context, **data)
2228        self.flash(_('Clearance form has been saved.'))
2229        return
2230
2231    def dataNotComplete(self):
2232        """To be implemented in the customization package.
2233        """
2234        return False
2235
2236    @action(_('Save and request clearance'), style='primary')
2237    def requestClearance(self, **data):
2238        self.applyData(self.context, **data)
2239        if self.dataNotComplete():
2240            self.flash(self.dataNotComplete())
2241            return
2242        self.flash(_('Clearance form has been saved.'))
2243        if self.context.clr_code:
2244            self.redirect(self.url(self.context, 'request_clearance'))
2245        else:
2246            # We bypass the request_clearance page if student
2247            # has been imported in state 'clearance started' and
2248            # no clr_code was entered before.
2249            state = IWorkflowState(self.context).getState()
2250            if state != CLEARANCE:
2251                # This shouldn't happen, but the application officer
2252                # might have forgotten to lock the form after changing the state
2253                self.flash(_('This form cannot be submitted. Wrong state!'))
2254                return
2255            IWorkflowInfo(self.context).fireTransition('request_clearance')
2256            self.flash(_('Clearance has been requested.'))
2257            self.redirect(self.url(self.context))
2258        return
2259
2260class RequestClearancePage(KofaPage):
2261    grok.context(IStudent)
2262    grok.name('request_clearance')
2263    grok.require('waeup.handleStudent')
2264    grok.template('enterpin')
2265    label = _('Request clearance')
2266    notice = _('Enter the CLR access code used for starting clearance.')
2267    ac_prefix = 'CLR'
2268    pnav = 4
2269    buttonname = _('Request clearance now')
2270    with_ac = True
2271
2272    def update(self, SUBMIT=None):
2273        if self.with_ac:
2274            self.ac_series = self.request.form.get('ac_series', None)
2275            self.ac_number = self.request.form.get('ac_number', None)
2276        if SUBMIT is None:
2277            return
2278        if self.with_ac:
2279            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2280            if self.context.clr_code and self.context.clr_code != pin:
2281                self.flash(_("This isn't your CLR access code."))
2282                return
2283        state = IWorkflowState(self.context).getState()
2284        if state != CLEARANCE:
2285            # This shouldn't happen, but the application officer
2286            # might have forgotten to lock the form after changing the state
2287            self.flash(_('This form cannot be submitted. Wrong state!'))
2288            return
2289        IWorkflowInfo(self.context).fireTransition('request_clearance')
2290        self.flash(_('Clearance has been requested.'))
2291        self.redirect(self.url(self.context))
2292        return
2293
2294class StartSessionPage(KofaPage):
2295    grok.context(IStudentStudyCourse)
2296    grok.name('start_session')
2297    grok.require('waeup.handleStudent')
2298    grok.template('enterpin')
2299    label = _('Start session')
2300    ac_prefix = 'SFE'
2301    notice = ''
2302    pnav = 4
2303    buttonname = _('Start now')
2304    with_ac = True
2305
2306    def update(self, SUBMIT=None):
2307        if not self.context.is_current:
2308            emit_lock_message(self)
2309            return
2310        super(StartSessionPage, self).update()
2311        if not self.context.next_session_allowed:
2312            self.flash(_("You are not entitled to start session."))
2313            self.redirect(self.url(self.context))
2314            return
2315        if self.with_ac:
2316            self.ac_series = self.request.form.get('ac_series', None)
2317            self.ac_number = self.request.form.get('ac_number', None)
2318        if SUBMIT is None:
2319            return
2320        if self.with_ac:
2321            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2322            code = get_access_code(pin)
2323            if not code:
2324                self.flash(_('Activation code is invalid.'))
2325                return
2326            # Mark pin as used (this also fires a pin related transition)
2327            if code.state == USED:
2328                self.flash(_('Activation code has already been used.'))
2329                return
2330            else:
2331                comment = _(u"invalidated")
2332                # Here we know that the ac is in state initialized so we do not
2333                # expect an error, but the owner might be different
2334                if not invalidate_accesscode(
2335                    pin,comment,self.context.student.student_id):
2336                    self.flash(_('You are not the owner of this access code.'))
2337                    return
2338        try:
2339            if self.context.student.state == CLEARED:
2340                IWorkflowInfo(self.context.student).fireTransition(
2341                    'pay_first_school_fee')
2342            elif self.context.student.state == RETURNING:
2343                IWorkflowInfo(self.context.student).fireTransition(
2344                    'pay_school_fee')
2345            elif self.context.student.state == PAID:
2346                IWorkflowInfo(self.context.student).fireTransition(
2347                    'pay_pg_fee')
2348        except ConstraintNotSatisfied:
2349            self.flash(_('An error occurred, please contact the system administrator.'))
2350            return
2351        self.flash(_('Session started.'))
2352        self.redirect(self.url(self.context))
2353        return
2354
2355class AddStudyLevelFormPage(KofaEditFormPage):
2356    """ Page for students to add current study levels
2357    """
2358    grok.context(IStudentStudyCourse)
2359    grok.name('add')
2360    grok.require('waeup.handleStudent')
2361    grok.template('studyleveladdpage')
2362    form_fields = grok.AutoFields(IStudentStudyCourse)
2363    pnav = 4
2364
2365    @property
2366    def label(self):
2367        studylevelsource = StudyLevelSource().factory
2368        code = self.context.current_level
2369        title = studylevelsource.getTitle(self.context, code)
2370        return _('Add current level ${a}', mapping = {'a':title})
2371
2372    def update(self):
2373        if not self.context.is_current:
2374            emit_lock_message(self)
2375            return
2376        if self.context.student.state != PAID:
2377            emit_lock_message(self)
2378            return
2379        super(AddStudyLevelFormPage, self).update()
2380        return
2381
2382    @action(_('Create course list now'), style='primary')
2383    def addStudyLevel(self, **data):
2384        studylevel = createObject(u'waeup.StudentStudyLevel')
2385        studylevel.level = self.context.current_level
2386        studylevel.level_session = self.context.current_session
2387        try:
2388            self.context.addStudentStudyLevel(
2389                self.context.certificate,studylevel)
2390        except KeyError:
2391            self.flash(_('This level exists.'))
2392        except RequiredMissing:
2393            self.flash(_('Your data are incomplete'))
2394        self.redirect(self.url(self.context))
2395        return
2396
2397class StudyLevelEditFormPage(KofaEditFormPage):
2398    """ Page to edit the student study level data by students
2399    """
2400    grok.context(IStudentStudyLevel)
2401    grok.name('edit')
2402    grok.require('waeup.editStudyLevel')
2403    grok.template('studyleveleditpage')
2404    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2405        'level_session', 'level_verdict')
2406    pnav = 4
2407
2408    def update(self, ADD=None, course=None):
2409        if not self.context.__parent__.is_current:
2410            emit_lock_message(self)
2411            return
2412        if self.context.student.state != PAID or \
2413            not self.context.is_current_level:
2414            emit_lock_message(self)
2415            return
2416        super(StudyLevelEditFormPage, self).update()
2417        datatable.need()
2418        warning.need()
2419        if ADD is not None:
2420            if not course:
2421                self.flash(_('No valid course code entered.'))
2422                return
2423            cat = queryUtility(ICatalog, name='courses_catalog')
2424            result = cat.searchResults(code=(course, course))
2425            if len(result) != 1:
2426                self.flash(_('Course not found.'))
2427                return
2428            course = list(result)[0]
2429            addCourseTicket(self, course)
2430        return
2431
2432    @property
2433    def label(self):
2434        # Here we know that the cookie has been set
2435        lang = self.request.cookies.get('kofa.language')
2436        level_title = translate(self.context.level_title, 'waeup.kofa',
2437            target_language=lang)
2438        return _('Edit course list of ${a}',
2439            mapping = {'a':level_title})
2440
2441    @property
2442    def translated_values(self):
2443        return translated_values(self)
2444
2445    def _delCourseTicket(self, **data):
2446        form = self.request.form
2447        if 'val_id' in form:
2448            child_id = form['val_id']
2449        else:
2450            self.flash(_('No ticket selected.'))
2451            self.redirect(self.url(self.context, '@@edit'))
2452            return
2453        if not isinstance(child_id, list):
2454            child_id = [child_id]
2455        deleted = []
2456        for id in child_id:
2457            # Students are not allowed to remove core tickets
2458            if id in self.context and \
2459                self.context[id].removable_by_student:
2460                del self.context[id]
2461                deleted.append(id)
2462        if len(deleted):
2463            self.flash(_('Successfully removed: ${a}',
2464                mapping = {'a':', '.join(deleted)}))
2465            self.context.writeLogMessage(
2466                self,'removed: %s at %s' %
2467                (', '.join(deleted), self.context.level))
2468        self.redirect(self.url(self.context, u'@@edit'))
2469        return
2470
2471    @jsaction(_('Remove selected tickets'))
2472    def delCourseTicket(self, **data):
2473        self._delCourseTicket(**data)
2474        return
2475
2476    def _registerCourses(self, **data):
2477        if self.context.student.is_postgrad and \
2478            not self.context.student.is_special_postgrad:
2479            self.flash(_(
2480                "You are a postgraduate student, "
2481                "your course list can't bee registered."))
2482            self.redirect(self.url(self.context))
2483            return
2484        students_utils = getUtility(IStudentsUtils)
2485        max_credits = students_utils.maxCredits(self.context)
2486        if self.context.total_credits > max_credits:
2487            self.flash(_('Maximum credits of ${a} exceeded.',
2488                mapping = {'a':max_credits}))
2489            return
2490        IWorkflowInfo(self.context.student).fireTransition(
2491            'register_courses')
2492        self.flash(_('Course list has been registered.'))
2493        self.redirect(self.url(self.context))
2494        return
2495
2496    @action(_('Register course list'))
2497    def registerCourses(self, **data):
2498        self._registerCourses(**data)
2499        return
2500
2501class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2502    """Add a course ticket by student.
2503    """
2504    grok.name('ctadd')
2505    grok.require('waeup.handleStudent')
2506    form_fields = grok.AutoFields(ICourseTicketAdd)
2507
2508    def update(self):
2509        if self.context.student.state != PAID or \
2510            not self.context.is_current_level:
2511            emit_lock_message(self)
2512            return
2513        super(CourseTicketAddFormPage2, self).update()
2514        return
2515
2516    @action(_('Add course ticket'))
2517    def addCourseTicket(self, **data):
2518        # Safety belt
2519        if self.context.student.state != PAID:
2520            return
2521        course = data['course']
2522        success = addCourseTicket(self, course)
2523        if success:
2524            self.redirect(self.url(self.context, u'@@edit'))
2525        return
2526
2527class SetPasswordPage(KofaPage):
2528    grok.context(IKofaObject)
2529    grok.name('setpassword')
2530    grok.require('waeup.Anonymous')
2531    grok.template('setpassword')
2532    label = _('Set password for first-time login')
2533    ac_prefix = 'PWD'
2534    pnav = 0
2535    set_button = _('Set')
2536
2537    def update(self, SUBMIT=None):
2538        self.reg_number = self.request.form.get('reg_number', None)
2539        self.ac_series = self.request.form.get('ac_series', None)
2540        self.ac_number = self.request.form.get('ac_number', None)
2541
2542        if SUBMIT is None:
2543            return
2544        hitlist = search(query=self.reg_number,
2545            searchtype='reg_number', view=self)
2546        if not hitlist:
2547            self.flash(_('No student found.'))
2548            return
2549        if len(hitlist) != 1:   # Cannot happen but anyway
2550            self.flash(_('More than one student found.'))
2551            return
2552        student = hitlist[0].context
2553        self.student_id = student.student_id
2554        student_pw = student.password
2555        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2556        code = get_access_code(pin)
2557        if not code:
2558            self.flash(_('Access code is invalid.'))
2559            return
2560        if student_pw and pin == student.adm_code:
2561            self.flash(_(
2562                'Password has already been set. Your Student Id is ${a}',
2563                mapping = {'a':self.student_id}))
2564            return
2565        elif student_pw:
2566            self.flash(
2567                _('Password has already been set. You are using the ' +
2568                'wrong Access Code.'))
2569            return
2570        # Mark pin as used (this also fires a pin related transition)
2571        # and set student password
2572        if code.state == USED:
2573            self.flash(_('Access code has already been used.'))
2574            return
2575        else:
2576            comment = _(u"invalidated")
2577            # Here we know that the ac is in state initialized so we do not
2578            # expect an exception
2579            invalidate_accesscode(pin,comment)
2580            IUserAccount(student).setPassword(self.ac_number)
2581            student.adm_code = pin
2582        self.flash(_('Password has been set. Your Student Id is ${a}',
2583            mapping = {'a':self.student_id}))
2584        return
2585
2586class StudentRequestPasswordPage(KofaAddFormPage):
2587    """Captcha'd registration page for applicants.
2588    """
2589    grok.name('requestpw')
2590    grok.require('waeup.Anonymous')
2591    grok.template('requestpw')
2592    form_fields = grok.AutoFields(IStudentRequestPW).select(
2593        'firstname','number','email')
2594    label = _('Request password for first-time login')
2595
2596    def update(self):
2597        # Handle captcha
2598        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2599        self.captcha_result = self.captcha.verify(self.request)
2600        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2601        return
2602
2603    def _redirect(self, email, password, student_id):
2604        # Forward only email to landing page in base package.
2605        self.redirect(self.url(self.context, 'requestpw_complete',
2606            data = dict(email=email)))
2607        return
2608
2609    def _pw_used(self):
2610        # XXX: False if password has not been used. We need an extra
2611        #      attribute which remembers if student logged in.
2612        return True
2613
2614    @action(_('Send login credentials to email address'), style='primary')
2615    def get_credentials(self, **data):
2616        if not self.captcha_result.is_valid:
2617            # Captcha will display error messages automatically.
2618            # No need to flash something.
2619            return
2620        number = data.get('number','')
2621        firstname = data.get('firstname','')
2622        cat = getUtility(ICatalog, name='students_catalog')
2623        results = list(
2624            cat.searchResults(reg_number=(number, number)))
2625        if not results:
2626            results = list(
2627                cat.searchResults(matric_number=(number, number)))
2628        if results:
2629            student = results[0]
2630            if getattr(student,'firstname',None) is None:
2631                self.flash(_('An error occurred.'))
2632                return
2633            elif student.firstname.lower() != firstname.lower():
2634                # Don't tell the truth here. Anonymous must not
2635                # know that a record was found and only the firstname
2636                # verification failed.
2637                self.flash(_('No student record found.'))
2638                return
2639            elif student.password is not None and self._pw_used:
2640                self.flash(_('Your password has already been set and used. '
2641                             'Please proceed to the login page.'))
2642                return
2643            # Store email address but nothing else.
2644            student.email = data['email']
2645            notify(grok.ObjectModifiedEvent(student))
2646        else:
2647            # No record found, this is the truth.
2648            self.flash(_('No student record found.'))
2649            return
2650
2651        kofa_utils = getUtility(IKofaUtils)
2652        password = kofa_utils.genPassword()
2653        mandate = PasswordMandate()
2654        mandate.params['password'] = password
2655        mandate.params['user'] = student
2656        site = grok.getSite()
2657        site['mandates'].addMandate(mandate)
2658        # Send email with credentials
2659        args = {'mandate_id':mandate.mandate_id}
2660        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2661        url_info = u'Confirmation link: %s' % mandate_url
2662        msg = _('You have successfully requested a password for the')
2663        if kofa_utils.sendCredentials(IUserAccount(student),
2664            password, url_info, msg):
2665            email_sent = student.email
2666        else:
2667            email_sent = None
2668        self._redirect(email=email_sent, password=password,
2669            student_id=student.student_id)
2670        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2671        self.context.logger.info(
2672            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2673        return
2674
2675class StudentRequestPasswordEmailSent(KofaPage):
2676    """Landing page after successful password request.
2677
2678    """
2679    grok.name('requestpw_complete')
2680    grok.require('waeup.Public')
2681    grok.template('requestpwmailsent')
2682    label = _('Your password request was successful.')
2683
2684    def update(self, email=None, student_id=None, password=None):
2685        self.email = email
2686        self.password = password
2687        self.student_id = student_id
2688        return
2689
2690class FilterStudentsInDepartmentPage(KofaPage):
2691    """Page that filters and lists students.
2692    """
2693    grok.context(IDepartment)
2694    grok.require('waeup.showStudents')
2695    grok.name('students')
2696    grok.template('filterstudentspage')
2697    pnav = 1
2698    session_label = _('Current Session')
2699    level_label = _('Current Level')
2700
2701    def label(self):
2702        return 'Students in %s' % self.context.longtitle()
2703
2704    def _set_session_values(self):
2705        vocab_terms = academic_sessions_vocab.by_value.values()
2706        self.sessions = sorted(
2707            [(x.title, x.token) for x in vocab_terms], reverse=True)
2708        self.sessions += [('All Sessions', 'all')]
2709        return
2710
2711    def _set_level_values(self):
2712        vocab_terms = course_levels.by_value.values()
2713        self.levels = sorted(
2714            [(x.title, x.token) for x in vocab_terms])
2715        self.levels += [('All Levels', 'all')]
2716        return
2717
2718    def _searchCatalog(self, session, level):
2719        if level not in (10, 999, None):
2720            start_level = 100 * (level // 100)
2721            end_level = start_level + 90
2722        else:
2723            start_level = end_level = level
2724        cat = queryUtility(ICatalog, name='students_catalog')
2725        students = cat.searchResults(
2726            current_session=(session, session),
2727            current_level=(start_level, end_level),
2728            depcode=(self.context.code, self.context.code)
2729            )
2730        hitlist = []
2731        for student in students:
2732            hitlist.append(StudentQueryResultItem(student, view=self))
2733        return hitlist
2734
2735    def update(self, SHOW=None, session=None, level=None):
2736        datatable.need()
2737        self.parent_url = self.url(self.context.__parent__)
2738        self._set_session_values()
2739        self._set_level_values()
2740        self.hitlist = []
2741        self.session_default = session
2742        self.level_default = level
2743        if SHOW is not None:
2744            if session != 'all':
2745                self.session = int(session)
2746                self.session_string = '%s %s/%s' % (
2747                    self.session_label, self.session, self.session+1)
2748            else:
2749                self.session = None
2750                self.session_string = _('in any session')
2751            if level != 'all':
2752                self.level = int(level)
2753                self.level_string = '%s %s' % (self.level_label, self.level)
2754            else:
2755                self.level = None
2756                self.level_string = _('at any level')
2757            self.hitlist = self._searchCatalog(self.session, self.level)
2758            if not self.hitlist:
2759                self.flash(_('No student found.'))
2760        return
2761
2762class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2763    """Page that filters and lists students.
2764    """
2765    grok.context(ICertificate)
2766
2767    def label(self):
2768        return 'Students studying %s' % self.context.longtitle()
2769
2770    def _searchCatalog(self, session, level):
2771        if level not in (10, 999, None):
2772            start_level = 100 * (level // 100)
2773            end_level = start_level + 90
2774        else:
2775            start_level = end_level = level
2776        cat = queryUtility(ICatalog, name='students_catalog')
2777        students = cat.searchResults(
2778            current_session=(session, session),
2779            current_level=(start_level, end_level),
2780            certcode=(self.context.code, self.context.code)
2781            )
2782        hitlist = []
2783        for student in students:
2784            hitlist.append(StudentQueryResultItem(student, view=self))
2785        return hitlist
2786
2787class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2788    """Page that filters and lists students.
2789    """
2790    grok.context(ICourse)
2791
2792    session_label = _('Session')
2793    level_label = _('Level')
2794
2795    def label(self):
2796        return 'Students registered for %s' % self.context.longtitle()
2797
2798    def _searchCatalog(self, session, level):
2799        if level not in (10, 999, None):
2800            start_level = 100 * (level // 100)
2801            end_level = start_level + 90
2802        else:
2803            start_level = end_level = level
2804        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2805        coursetickets = cat.searchResults(
2806            session=(session, session),
2807            level=(start_level, end_level),
2808            code=(self.context.code, self.context.code)
2809            )
2810        hitlist = []
2811        for ticket in coursetickets:
2812            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2813        return list(set(hitlist))
2814
2815class ExportJobContainerOverview(KofaPage):
2816    """Page that lists active student data export jobs and provides links
2817    to discard or download CSV files.
2818
2819    """
2820    grok.context(VirtualExportJobContainer)
2821    grok.require('waeup.showStudents')
2822    grok.name('index.html')
2823    grok.template('exportjobsindex')
2824    label = _('Student Data Exports')
2825    pnav = 1
2826
2827    def update(self, CREATE=None, DISCARD=None, job_id=None):
2828        if CREATE:
2829            self.redirect(self.url('@@exportconfig'))
2830            return
2831        if DISCARD and job_id:
2832            entry = self.context.entry_from_job_id(job_id)
2833            self.context.delete_export_entry(entry)
2834            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2835            self.context.logger.info(
2836                '%s - discarded: job_id=%s' % (ob_class, job_id))
2837            self.flash(_('Discarded export') + ' %s' % job_id)
2838        self.entries = doll_up(self, user=self.request.principal.id)
2839        return
2840
2841class ExportJobContainerJobConfig(KofaPage):
2842    """Page that configures a students export job.
2843
2844    This is a baseclass.
2845    """
2846    grok.baseclass()
2847    grok.name('exportconfig')
2848    grok.require('waeup.showStudents')
2849    grok.template('exportconfig')
2850    label = _('Configure student data export')
2851    pnav = 1
2852    redirect_target = ''
2853
2854    def _set_session_values(self):
2855        vocab_terms = academic_sessions_vocab.by_value.values()
2856        self.sessions = sorted(
2857            [(x.title, x.token) for x in vocab_terms], reverse=True)
2858        self.sessions += [(_('All Sessions'), 'all')]
2859        return
2860
2861    def _set_level_values(self):
2862        vocab_terms = course_levels.by_value.values()
2863        self.levels = sorted(
2864            [(x.title, x.token) for x in vocab_terms])
2865        self.levels += [(_('All Levels'), 'all')]
2866        return
2867
2868    def _set_mode_values(self):
2869        utils = getUtility(IKofaUtils)
2870        self.modes = sorted([(value, key) for key, value in
2871                      utils.STUDY_MODES_DICT.items()])
2872        self.modes +=[(_('All Modes'), 'all')]
2873        return
2874
2875    def _set_exporter_values(self):
2876        # We provide all student exporters, nothing else, yet.
2877        # Bursary Officers don't have the general exportData permission
2878        # and are only allowed to export bursary data.
2879        if not checkPermission('waeup.exportData', self.context):
2880            self.exporters = [('Bursary Data', 'bursary')]
2881            return
2882        exporters = []
2883        for name in EXPORTER_NAMES:
2884            util = getUtility(ICSVExporter, name=name)
2885            exporters.append((util.title, name),)
2886        self.exporters = exporters
2887        return
2888
2889    @property
2890    def depcode(self):
2891        return None
2892
2893    @property
2894    def certcode(self):
2895        return None
2896
2897    def update(self, START=None, session=None, level=None, mode=None,
2898               exporter=None):
2899        self._set_session_values()
2900        self._set_level_values()
2901        self._set_mode_values()
2902        self._set_exporter_values()
2903        if START is None:
2904            return
2905        if session == 'all':
2906            session=None
2907        if level == 'all':
2908            level = None
2909        if mode == 'all':
2910            mode = None
2911        if (mode, level, session,
2912            self.depcode, self.certcode) == (None, None, None, None, None):
2913            # Export all students including those without certificate
2914            job_id = self.context.start_export_job(exporter,
2915                                          self.request.principal.id)
2916        else:
2917            job_id = self.context.start_export_job(exporter,
2918                                          self.request.principal.id,
2919                                          current_session=session,
2920                                          current_level=level,
2921                                          current_mode=mode,
2922                                          depcode=self.depcode,
2923                                          certcode=self.certcode)
2924        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2925        self.context.logger.info(
2926            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2927            % (ob_class, exporter, session, level, mode, self.depcode,
2928            self.certcode, job_id))
2929        self.flash(_('Export started for students with') +
2930                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2931                   session, level, mode))
2932        self.redirect(self.url(self.redirect_target))
2933        return
2934
2935class ExportJobContainerDownload(ExportCSVView):
2936    """Page that downloads a students export csv file.
2937
2938    """
2939    grok.context(VirtualExportJobContainer)
2940    grok.require('waeup.showStudents')
2941
2942class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
2943    """Page that configures a students export job in datacenter.
2944
2945    """
2946    grok.context(IDataCenter)
2947    redirect_target = '@@export'
2948
2949class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
2950    """Page that configures a students export job in facultiescontainer.
2951
2952    """
2953    grok.context(VirtualFacultiesExportJobContainer)
2954
2955class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
2956    """Page that configures a students export job in departments.
2957
2958    """
2959    grok.context(VirtualDepartmentExportJobContainer)
2960
2961    @property
2962    def depcode(self):
2963        return self.context.__parent__.code
2964
2965class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
2966    """Page that configures a students export job for certificates.
2967
2968    """
2969    grok.context(VirtualCertificateExportJobContainer)
2970    grok.template('exportconfig_certificate')
2971
2972    @property
2973    def certcode(self):
2974        return self.context.__parent__.code
2975
2976class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
2977    """Page that configures a students export job for courses.
2978
2979    In contrast to department or certificate student data exports the
2980    coursetickets_catalog is searched here. Therefore the update
2981    method from the base class is customized.
2982    """
2983    grok.context(VirtualCourseExportJobContainer)
2984    grok.template('exportconfig_course')
2985
2986    def _set_exporter_values(self):
2987        # We provide only two exporters.
2988        exporters = []
2989        for name in ('students', 'coursetickets'):
2990            util = getUtility(ICSVExporter, name=name)
2991            exporters.append((util.title, name),)
2992        self.exporters = exporters
2993
2994    def update(self, START=None, session=None, level=None, mode=None,
2995               exporter=None):
2996        self._set_session_values()
2997        self._set_level_values()
2998        self._set_mode_values()
2999        self._set_exporter_values()
3000        if START is None:
3001            return
3002        if session == 'all':
3003            session = None
3004        if level == 'all':
3005            level = None
3006        job_id = self.context.start_export_job(exporter,
3007                                      self.request.principal.id,
3008                                      # Use a different catalog and
3009                                      # pass different keywords than
3010                                      # for the (default) students_catalog
3011                                      catalog='coursetickets',
3012                                      session=session,
3013                                      level=level,
3014                                      code=self.context.__parent__.code)
3015        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3016        self.context.logger.info(
3017            '%s - exported: %s (%s, %s, %s), job_id=%s'
3018            % (ob_class, exporter, session, level,
3019            self.context.__parent__.code, job_id))
3020        self.flash(_('Export started for course tickets with') +
3021                   ' level_session=%s, level=%s' % (
3022                   session, level))
3023        self.redirect(self.url(self.redirect_target))
3024        return
Note: See TracBrowser for help on using the repository browser.