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

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

Add browser components to facilitate transfer reversion.

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