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

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

Use keywords not only to filter students but also to restrict access to coursetickets which belong to the course so that lecturers (who gain access to courses) can download only coursetickets of their own course.

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