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

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

Change permissions for PaymentsManageFormPage?. The view permission is sufficient to view the manage page but no payment can be removed.

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