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

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

FCEOkene wants to hide the bed coordinates if maintenance fee is not yet paid. Thus we need additional property attributes which return the p_item (payment tickets) and the bed_coordinates attributes (bed tickets) by default and can be easily customized to hide this information in certain cases. bed_coordinates and p_item must be omitted on forms. The new display_ attributes are displayed instead.

All packages must now be adjusted.

The 'cost-benefit ratio' of these kinds of customizations is quite bad and we should think about declining such customization requests. However, I started customization and these are the changed made in the base package.

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