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

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

Implement BalancePaymentAddFormPage? and adjust interfaces (tests and further components will follow).

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