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

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

Reset default value of body field to avoid that the last officer comment is pre-filled in all contact forms.

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