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

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

Test if the transcript pdf slip can be opened.

Add date_of_birth to base data.

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