source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/browser.py @ 10771

Last change on this file since 10771 was 10655, checked in by Henrik Bettermann, 11 years ago

Implement an VirtualApplicantsExportJobContainer? which allows to export applicants locally. On each container page there is now an'Export applicants' button which directs to the exports overview page. Unlike student exporters, the applicants exporter can't be configured. It just exports all applicants in the container.

  • Property svn:keywords set to Id
File size: 44.0 KB
RevLine 
[5273]1## $Id: browser.py 10655 2013-09-26 09:37:25Z henrik $
[6078]2##
[7192]3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
[5273]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.
[6078]8##
[5273]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.
[6078]13##
[5273]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##
[5824]18"""UI components for basic applicants and related components.
[5273]19"""
[7063]20import os
[6082]21import sys
[5273]22import grok
[7370]23from datetime import datetime, date
[8042]24from zope.event import notify
[7392]25from zope.component import getUtility, createObject, getAdapter
[8033]26from zope.catalog.interfaces import ICatalog
[7714]27from zope.i18n import translate
[7322]28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
[7811]30from waeup.kofa.applicants.interfaces import (
[7363]31    IApplicant, IApplicantEdit, IApplicantsRoot,
[7683]32    IApplicantsContainer, IApplicantsContainerAdd,
[8033]33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
[8037]34    IApplicantRegisterUpdate
[7363]35    )
[10655]36from waeup.kofa.applicants.container import (
37    ApplicantsContainer, VirtualApplicantsExportJobContainer)
[8404]38from waeup.kofa.applicants.applicant import search
[8636]39from waeup.kofa.applicants.workflow import (
40    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
[7811]41from waeup.kofa.browser import (
[9217]42#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
[7363]43    DEFAULT_PASSPORT_IMAGE_PATH)
[9217]44from waeup.kofa.browser.layout import (
45    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
[7811]46from waeup.kofa.browser.interfaces import ICaptchaManager
47from waeup.kofa.browser.breadcrumbs import Breadcrumb
[8314]48from waeup.kofa.browser.resources import toggleall
[7811]49from waeup.kofa.browser.layout import (
[8550]50    NullValidator, jsaction, action, UtilityView, JSAction)
[10655]51from waeup.kofa.browser.pages import (
52    add_local_role, del_local_roles, doll_up, ExportCSVView)
[7811]53from waeup.kofa.browser.resources import datepicker, tabs, datatable, warning
54from waeup.kofa.interfaces import (
[7819]55    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
56    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
[7811]57from waeup.kofa.interfaces import MessageFactory as _
58from waeup.kofa.permissions import get_users_with_local_roles
59from waeup.kofa.students.interfaces import IStudentsUtils
[8186]60from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
[8170]61from waeup.kofa.widgets.datewidget import (
62    FriendlyDateDisplayWidget, FriendlyDateDisplayWidget,
63    FriendlyDatetimeDisplayWidget)
[8365]64from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
[5320]65
[7819]66grok.context(IKofaObject) # Make IKofaObject the default context
[5273]67
[8550]68class SubmitJSAction(JSAction):
69
70    msg = _('\'You can not edit your application records after final submission.'
71            ' You really want to submit?\'')
72
73class submitaction(grok.action):
74
75    def __call__(self, success):
76        action = SubmitJSAction(self.label, success=success, **self.options)
77        self.actions.append(action)
78        return action
79
[8388]80class ApplicantsRootPage(KofaDisplayFormPage):
[5822]81    grok.context(IApplicantsRoot)
82    grok.name('index')
[6153]83    grok.require('waeup.Public')
[8388]84    form_fields = grok.AutoFields(IApplicantsRoot)
85    form_fields['description'].custom_widget = HTMLDisplayWidget
[7710]86    label = _('Application Section')
[5843]87    pnav = 3
[6012]88
89    def update(self):
[6067]90        super(ApplicantsRootPage, self).update()
[6012]91        return
92
[8388]93    @property
94    def introduction(self):
95        # Here we know that the cookie has been set
96        lang = self.request.cookies.get('kofa.language')
97        html = self.context.description_dict.get(lang,'')
98        if html == '':
99            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
100            html = self.context.description_dict.get(portal_language,'')
101        return html
102
[10097]103    @property
104    def containers(self):
[10098]105        if self.layout.isAuthenticated():
106            return self.context.values()
[10097]107        return [value for value in self.context.values() if not value.hidden]
108
[8404]109class ApplicantsSearchPage(KofaPage):
110    grok.context(IApplicantsRoot)
111    grok.name('search')
112    grok.require('waeup.viewApplication')
[10644]113    label = _('Find applicants')
[10645]114    search_button = _('Find applicant')
[8404]115    pnav = 3
116
117    def update(self, *args, **kw):
118        datatable.need()
119        form = self.request.form
120        self.results = []
121        if 'searchterm' in form and form['searchterm']:
122            self.searchterm = form['searchterm']
123            self.searchtype = form['searchtype']
124        elif 'old_searchterm' in form:
125            self.searchterm = form['old_searchterm']
126            self.searchtype = form['old_searchtype']
127        else:
128            if 'search' in form:
129                self.flash(_('Empty search string'))
130            return
131        self.results = search(query=self.searchterm,
132            searchtype=self.searchtype, view=self)
133        if not self.results:
134            self.flash(_('No applicant found.'))
135        return
136
[7819]137class ApplicantsRootManageFormPage(KofaEditFormPage):
[5828]138    grok.context(IApplicantsRoot)
139    grok.name('manage')
[6107]140    grok.template('applicantsrootmanagepage')
[8388]141    form_fields = grok.AutoFields(IApplicantsRoot)
[7710]142    label = _('Manage application section')
[5843]143    pnav = 3
[7136]144    grok.require('waeup.manageApplication')
[8388]145    taboneactions = [_('Save')]
146    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
147    tabthreeactions1 = [_('Remove selected local roles')]
148    tabthreeactions2 = [_('Add local role')]
[7710]149    subunits = _('Applicants Containers')
[6078]150
[6069]151    def update(self):
152        tabs.need()
[6108]153        datatable.need()
[7330]154        warning.need()
[8388]155        self.tab1 = self.tab2 = self.tab3 = ''
156        qs = self.request.get('QUERY_STRING', '')
157        if not qs:
158            qs = 'tab1'
159        setattr(self, qs, 'active')
[6069]160        return super(ApplicantsRootManageFormPage, self).update()
[5828]161
[6184]162    def getLocalRoles(self):
163        roles = ILocalRolesAssignable(self.context)
164        return roles()
165
166    def getUsers(self):
167        """Get a list of all users.
168        """
169        for key, val in grok.getSite()['users'].items():
170            url = self.url(val)
171            yield(dict(url=url, name=key, val=val))
172
173    def getUsersWithLocalRoles(self):
174        return get_users_with_local_roles(self.context)
175
[7710]176    @jsaction(_('Remove selected'))
[6069]177    def delApplicantsContainers(self, **data):
178        form = self.request.form
[9701]179        if 'val_id' in form:
[8388]180            child_id = form['val_id']
181        else:
182            self.flash(_('No container selected!'))
183            self.redirect(self.url(self.context, '@@manage')+'?tab2')
184            return
[6069]185        if not isinstance(child_id, list):
186            child_id = [child_id]
187        deleted = []
188        for id in child_id:
189            try:
190                del self.context[id]
191                deleted.append(id)
192            except:
[7710]193                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[6069]194                        id, sys.exc_info()[0], sys.exc_info()[1]))
195        if len(deleted):
[7738]196            self.flash(_('Successfully removed: ${a}',
197                mapping = {'a':', '.join(deleted)}))
[8388]198        self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6078]199        return
[5828]200
[7710]201    @action(_('Add applicants container'), validator=NullValidator)
[6069]202    def addApplicantsContainer(self, **data):
203        self.redirect(self.url(self.context, '@@add'))
[6078]204        return
205
[7710]206    @action(_('Add local role'), validator=NullValidator)
[6184]207    def addLocalRole(self, **data):
[7484]208        return add_local_role(self,3, **data)
[6184]209
[7710]210    @action(_('Remove selected local roles'))
[6184]211    def delLocalRoles(self, **data):
[7484]212        return del_local_roles(self,3,**data)
[6184]213
[8388]214    def _description(self):
215        view = ApplicantsRootPage(
216            self.context,self.request)
217        view.setUpWidgets()
218        return view.widgets['description']()
219
220    @action(_('Save'), style='primary')
221    def save(self, **data):
222        self.applyData(self.context, **data)
223        self.context.description_dict = self._description()
[8390]224        self.flash(_('Form has been saved.'))
[8388]225        return
226
[7819]227class ApplicantsContainerAddFormPage(KofaAddFormPage):
[5822]228    grok.context(IApplicantsRoot)
[7136]229    grok.require('waeup.manageApplication')
[5822]230    grok.name('add')
[6107]231    grok.template('applicantscontaineraddpage')
[7710]232    label = _('Add applicants container')
[5843]233    pnav = 3
[6078]234
[6103]235    form_fields = grok.AutoFields(
[7903]236        IApplicantsContainerAdd).omit('code').omit('title')
[6078]237
[6083]238    def update(self):
239        datepicker.need() # Enable jQuery datepicker in date fields.
240        return super(ApplicantsContainerAddFormPage, self).update()
241
[7710]242    @action(_('Add applicants container'))
[6069]243    def addApplicantsContainer(self, **data):
[6103]244        year = data['year']
245        code = u'%s%s' % (data['prefix'], year)
[9529]246        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
247        title = apptypes_dict[data['prefix']][0]
[7685]248        title = u'%s %s/%s' % (title, year, year + 1)
[6087]249        if code in self.context.keys():
[6105]250            self.flash(
[7710]251                _('An applicants container for the same application type and entrance year exists already in the database.'))
[5822]252            return
253        # Add new applicants container...
[8009]254        container = createObject(u'waeup.ApplicantsContainer')
[6069]255        self.applyData(container, **data)
[6087]256        container.code = code
257        container.title = title
258        self.context[code] = container
[7710]259        self.flash(_('Added:') + ' "%s".' % code)
[7484]260        self.redirect(self.url(self.context, u'@@manage'))
[5822]261        return
[6078]262
[7710]263    @action(_('Cancel'), validator=NullValidator)
[6069]264    def cancel(self, **data):
[7484]265        self.redirect(self.url(self.context, '@@manage'))
[6078]266
[5845]267class ApplicantsRootBreadcrumb(Breadcrumb):
268    """A breadcrumb for applicantsroot.
269    """
270    grok.context(IApplicantsRoot)
[7710]271    title = _(u'Applicants')
[6078]272
[5845]273class ApplicantsContainerBreadcrumb(Breadcrumb):
274    """A breadcrumb for applicantscontainers.
275    """
276    grok.context(IApplicantsContainer)
[6319]277
[10655]278
279class ApplicantsExportsBreadcrumb(Breadcrumb):
280    """A breadcrumb for exports.
281    """
282    grok.context(VirtualApplicantsExportJobContainer)
283    title = _(u'Applicant Data Exports')
284    target = None
285
[6153]286class ApplicantBreadcrumb(Breadcrumb):
287    """A breadcrumb for applicants.
288    """
289    grok.context(IApplicant)
[6319]290
[6153]291    @property
292    def title(self):
293        """Get a title for a context.
294        """
[7240]295        return self.context.application_number
[5828]296
[7250]297class OnlinePaymentBreadcrumb(Breadcrumb):
298    """A breadcrumb for payments.
299    """
300    grok.context(IApplicantOnlinePayment)
301
302    @property
303    def title(self):
304        return self.context.p_id
305
[8563]306class ApplicantsStatisticsPage(KofaDisplayFormPage):
307    """Some statistics about applicants in a container.
308    """
309    grok.context(IApplicantsContainer)
310    grok.name('statistics')
[8565]311    grok.require('waeup.viewApplicationStatistics')
[8563]312    grok.template('applicantcontainerstatistics')
313
314    @property
315    def label(self):
316        return "%s" % self.context.title
317
[7819]318class ApplicantsContainerPage(KofaDisplayFormPage):
[5830]319    """The standard view for regular applicant containers.
320    """
321    grok.context(IApplicantsContainer)
322    grok.name('index')
[6153]323    grok.require('waeup.Public')
[6029]324    grok.template('applicantscontainerpage')
[5850]325    pnav = 3
[6053]326
[9078]327    @property
328    def form_fields(self):
329        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
330        form_fields['description'].custom_widget = HTMLDisplayWidget
331        form_fields[
332            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
333        form_fields[
334            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
335        if self.request.principal.id == 'zope.anybody':
336            form_fields = form_fields.omit(
[10101]337                'code', 'prefix', 'year', 'mode', 'hidden',
[9078]338                'strict_deadline', 'application_category')
339        return form_fields
[6053]340
[5837]341    @property
[7708]342    def introduction(self):
[7833]343        # Here we know that the cookie has been set
344        lang = self.request.cookies.get('kofa.language')
[7708]345        html = self.context.description_dict.get(lang,'')
[8388]346        if html == '':
[7833]347            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7708]348            html = self.context.description_dict.get(portal_language,'')
[8388]349        return html
[7708]350
351    @property
[7467]352    def label(self):
[7493]353        return "%s" % self.context.title
[5837]354
[7819]355class ApplicantsContainerManageFormPage(KofaEditFormPage):
[5837]356    grok.context(IApplicantsContainer)
[5850]357    grok.name('manage')
[6107]358    grok.template('applicantscontainermanagepage')
[10625]359    form_fields = grok.AutoFields(IApplicantsContainer)
[7710]360    taboneactions = [_('Save'),_('Cancel')]
[8684]361    tabtwoactions = [_('Remove selected'),_('Cancel'),
[8314]362        _('Create students from selected')]
[7710]363    tabthreeactions1 = [_('Remove selected local roles')]
364    tabthreeactions2 = [_('Add local role')]
[5844]365    # Use friendlier date widget...
[7136]366    grok.require('waeup.manageApplication')
[5850]367
368    @property
369    def label(self):
[7710]370        return _('Manage applicants container')
[5850]371
[5845]372    pnav = 3
[5837]373
[8547]374    @property
375    def showApplicants(self):
376        if len(self.context) < 5000:
377            return True
378        return False
379
[5837]380    def update(self):
[5850]381        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]382        tabs.need()
[8314]383        toggleall.need()
[7484]384        self.tab1 = self.tab2 = self.tab3 = ''
385        qs = self.request.get('QUERY_STRING', '')
386        if not qs:
387            qs = 'tab1'
388        setattr(self, qs, 'active')
[7330]389        warning.need()
[6015]390        datatable.need()  # Enable jQurey datatables for contents listing
[6107]391        return super(ApplicantsContainerManageFormPage, self).update()
[5837]392
[6184]393    def getLocalRoles(self):
394        roles = ILocalRolesAssignable(self.context)
395        return roles()
396
397    def getUsers(self):
398        """Get a list of all users.
399        """
400        for key, val in grok.getSite()['users'].items():
401            url = self.url(val)
402            yield(dict(url=url, name=key, val=val))
403
404    def getUsersWithLocalRoles(self):
405        return get_users_with_local_roles(self.context)
406
[7708]407    def _description(self):
408        view = ApplicantsContainerPage(
409            self.context,self.request)
410        view.setUpWidgets()
411        return view.widgets['description']()
412
[7714]413    @action(_('Save'), style='primary')
[7489]414    def save(self, **data):
[9531]415        changed_fields = self.applyData(self.context, **data)
416        if changed_fields:
417            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
418        else:
419            changed_fields = []
[7708]420        self.context.description_dict = self._description()
[8562]421        # Always refresh title. So we can change titles
422        # if APP_TYPES_DICT has been edited.
[9529]423        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
424        title = apptypes_dict[self.context.prefix][0]
[10625]425        #self.context.title = u'%s %s/%s' % (
426        #    title, self.context.year, self.context.year + 1)
[7710]427        self.flash(_('Form has been saved.'))
[9531]428        fields_string = ' + '.join(changed_fields)
429        self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[5837]430        return
[6078]431
[7710]432    @jsaction(_('Remove selected'))
[6105]433    def delApplicant(self, **data):
[6189]434        form = self.request.form
[9701]435        if 'val_id' in form:
[6189]436            child_id = form['val_id']
437        else:
[7710]438            self.flash(_('No applicant selected!'))
[7484]439            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6189]440            return
441        if not isinstance(child_id, list):
442            child_id = [child_id]
443        deleted = []
444        for id in child_id:
445            try:
446                del self.context[id]
447                deleted.append(id)
448            except:
[7710]449                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[6189]450                        id, sys.exc_info()[0], sys.exc_info()[1]))
451        if len(deleted):
[7741]452            self.flash(_('Successfully removed: ${a}',
[7738]453                mapping = {'a':', '.join(deleted)}))
[7484]454        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6189]455        return
[6105]456
[8314]457    @action(_('Create students from selected'))
458    def createStudents(self, **data):
459        form = self.request.form
[9701]460        if 'val_id' in form:
[8314]461            child_id = form['val_id']
462        else:
463            self.flash(_('No applicant selected!'))
464            self.redirect(self.url(self.context, '@@manage')+'?tab2')
465            return
466        if not isinstance(child_id, list):
467            child_id = [child_id]
468        created = []
469        for id in child_id:
470            success, msg = self.context[id].createStudent(view=self)
471            if success:
472                created.append(id)
473        if len(created):
474            self.flash(_('${a} students successfully created.',
475                mapping = {'a': len(created)}))
476        else:
477            self.flash(_('No student could be created.'))
478        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
479        return
480
[7710]481    @action(_('Cancel'), validator=NullValidator)
[5837]482    def cancel(self, **data):
483        self.redirect(self.url(self.context))
484        return
[5886]485
[7710]486    @action(_('Add local role'), validator=NullValidator)
[6184]487    def addLocalRole(self, **data):
488        return add_local_role(self,3, **data)
[6105]489
[7710]490    @action(_('Remove selected local roles'))
[6184]491    def delLocalRoles(self, **data):
492        return del_local_roles(self,3,**data)
493
[7819]494class ApplicantAddFormPage(KofaAddFormPage):
[6622]495    """Add-form to add an applicant.
[6327]496    """
497    grok.context(IApplicantsContainer)
[7136]498    grok.require('waeup.manageApplication')
[6327]499    grok.name('addapplicant')
[7240]500    #grok.template('applicantaddpage')
501    form_fields = grok.AutoFields(IApplicant).select(
[7356]502        'firstname', 'middlename', 'lastname',
[7240]503        'email', 'phone')
[7714]504    label = _('Add applicant')
[6327]505    pnav = 3
506
[7714]507    @action(_('Create application record'))
[6327]508    def addApplicant(self, **data):
[8008]509        applicant = createObject(u'waeup.Applicant')
[7240]510        self.applyData(applicant, **data)
511        self.context.addApplicant(applicant)
[7714]512        self.flash(_('Applicant record created.'))
[7363]513        self.redirect(
514            self.url(self.context[applicant.application_number], 'index'))
[6327]515        return
516
[7819]517class ApplicantDisplayFormPage(KofaDisplayFormPage):
[8014]518    """A display view for applicant data.
519    """
[5273]520    grok.context(IApplicant)
521    grok.name('index')
[7113]522    grok.require('waeup.viewApplication')
[7200]523    grok.template('applicantdisplaypage')
[6320]524    form_fields = grok.AutoFields(IApplicant).omit(
[8983]525        'locked', 'course_admitted', 'password', 'suspended')
[7714]526    label = _('Applicant')
[5843]527    pnav = 3
[8922]528    hide_hint = False
[5273]529
[8046]530    @property
[10534]531    def target(self):
532        return getattr(self.context.__parent__, 'prefix', None)
533
534    @property
[8046]535    def separators(self):
536        return getUtility(IApplicantsUtils).SEPARATORS_DICT
537
[7063]538    def update(self):
539        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]540        # Mark application as started if applicant logs in for the first time
[7272]541        usertype = getattr(self.request.principal, 'user_type', None)
542        if usertype == 'applicant' and \
543            IWorkflowState(self.context).getState() == INITIALIZED:
[7240]544            IWorkflowInfo(self.context).fireTransition('start')
[7063]545        return
546
[6196]547    @property
[7240]548    def hasPassword(self):
549        if self.context.password:
[7714]550            return _('set')
551        return _('unset')
[7240]552
553    @property
[6196]554    def label(self):
555        container_title = self.context.__parent__.title
[8096]556        return _('${a} <br /> Application Record ${b}', mapping = {
[7714]557            'a':container_title, 'b':self.context.application_number})
[6196]558
[7347]559    def getCourseAdmitted(self):
560        """Return link, title and code in html format to the certificate
561           admitted.
562        """
563        course_admitted = self.context.course_admitted
[7351]564        if getattr(course_admitted, '__parent__',None):
[7347]565            url = self.url(course_admitted)
566            title = course_admitted.title
567            code = course_admitted.code
568            return '<a href="%s">%s - %s</a>' %(url,code,title)
569        return ''
[6254]570
[7259]571class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
572    grok.context(IApplicant)
573    grok.name('base')
574    form_fields = grok.AutoFields(IApplicant).select(
[9141]575        'applicant_id','email', 'course1')
[7259]576
[7459]577class CreateStudentPage(UtilityView, grok.View):
[8636]578    """Create a student object from applicant data.
[7341]579    """
580    grok.context(IApplicant)
581    grok.name('createstudent')
582    grok.require('waeup.manageStudent')
583
584    def update(self):
[8314]585        msg = self.context.createStudent(view=self)[1]
[7341]586        self.flash(msg)
587        self.redirect(self.url(self.context))
588        return
589
590    def render(self):
591        return
592
[8636]593class CreateAllStudentsPage(UtilityView, grok.View):
594    """Create all student objects from applicant data
595    in a container.
596
597    This is a hidden page, no link or button will
598    be provided and only PortalManagers can do this.
599    """
[9900]600    #grok.context(IApplicantsContainer)
[8636]601    grok.name('createallstudents')
602    grok.require('waeup.managePortal')
603
604    def update(self):
605        cat = getUtility(ICatalog, name='applicants_catalog')
606        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
607        created = []
[9900]608        container_only = False
609        applicants_root = grok.getSite()['applicants']
610        if isinstance(self.context, ApplicantsContainer):
611            container_only = True
[8636]612        for result in results:
[9900]613            if container_only and result.__parent__ is not self.context:
[8636]614                continue
615            success, msg = result.createStudent(view=self)
616            if success:
617                created.append(result.applicant_id)
618            else:
619                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
[9900]620                applicants_root.logger.info(
[8742]621                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
[8636]622        if len(created):
623            self.flash(_('${a} students successfully created.',
624                mapping = {'a': len(created)}))
625        else:
626            self.flash(_('No student could be created.'))
[9900]627        self.redirect(self.url(self.context))
[8636]628        return
629
630    def render(self):
631        return
632
[8260]633class ApplicationFeePaymentAddPage(UtilityView, grok.View):
[7250]634    """ Page to add an online payment ticket
635    """
636    grok.context(IApplicant)
637    grok.name('addafp')
638    grok.require('waeup.payApplicant')
[8243]639    factory = u'waeup.ApplicantOnlinePayment'
[7250]640
641    def update(self):
642        for key in self.context.keys():
643            ticket = self.context[key]
644            if ticket.p_state == 'paid':
645                  self.flash(
[7714]646                      _('This type of payment has already been made.'))
[7250]647                  self.redirect(self.url(self.context))
648                  return
[8524]649        applicants_utils = getUtility(IApplicantsUtils)
650        container = self.context.__parent__
[8243]651        payment = createObject(self.factory)
[8524]652        error = applicants_utils.setPaymentDetails(container, payment)
653        if error is not None:
654            self.flash(error)
655            self.redirect(self.url(self.context))
656            return
[7250]657        self.context[payment.p_id] = payment
[7714]658        self.flash(_('Payment ticket created.'))
[8280]659        self.redirect(self.url(payment))
[7250]660        return
661
662    def render(self):
663        return
664
665
[7819]666class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[7250]667    """ Page to view an online payment ticket
668    """
669    grok.context(IApplicantOnlinePayment)
670    grok.name('index')
671    grok.require('waeup.viewApplication')
[9984]672    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8170]673    form_fields[
674        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
675    form_fields[
676        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]677    pnav = 3
678
679    @property
680    def label(self):
[7714]681        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8170]682            'a':self.context.__parent__.display_fullname,
683            'b':self.context.p_id})
[7250]684
[8420]685class OnlinePaymentApprovePage(UtilityView, grok.View):
686    """ Approval view
[7250]687    """
688    grok.context(IApplicantOnlinePayment)
[8420]689    grok.name('approve')
690    grok.require('waeup.managePortal')
[7250]691
692    def update(self):
[8428]693        success, msg, log = self.context.approveApplicantPayment()
694        if log is not None:
[9771]695            applicant = self.context.__parent__
696            # Add log message to applicants.log
697            applicant.writeLogMessage(self, log)
698            # Add log message to payments.log
699            self.context.logger.info(
[9795]700                '%s,%s,%s,%s,%s,,,,,,' % (
[9771]701                applicant.applicant_id,
702                self.context.p_id, self.context.p_category,
703                self.context.amount_auth, self.context.r_code))
[8422]704        self.flash(msg)
[7250]705        return
706
707    def render(self):
708        self.redirect(self.url(self.context, '@@index'))
709        return
710
[7459]711class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7250]712    """Deliver a PDF slip of the context.
713    """
714    grok.context(IApplicantOnlinePayment)
[8262]715    grok.name('payment_slip.pdf')
[7250]716    grok.require('waeup.viewApplication')
[9984]717    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8173]718    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
719    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]720    prefix = 'form'
[8258]721    note = None
[7250]722
723    @property
[7714]724    def title(self):
[7819]725        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]726        return translate(_('Payment Data'), 'waeup.kofa',
[7714]727            target_language=portal_language)
728
729    @property
[7250]730    def label(self):
[7819]731        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8262]732        return translate(_('Online Payment Slip'),
[7811]733            'waeup.kofa', target_language=portal_language) \
[7714]734            + ' %s' % self.context.p_id
[7250]735
736    def render(self):
[8262]737        #if self.context.p_state != 'paid':
738        #    self.flash(_('Ticket not yet paid.'))
739        #    self.redirect(self.url(self.context))
740        #    return
[7259]741        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
[7250]742            self.request)
743        students_utils = getUtility(IStudentsUtils)
[8262]744        return students_utils.renderPDF(self,'payment_slip.pdf',
[8258]745            self.context.__parent__, applicantview, note=self.note)
[7250]746
[10571]747class ExportPDFPageApplicationSlip(UtilityView, grok.View):
[6358]748    """Deliver a PDF slip of the context.
749    """
750    grok.context(IApplicant)
751    grok.name('application_slip.pdf')
[7136]752    grok.require('waeup.viewApplication')
[6358]753    prefix = 'form'
754
[8666]755    def update(self):
[9051]756        if self.context.state in ('initialized', 'started', 'paid'):
[8666]757            self.flash(
[9051]758                _('Please pay and submit before trying to download the application slip.'))
[8666]759            return self.redirect(self.url(self.context))
760        return
761
[6358]762    def render(self):
[7392]763        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
764            view=self)
[6358]765        self.response.setHeader(
766            'Content-Type', 'application/pdf')
[7392]767        return pdfstream
[6358]768
[7081]769def handle_img_upload(upload, context, view):
[7063]770    """Handle upload of applicant image.
[7081]771
772    Returns `True` in case of success or `False`.
773
774    Please note that file pointer passed in (`upload`) most probably
775    points to end of file when leaving this function.
[7063]776    """
[7081]777    size = file_size(upload)
778    if size > MAX_UPLOAD_SIZE:
[7714]779        view.flash(_('Uploaded image is too big!'))
[7081]780        return False
[7247]781    dummy, ext = os.path.splitext(upload.filename)
782    ext.lower()
783    if ext != '.jpg':
[7714]784        view.flash(_('jpg file extension expected.'))
[7247]785        return False
[7081]786    upload.seek(0) # file pointer moved when determining size
[7063]787    store = getUtility(IExtFileStore)
788    file_id = IFileStoreNameChooser(context).chooseName()
789    store.createFile(file_id, upload)
[7081]790    return True
[7063]791
[7819]792class ApplicantManageFormPage(KofaEditFormPage):
[6196]793    """A full edit view for applicant data.
794    """
795    grok.context(IApplicant)
[7200]796    grok.name('manage')
[7136]797    grok.require('waeup.manageApplication')
[6476]798    form_fields = grok.AutoFields(IApplicant)
[7351]799    form_fields['student_id'].for_display = True
[7378]800    form_fields['applicant_id'].for_display = True
[7200]801    grok.template('applicanteditpage')
[6322]802    manage_applications = True
[6196]803    pnav = 3
[7714]804    display_actions = [[_('Save'), _('Final Submit')],
805        [_('Add online payment ticket'),_('Remove selected tickets')]]
[6196]806
[8046]807    @property
[10534]808    def target(self):
809        return getattr(self.context.__parent__, 'prefix', None)
810
811    @property
[8046]812    def separators(self):
813        return getUtility(IApplicantsUtils).SEPARATORS_DICT
814
[6196]815    def update(self):
816        datepicker.need() # Enable jQuery datepicker in date fields.
[7330]817        warning.need()
[7200]818        super(ApplicantManageFormPage, self).update()
[6353]819        self.wf_info = IWorkflowInfo(self.context)
[7081]820        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[10090]821        self.upload_success = None
[6598]822        upload = self.request.form.get('form.passport', None)
823        if upload:
[10090]824            # We got a fresh upload, upload_success is
825            # either True or False
826            self.upload_success = handle_img_upload(
[7084]827                upload, self.context, self)
[10090]828            if self.upload_success:
[10095]829                self.context.writeLogMessage(self, 'saved: passport')
[6196]830        return
831
832    @property
833    def label(self):
834        container_title = self.context.__parent__.title
[8096]835        return _('${a} <br /> Application Form ${b}', mapping = {
[7714]836            'a':container_title, 'b':self.context.application_number})
[6196]837
[6303]838    def getTransitions(self):
[6351]839        """Return a list of dicts of allowed transition ids and titles.
[6353]840
841        Each list entry provides keys ``name`` and ``title`` for
842        internal name and (human readable) title of a single
843        transition.
[6349]844        """
[8434]845        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
846            if not t[0] == 'pay']
[7687]847        return [dict(name='', title=_('No transition'))] +[
[6355]848            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]849
[7714]850    @action(_('Save'), style='primary')
[6196]851    def save(self, **data):
[7240]852        form = self.request.form
853        password = form.get('password', None)
854        password_ctl = form.get('control_password', None)
855        if password:
856            validator = getUtility(IPasswordValidator)
857            errors = validator.validate_password(password, password_ctl)
858            if errors:
859                self.flash( ' '.join(errors))
860                return
[10090]861        if self.upload_success is False:  # False is not None!
862            # Error during image upload. Ignore other values.
863            return
[6475]864        changed_fields = self.applyData(self.context, **data)
[7199]865        # Turn list of lists into single list
866        if changed_fields:
867            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]868        else:
869            changed_fields = []
870        if password:
871            # Now we know that the form has no errors and can set password ...
872            IUserAccount(self.context).setPassword(password)
873            changed_fields.append('password')
[7199]874        fields_string = ' + '.join(changed_fields)
[7085]875        trans_id = form.get('transition', None)
876        if trans_id:
877            self.wf_info.fireTransition(trans_id)
[7714]878        self.flash(_('Form has been saved.'))
[6644]879        if fields_string:
[8742]880            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[6196]881        return
882
[7250]883    def unremovable(self, ticket):
[7330]884        return False
[7250]885
886    # This method is also used by the ApplicantEditFormPage
887    def delPaymentTickets(self, **data):
888        form = self.request.form
[9701]889        if 'val_id' in form:
[7250]890            child_id = form['val_id']
891        else:
[7714]892            self.flash(_('No payment selected.'))
[7250]893            self.redirect(self.url(self.context))
894            return
895        if not isinstance(child_id, list):
896            child_id = [child_id]
897        deleted = []
898        for id in child_id:
899            # Applicants are not allowed to remove used payment tickets
900            if not self.unremovable(self.context[id]):
901                try:
902                    del self.context[id]
903                    deleted.append(id)
904                except:
[7714]905                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[7250]906                            id, sys.exc_info()[0], sys.exc_info()[1]))
907        if len(deleted):
[7741]908            self.flash(_('Successfully removed: ${a}',
[7738]909                mapping = {'a':', '.join(deleted)}))
[8742]910            self.context.writeLogMessage(
911                self, 'removed: % s' % ', '.join(deleted))
[7250]912        return
913
[7252]914    # We explicitely want the forms to be validated before payment tickets
915    # can be created. If no validation is requested, use
[7459]916    # 'validator=NullValidator' in the action directive
[7714]917    @action(_('Add online payment ticket'))
[7250]918    def addPaymentTicket(self, **data):
919        self.redirect(self.url(self.context, '@@addafp'))
[7252]920        return
[7250]921
[7714]922    @jsaction(_('Remove selected tickets'))
[7250]923    def removePaymentTickets(self, **data):
924        self.delPaymentTickets(**data)
925        self.redirect(self.url(self.context) + '/@@manage')
926        return
927
[10094]928    # Not used in base package
929    def file_exists(self, attr):
930        file = getUtility(IExtFileStore).getFileByContext(
931            self.context, attr=attr)
932        if file:
933            return True
934        else:
935            return False
936
[7200]937class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]938    """An applicant-centered edit view for applicant data.
939    """
[6196]940    grok.context(IApplicantEdit)
[5273]941    grok.name('edit')
[6198]942    grok.require('waeup.handleApplication')
[6459]943    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]944        'locked', 'course_admitted', 'student_id',
[9047]945        'suspended'
[6459]946        )
[7459]947    form_fields['applicant_id'].for_display = True
[8039]948    form_fields['reg_number'].for_display = True
[7200]949    grok.template('applicanteditpage')
[6322]950    manage_applications = False
[10358]951    submit_state = PAID
[5484]952
[7250]953    @property
954    def display_actions(self):
[8286]955        state = IWorkflowState(self.context).getState()
[10358]956        actions = [[],[]]
957        if state == STARTED:
[7714]958            actions = [[_('Save')],
959                [_('Add online payment ticket'),_('Remove selected tickets')]]
[8286]960        elif state == PAID:
[7714]961            actions = [[_('Save'), _('Final Submit')],
962                [_('Remove selected tickets')]]
[7250]963        return actions
964
[7330]965    def unremovable(self, ticket):
[8286]966        state = IWorkflowState(self.context).getState()
967        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
[7330]968
[7145]969    def emit_lock_message(self):
[7714]970        self.flash(_('The requested form is locked (read-only).'))
[5941]971        self.redirect(self.url(self.context))
972        return
[6078]973
[5686]974    def update(self):
[8665]975        if self.context.locked or (
976            self.context.__parent__.expired and
977            self.context.__parent__.strict_deadline):
[7145]978            self.emit_lock_message()
[5941]979            return
[7200]980        super(ApplicantEditFormPage, self).update()
[5686]981        return
[5952]982
[6196]983    def dataNotComplete(self):
[7252]984        store = getUtility(IExtFileStore)
985        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
[7714]986            return _('No passport picture uploaded.')
[6322]987        if not self.request.form.get('confirm_passport', False):
[7714]988            return _('Passport picture confirmation box not ticked.')
[6196]989        return False
[5952]990
[7252]991    # We explicitely want the forms to be validated before payment tickets
992    # can be created. If no validation is requested, use
[7459]993    # 'validator=NullValidator' in the action directive
[7714]994    @action(_('Add online payment ticket'))
[7250]995    def addPaymentTicket(self, **data):
996        self.redirect(self.url(self.context, '@@addafp'))
[7252]997        return
[7250]998
[7714]999    @jsaction(_('Remove selected tickets'))
[7250]1000    def removePaymentTickets(self, **data):
1001        self.delPaymentTickets(**data)
1002        self.redirect(self.url(self.context) + '/@@edit')
1003        return
1004
[7996]1005    @action(_('Save'), style='primary')
[5273]1006    def save(self, **data):
[10090]1007        if self.upload_success is False:  # False is not None!
1008            # Error during image upload. Ignore other values.
1009            return
[10219]1010        if data.get('course1', 1) == data.get('course2', 2):
[10210]1011            self.flash(_('1st and 2nd choice must be different.'))
1012            return
[5273]1013        self.applyData(self.context, **data)
[10210]1014        self.flash(_('Form has been saved.'))
[5273]1015        return
1016
[8550]1017    @submitaction(_('Final Submit'))
[5484]1018    def finalsubmit(self, **data):
[10090]1019        if self.upload_success is False:  # False is not None!
[7084]1020            return # error during image upload. Ignore other values
[6196]1021        if self.dataNotComplete():
1022            self.flash(self.dataNotComplete())
[5941]1023            return
[7252]1024        self.applyData(self.context, **data)
[8286]1025        state = IWorkflowState(self.context).getState()
[6322]1026        # This shouldn't happen, but the application officer
1027        # might have forgotten to lock the form after changing the state
[10358]1028        if state != self.submit_state:
[8589]1029            self.flash(_('The form cannot be submitted. Wrong state!'))
[6303]1030            return
1031        IWorkflowInfo(self.context).fireTransition('submit')
[8589]1032        # application_date is used in export files for sorting.
1033        # We can thus store utc.
[8194]1034        self.context.application_date = datetime.utcnow()
[7714]1035        self.flash(_('Form has been submitted.'))
[6196]1036        self.redirect(self.url(self.context))
[5273]1037        return
[5941]1038
[7063]1039class PassportImage(grok.View):
1040    """Renders the passport image for applicants.
1041    """
1042    grok.name('passport.jpg')
1043    grok.context(IApplicant)
[7113]1044    grok.require('waeup.viewApplication')
[7063]1045
1046    def render(self):
1047        # A filename chooser turns a context into a filename suitable
1048        # for file storage.
1049        image = getUtility(IExtFileStore).getFileByContext(self.context)
1050        self.response.setHeader(
1051            'Content-Type', 'image/jpeg')
1052        if image is None:
1053            # show placeholder image
[7089]1054            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1055        return image
[7363]1056
[7819]1057class ApplicantRegistrationPage(KofaAddFormPage):
[7363]1058    """Captcha'd registration page for applicants.
1059    """
1060    grok.context(IApplicantsContainer)
1061    grok.name('register')
[7373]1062    grok.require('waeup.Anonymous')
[7363]1063    grok.template('applicantregister')
1064
[7368]1065    @property
[8033]1066    def form_fields(self):
1067        form_fields = None
[8128]1068        if self.context.mode == 'update':
1069            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1070                'firstname','reg_number','email')
1071        else: #if self.context.mode == 'create':
[8033]1072            form_fields = grok.AutoFields(IApplicantEdit).select(
1073                'firstname', 'middlename', 'lastname', 'email', 'phone')
1074        return form_fields
1075
1076    @property
[7368]1077    def label(self):
[8078]1078        return _('Apply for ${a}',
[7714]1079            mapping = {'a':self.context.title})
[7368]1080
[7363]1081    def update(self):
[8665]1082        if self.context.expired:
1083            self.flash(_('Outside application period.'))
[7368]1084            self.redirect(self.url(self.context))
1085            return
1086        # Handle captcha
[7363]1087        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1088        self.captcha_result = self.captcha.verify(self.request)
1089        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1090        return
1091
[8629]1092    def _redirect(self, email, password, applicant_id):
1093        # Forward only email to landing page in base package.
1094        self.redirect(self.url(self.context, 'registration_complete',
1095            data = dict(email=email)))
1096        return
1097
[9178]1098    @action(_('Send login credentials to email address'), style='primary')
[7363]1099    def register(self, **data):
1100        if not self.captcha_result.is_valid:
[8037]1101            # Captcha will display error messages automatically.
[7363]1102            # No need to flash something.
1103            return
[8033]1104        if self.context.mode == 'create':
1105            # Add applicant
1106            applicant = createObject(u'waeup.Applicant')
1107            self.applyData(applicant, **data)
1108            self.context.addApplicant(applicant)
[8042]1109            applicant.reg_number = applicant.applicant_id
1110            notify(grok.ObjectModifiedEvent(applicant))
[8033]1111        elif self.context.mode == 'update':
1112            # Update applicant
[8037]1113            reg_number = data.get('reg_number','')
1114            firstname = data.get('firstname','')
[8033]1115            cat = getUtility(ICatalog, name='applicants_catalog')
1116            results = list(
1117                cat.searchResults(reg_number=(reg_number, reg_number)))
1118            if results:
1119                applicant = results[0]
[8042]1120                if getattr(applicant,'firstname',None) is None:
[8037]1121                    self.flash(_('An error occurred.'))
1122                    return
1123                elif applicant.firstname.lower() != firstname.lower():
[8042]1124                    # Don't tell the truth here. Anonymous must not
1125                    # know that a record was found and only the firstname
1126                    # verification failed.
[8037]1127                    self.flash(_('No application record found.'))
1128                    return
[8627]1129                elif applicant.password is not None and \
1130                    applicant.state != INITIALIZED:
1131                    self.flash(_('Your password has already been set and used. '
[8042]1132                                 'Please proceed to the login page.'))
1133                    return
1134                # Store email address but nothing else.
[8033]1135                applicant.email = data['email']
[8042]1136                notify(grok.ObjectModifiedEvent(applicant))
[8033]1137            else:
[8042]1138                # No record found, this is the truth.
[8033]1139                self.flash(_('No application record found.'))
1140                return
1141        else:
[8042]1142            # Does not happen but anyway ...
[8033]1143            return
[7819]1144        kofa_utils = getUtility(IKofaUtils)
[7811]1145        password = kofa_utils.genPassword()
[7380]1146        IUserAccount(applicant).setPassword(password)
[7365]1147        # Send email with credentials
[7399]1148        login_url = self.url(grok.getSite(), 'login')
[8853]1149        url_info = u'Login: %s' % login_url
[7714]1150        msg = _('You have successfully been registered for the')
[7811]1151        if kofa_utils.sendCredentials(IUserAccount(applicant),
[8853]1152            password, url_info, msg):
[8629]1153            email_sent = applicant.email
[7380]1154        else:
[8629]1155            email_sent = None
1156        self._redirect(email=email_sent, password=password,
1157            applicant_id=applicant.applicant_id)
[7380]1158        return
1159
[7819]1160class ApplicantRegistrationEmailSent(KofaPage):
[7380]1161    """Landing page after successful registration.
[8629]1162
[7380]1163    """
1164    grok.name('registration_complete')
1165    grok.require('waeup.Public')
1166    grok.template('applicantregemailsent')
[7714]1167    label = _('Your registration was successful.')
[7380]1168
[8629]1169    def update(self, email=None, applicant_id=None, password=None):
[7380]1170        self.email = email
[8629]1171        self.password = password
1172        self.applicant_id = applicant_id
[7380]1173        return
[10655]1174
1175class ExportJobContainerOverview(KofaPage):
1176    """Page that lists active applicant data export jobs and provides links
1177    to discard or download CSV files.
1178
1179    """
1180    grok.context(VirtualApplicantsExportJobContainer)
1181    grok.require('waeup.manageApplication')
1182    grok.name('index.html')
1183    grok.template('exportjobsindex')
1184    label = _('Applicant Data Exports')
1185    pnav = 3
1186
1187    def update(self, CREATE=None, DISCARD=None, job_id=None):
1188        if CREATE:
1189            self.redirect(self.url('@@start_export'))
1190            return
1191        if DISCARD and job_id:
1192            entry = self.context.entry_from_job_id(job_id)
1193            self.context.delete_export_entry(entry)
1194            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1195            self.context.logger.info(
1196                '%s - discarded: job_id=%s' % (ob_class, job_id))
1197            self.flash(_('Discarded export') + ' %s' % job_id)
1198        self.entries = doll_up(self, user=self.request.principal.id)
1199        return
1200
1201class ExportJobContainerJobStart(KofaPage):
1202    """Page that starts an applicants export job.
1203
1204    """
1205    grok.context(VirtualApplicantsExportJobContainer)
1206    grok.require('waeup.manageApplication')
1207    grok.name('start_export')
1208
1209    def update(self):
1210        exporter = 'applicants'
1211        container_code = self.context.__parent__.code
1212        job_id = self.context.start_export_job(exporter,
1213                                      self.request.principal.id,
1214                                      container=container_code)
1215
1216        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1217        self.context.logger.info(
1218            '%s - exported: %s (%s), job_id=%s'
1219            % (ob_class, exporter, container_code, job_id))
1220        self.flash(_('Export started.'))
1221        self.redirect(self.url(self.context))
1222        return
1223
1224    def render(self):
1225        return
1226
1227class ExportJobContainerDownload(ExportCSVView):
1228    """Page that downloads a students export csv file.
1229
1230    """
1231    grok.context(VirtualApplicantsExportJobContainer)
1232    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.