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

Last change on this file since 10252 was 10250, checked in by Henrik Bettermann, 12 years ago

Implement ExportPDFTranscriptPage.

  • Property svn:keywords set to Id
File size: 112.2 KB
Line 
1## $Id: browser.py 10250 2013-05-30 11:30:31Z 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, IStudentStudyCourseTranscript,
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            omit_fields=self.omit_fields)
724
725class StudentClearanceManageFormPage(KofaEditFormPage):
726    """ Page to manage student clearance data
727    """
728    grok.context(IStudent)
729    grok.name('manage_clearance')
730    grok.require('waeup.manageStudent')
731    grok.template('clearanceeditpage')
732    label = _('Manage clearance data')
733    pnav = 4
734
735    @property
736    def separators(self):
737        return getUtility(IStudentsUtils).SEPARATORS_DICT
738
739    @property
740    def form_fields(self):
741        if self.context.is_postgrad:
742            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
743        else:
744            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
745        return form_fields
746
747    def update(self):
748        datepicker.need() # Enable jQuery datepicker in date fields.
749        tabs.need()
750        self.tab1 = self.tab2 = ''
751        qs = self.request.get('QUERY_STRING', '')
752        if not qs:
753            qs = 'tab1'
754        setattr(self, qs, 'active')
755        return super(StudentClearanceManageFormPage, self).update()
756
757    @action(_('Save'), style='primary')
758    def save(self, **data):
759        msave(self, **data)
760        return
761
762class StudentClearPage(UtilityView, grok.View):
763    """ Clear student by clearance officer
764    """
765    grok.context(IStudent)
766    grok.name('clear')
767    grok.require('waeup.clearStudent')
768
769    def update(self):
770        if clearance_disabled_message(self.context):
771            self.flash(clearance_disabled_message(self.context))
772            self.redirect(self.url(self.context,'view_clearance'))
773            return
774        if self.context.state == REQUESTED:
775            IWorkflowInfo(self.context).fireTransition('clear')
776            self.flash(_('Student has been cleared.'))
777        else:
778            self.flash(_('Student is in wrong state.'))
779        self.redirect(self.url(self.context,'view_clearance'))
780        return
781
782    def render(self):
783        return
784
785class StudentRejectClearancePage(KofaEditFormPage):
786    """ Reject clearance by clearance officers
787    """
788    grok.context(IStudent)
789    grok.name('reject_clearance')
790    label = _('Reject clearance')
791    grok.require('waeup.clearStudent')
792    form_fields = grok.AutoFields(
793        IUGStudentClearance).select('officer_comment')
794
795    def update(self):
796        if clearance_disabled_message(self.context):
797            self.flash(clearance_disabled_message(self.context))
798            self.redirect(self.url(self.context,'view_clearance'))
799            return
800        return super(StudentRejectClearancePage, self).update()
801
802    @action(_('Save comment and reject clearance now'), style='primary')
803    def reject(self, **data):
804        if self.context.state == CLEARED:
805            IWorkflowInfo(self.context).fireTransition('reset4')
806            message = _('Clearance has been annulled.')
807            self.flash(message)
808        elif self.context.state == REQUESTED:
809            IWorkflowInfo(self.context).fireTransition('reset3')
810            message = _('Clearance request has been rejected.')
811            self.flash(message)
812        else:
813            self.flash(_('Student is in wrong state.'))
814            self.redirect(self.url(self.context,'view_clearance'))
815            return
816        self.applyData(self.context, **data)
817        comment = data['officer_comment']
818        if comment:
819            self.context.writeLogMessage(
820                self, 'comment: %s' % comment.replace('\n', '<br>'))
821            args = {'subject':message, 'body':comment}
822        else:
823            args = {'subject':message,}
824        self.redirect(self.url(self.context) +
825            '/contactstudent?%s' % urlencode(args))
826        return
827
828
829class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
830    """ Page to display student personal data
831    """
832    grok.context(IStudent)
833    grok.name('view_personal')
834    grok.require('waeup.viewStudent')
835    form_fields = grok.AutoFields(IStudentPersonal)
836    form_fields['perm_address'].custom_widget = BytesDisplayWidget
837    form_fields[
838        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
839    pnav = 4
840
841    @property
842    def label(self):
843        return _('${a}: Personal Data',
844            mapping = {'a':self.context.display_fullname})
845
846class StudentPersonalManageFormPage(KofaEditFormPage):
847    """ Page to manage personal data
848    """
849    grok.context(IStudent)
850    grok.name('manage_personal')
851    grok.require('waeup.manageStudent')
852    form_fields = grok.AutoFields(IStudentPersonal)
853    form_fields['personal_updated'].for_display = True
854    form_fields[
855        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
856    label = _('Manage personal data')
857    pnav = 4
858
859    @action(_('Save'), style='primary')
860    def save(self, **data):
861        msave(self, **data)
862        return
863
864class StudentPersonalEditFormPage(KofaEditFormPage):
865    """ Page to edit personal data
866    """
867    grok.context(IStudent)
868    grok.name('edit_personal')
869    grok.require('waeup.handleStudent')
870    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
871    label = _('Edit personal data')
872    pnav = 4
873
874    @action(_('Save/Confirm'), style='primary')
875    def save(self, **data):
876        msave(self, **data)
877        self.context.personal_updated = datetime.utcnow()
878        return
879
880class StudyCourseDisplayFormPage(KofaDisplayFormPage):
881    """ Page to display the student study course data
882    """
883    grok.context(IStudentStudyCourse)
884    grok.name('index')
885    grok.require('waeup.viewStudent')
886    grok.template('studycoursepage')
887    pnav = 4
888
889    @property
890    def form_fields(self):
891        if self.context.is_postgrad:
892            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
893                'previous_verdict')
894        else:
895            form_fields = grok.AutoFields(IStudentStudyCourse)
896        return form_fields
897
898    @property
899    def label(self):
900        if self.context.is_current:
901            return _('${a}: Study Course',
902                mapping = {'a':self.context.__parent__.display_fullname})
903        else:
904            return _('${a}: Previous Study Course',
905                mapping = {'a':self.context.__parent__.display_fullname})
906
907    @property
908    def current_mode(self):
909        if self.context.certificate is not None:
910            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
911            return studymodes_dict[self.context.certificate.study_mode]
912        return
913
914    @property
915    def department(self):
916        if self.context.certificate is not None:
917            return self.context.certificate.__parent__.__parent__
918        return
919
920    @property
921    def faculty(self):
922        if self.context.certificate is not None:
923            return self.context.certificate.__parent__.__parent__.__parent__
924        return
925
926    @property
927    def prev_studycourses(self):
928        if self.context.is_current:
929            if self.context.__parent__.get('studycourse_2', None) is not None:
930                return (
931                        {'href':self.url(self.context.student) + '/studycourse_1',
932                        'title':_('First Study Course, ')},
933                        {'href':self.url(self.context.student) + '/studycourse_2',
934                        'title':_('Second Study Course')}
935                        )
936            if self.context.__parent__.get('studycourse_1', None) is not None:
937                return (
938                        {'href':self.url(self.context.student) + '/studycourse_1',
939                        'title':_('First Study Course')},
940                        )
941        return
942
943class StudyCourseManageFormPage(KofaEditFormPage):
944    """ Page to edit the student study course data
945    """
946    grok.context(IStudentStudyCourse)
947    grok.name('manage')
948    grok.require('waeup.manageStudent')
949    grok.template('studycoursemanagepage')
950    label = _('Manage study course')
951    pnav = 4
952    taboneactions = [_('Save'),_('Cancel')]
953    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
954    tabthreeactions = [_('Add study level')]
955
956    @property
957    def form_fields(self):
958        if self.context.is_postgrad:
959            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
960                'previous_verdict')
961        else:
962            form_fields = grok.AutoFields(IStudentStudyCourse)
963        return form_fields
964
965    def update(self):
966        if not self.context.is_current:
967            emit_lock_message(self)
968            return
969        super(StudyCourseManageFormPage, self).update()
970        tabs.need()
971        self.tab1 = self.tab2 = ''
972        qs = self.request.get('QUERY_STRING', '')
973        if not qs:
974            qs = 'tab1'
975        setattr(self, qs, 'active')
976        warning.need()
977        datatable.need()
978        return
979
980    @action(_('Save'), style='primary')
981    def save(self, **data):
982        try:
983            msave(self, **data)
984        except ConstraintNotSatisfied:
985            # The selected level might not exist in certificate
986            self.flash(_('Current level not available for certificate.'))
987            return
988        notify(grok.ObjectModifiedEvent(self.context.__parent__))
989        return
990
991    @property
992    def level_dict(self):
993        studylevelsource = StudyLevelSource().factory
994        for code in studylevelsource.getValues(self.context):
995            title = studylevelsource.getTitle(self.context, code)
996            yield(dict(code=code, title=title))
997
998    @property
999    def session_dict(self):
1000        yield(dict(code='', title='--'))
1001        for item in academic_sessions():
1002            code = item[1]
1003            title = item[0]
1004            yield(dict(code=code, title=title))
1005
1006    @action(_('Add study level'))
1007    def addStudyLevel(self, **data):
1008        level_code = self.request.form.get('addlevel', None)
1009        level_session = self.request.form.get('level_session', None)
1010        if not level_session:
1011            self.flash(_('You must select a session for the level.'))
1012            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1013            return
1014        studylevel = createObject(u'waeup.StudentStudyLevel')
1015        studylevel.level = int(level_code)
1016        studylevel.level_session = int(level_session)
1017        try:
1018            self.context.addStudentStudyLevel(
1019                self.context.certificate,studylevel)
1020            self.flash(_('Study level has been added.'))
1021        except KeyError:
1022            self.flash(_('This level exists.'))
1023        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1024        return
1025
1026    @jsaction(_('Remove selected levels'))
1027    def delStudyLevels(self, **data):
1028        form = self.request.form
1029        if 'val_id' in form:
1030            child_id = form['val_id']
1031        else:
1032            self.flash(_('No study level selected.'))
1033            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1034            return
1035        if not isinstance(child_id, list):
1036            child_id = [child_id]
1037        deleted = []
1038        for id in child_id:
1039            del self.context[id]
1040            deleted.append(id)
1041        if len(deleted):
1042            self.flash(_('Successfully removed: ${a}',
1043                mapping = {'a':', '.join(deleted)}))
1044            self.context.writeLogMessage(
1045                self,'removed: %s' % ', '.join(deleted))
1046        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1047        return
1048
1049class StudyCourseTranscriptPage(KofaDisplayFormPage):
1050    """ Page to display the student's transcript.
1051    """
1052    grok.context(IStudentStudyCourseTranscript)
1053    grok.name('transcript')
1054    grok.require('waeup.viewStudent')
1055    grok.template('transcript')
1056    pnav = 4
1057
1058    def update(self):
1059        super(StudyCourseTranscriptPage, self).update()
1060        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1061        self.session_dict = dict(
1062            [(item[1], item[0]) for item in academic_sessions()])
1063        self.course_levels = course_levels
1064        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1065        return
1066
1067    @property
1068    def label(self):
1069        # Here we know that the cookie has been set
1070        lang = self.request.cookies.get('kofa.language')
1071        return _('${a}: Transcript Data', mapping = {
1072            'a':self.context.student.display_fullname})
1073
1074class ExportPDFTranscriptPage(UtilityView, grok.View):
1075    """Deliver a PDF slip of the context.
1076    """
1077    grok.context(IStudentStudyCourse)
1078    grok.name('transcript.pdf')
1079    grok.require('waeup.viewStudent')
1080    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1081    prefix = 'form'
1082    omit_fields = (
1083        'department', 'faculty', 'entry_session', 'certificate',
1084        'password', 'suspended', 'phone', 'email',
1085        'adm_code', 'sex', 'suspended_comment')
1086
1087    def update(self):
1088        super(ExportPDFTranscriptPage, self).update()
1089        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1090        self.session_dict = dict(
1091            [(item[1], item[0]) for item in academic_sessions()])
1092        self.course_levels = course_levels
1093        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1094        return
1095
1096    @property
1097    def label(self):
1098        # Here we know that the cookie has been set
1099        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1100        return translate(_('Academic Transcript'),
1101            'waeup.kofa', target_language=portal_language)
1102
1103    def render(self):
1104        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1105        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1106        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1107        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1108        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1109        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1110        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1111        studentview = StudentBasePDFFormPage(self.context.student,
1112            self.request, self.omit_fields)
1113        students_utils = getUtility(IStudentsUtils)
1114
1115        tableheader = [(Code,'code', 2.5),
1116                         (Title,'title', 7),
1117                         (Sem, 'semester', 1.5),
1118                         (Cred, 'credits', 1.5),
1119                         (Score, 'score', 1.5),
1120                         (Grade, 'grade', 1.5),
1121                         ]
1122
1123        return students_utils.renderPDFTranscript(
1124            self, 'transcript.pdf',
1125            self.context.student, studentview,
1126            omit_fields=self.omit_fields,
1127            tableheader=tableheader
1128            )
1129
1130class StudentTransferFormPage(KofaAddFormPage):
1131    """Page to transfer the student.
1132    """
1133    grok.context(IStudent)
1134    grok.name('transfer')
1135    grok.require('waeup.manageStudent')
1136    label = _('Transfer student')
1137    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1138        'entry_mode', 'entry_session')
1139    pnav = 4
1140
1141    def update(self):
1142        super(StudentTransferFormPage, self).update()
1143        warning.need()
1144        return
1145
1146    @jsaction(_('Transfer'))
1147    def transferStudent(self, **data):
1148        error = self.context.transfer(**data)
1149        if error == -1:
1150            self.flash(_('Current level does not match certificate levels.'))
1151        elif error == -2:
1152            self.flash(_('Former study course record incomplete.'))
1153        elif error == -3:
1154            self.flash(_('Maximum number of transfers exceeded.'))
1155        else:
1156            self.flash(_('Successfully transferred.'))
1157        return
1158
1159class RevertTransferFormPage(KofaEditFormPage):
1160    """View that reverts the previous transfer.
1161    """
1162    grok.context(IStudent)
1163    grok.name('revert_transfer')
1164    grok.require('waeup.manageStudent')
1165    grok.template('reverttransfer')
1166    label = _('Revert previous transfer')
1167
1168    def update(self):
1169        warning.need()
1170        if not self.context.has_key('studycourse_1'):
1171            self.flash(_('No previous transfer.'))
1172            self.redirect(self.url(self.context))
1173            return
1174        return
1175
1176    @jsaction(_('Revert now'))
1177    def transferStudent(self, **data):
1178        self.context.revert_transfer()
1179        self.flash(_('Previous transfer reverted.'))
1180        self.redirect(self.url(self.context, 'studycourse'))
1181        return
1182
1183class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1184    """ Page to display student study levels
1185    """
1186    grok.context(IStudentStudyLevel)
1187    grok.name('index')
1188    grok.require('waeup.viewStudent')
1189    form_fields = grok.AutoFields(IStudentStudyLevel)
1190    form_fields[
1191        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1192    grok.template('studylevelpage')
1193    pnav = 4
1194
1195    def update(self):
1196        super(StudyLevelDisplayFormPage, self).update()
1197        datatable.need()
1198        return
1199
1200    @property
1201    def translated_values(self):
1202        return translated_values(self)
1203
1204    @property
1205    def label(self):
1206        # Here we know that the cookie has been set
1207        lang = self.request.cookies.get('kofa.language')
1208        level_title = translate(self.context.level_title, 'waeup.kofa',
1209            target_language=lang)
1210        return _('${a}: Study Level ${b}', mapping = {
1211            'a':self.context.student.display_fullname,
1212            'b':level_title})
1213
1214class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1215    """Deliver a PDF slip of the context.
1216    """
1217    grok.context(IStudentStudyLevel)
1218    grok.name('course_registration_slip.pdf')
1219    grok.require('waeup.viewStudent')
1220    form_fields = grok.AutoFields(IStudentStudyLevel)
1221    form_fields[
1222        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1223    prefix = 'form'
1224    omit_fields = (
1225        'password', 'suspended', 'phone',
1226        'adm_code', 'sex', 'suspended_comment')
1227
1228    @property
1229    def title(self):
1230        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1231        return translate(_('Level Data'), 'waeup.kofa',
1232            target_language=portal_language)
1233
1234    @property
1235    def content_title_1(self):
1236        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1237        return translate(_('1st Semester Courses'), 'waeup.kofa',
1238            target_language=portal_language)
1239
1240    @property
1241    def content_title_2(self):
1242        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1243        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1244            target_language=portal_language)
1245
1246    @property
1247    def content_title_3(self):
1248        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1249        return translate(_('Level Courses'), 'waeup.kofa',
1250            target_language=portal_language)
1251
1252    @property
1253    def label(self):
1254        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1255        lang = self.request.cookies.get('kofa.language', portal_language)
1256        level_title = translate(self.context.level_title, 'waeup.kofa',
1257            target_language=lang)
1258        return translate(_('Course Registration Slip'),
1259            'waeup.kofa', target_language=portal_language) \
1260            + ' %s' % level_title
1261
1262    def render(self):
1263        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1264        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1265        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1266        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1267        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1268        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1269        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1270        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1271        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1272        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1273        studentview = StudentBasePDFFormPage(self.context.student,
1274            self.request, self.omit_fields)
1275        students_utils = getUtility(IStudentsUtils)
1276        tabledata_1 = sorted(
1277            [value for value in self.context.values() if value.semester == 1],
1278            key=lambda value: str(value.semester) + value.code)
1279        tabledata_2 = sorted(
1280            [value for value in self.context.values() if value.semester == 2],
1281            key=lambda value: str(value.semester) + value.code)
1282        tabledata_3 = sorted(
1283            [value for value in self.context.values() if value.semester == 3],
1284            key=lambda value: str(value.semester) + value.code)
1285        tableheader = [(Code,'code', 2.5),
1286                         (Title,'title', 5),
1287                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1288                         (Cred, 'credits', 1.5),
1289                         #(Mand, 'mandatory', 1.5),
1290                         (Score, 'score', 1.5),
1291                         (Grade, 'grade', 1.5),
1292                         #('Auto', 'automatic', 1.5)
1293                         ]
1294        return students_utils.renderPDF(
1295            self, 'course_registration_slip.pdf',
1296            self.context.student, studentview,
1297            tableheader_1=tableheader,
1298            tabledata_1=tabledata_1,
1299            tableheader_2=tableheader,
1300            tabledata_2=tabledata_2,
1301            tableheader_3=tableheader,
1302            tabledata_3=tabledata_3,
1303            omit_fields=self.omit_fields
1304            )
1305
1306class StudyLevelManageFormPage(KofaEditFormPage):
1307    """ Page to edit the student study level data
1308    """
1309    grok.context(IStudentStudyLevel)
1310    grok.name('manage')
1311    grok.require('waeup.manageStudent')
1312    grok.template('studylevelmanagepage')
1313    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1314        'validation_date', 'validated_by', 'total_credits', 'gpa')
1315    pnav = 4
1316    taboneactions = [_('Save'),_('Cancel')]
1317    tabtwoactions = [_('Add course ticket'),
1318        _('Remove selected tickets'),_('Cancel')]
1319
1320    def update(self, ADD=None, course=None):
1321        if not self.context.__parent__.is_current:
1322            emit_lock_message(self)
1323            return
1324        super(StudyLevelManageFormPage, self).update()
1325        tabs.need()
1326        self.tab1 = self.tab2 = ''
1327        qs = self.request.get('QUERY_STRING', '')
1328        if not qs:
1329            qs = 'tab1'
1330        setattr(self, qs, 'active')
1331        warning.need()
1332        datatable.need()
1333        if ADD is not None:
1334            if not course:
1335                self.flash(_('No valid course code entered.'))
1336                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1337                return
1338            cat = queryUtility(ICatalog, name='courses_catalog')
1339            result = cat.searchResults(code=(course, course))
1340            if len(result) != 1:
1341                self.flash(_('Course not found.'))
1342            else:
1343                course = list(result)[0]
1344                addCourseTicket(self, course)
1345            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1346        return
1347
1348    @property
1349    def translated_values(self):
1350        return translated_values(self)
1351
1352    @property
1353    def label(self):
1354        # Here we know that the cookie has been set
1355        lang = self.request.cookies.get('kofa.language')
1356        level_title = translate(self.context.level_title, 'waeup.kofa',
1357            target_language=lang)
1358        return _('Manage study level ${a}',
1359            mapping = {'a':level_title})
1360
1361    @action(_('Save'), style='primary')
1362    def save(self, **data):
1363        msave(self, **data)
1364        return
1365
1366    @jsaction(_('Remove selected tickets'))
1367    def delCourseTicket(self, **data):
1368        form = self.request.form
1369        if 'val_id' in form:
1370            child_id = form['val_id']
1371        else:
1372            self.flash(_('No ticket selected.'))
1373            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1374            return
1375        if not isinstance(child_id, list):
1376            child_id = [child_id]
1377        deleted = []
1378        for id in child_id:
1379            del self.context[id]
1380            deleted.append(id)
1381        if len(deleted):
1382            self.flash(_('Successfully removed: ${a}',
1383                mapping = {'a':', '.join(deleted)}))
1384            self.context.writeLogMessage(
1385                self,'removed: %s at %s' %
1386                (', '.join(deleted), self.context.level))
1387        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1388        return
1389
1390class ValidateCoursesPage(UtilityView, grok.View):
1391    """ Validate course list by course adviser
1392    """
1393    grok.context(IStudentStudyLevel)
1394    grok.name('validate_courses')
1395    grok.require('waeup.validateStudent')
1396
1397    def update(self):
1398        if not self.context.__parent__.is_current:
1399            emit_lock_message(self)
1400            return
1401        if str(self.context.__parent__.current_level) != self.context.__name__:
1402            self.flash(_('This level does not correspond current level.'))
1403        elif self.context.student.state == REGISTERED:
1404            IWorkflowInfo(self.context.student).fireTransition(
1405                'validate_courses')
1406            self.flash(_('Course list has been validated.'))
1407        else:
1408            self.flash(_('Student is in the wrong state.'))
1409        self.redirect(self.url(self.context))
1410        return
1411
1412    def render(self):
1413        return
1414
1415class RejectCoursesPage(UtilityView, grok.View):
1416    """ Reject course list by course adviser
1417    """
1418    grok.context(IStudentStudyLevel)
1419    grok.name('reject_courses')
1420    grok.require('waeup.validateStudent')
1421
1422    def update(self):
1423        if not self.context.__parent__.is_current:
1424            emit_lock_message(self)
1425            return
1426        if str(self.context.__parent__.current_level) != self.context.__name__:
1427            self.flash(_('This level does not correspond current level.'))
1428            self.redirect(self.url(self.context))
1429            return
1430        elif self.context.student.state == VALIDATED:
1431            IWorkflowInfo(self.context.student).fireTransition('reset8')
1432            message = _('Course list request has been annulled.')
1433            self.flash(message)
1434        elif self.context.student.state == REGISTERED:
1435            IWorkflowInfo(self.context.student).fireTransition('reset7')
1436            message = _('Course list request has been rejected:')
1437            self.flash(message)
1438        else:
1439            self.flash(_('Student is in the wrong state.'))
1440            self.redirect(self.url(self.context))
1441            return
1442        args = {'subject':message}
1443        self.redirect(self.url(self.context.student) +
1444            '/contactstudent?%s' % urlencode(args))
1445        return
1446
1447    def render(self):
1448        return
1449
1450class CourseTicketAddFormPage(KofaAddFormPage):
1451    """Add a course ticket.
1452    """
1453    grok.context(IStudentStudyLevel)
1454    grok.name('add')
1455    grok.require('waeup.manageStudent')
1456    label = _('Add course ticket')
1457    form_fields = grok.AutoFields(ICourseTicketAdd)
1458    pnav = 4
1459
1460    def update(self):
1461        if not self.context.__parent__.is_current:
1462            emit_lock_message(self)
1463            return
1464        super(CourseTicketAddFormPage, self).update()
1465        return
1466
1467    @action(_('Add course ticket'))
1468    def addCourseTicket(self, **data):
1469        course = data['course']
1470        success = addCourseTicket(self, course)
1471        if success:
1472            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1473        return
1474
1475    @action(_('Cancel'), validator=NullValidator)
1476    def cancel(self, **data):
1477        self.redirect(self.url(self.context))
1478
1479class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1480    """ Page to display course tickets
1481    """
1482    grok.context(ICourseTicket)
1483    grok.name('index')
1484    grok.require('waeup.viewStudent')
1485    form_fields = grok.AutoFields(ICourseTicket)
1486    grok.template('courseticketpage')
1487    pnav = 4
1488
1489    @property
1490    def label(self):
1491        return _('${a}: Course Ticket ${b}', mapping = {
1492            'a':self.context.student.display_fullname,
1493            'b':self.context.code})
1494
1495class CourseTicketManageFormPage(KofaEditFormPage):
1496    """ Page to manage course tickets
1497    """
1498    grok.context(ICourseTicket)
1499    grok.name('manage')
1500    grok.require('waeup.manageStudent')
1501    form_fields = grok.AutoFields(ICourseTicket)
1502    form_fields['title'].for_display = True
1503    form_fields['fcode'].for_display = True
1504    form_fields['dcode'].for_display = True
1505    form_fields['semester'].for_display = True
1506    form_fields['passmark'].for_display = True
1507    form_fields['credits'].for_display = True
1508    form_fields['mandatory'].for_display = False
1509    form_fields['automatic'].for_display = True
1510    form_fields['carry_over'].for_display = True
1511    pnav = 4
1512    grok.template('courseticketmanagepage')
1513
1514    @property
1515    def label(self):
1516        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1517
1518    @action('Save', style='primary')
1519    def save(self, **data):
1520        msave(self, **data)
1521        return
1522
1523class PaymentsManageFormPage(KofaEditFormPage):
1524    """ Page to manage the student payments
1525
1526    This manage form page is for both students and students officers.
1527    """
1528    grok.context(IStudentPaymentsContainer)
1529    grok.name('index')
1530    grok.require('waeup.viewStudent')
1531    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1532    grok.template('paymentsmanagepage')
1533    pnav = 4
1534
1535    @property
1536    def manage_payments_allowed(self):
1537        return checkPermission('waeup.payStudent', self.context)
1538
1539    def unremovable(self, ticket):
1540        usertype = getattr(self.request.principal, 'user_type', None)
1541        if not usertype:
1542            return False
1543        if not self.manage_payments_allowed:
1544            return True
1545        return (self.request.principal.user_type == 'student' and ticket.r_code)
1546
1547    @property
1548    def label(self):
1549        return _('${a}: Payments',
1550            mapping = {'a':self.context.__parent__.display_fullname})
1551
1552    def update(self):
1553        super(PaymentsManageFormPage, self).update()
1554        datatable.need()
1555        warning.need()
1556        return
1557
1558    @jsaction(_('Remove selected tickets'))
1559    def delPaymentTicket(self, **data):
1560        form = self.request.form
1561        if 'val_id' in form:
1562            child_id = form['val_id']
1563        else:
1564            self.flash(_('No payment selected.'))
1565            self.redirect(self.url(self.context))
1566            return
1567        if not isinstance(child_id, list):
1568            child_id = [child_id]
1569        deleted = []
1570        for id in child_id:
1571            # Students are not allowed to remove used payment tickets
1572            ticket = self.context.get(id, None)
1573            if ticket is not None and not self.unremovable(ticket):
1574                del self.context[id]
1575                deleted.append(id)
1576        if len(deleted):
1577            self.flash(_('Successfully removed: ${a}',
1578                mapping = {'a': ', '.join(deleted)}))
1579            self.context.writeLogMessage(
1580                self,'removed: %s' % ', '.join(deleted))
1581        self.redirect(self.url(self.context))
1582        return
1583
1584    #@action(_('Add online payment ticket'))
1585    #def addPaymentTicket(self, **data):
1586    #    self.redirect(self.url(self.context, '@@addop'))
1587
1588class OnlinePaymentAddFormPage(KofaAddFormPage):
1589    """ Page to add an online payment ticket
1590    """
1591    grok.context(IStudentPaymentsContainer)
1592    grok.name('addop')
1593    grok.template('onlinepaymentaddform')
1594    grok.require('waeup.payStudent')
1595    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1596        'p_category')
1597    label = _('Add online payment')
1598    pnav = 4
1599
1600    @property
1601    def selectable_categories(self):
1602        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1603        return sorted(categories.items())
1604
1605    @action(_('Create ticket'), style='primary')
1606    def createTicket(self, **data):
1607        p_category = data['p_category']
1608        previous_session = data.get('p_session', None)
1609        previous_level = data.get('p_level', None)
1610        student = self.context.__parent__
1611        if p_category == 'bed_allocation' and student[
1612            'studycourse'].current_session != grok.getSite()[
1613            'hostels'].accommodation_session:
1614                self.flash(
1615                    _('Your current session does not match ' + \
1616                    'accommodation session.'))
1617                return
1618        if 'maintenance' in p_category:
1619            current_session = str(student['studycourse'].current_session)
1620            if not current_session in student['accommodation']:
1621                self.flash(_('You have not yet booked accommodation.'))
1622                return
1623        students_utils = getUtility(IStudentsUtils)
1624        error, payment = students_utils.setPaymentDetails(
1625            p_category, student, previous_session, previous_level)
1626        if error is not None:
1627            self.flash(error)
1628            return
1629        self.context[payment.p_id] = payment
1630        self.flash(_('Payment ticket created.'))
1631        self.redirect(self.url(self.context))
1632        return
1633
1634    @action(_('Cancel'), validator=NullValidator)
1635    def cancel(self, **data):
1636        self.redirect(self.url(self.context))
1637
1638class PreviousPaymentAddFormPage(KofaAddFormPage):
1639    """ Page to add an online payment ticket for previous sessions
1640    """
1641    grok.context(IStudentPaymentsContainer)
1642    grok.name('addpp')
1643    grok.require('waeup.payStudent')
1644    form_fields = grok.AutoFields(IStudentPreviousPayment)
1645    label = _('Add previous session online payment')
1646    pnav = 4
1647
1648    def update(self):
1649        if self.context.student.before_payment:
1650            self.flash(_("No previous payment to be made."))
1651            self.redirect(self.url(self.context))
1652        super(PreviousPaymentAddFormPage, self).update()
1653        return
1654
1655    @action(_('Create ticket'), style='primary')
1656    def createTicket(self, **data):
1657        p_category = data['p_category']
1658        previous_session = data.get('p_session', None)
1659        previous_level = data.get('p_level', None)
1660        student = self.context.__parent__
1661        students_utils = getUtility(IStudentsUtils)
1662        error, payment = students_utils.setPaymentDetails(
1663            p_category, student, previous_session, previous_level)
1664        if error is not None:
1665            self.flash(error)
1666            return
1667        self.context[payment.p_id] = payment
1668        self.flash(_('Payment ticket created.'))
1669        self.redirect(self.url(self.context))
1670        return
1671
1672    @action(_('Cancel'), validator=NullValidator)
1673    def cancel(self, **data):
1674        self.redirect(self.url(self.context))
1675
1676class BalancePaymentAddFormPage(KofaAddFormPage):
1677    """ Page to add an online payment ticket for balance sessions
1678    """
1679    grok.context(IStudentPaymentsContainer)
1680    grok.name('addbp')
1681    grok.require('waeup.manageStudent')
1682    form_fields = grok.AutoFields(IStudentBalancePayment)
1683    label = _('Add balance')
1684    pnav = 4
1685
1686    @action(_('Create ticket'), style='primary')
1687    def createTicket(self, **data):
1688        p_category = data['p_category']
1689        balance_session = data.get('balance_session', None)
1690        balance_level = data.get('balance_level', None)
1691        balance_amount = data.get('balance_amount', None)
1692        student = self.context.__parent__
1693        students_utils = getUtility(IStudentsUtils)
1694        error, payment = students_utils.setBalanceDetails(
1695            p_category, student, balance_session,
1696            balance_level, balance_amount)
1697        if error is not None:
1698            self.flash(error)
1699            return
1700        self.context[payment.p_id] = payment
1701        self.flash(_('Payment ticket created.'))
1702        self.redirect(self.url(self.context))
1703        return
1704
1705    @action(_('Cancel'), validator=NullValidator)
1706    def cancel(self, **data):
1707        self.redirect(self.url(self.context))
1708
1709class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1710    """ Page to view an online payment ticket
1711    """
1712    grok.context(IStudentOnlinePayment)
1713    grok.name('index')
1714    grok.require('waeup.viewStudent')
1715    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1716    form_fields[
1717        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1718    form_fields[
1719        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1720    pnav = 4
1721
1722    @property
1723    def label(self):
1724        return _('${a}: Online Payment Ticket ${b}', mapping = {
1725            'a':self.context.student.display_fullname,
1726            'b':self.context.p_id})
1727
1728class OnlinePaymentApprovePage(UtilityView, grok.View):
1729    """ Callback view
1730    """
1731    grok.context(IStudentOnlinePayment)
1732    grok.name('approve')
1733    grok.require('waeup.managePortal')
1734
1735    def update(self):
1736        success, msg, log = self.context.approveStudentPayment()
1737        if log is not None:
1738            # Add log message to students.log
1739            self.context.writeLogMessage(self,log)
1740            # Add log message to payments.log
1741            self.context.logger.info(
1742                '%s,%s,%s,%s,%s,,,,,,' % (
1743                self.context.student.student_id,
1744                self.context.p_id, self.context.p_category,
1745                self.context.amount_auth, self.context.r_code))
1746        self.flash(msg)
1747        return
1748
1749    def render(self):
1750        self.redirect(self.url(self.context, '@@index'))
1751        return
1752
1753class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1754    """ Approval view for students.
1755
1756    This view is used for browser tests only and
1757    must be neutralized in custom pages!
1758    """
1759
1760    grok.name('fake_approve')
1761    grok.require('waeup.payStudent')
1762
1763class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1764    """Deliver a PDF slip of the context.
1765    """
1766    grok.context(IStudentOnlinePayment)
1767    grok.name('payment_slip.pdf')
1768    grok.require('waeup.viewStudent')
1769    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1770    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1771    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1772    prefix = 'form'
1773    note = None
1774    omit_fields = (
1775        'password', 'suspended', 'phone',
1776        'adm_code', 'sex', 'suspended_comment')
1777
1778    @property
1779    def title(self):
1780        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1781        return translate(_('Payment Data'), 'waeup.kofa',
1782            target_language=portal_language)
1783
1784    @property
1785    def label(self):
1786        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1787        return translate(_('Online Payment Slip'),
1788            'waeup.kofa', target_language=portal_language) \
1789            + ' %s' % self.context.p_id
1790
1791    def render(self):
1792        #if self.context.p_state != 'paid':
1793        #    self.flash('Ticket not yet paid.')
1794        #    self.redirect(self.url(self.context))
1795        #    return
1796        studentview = StudentBasePDFFormPage(self.context.student,
1797            self.request, self.omit_fields)
1798        students_utils = getUtility(IStudentsUtils)
1799        return students_utils.renderPDF(self, 'payment_slip.pdf',
1800            self.context.student, studentview, note=self.note,
1801            omit_fields=self.omit_fields)
1802
1803
1804class AccommodationManageFormPage(KofaEditFormPage):
1805    """ Page to manage bed tickets.
1806
1807    This manage form page is for both students and students officers.
1808    """
1809    grok.context(IStudentAccommodation)
1810    grok.name('index')
1811    grok.require('waeup.handleAccommodation')
1812    form_fields = grok.AutoFields(IStudentAccommodation)
1813    grok.template('accommodationmanagepage')
1814    pnav = 4
1815    officers_only_actions = [_('Remove selected')]
1816
1817    @property
1818    def label(self):
1819        return _('${a}: Accommodation',
1820            mapping = {'a':self.context.__parent__.display_fullname})
1821
1822    def update(self):
1823        super(AccommodationManageFormPage, self).update()
1824        datatable.need()
1825        warning.need()
1826        return
1827
1828    @jsaction(_('Remove selected'))
1829    def delBedTickets(self, **data):
1830        if getattr(self.request.principal, 'user_type', None) == 'student':
1831            self.flash(_('You are not allowed to remove bed tickets.'))
1832            self.redirect(self.url(self.context))
1833            return
1834        form = self.request.form
1835        if 'val_id' in form:
1836            child_id = form['val_id']
1837        else:
1838            self.flash(_('No bed ticket selected.'))
1839            self.redirect(self.url(self.context))
1840            return
1841        if not isinstance(child_id, list):
1842            child_id = [child_id]
1843        deleted = []
1844        for id in child_id:
1845            del self.context[id]
1846            deleted.append(id)
1847        if len(deleted):
1848            self.flash(_('Successfully removed: ${a}',
1849                mapping = {'a':', '.join(deleted)}))
1850            self.context.writeLogMessage(
1851                self,'removed: % s' % ', '.join(deleted))
1852        self.redirect(self.url(self.context))
1853        return
1854
1855    @property
1856    def selected_actions(self):
1857        if getattr(self.request.principal, 'user_type', None) == 'student':
1858            return [action for action in self.actions
1859                    if not action.label in self.officers_only_actions]
1860        return self.actions
1861
1862class BedTicketAddPage(KofaPage):
1863    """ Page to add an online payment ticket
1864    """
1865    grok.context(IStudentAccommodation)
1866    grok.name('add')
1867    grok.require('waeup.handleAccommodation')
1868    grok.template('enterpin')
1869    ac_prefix = 'HOS'
1870    label = _('Add bed ticket')
1871    pnav = 4
1872    buttonname = _('Create bed ticket')
1873    notice = ''
1874    with_ac = True
1875
1876    def update(self, SUBMIT=None):
1877        student = self.context.student
1878        students_utils = getUtility(IStudentsUtils)
1879        acc_details  = students_utils.getAccommodationDetails(student)
1880        if acc_details.get('expired', False):
1881            startdate = acc_details.get('startdate')
1882            enddate = acc_details.get('enddate')
1883            if startdate and enddate:
1884                tz = getUtility(IKofaUtils).tzinfo
1885                startdate = to_timezone(
1886                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1887                enddate = to_timezone(
1888                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1889                self.flash(_("Outside booking period: ${a} - ${b}",
1890                    mapping = {'a': startdate, 'b': enddate}))
1891            else:
1892                self.flash(_("Outside booking period."))
1893            self.redirect(self.url(self.context))
1894            return
1895        if not acc_details:
1896            self.flash(_("Your data are incomplete."))
1897            self.redirect(self.url(self.context))
1898            return
1899        if not student.state in acc_details['allowed_states']:
1900            self.flash(_("You are in the wrong registration state."))
1901            self.redirect(self.url(self.context))
1902            return
1903        if student['studycourse'].current_session != acc_details[
1904            'booking_session']:
1905            self.flash(
1906                _('Your current session does not match accommodation session.'))
1907            self.redirect(self.url(self.context))
1908            return
1909        if str(acc_details['booking_session']) in self.context.keys():
1910            self.flash(
1911                _('You already booked a bed space in current ' \
1912                    + 'accommodation session.'))
1913            self.redirect(self.url(self.context))
1914            return
1915        if self.with_ac:
1916            self.ac_series = self.request.form.get('ac_series', None)
1917            self.ac_number = self.request.form.get('ac_number', None)
1918        if SUBMIT is None:
1919            return
1920        if self.with_ac:
1921            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1922            code = get_access_code(pin)
1923            if not code:
1924                self.flash(_('Activation code is invalid.'))
1925                return
1926        # Search and book bed
1927        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1928        entries = cat.searchResults(
1929            owner=(student.student_id,student.student_id))
1930        if len(entries):
1931            # If bed space has been manually allocated use this bed
1932            bed = [entry for entry in entries][0]
1933            # Safety belt for paranoids: Does this bed really exist on portal?
1934            # XXX: Can be remove if nobody complains.
1935            if bed.__parent__.__parent__ is None:
1936                self.flash(_('System error: Please contact the adminsitrator.'))
1937                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1938                return
1939        else:
1940            # else search for other available beds
1941            entries = cat.searchResults(
1942                bed_type=(acc_details['bt'],acc_details['bt']))
1943            available_beds = [
1944                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1945            if available_beds:
1946                students_utils = getUtility(IStudentsUtils)
1947                bed = students_utils.selectBed(available_beds)
1948                # Safety belt for paranoids: Does this bed really exist in portal?
1949                # XXX: Can be remove if nobody complains.
1950                if bed.__parent__.__parent__ is None:
1951                    self.flash(_('System error: Please contact the adminsitrator.'))
1952                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1953                    return
1954                bed.bookBed(student.student_id)
1955            else:
1956                self.flash(_('There is no free bed in your category ${a}.',
1957                    mapping = {'a':acc_details['bt']}))
1958                return
1959        if self.with_ac:
1960            # Mark pin as used (this also fires a pin related transition)
1961            if code.state == USED:
1962                self.flash(_('Activation code has already been used.'))
1963                return
1964            else:
1965                comment = _(u'invalidated')
1966                # Here we know that the ac is in state initialized so we do not
1967                # expect an exception, but the owner might be different
1968                if not invalidate_accesscode(
1969                    pin,comment,self.context.student.student_id):
1970                    self.flash(_('You are not the owner of this access code.'))
1971                    return
1972        # Create bed ticket
1973        bedticket = createObject(u'waeup.BedTicket')
1974        if self.with_ac:
1975            bedticket.booking_code = pin
1976        bedticket.booking_session = acc_details['booking_session']
1977        bedticket.bed_type = acc_details['bt']
1978        bedticket.bed = bed
1979        hall_title = bed.__parent__.hostel_name
1980        coordinates = bed.coordinates[1:]
1981        block, room_nr, bed_nr = coordinates
1982        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1983            'a':hall_title, 'b':block,
1984            'c':room_nr, 'd':bed_nr,
1985            'e':bed.bed_type})
1986        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1987        bedticket.bed_coordinates = translate(
1988            bc, 'waeup.kofa',target_language=portal_language)
1989        self.context.addBedTicket(bedticket)
1990        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1991        self.flash(_('Bed ticket created and bed booked: ${a}',
1992            mapping = {'a':bedticket.display_coordinates}))
1993        self.redirect(self.url(self.context))
1994        return
1995
1996class BedTicketDisplayFormPage(KofaDisplayFormPage):
1997    """ Page to display bed tickets
1998    """
1999    grok.context(IBedTicket)
2000    grok.name('index')
2001    grok.require('waeup.handleAccommodation')
2002    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2003    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2004    pnav = 4
2005
2006    @property
2007    def label(self):
2008        return _('Bed Ticket for Session ${a}',
2009            mapping = {'a':self.context.getSessionString()})
2010
2011class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
2012    """Deliver a PDF slip of the context.
2013    """
2014    grok.context(IBedTicket)
2015    grok.name('bed_allocation_slip.pdf')
2016    grok.require('waeup.handleAccommodation')
2017    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2018    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2019    prefix = 'form'
2020    omit_fields = (
2021        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
2022
2023    @property
2024    def title(self):
2025        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2026        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2027            target_language=portal_language)
2028
2029    @property
2030    def label(self):
2031        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2032        #return translate(_('Bed Allocation: '),
2033        #    'waeup.kofa', target_language=portal_language) \
2034        #    + ' %s' % self.context.bed_coordinates
2035        return translate(_('Bed Allocation Slip'),
2036            'waeup.kofa', target_language=portal_language) \
2037            + ' %s' % self.context.getSessionString()
2038
2039    def render(self):
2040        studentview = StudentBasePDFFormPage(self.context.student,
2041            self.request, self.omit_fields)
2042        students_utils = getUtility(IStudentsUtils)
2043        return students_utils.renderPDF(
2044            self, 'bed_allocation_slip.pdf',
2045            self.context.student, studentview,
2046            omit_fields=self.omit_fields)
2047
2048class BedTicketRelocationPage(UtilityView, grok.View):
2049    """ Callback view
2050    """
2051    grok.context(IBedTicket)
2052    grok.name('relocate')
2053    grok.require('waeup.manageHostels')
2054
2055    # Relocate student if student parameters have changed or the bed_type
2056    # of the bed has changed
2057    def update(self):
2058        student = self.context.student
2059        students_utils = getUtility(IStudentsUtils)
2060        acc_details  = students_utils.getAccommodationDetails(student)
2061        if self.context.bed != None and \
2062              'reserved' in self.context.bed.bed_type:
2063            self.flash(_("Students in reserved beds can't be relocated."))
2064            self.redirect(self.url(self.context))
2065            return
2066        if acc_details['bt'] == self.context.bed_type and \
2067                self.context.bed != None and \
2068                self.context.bed.bed_type == self.context.bed_type:
2069            self.flash(_("Student can't be relocated."))
2070            self.redirect(self.url(self.context))
2071            return
2072        # Search a bed
2073        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2074        entries = cat.searchResults(
2075            owner=(student.student_id,student.student_id))
2076        if len(entries) and self.context.bed == None:
2077            # If booking has been cancelled but other bed space has been
2078            # manually allocated after cancellation use this bed
2079            new_bed = [entry for entry in entries][0]
2080        else:
2081            # Search for other available beds
2082            entries = cat.searchResults(
2083                bed_type=(acc_details['bt'],acc_details['bt']))
2084            available_beds = [
2085                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2086            if available_beds:
2087                students_utils = getUtility(IStudentsUtils)
2088                new_bed = students_utils.selectBed(available_beds)
2089                new_bed.bookBed(student.student_id)
2090            else:
2091                self.flash(_('There is no free bed in your category ${a}.',
2092                    mapping = {'a':acc_details['bt']}))
2093                self.redirect(self.url(self.context))
2094                return
2095        # Release old bed if exists
2096        if self.context.bed != None:
2097            self.context.bed.owner = NOT_OCCUPIED
2098            notify(grok.ObjectModifiedEvent(self.context.bed))
2099        # Alocate new bed
2100        self.context.bed_type = acc_details['bt']
2101        self.context.bed = new_bed
2102        hall_title = new_bed.__parent__.hostel_name
2103        coordinates = new_bed.coordinates[1:]
2104        block, room_nr, bed_nr = coordinates
2105        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2106            'a':hall_title, 'b':block,
2107            'c':room_nr, 'd':bed_nr,
2108            'e':new_bed.bed_type})
2109        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2110        self.context.bed_coordinates = translate(
2111            bc, 'waeup.kofa',target_language=portal_language)
2112        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2113        self.flash(_('Student relocated: ${a}',
2114            mapping = {'a':self.context.display_coordinates}))
2115        self.redirect(self.url(self.context))
2116        return
2117
2118    def render(self):
2119        return
2120
2121class StudentHistoryPage(KofaPage):
2122    """ Page to display student clearance data
2123    """
2124    grok.context(IStudent)
2125    grok.name('history')
2126    grok.require('waeup.viewStudent')
2127    grok.template('studenthistory')
2128    pnav = 4
2129
2130    @property
2131    def label(self):
2132        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2133
2134# Pages for students only
2135
2136class StudentBaseEditFormPage(KofaEditFormPage):
2137    """ View to edit student base data
2138    """
2139    grok.context(IStudent)
2140    grok.name('edit_base')
2141    grok.require('waeup.handleStudent')
2142    form_fields = grok.AutoFields(IStudentBase).select(
2143        'email', 'phone')
2144    label = _('Edit base data')
2145    pnav = 4
2146
2147    @action(_('Save'), style='primary')
2148    def save(self, **data):
2149        msave(self, **data)
2150        return
2151
2152class StudentChangePasswordPage(KofaEditFormPage):
2153    """ View to manage student base data
2154    """
2155    grok.context(IStudent)
2156    grok.name('change_password')
2157    grok.require('waeup.handleStudent')
2158    grok.template('change_password')
2159    label = _('Change password')
2160    pnav = 4
2161
2162    @action(_('Save'), style='primary')
2163    def save(self, **data):
2164        form = self.request.form
2165        password = form.get('change_password', None)
2166        password_ctl = form.get('change_password_repeat', None)
2167        if password:
2168            validator = getUtility(IPasswordValidator)
2169            errors = validator.validate_password(password, password_ctl)
2170            if not errors:
2171                IUserAccount(self.context).setPassword(password)
2172                self.context.writeLogMessage(self, 'saved: password')
2173                self.flash(_('Password changed.'))
2174            else:
2175                self.flash( ' '.join(errors))
2176        return
2177
2178class StudentFilesUploadPage(KofaPage):
2179    """ View to upload files by student
2180    """
2181    grok.context(IStudent)
2182    grok.name('change_portrait')
2183    grok.require('waeup.uploadStudentFile')
2184    grok.template('filesuploadpage')
2185    label = _('Upload portrait')
2186    pnav = 4
2187
2188    def update(self):
2189        if self.context.student.state != ADMITTED:
2190            emit_lock_message(self)
2191            return
2192        super(StudentFilesUploadPage, self).update()
2193        return
2194
2195class StartClearancePage(KofaPage):
2196    grok.context(IStudent)
2197    grok.name('start_clearance')
2198    grok.require('waeup.handleStudent')
2199    grok.template('enterpin')
2200    label = _('Start clearance')
2201    ac_prefix = 'CLR'
2202    notice = ''
2203    pnav = 4
2204    buttonname = _('Start clearance now')
2205    with_ac = True
2206
2207    @property
2208    def all_required_fields_filled(self):
2209        if self.context.email and self.context.phone:
2210            return True
2211        return False
2212
2213    @property
2214    def portrait_uploaded(self):
2215        store = getUtility(IExtFileStore)
2216        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2217            return True
2218        return False
2219
2220    def update(self, SUBMIT=None):
2221        if not self.context.state == ADMITTED:
2222            self.flash(_("Wrong state"))
2223            self.redirect(self.url(self.context))
2224            return
2225        if not self.portrait_uploaded:
2226            self.flash(_("No portrait uploaded."))
2227            self.redirect(self.url(self.context, 'change_portrait'))
2228            return
2229        if not self.all_required_fields_filled:
2230            self.flash(_("Not all required fields filled."))
2231            self.redirect(self.url(self.context, 'edit_base'))
2232            return
2233        if self.with_ac:
2234            self.ac_series = self.request.form.get('ac_series', None)
2235            self.ac_number = self.request.form.get('ac_number', None)
2236        if SUBMIT is None:
2237            return
2238        if self.with_ac:
2239            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2240            code = get_access_code(pin)
2241            if not code:
2242                self.flash(_('Activation code is invalid.'))
2243                return
2244            if code.state == USED:
2245                self.flash(_('Activation code has already been used.'))
2246                return
2247            # Mark pin as used (this also fires a pin related transition)
2248            # and fire transition start_clearance
2249            comment = _(u"invalidated")
2250            # Here we know that the ac is in state initialized so we do not
2251            # expect an exception, but the owner might be different
2252            if not invalidate_accesscode(pin, comment, self.context.student_id):
2253                self.flash(_('You are not the owner of this access code.'))
2254                return
2255            self.context.clr_code = pin
2256        IWorkflowInfo(self.context).fireTransition('start_clearance')
2257        self.flash(_('Clearance process has been started.'))
2258        self.redirect(self.url(self.context,'cedit'))
2259        return
2260
2261class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2262    """ View to edit student clearance data by student
2263    """
2264    grok.context(IStudent)
2265    grok.name('cedit')
2266    grok.require('waeup.handleStudent')
2267    label = _('Edit clearance data')
2268
2269    @property
2270    def form_fields(self):
2271        if self.context.is_postgrad:
2272            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2273                'clearance_locked', 'clr_code', 'officer_comment')
2274        else:
2275            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2276                'clearance_locked', 'clr_code', 'officer_comment')
2277        return form_fields
2278
2279    def update(self):
2280        if self.context.clearance_locked:
2281            emit_lock_message(self)
2282            return
2283        return super(StudentClearanceEditFormPage, self).update()
2284
2285    @action(_('Save'), style='primary')
2286    def save(self, **data):
2287        self.applyData(self.context, **data)
2288        self.flash(_('Clearance form has been saved.'))
2289        return
2290
2291    def dataNotComplete(self):
2292        """To be implemented in the customization package.
2293        """
2294        return False
2295
2296    @action(_('Save and request clearance'), style='primary')
2297    def requestClearance(self, **data):
2298        self.applyData(self.context, **data)
2299        if self.dataNotComplete():
2300            self.flash(self.dataNotComplete())
2301            return
2302        self.flash(_('Clearance form has been saved.'))
2303        if self.context.clr_code:
2304            self.redirect(self.url(self.context, 'request_clearance'))
2305        else:
2306            # We bypass the request_clearance page if student
2307            # has been imported in state 'clearance started' and
2308            # no clr_code was entered before.
2309            state = IWorkflowState(self.context).getState()
2310            if state != CLEARANCE:
2311                # This shouldn't happen, but the application officer
2312                # might have forgotten to lock the form after changing the state
2313                self.flash(_('This form cannot be submitted. Wrong state!'))
2314                return
2315            IWorkflowInfo(self.context).fireTransition('request_clearance')
2316            self.flash(_('Clearance has been requested.'))
2317            self.redirect(self.url(self.context))
2318        return
2319
2320class RequestClearancePage(KofaPage):
2321    grok.context(IStudent)
2322    grok.name('request_clearance')
2323    grok.require('waeup.handleStudent')
2324    grok.template('enterpin')
2325    label = _('Request clearance')
2326    notice = _('Enter the CLR access code used for starting clearance.')
2327    ac_prefix = 'CLR'
2328    pnav = 4
2329    buttonname = _('Request clearance now')
2330    with_ac = True
2331
2332    def update(self, SUBMIT=None):
2333        if self.with_ac:
2334            self.ac_series = self.request.form.get('ac_series', None)
2335            self.ac_number = self.request.form.get('ac_number', None)
2336        if SUBMIT is None:
2337            return
2338        if self.with_ac:
2339            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2340            if self.context.clr_code and self.context.clr_code != pin:
2341                self.flash(_("This isn't your CLR access code."))
2342                return
2343        state = IWorkflowState(self.context).getState()
2344        if state != CLEARANCE:
2345            # This shouldn't happen, but the application officer
2346            # might have forgotten to lock the form after changing the state
2347            self.flash(_('This form cannot be submitted. Wrong state!'))
2348            return
2349        IWorkflowInfo(self.context).fireTransition('request_clearance')
2350        self.flash(_('Clearance has been requested.'))
2351        self.redirect(self.url(self.context))
2352        return
2353
2354class StartSessionPage(KofaPage):
2355    grok.context(IStudentStudyCourse)
2356    grok.name('start_session')
2357    grok.require('waeup.handleStudent')
2358    grok.template('enterpin')
2359    label = _('Start session')
2360    ac_prefix = 'SFE'
2361    notice = ''
2362    pnav = 4
2363    buttonname = _('Start now')
2364    with_ac = True
2365
2366    def update(self, SUBMIT=None):
2367        if not self.context.is_current:
2368            emit_lock_message(self)
2369            return
2370        super(StartSessionPage, self).update()
2371        if not self.context.next_session_allowed:
2372            self.flash(_("You are not entitled to start session."))
2373            self.redirect(self.url(self.context))
2374            return
2375        if self.with_ac:
2376            self.ac_series = self.request.form.get('ac_series', None)
2377            self.ac_number = self.request.form.get('ac_number', None)
2378        if SUBMIT is None:
2379            return
2380        if self.with_ac:
2381            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2382            code = get_access_code(pin)
2383            if not code:
2384                self.flash(_('Activation code is invalid.'))
2385                return
2386            # Mark pin as used (this also fires a pin related transition)
2387            if code.state == USED:
2388                self.flash(_('Activation code has already been used.'))
2389                return
2390            else:
2391                comment = _(u"invalidated")
2392                # Here we know that the ac is in state initialized so we do not
2393                # expect an error, but the owner might be different
2394                if not invalidate_accesscode(
2395                    pin,comment,self.context.student.student_id):
2396                    self.flash(_('You are not the owner of this access code.'))
2397                    return
2398        try:
2399            if self.context.student.state == CLEARED:
2400                IWorkflowInfo(self.context.student).fireTransition(
2401                    'pay_first_school_fee')
2402            elif self.context.student.state == RETURNING:
2403                IWorkflowInfo(self.context.student).fireTransition(
2404                    'pay_school_fee')
2405            elif self.context.student.state == PAID:
2406                IWorkflowInfo(self.context.student).fireTransition(
2407                    'pay_pg_fee')
2408        except ConstraintNotSatisfied:
2409            self.flash(_('An error occurred, please contact the system administrator.'))
2410            return
2411        self.flash(_('Session started.'))
2412        self.redirect(self.url(self.context))
2413        return
2414
2415class AddStudyLevelFormPage(KofaEditFormPage):
2416    """ Page for students to add current study levels
2417    """
2418    grok.context(IStudentStudyCourse)
2419    grok.name('add')
2420    grok.require('waeup.handleStudent')
2421    grok.template('studyleveladdpage')
2422    form_fields = grok.AutoFields(IStudentStudyCourse)
2423    pnav = 4
2424
2425    @property
2426    def label(self):
2427        studylevelsource = StudyLevelSource().factory
2428        code = self.context.current_level
2429        title = studylevelsource.getTitle(self.context, code)
2430        return _('Add current level ${a}', mapping = {'a':title})
2431
2432    def update(self):
2433        if not self.context.is_current:
2434            emit_lock_message(self)
2435            return
2436        if self.context.student.state != PAID:
2437            emit_lock_message(self)
2438            return
2439        super(AddStudyLevelFormPage, self).update()
2440        return
2441
2442    @action(_('Create course list now'), style='primary')
2443    def addStudyLevel(self, **data):
2444        studylevel = createObject(u'waeup.StudentStudyLevel')
2445        studylevel.level = self.context.current_level
2446        studylevel.level_session = self.context.current_session
2447        try:
2448            self.context.addStudentStudyLevel(
2449                self.context.certificate,studylevel)
2450        except KeyError:
2451            self.flash(_('This level exists.'))
2452        except RequiredMissing:
2453            self.flash(_('Your data are incomplete'))
2454        self.redirect(self.url(self.context))
2455        return
2456
2457class StudyLevelEditFormPage(KofaEditFormPage):
2458    """ Page to edit the student study level data by students
2459    """
2460    grok.context(IStudentStudyLevel)
2461    grok.name('edit')
2462    grok.require('waeup.editStudyLevel')
2463    grok.template('studyleveleditpage')
2464    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2465        'level_session', 'level_verdict')
2466    pnav = 4
2467
2468    def update(self, ADD=None, course=None):
2469        if not self.context.__parent__.is_current:
2470            emit_lock_message(self)
2471            return
2472        if self.context.student.state != PAID or \
2473            not self.context.is_current_level:
2474            emit_lock_message(self)
2475            return
2476        super(StudyLevelEditFormPage, self).update()
2477        datatable.need()
2478        warning.need()
2479        if ADD is not None:
2480            if not course:
2481                self.flash(_('No valid course code entered.'))
2482                return
2483            cat = queryUtility(ICatalog, name='courses_catalog')
2484            result = cat.searchResults(code=(course, course))
2485            if len(result) != 1:
2486                self.flash(_('Course not found.'))
2487                return
2488            course = list(result)[0]
2489            addCourseTicket(self, course)
2490        return
2491
2492    @property
2493    def label(self):
2494        # Here we know that the cookie has been set
2495        lang = self.request.cookies.get('kofa.language')
2496        level_title = translate(self.context.level_title, 'waeup.kofa',
2497            target_language=lang)
2498        return _('Edit course list of ${a}',
2499            mapping = {'a':level_title})
2500
2501    @property
2502    def translated_values(self):
2503        return translated_values(self)
2504
2505    def _delCourseTicket(self, **data):
2506        form = self.request.form
2507        if 'val_id' in form:
2508            child_id = form['val_id']
2509        else:
2510            self.flash(_('No ticket selected.'))
2511            self.redirect(self.url(self.context, '@@edit'))
2512            return
2513        if not isinstance(child_id, list):
2514            child_id = [child_id]
2515        deleted = []
2516        for id in child_id:
2517            # Students are not allowed to remove core tickets
2518            if id in self.context and \
2519                self.context[id].removable_by_student:
2520                del self.context[id]
2521                deleted.append(id)
2522        if len(deleted):
2523            self.flash(_('Successfully removed: ${a}',
2524                mapping = {'a':', '.join(deleted)}))
2525            self.context.writeLogMessage(
2526                self,'removed: %s at %s' %
2527                (', '.join(deleted), self.context.level))
2528        self.redirect(self.url(self.context, u'@@edit'))
2529        return
2530
2531    @jsaction(_('Remove selected tickets'))
2532    def delCourseTicket(self, **data):
2533        self._delCourseTicket(**data)
2534        return
2535
2536    def _registerCourses(self, **data):
2537        if self.context.student.is_postgrad and \
2538            not self.context.student.is_special_postgrad:
2539            self.flash(_(
2540                "You are a postgraduate student, "
2541                "your course list can't bee registered."))
2542            self.redirect(self.url(self.context))
2543            return
2544        students_utils = getUtility(IStudentsUtils)
2545        max_credits = students_utils.maxCredits(self.context)
2546        if self.context.total_credits > max_credits:
2547            self.flash(_('Maximum credits of ${a} exceeded.',
2548                mapping = {'a':max_credits}))
2549            return
2550        IWorkflowInfo(self.context.student).fireTransition(
2551            'register_courses')
2552        self.flash(_('Course list has been registered.'))
2553        self.redirect(self.url(self.context))
2554        return
2555
2556    @action(_('Register course list'))
2557    def registerCourses(self, **data):
2558        self._registerCourses(**data)
2559        return
2560
2561class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2562    """Add a course ticket by student.
2563    """
2564    grok.name('ctadd')
2565    grok.require('waeup.handleStudent')
2566    form_fields = grok.AutoFields(ICourseTicketAdd)
2567
2568    def update(self):
2569        if self.context.student.state != PAID or \
2570            not self.context.is_current_level:
2571            emit_lock_message(self)
2572            return
2573        super(CourseTicketAddFormPage2, self).update()
2574        return
2575
2576    @action(_('Add course ticket'))
2577    def addCourseTicket(self, **data):
2578        # Safety belt
2579        if self.context.student.state != PAID:
2580            return
2581        course = data['course']
2582        success = addCourseTicket(self, course)
2583        if success:
2584            self.redirect(self.url(self.context, u'@@edit'))
2585        return
2586
2587class SetPasswordPage(KofaPage):
2588    grok.context(IKofaObject)
2589    grok.name('setpassword')
2590    grok.require('waeup.Anonymous')
2591    grok.template('setpassword')
2592    label = _('Set password for first-time login')
2593    ac_prefix = 'PWD'
2594    pnav = 0
2595    set_button = _('Set')
2596
2597    def update(self, SUBMIT=None):
2598        self.reg_number = self.request.form.get('reg_number', None)
2599        self.ac_series = self.request.form.get('ac_series', None)
2600        self.ac_number = self.request.form.get('ac_number', None)
2601
2602        if SUBMIT is None:
2603            return
2604        hitlist = search(query=self.reg_number,
2605            searchtype='reg_number', view=self)
2606        if not hitlist:
2607            self.flash(_('No student found.'))
2608            return
2609        if len(hitlist) != 1:   # Cannot happen but anyway
2610            self.flash(_('More than one student found.'))
2611            return
2612        student = hitlist[0].context
2613        self.student_id = student.student_id
2614        student_pw = student.password
2615        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2616        code = get_access_code(pin)
2617        if not code:
2618            self.flash(_('Access code is invalid.'))
2619            return
2620        if student_pw and pin == student.adm_code:
2621            self.flash(_(
2622                'Password has already been set. Your Student Id is ${a}',
2623                mapping = {'a':self.student_id}))
2624            return
2625        elif student_pw:
2626            self.flash(
2627                _('Password has already been set. You are using the ' +
2628                'wrong Access Code.'))
2629            return
2630        # Mark pin as used (this also fires a pin related transition)
2631        # and set student password
2632        if code.state == USED:
2633            self.flash(_('Access code has already been used.'))
2634            return
2635        else:
2636            comment = _(u"invalidated")
2637            # Here we know that the ac is in state initialized so we do not
2638            # expect an exception
2639            invalidate_accesscode(pin,comment)
2640            IUserAccount(student).setPassword(self.ac_number)
2641            student.adm_code = pin
2642        self.flash(_('Password has been set. Your Student Id is ${a}',
2643            mapping = {'a':self.student_id}))
2644        return
2645
2646class StudentRequestPasswordPage(KofaAddFormPage):
2647    """Captcha'd registration page for applicants.
2648    """
2649    grok.name('requestpw')
2650    grok.require('waeup.Anonymous')
2651    grok.template('requestpw')
2652    form_fields = grok.AutoFields(IStudentRequestPW).select(
2653        'firstname','number','email')
2654    label = _('Request password for first-time login')
2655
2656    def update(self):
2657        # Handle captcha
2658        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2659        self.captcha_result = self.captcha.verify(self.request)
2660        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2661        return
2662
2663    def _redirect(self, email, password, student_id):
2664        # Forward only email to landing page in base package.
2665        self.redirect(self.url(self.context, 'requestpw_complete',
2666            data = dict(email=email)))
2667        return
2668
2669    def _pw_used(self):
2670        # XXX: False if password has not been used. We need an extra
2671        #      attribute which remembers if student logged in.
2672        return True
2673
2674    @action(_('Send login credentials to email address'), style='primary')
2675    def get_credentials(self, **data):
2676        if not self.captcha_result.is_valid:
2677            # Captcha will display error messages automatically.
2678            # No need to flash something.
2679            return
2680        number = data.get('number','')
2681        firstname = data.get('firstname','')
2682        cat = getUtility(ICatalog, name='students_catalog')
2683        results = list(
2684            cat.searchResults(reg_number=(number, number)))
2685        if not results:
2686            results = list(
2687                cat.searchResults(matric_number=(number, number)))
2688        if results:
2689            student = results[0]
2690            if getattr(student,'firstname',None) is None:
2691                self.flash(_('An error occurred.'))
2692                return
2693            elif student.firstname.lower() != firstname.lower():
2694                # Don't tell the truth here. Anonymous must not
2695                # know that a record was found and only the firstname
2696                # verification failed.
2697                self.flash(_('No student record found.'))
2698                return
2699            elif student.password is not None and self._pw_used:
2700                self.flash(_('Your password has already been set and used. '
2701                             'Please proceed to the login page.'))
2702                return
2703            # Store email address but nothing else.
2704            student.email = data['email']
2705            notify(grok.ObjectModifiedEvent(student))
2706        else:
2707            # No record found, this is the truth.
2708            self.flash(_('No student record found.'))
2709            return
2710
2711        kofa_utils = getUtility(IKofaUtils)
2712        password = kofa_utils.genPassword()
2713        mandate = PasswordMandate()
2714        mandate.params['password'] = password
2715        mandate.params['user'] = student
2716        site = grok.getSite()
2717        site['mandates'].addMandate(mandate)
2718        # Send email with credentials
2719        args = {'mandate_id':mandate.mandate_id}
2720        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2721        url_info = u'Confirmation link: %s' % mandate_url
2722        msg = _('You have successfully requested a password for the')
2723        if kofa_utils.sendCredentials(IUserAccount(student),
2724            password, url_info, msg):
2725            email_sent = student.email
2726        else:
2727            email_sent = None
2728        self._redirect(email=email_sent, password=password,
2729            student_id=student.student_id)
2730        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2731        self.context.logger.info(
2732            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2733        return
2734
2735class StudentRequestPasswordEmailSent(KofaPage):
2736    """Landing page after successful password request.
2737
2738    """
2739    grok.name('requestpw_complete')
2740    grok.require('waeup.Public')
2741    grok.template('requestpwmailsent')
2742    label = _('Your password request was successful.')
2743
2744    def update(self, email=None, student_id=None, password=None):
2745        self.email = email
2746        self.password = password
2747        self.student_id = student_id
2748        return
2749
2750class FilterStudentsInDepartmentPage(KofaPage):
2751    """Page that filters and lists students.
2752    """
2753    grok.context(IDepartment)
2754    grok.require('waeup.showStudents')
2755    grok.name('students')
2756    grok.template('filterstudentspage')
2757    pnav = 1
2758    session_label = _('Current Session')
2759    level_label = _('Current Level')
2760
2761    def label(self):
2762        return 'Students in %s' % self.context.longtitle()
2763
2764    def _set_session_values(self):
2765        vocab_terms = academic_sessions_vocab.by_value.values()
2766        self.sessions = sorted(
2767            [(x.title, x.token) for x in vocab_terms], reverse=True)
2768        self.sessions += [('All Sessions', 'all')]
2769        return
2770
2771    def _set_level_values(self):
2772        vocab_terms = course_levels.by_value.values()
2773        self.levels = sorted(
2774            [(x.title, x.token) for x in vocab_terms])
2775        self.levels += [('All Levels', 'all')]
2776        return
2777
2778    def _searchCatalog(self, session, level):
2779        if level not in (10, 999, None):
2780            start_level = 100 * (level // 100)
2781            end_level = start_level + 90
2782        else:
2783            start_level = end_level = level
2784        cat = queryUtility(ICatalog, name='students_catalog')
2785        students = cat.searchResults(
2786            current_session=(session, session),
2787            current_level=(start_level, end_level),
2788            depcode=(self.context.code, self.context.code)
2789            )
2790        hitlist = []
2791        for student in students:
2792            hitlist.append(StudentQueryResultItem(student, view=self))
2793        return hitlist
2794
2795    def update(self, SHOW=None, session=None, level=None):
2796        datatable.need()
2797        self.parent_url = self.url(self.context.__parent__)
2798        self._set_session_values()
2799        self._set_level_values()
2800        self.hitlist = []
2801        self.session_default = session
2802        self.level_default = level
2803        if SHOW is not None:
2804            if session != 'all':
2805                self.session = int(session)
2806                self.session_string = '%s %s/%s' % (
2807                    self.session_label, self.session, self.session+1)
2808            else:
2809                self.session = None
2810                self.session_string = _('in any session')
2811            if level != 'all':
2812                self.level = int(level)
2813                self.level_string = '%s %s' % (self.level_label, self.level)
2814            else:
2815                self.level = None
2816                self.level_string = _('at any level')
2817            self.hitlist = self._searchCatalog(self.session, self.level)
2818            if not self.hitlist:
2819                self.flash(_('No student found.'))
2820        return
2821
2822class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2823    """Page that filters and lists students.
2824    """
2825    grok.context(ICertificate)
2826
2827    def label(self):
2828        return 'Students studying %s' % self.context.longtitle()
2829
2830    def _searchCatalog(self, session, level):
2831        if level not in (10, 999, None):
2832            start_level = 100 * (level // 100)
2833            end_level = start_level + 90
2834        else:
2835            start_level = end_level = level
2836        cat = queryUtility(ICatalog, name='students_catalog')
2837        students = cat.searchResults(
2838            current_session=(session, session),
2839            current_level=(start_level, end_level),
2840            certcode=(self.context.code, self.context.code)
2841            )
2842        hitlist = []
2843        for student in students:
2844            hitlist.append(StudentQueryResultItem(student, view=self))
2845        return hitlist
2846
2847class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2848    """Page that filters and lists students.
2849    """
2850    grok.context(ICourse)
2851
2852    session_label = _('Session')
2853    level_label = _('Level')
2854
2855    def label(self):
2856        return 'Students registered for %s' % self.context.longtitle()
2857
2858    def _searchCatalog(self, session, level):
2859        if level not in (10, 999, None):
2860            start_level = 100 * (level // 100)
2861            end_level = start_level + 90
2862        else:
2863            start_level = end_level = level
2864        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2865        coursetickets = cat.searchResults(
2866            session=(session, session),
2867            level=(start_level, end_level),
2868            code=(self.context.code, self.context.code)
2869            )
2870        hitlist = []
2871        for ticket in coursetickets:
2872            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2873        return list(set(hitlist))
2874
2875class ExportJobContainerOverview(KofaPage):
2876    """Page that lists active student data export jobs and provides links
2877    to discard or download CSV files.
2878
2879    """
2880    grok.context(VirtualExportJobContainer)
2881    grok.require('waeup.showStudents')
2882    grok.name('index.html')
2883    grok.template('exportjobsindex')
2884    label = _('Student Data Exports')
2885    pnav = 1
2886
2887    def update(self, CREATE=None, DISCARD=None, job_id=None):
2888        if CREATE:
2889            self.redirect(self.url('@@exportconfig'))
2890            return
2891        if DISCARD and job_id:
2892            entry = self.context.entry_from_job_id(job_id)
2893            self.context.delete_export_entry(entry)
2894            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2895            self.context.logger.info(
2896                '%s - discarded: job_id=%s' % (ob_class, job_id))
2897            self.flash(_('Discarded export') + ' %s' % job_id)
2898        self.entries = doll_up(self, user=self.request.principal.id)
2899        return
2900
2901class ExportJobContainerJobConfig(KofaPage):
2902    """Page that configures a students export job.
2903
2904    This is a baseclass.
2905    """
2906    grok.baseclass()
2907    grok.name('exportconfig')
2908    grok.require('waeup.showStudents')
2909    grok.template('exportconfig')
2910    label = _('Configure student data export')
2911    pnav = 1
2912    redirect_target = ''
2913
2914    def _set_session_values(self):
2915        vocab_terms = academic_sessions_vocab.by_value.values()
2916        self.sessions = sorted(
2917            [(x.title, x.token) for x in vocab_terms], reverse=True)
2918        self.sessions += [(_('All Sessions'), 'all')]
2919        return
2920
2921    def _set_level_values(self):
2922        vocab_terms = course_levels.by_value.values()
2923        self.levels = sorted(
2924            [(x.title, x.token) for x in vocab_terms])
2925        self.levels += [(_('All Levels'), 'all')]
2926        return
2927
2928    def _set_mode_values(self):
2929        utils = getUtility(IKofaUtils)
2930        self.modes = sorted([(value, key) for key, value in
2931                      utils.STUDY_MODES_DICT.items()])
2932        self.modes +=[(_('All Modes'), 'all')]
2933        return
2934
2935    def _set_exporter_values(self):
2936        # We provide all student exporters, nothing else, yet.
2937        # Bursary Officers don't have the general exportData permission
2938        # and are only allowed to export bursary data.
2939        if not checkPermission('waeup.exportData', self.context):
2940            self.exporters = [('Bursary Data', 'bursary')]
2941            return
2942        exporters = []
2943        for name in EXPORTER_NAMES:
2944            util = getUtility(ICSVExporter, name=name)
2945            exporters.append((util.title, name),)
2946        self.exporters = exporters
2947        return
2948
2949    @property
2950    def depcode(self):
2951        return None
2952
2953    @property
2954    def certcode(self):
2955        return None
2956
2957    def update(self, START=None, session=None, level=None, mode=None,
2958               exporter=None):
2959        self._set_session_values()
2960        self._set_level_values()
2961        self._set_mode_values()
2962        self._set_exporter_values()
2963        if START is None:
2964            return
2965        if session == 'all':
2966            session=None
2967        if level == 'all':
2968            level = None
2969        if mode == 'all':
2970            mode = None
2971        if (mode, level, session,
2972            self.depcode, self.certcode) == (None, None, None, None, None):
2973            # Export all students including those without certificate
2974            job_id = self.context.start_export_job(exporter,
2975                                          self.request.principal.id)
2976        else:
2977            job_id = self.context.start_export_job(exporter,
2978                                          self.request.principal.id,
2979                                          current_session=session,
2980                                          current_level=level,
2981                                          current_mode=mode,
2982                                          depcode=self.depcode,
2983                                          certcode=self.certcode)
2984        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2985        self.context.logger.info(
2986            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2987            % (ob_class, exporter, session, level, mode, self.depcode,
2988            self.certcode, job_id))
2989        self.flash(_('Export started for students with') +
2990                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2991                   session, level, mode))
2992        self.redirect(self.url(self.redirect_target))
2993        return
2994
2995class ExportJobContainerDownload(ExportCSVView):
2996    """Page that downloads a students export csv file.
2997
2998    """
2999    grok.context(VirtualExportJobContainer)
3000    grok.require('waeup.showStudents')
3001
3002class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3003    """Page that configures a students export job in datacenter.
3004
3005    """
3006    grok.context(IDataCenter)
3007    redirect_target = '@@export'
3008
3009class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3010    """Page that configures a students export job in facultiescontainer.
3011
3012    """
3013    grok.context(VirtualFacultiesExportJobContainer)
3014
3015class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3016    """Page that configures a students export job in departments.
3017
3018    """
3019    grok.context(VirtualDepartmentExportJobContainer)
3020
3021    @property
3022    def depcode(self):
3023        return self.context.__parent__.code
3024
3025class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3026    """Page that configures a students export job for certificates.
3027
3028    """
3029    grok.context(VirtualCertificateExportJobContainer)
3030    grok.template('exportconfig_certificate')
3031
3032    @property
3033    def certcode(self):
3034        return self.context.__parent__.code
3035
3036class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3037    """Page that configures a students export job for courses.
3038
3039    In contrast to department or certificate student data exports the
3040    coursetickets_catalog is searched here. Therefore the update
3041    method from the base class is customized.
3042    """
3043    grok.context(VirtualCourseExportJobContainer)
3044    grok.template('exportconfig_course')
3045
3046    def _set_exporter_values(self):
3047        # We provide only two exporters.
3048        exporters = []
3049        for name in ('students', 'coursetickets'):
3050            util = getUtility(ICSVExporter, name=name)
3051            exporters.append((util.title, name),)
3052        self.exporters = exporters
3053
3054    def update(self, START=None, session=None, level=None, mode=None,
3055               exporter=None):
3056        self._set_session_values()
3057        self._set_level_values()
3058        self._set_mode_values()
3059        self._set_exporter_values()
3060        if START is None:
3061            return
3062        if session == 'all':
3063            session = None
3064        if level == 'all':
3065            level = None
3066        job_id = self.context.start_export_job(exporter,
3067                                      self.request.principal.id,
3068                                      # Use a different catalog and
3069                                      # pass different keywords than
3070                                      # for the (default) students_catalog
3071                                      catalog='coursetickets',
3072                                      session=session,
3073                                      level=level,
3074                                      code=self.context.__parent__.code)
3075        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3076        self.context.logger.info(
3077            '%s - exported: %s (%s, %s, %s), job_id=%s'
3078            % (ob_class, exporter, session, level,
3079            self.context.__parent__.code, job_id))
3080        self.flash(_('Export started for course tickets with') +
3081                   ' level_session=%s, level=%s' % (
3082                   session, level))
3083        self.redirect(self.url(self.redirect_target))
3084        return
Note: See TracBrowser for help on using the repository browser.