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

Last change on this file since 16217 was 16217, checked in by Henrik Bettermann, 4 years ago

Catch error if courses can't be saved (e.g. if not selected).

  • Property svn:keywords set to Id
File size: 77.0 KB
Line 
1## $Id: browser.py 16217 2020-08-28 06:48:35Z 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 basic applicants and related components.
19"""
20import os
21import sys
22import grok
23import transaction
24from cgi import escape
25from urllib import urlencode
26from datetime import datetime, date
27from time import time, sleep
28import xmlrpclib
29from zope.event import notify
30from zope.component import getUtility, queryUtility, createObject, getAdapter
31from zope.catalog.interfaces import ICatalog
32from zope.i18n import translate
33from zope.security import checkPermission
34from hurry.workflow.interfaces import (
35    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
36from reportlab.platypus.doctemplate import LayoutError
37from waeup.kofa.mandates.mandate import RefereeReportMandate
38from waeup.kofa.applicants.interfaces import (
39    IApplicant, IApplicantEdit, IApplicantsRoot,
40    IApplicantsContainer, IApplicantsContainerAdd,
41    IApplicantOnlinePayment, IApplicantsUtils,
42    IApplicantRegisterUpdate, ISpecialApplicant,
43    IApplicantRefereeReport
44    )
45from waeup.kofa.utils.helpers import html2dict
46from waeup.kofa.applicants.container import (
47    ApplicantsContainer, VirtualApplicantsExportJobContainer)
48from waeup.kofa.applicants.applicant import search
49from waeup.kofa.applicants.workflow import (
50    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED)
51from waeup.kofa.browser import (
52#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
53    DEFAULT_PASSPORT_IMAGE_PATH)
54from waeup.kofa.browser.layout import (
55    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
56from waeup.kofa.browser.interfaces import ICaptchaManager
57from waeup.kofa.browser.breadcrumbs import Breadcrumb
58from waeup.kofa.browser.layout import (
59    NullValidator, jsaction, action, UtilityView)
60from waeup.kofa.browser.pages import (
61    add_local_role, del_local_roles, doll_up, ExportCSVView)
62from waeup.kofa.interfaces import (
63    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF, DOCLINK,
64    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
65from waeup.kofa.interfaces import MessageFactory as _
66from waeup.kofa.permissions import get_users_with_local_roles
67from waeup.kofa.students.interfaces import IStudentsUtils
68from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
69from waeup.kofa.widgets.datewidget import (
70    FriendlyDateDisplayWidget,
71    FriendlyDatetimeDisplayWidget)
72
73grok.context(IKofaObject) # Make IKofaObject the default context
74
75WARNING = _('You can not edit your application records after final submission.'
76            ' You really want to submit?')
77
78class ApplicantsRootPage(KofaDisplayFormPage):
79    grok.context(IApplicantsRoot)
80    grok.name('index')
81    grok.require('waeup.Public')
82    form_fields = grok.AutoFields(IApplicantsRoot)
83    label = _('Applicants Section')
84    pnav = 3
85
86    def update(self):
87        super(ApplicantsRootPage, self).update()
88        return
89
90    @property
91    def introduction(self):
92        # Here we know that the cookie has been set
93        lang = self.request.cookies.get('kofa.language')
94        html = self.context.description_dict.get(lang,'')
95        if html == '':
96            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
97            html = self.context.description_dict.get(portal_language,'')
98        return html
99
100    @property
101    def containers(self):
102        if self.layout.isAuthenticated():
103            return self.context.values()
104        values = sorted([container for container in self.context.values()
105                         if not container.hidden and container.enddate],
106                        key=lambda value: value.enddate, reverse=True)
107        return values
108
109class ApplicantsSearchPage(KofaPage):
110    grok.context(IApplicantsRoot)
111    grok.name('search')
112    grok.require('waeup.viewApplication')
113    label = _('Find applicants')
114    search_button = _('Find applicant')
115    pnav = 3
116
117    def update(self, *args, **kw):
118        form = self.request.form
119        self.results = []
120        if 'searchterm' in form and form['searchterm']:
121            self.searchterm = form['searchterm']
122            self.searchtype = form['searchtype']
123        elif 'old_searchterm' in form:
124            self.searchterm = form['old_searchterm']
125            self.searchtype = form['old_searchtype']
126        else:
127            if 'search' in form:
128                self.flash(_('Empty search string'), type='warning')
129            return
130        self.results = search(query=self.searchterm,
131            searchtype=self.searchtype, view=self)
132        if not self.results:
133            self.flash(_('No applicant found.'), type='warning')
134        return
135
136class ApplicantsRootManageFormPage(KofaEditFormPage):
137    grok.context(IApplicantsRoot)
138    grok.name('manage')
139    grok.template('applicantsrootmanagepage')
140    form_fields = grok.AutoFields(IApplicantsRoot)
141    label = _('Manage applicants section')
142    pnav = 3
143    grok.require('waeup.manageApplication')
144    taboneactions = [_('Save')]
145    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
146    tabthreeactions1 = [_('Remove selected local roles')]
147    tabthreeactions2 = [_('Add local role')]
148    subunits = _('Applicants Containers')
149    doclink = DOCLINK + '/applicants.html'
150
151    def getLocalRoles(self):
152        roles = ILocalRolesAssignable(self.context)
153        return roles()
154
155    def getUsers(self):
156        return getUtility(IKofaUtils).getUsers()
157
158    #def getUsers(self):
159    #    """Get a list of all users.
160    #    """
161    #    for key, val in grok.getSite()['users'].items():
162    #        url = self.url(val)
163    #        yield(dict(url=url, name=key, val=val))
164
165    def getUsersWithLocalRoles(self):
166        return get_users_with_local_roles(self.context)
167
168    @jsaction(_('Remove selected'))
169    def delApplicantsContainers(self, **data):
170        form = self.request.form
171        if 'val_id' in form:
172            child_id = form['val_id']
173        else:
174            self.flash(_('No container selected!'), type='warning')
175            self.redirect(self.url(self.context, '@@manage')+'#tab2')
176            return
177        if not isinstance(child_id, list):
178            child_id = [child_id]
179        deleted = []
180        for id in child_id:
181            try:
182                del self.context[id]
183                deleted.append(id)
184            except:
185                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
186                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
187        if len(deleted):
188            self.flash(_('Successfully removed: ${a}',
189                mapping = {'a':', '.join(deleted)}))
190        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
191        self.context.logger.info(
192            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
193        self.redirect(self.url(self.context, '@@manage')+'#tab2')
194        return
195
196    @action(_('Add applicants container'), validator=NullValidator)
197    def addApplicantsContainer(self, **data):
198        self.redirect(self.url(self.context, '@@add'))
199        return
200
201    @action(_('Add local role'), validator=NullValidator)
202    def addLocalRole(self, **data):
203        return add_local_role(self,3, **data)
204
205    @action(_('Remove selected local roles'))
206    def delLocalRoles(self, **data):
207        return del_local_roles(self,3,**data)
208
209    @action(_('Save'), style='primary')
210    def save(self, **data):
211        self.applyData(self.context, **data)
212        description = getattr(self.context, 'description', None)
213        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
214        self.context.description_dict = html2dict(description, portal_language)
215        self.flash(_('Form has been saved.'))
216        return
217
218class ApplicantsContainerAddFormPage(KofaAddFormPage):
219    grok.context(IApplicantsRoot)
220    grok.require('waeup.manageApplication')
221    grok.name('add')
222    grok.template('applicantscontaineraddpage')
223    label = _('Add applicants container')
224    pnav = 3
225
226    form_fields = grok.AutoFields(
227        IApplicantsContainerAdd).omit('code').omit('title')
228
229    @action(_('Add applicants container'))
230    def addApplicantsContainer(self, **data):
231        year = data['year']
232        if not data['container_number'] and not data['year']:
233            self.flash(
234              _('You must select either Year of Entrance or Container Number.'),
235                type='warning')
236            return
237        if not data['container_number']:
238            code = u'%s%s' % (data['prefix'], year)
239        else:
240            code = u'%s%s' % (data['prefix'], data['container_number'])
241        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
242        title = u'%s' % apptypes_dict[data['prefix']][0]
243        if year:
244            title = u'%s %s/%s' % (title, year, year + 1)
245        if code in self.context.keys():
246            self.flash(
247              _('An applicants container for the same application '
248                'type and entrance year or container number '
249                'already exists in the database.'),
250                type='warning')
251            return
252        # Add new applicants container...
253        container = createObject(u'waeup.ApplicantsContainer')
254        self.applyData(container, **data)
255        container.code = code
256        container.title = title
257        self.context[code] = container
258        self.flash(_('Added:') + ' "%s".' % code)
259        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
260        self.context.logger.info('%s - added: %s' % (ob_class, code))
261        self.redirect(self.url(self.context, u'@@manage'))
262        return
263
264    @action(_('Cancel'), validator=NullValidator)
265    def cancel(self, **data):
266        self.redirect(self.url(self.context, '@@manage'))
267
268class ApplicantsRootBreadcrumb(Breadcrumb):
269    """A breadcrumb for applicantsroot.
270    """
271    grok.context(IApplicantsRoot)
272    title = _(u'Applicants')
273
274class ApplicantsContainerBreadcrumb(Breadcrumb):
275    """A breadcrumb for applicantscontainers.
276    """
277    grok.context(IApplicantsContainer)
278
279
280class ApplicantsExportsBreadcrumb(Breadcrumb):
281    """A breadcrumb for exports.
282    """
283    grok.context(VirtualApplicantsExportJobContainer)
284    title = _(u'Applicant Data Exports')
285    target = None
286
287class ApplicantBreadcrumb(Breadcrumb):
288    """A breadcrumb for applicants.
289    """
290    grok.context(IApplicant)
291
292    @property
293    def title(self):
294        """Get a title for a context.
295        """
296        return self.context.application_number
297
298class OnlinePaymentBreadcrumb(Breadcrumb):
299    """A breadcrumb for payments.
300    """
301    grok.context(IApplicantOnlinePayment)
302
303    @property
304    def title(self):
305        return self.context.p_id
306
307class RefereeReportBreadcrumb(Breadcrumb):
308    """A breadcrumb for referee reports.
309    """
310    grok.context(IApplicantRefereeReport)
311
312    @property
313    def title(self):
314        return self.context.r_id
315
316class ApplicantsStatisticsPage(KofaDisplayFormPage):
317    """Some statistics about applicants in a container.
318    """
319    grok.context(IApplicantsContainer)
320    grok.name('statistics')
321    grok.require('waeup.viewApplicationStatistics')
322    grok.template('applicantcontainerstatistics')
323
324    @property
325    def label(self):
326        return "%s" % self.context.title
327
328class ApplicantsContainerPage(KofaDisplayFormPage):
329    """The standard view for regular applicant containers.
330    """
331    grok.context(IApplicantsContainer)
332    grok.name('index')
333    grok.require('waeup.Public')
334    grok.template('applicantscontainerpage')
335    pnav = 3
336
337    @property
338    def form_fields(self):
339        form_fields = grok.AutoFields(IApplicantsContainer).omit(
340            'title', 'description')
341        form_fields[
342            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
343        form_fields[
344            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
345        if self.request.principal.id == 'zope.anybody':
346            form_fields = form_fields.omit(
347                'code', 'prefix', 'year', 'mode', 'hidden',
348                'strict_deadline', 'application_category',
349                'application_slip_notice', 'with_picture')
350        return form_fields
351
352    @property
353    def introduction(self):
354        # Here we know that the cookie has been set
355        lang = self.request.cookies.get('kofa.language')
356        html = self.context.description_dict.get(lang,'')
357        if html == '':
358            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
359            html = self.context.description_dict.get(portal_language,'')
360        return html
361
362    @property
363    def label(self):
364        return "%s" % self.context.title
365
366class ApplicantsContainerManageFormPage(KofaEditFormPage):
367    grok.context(IApplicantsContainer)
368    grok.name('manage')
369    grok.template('applicantscontainermanagepage')
370    form_fields = grok.AutoFields(IApplicantsContainer)
371    taboneactions = [_('Save'),_('Cancel')]
372    tabtwoactions = [_('Remove selected'),_('Cancel'),
373        _('Create students from selected')]
374    tabthreeactions1 = [_('Remove selected local roles')]
375    tabthreeactions2 = [_('Add local role')]
376    # Use friendlier date widget...
377    grok.require('waeup.manageApplication')
378    doclink = DOCLINK + '/applicants.html'
379
380    @property
381    def label(self):
382        return _('Manage applicants container')
383
384    pnav = 3
385
386    @property
387    def showApplicants(self):
388        if self.context.counts[1] < 1000:
389            return True
390        return False
391
392    def getLocalRoles(self):
393        roles = ILocalRolesAssignable(self.context)
394        return roles()
395
396    #def getUsers(self):
397    #    """Get a list of all users.
398    #    """
399    #    for key, val in grok.getSite()['users'].items():
400    #        url = self.url(val)
401    #        yield(dict(url=url, name=key, val=val))
402
403    def getUsers(self):
404        return getUtility(IKofaUtils).getUsers()
405
406    def getUsersWithLocalRoles(self):
407        return get_users_with_local_roles(self.context)
408
409    @action(_('Save'), style='primary')
410    def save(self, **data):
411        changed_fields = self.applyData(self.context, **data)
412        if changed_fields:
413            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
414        else:
415            changed_fields = []
416        description = getattr(self.context, 'description', None)
417        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
418        self.context.description_dict = html2dict(description, portal_language)
419        self.flash(_('Form has been saved.'))
420        fields_string = ' + '.join(changed_fields)
421        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
422        return
423
424    @jsaction(_('Remove selected'))
425    def delApplicant(self, **data):
426        form = self.request.form
427        if 'val_id' in form:
428            child_id = form['val_id']
429        else:
430            self.flash(_('No applicant selected!'), type='warning')
431            self.redirect(self.url(self.context, '@@manage')+'#tab2')
432            return
433        if not isinstance(child_id, list):
434            child_id = [child_id]
435        deleted = []
436        for id in child_id:
437            try:
438                del self.context[id]
439                deleted.append(id)
440            except:
441                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
442                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
443        if len(deleted):
444            self.flash(_('Successfully removed: ${a}',
445                mapping = {'a':', '.join(deleted)}))
446        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
447        return
448
449    @action(_('Create students from selected'))
450    def createStudents(self, **data):
451        if not checkPermission('waeup.createStudents', self.context):
452            self.flash(
453                _('You don\'t have permission to create student records.'),
454                type='warning')
455            self.redirect(self.url(self.context, '@@manage')+'#tab2')
456            return
457        form = self.request.form
458        if 'val_id' in form:
459            child_id = form['val_id']
460        else:
461            self.flash(_('No applicant selected!'), type='warning')
462            self.redirect(self.url(self.context, '@@manage')+'#tab2')
463            return
464        if not isinstance(child_id, list):
465            child_id = [child_id]
466        created = []
467        if len(child_id) > 10 and self.request.principal.id != 'admin':
468            self.flash(_('A maximum of 10 applicants can be selected!'),
469                       type='warning')
470            self.redirect(self.url(self.context, '@@manage')+'#tab2')
471            return
472        for id in child_id:
473            success, msg = self.context[id].createStudent(view=self)
474            if success:
475                created.append(id)
476        if len(created):
477            self.flash(_('${a} students successfully created.',
478                mapping = {'a': len(created)}))
479        else:
480            self.flash(_('No student could be created.'), type='warning')
481        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
482        return
483
484    @action(_('Cancel'), validator=NullValidator)
485    def cancel(self, **data):
486        self.redirect(self.url(self.context))
487        return
488
489    @action(_('Add local role'), validator=NullValidator)
490    def addLocalRole(self, **data):
491        return add_local_role(self,3, **data)
492
493    @action(_('Remove selected local roles'))
494    def delLocalRoles(self, **data):
495        return del_local_roles(self,3,**data)
496
497class ApplicantAddFormPage(KofaAddFormPage):
498    """Add-form to add an applicant.
499    """
500    grok.context(IApplicantsContainer)
501    grok.require('waeup.manageApplication')
502    grok.name('addapplicant')
503    #grok.template('applicantaddpage')
504    form_fields = grok.AutoFields(IApplicant).select(
505        'firstname', 'middlename', 'lastname',
506        'email', 'phone')
507    label = _('Add applicant')
508    pnav = 3
509    doclink = DOCLINK + '/applicants.html'
510
511    @action(_('Create application record'))
512    def addApplicant(self, **data):
513        applicant = createObject(u'waeup.Applicant')
514        self.applyData(applicant, **data)
515        self.context.addApplicant(applicant)
516        self.flash(_('Application record created.'))
517        self.redirect(
518            self.url(self.context[applicant.application_number], 'index'))
519        return
520
521class ApplicantsContainerPrefillFormPage(KofaAddFormPage):
522    """Form to pre-fill applicants containers.
523    """
524    grok.context(IApplicantsContainer)
525    grok.require('waeup.manageApplication')
526    grok.name('prefill')
527    grok.template('prefillcontainer')
528    label = _('Pre-fill container')
529    pnav = 3
530    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
531
532    def update(self):
533        if self.context.mode == 'update':
534            self.flash(_('Container must be in create mode to be pre-filled.'),
535                type='danger')
536            self.redirect(self.url(self.context))
537            return
538        super(ApplicantsContainerPrefillFormPage, self).update()
539        return
540
541    @action(_('Pre-fill now'), style='primary')
542    def addApplicants(self):
543        form = self.request.form
544        if 'number' in form and form['number']:
545            number = int(form['number'])
546        for i in range(number):
547            applicant = createObject(u'waeup.Applicant')
548            self.context.addApplicant(applicant)
549        self.flash(_('%s application records created.' % number))
550        self.context.writeLogMessage(self, '%s applicants created' % (number))
551        self.redirect(self.url(self.context, 'index'))
552        return
553
554    @action(_('Cancel'), validator=NullValidator)
555    def cancel(self, **data):
556        self.redirect(self.url(self.context))
557        return
558
559class ApplicantsContainerPurgeFormPage(KofaEditFormPage):
560    """Form to purge applicants containers.
561    """
562    grok.context(IApplicantsContainer)
563    grok.require('waeup.manageApplication')
564    grok.name('purge')
565    grok.template('purgecontainer')
566    label = _('Purge container')
567    pnav = 3
568    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
569
570    @action(_('Remove initialized records'),
571              tooltip=_('Don\'t use if application is in progress!'),
572              warning=_('Are you really sure?'),
573              style='primary')
574    def purgeInitialized(self):
575        form = self.request.form
576        purged = 0
577        keys = [key for key in self.context.keys()]
578        for key in keys:
579            if self.context[key].state == 'initialized':
580                del self.context[key]
581                purged += 1
582        self.flash(_('%s application records purged.' % purged))
583        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
584        self.redirect(self.url(self.context, 'index'))
585        return
586
587    @action(_('Cancel'), validator=NullValidator)
588    def cancel(self, **data):
589        self.redirect(self.url(self.context))
590        return
591
592class ApplicantDisplayFormPage(KofaDisplayFormPage):
593    """A display view for applicant data.
594    """
595    grok.context(IApplicant)
596    grok.name('index')
597    grok.require('waeup.viewApplication')
598    grok.template('applicantdisplaypage')
599    label = _('Applicant')
600    pnav = 3
601    hide_hint = False
602
603    @property
604    def display_refereereports(self):
605        if self.context.refereereports:
606            return True
607        return False
608
609    @property
610    def file_links(self):
611        html = ''
612        file_store = getUtility(IExtFileStore)
613        additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
614        for filename in additional_files:
615            pdf = getUtility(IExtFileStore).getFileByContext(
616                self.context, attr=filename[1])
617            if pdf:
618                html += '<a href="%s">%s</a>, ' % (self.url(
619                    self.context, filename[1]), filename[0])
620        html = html.strip(', ')
621        return html
622
623    @property
624    def display_payments(self):
625        if self.context.payments:
626            return True
627        if self.context.special:
628            return True
629        return getattr(self.context.__parent__, 'application_fee', None)
630
631    @property
632    def form_fields(self):
633        if self.context.special:
634            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
635        else:
636            form_fields = grok.AutoFields(IApplicant).omit(
637                'locked', 'course_admitted', 'password', 'suspended')
638        return form_fields
639
640    @property
641    def target(self):
642        return getattr(self.context.__parent__, 'prefix', None)
643
644    @property
645    def separators(self):
646        return getUtility(IApplicantsUtils).SEPARATORS_DICT
647
648    def update(self):
649        self.passport_url = self.url(self.context, 'passport.jpg')
650        # Mark application as started if applicant logs in for the first time
651        usertype = getattr(self.request.principal, 'user_type', None)
652        if usertype == 'applicant' and \
653            IWorkflowState(self.context).getState() == INITIALIZED:
654            IWorkflowInfo(self.context).fireTransition('start')
655        if usertype == 'applicant' and self.context.state == 'created':
656            session = '%s/%s' % (self.context.__parent__.year,
657                                 self.context.__parent__.year+1)
658            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
659            msg = _(
660                '\n <strong>Congratulations!</strong>' +
661                ' You have been offered provisional admission into the' +
662                ' ${c} Academic Session of ${d}.'
663                ' Your student record has been created for you.' +
664                ' Please, logout again and proceed to the' +
665                ' login page of the portal.'
666                ' Then enter your new student credentials:' +
667                ' user name= ${a}, password = ${b}.' +
668                ' Change your password when you have logged in.',
669                mapping = {
670                    'a':self.context.student_id,
671                    'b':self.context.application_number,
672                    'c':session,
673                    'd':title}
674                )
675            self.flash(msg)
676        return
677
678    @property
679    def hasPassword(self):
680        if self.context.password:
681            return _('set')
682        return _('unset')
683
684    @property
685    def label(self):
686        container_title = self.context.__parent__.title
687        return _('${a} <br /> Application Record ${b}', mapping = {
688            'a':container_title, 'b':self.context.application_number})
689
690    def getCourseAdmitted(self):
691        """Return link, title and code in html format to the certificate
692           admitted.
693        """
694        course_admitted = self.context.course_admitted
695        if getattr(course_admitted, '__parent__',None):
696            url = self.url(course_admitted)
697            title = course_admitted.title
698            code = course_admitted.code
699            return '<a href="%s">%s - %s</a>' %(url,code,title)
700        return ''
701
702class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
703    grok.context(IApplicant)
704    grok.name('base')
705
706    @property
707    def form_fields(self):
708        form_fields = grok.AutoFields(IApplicant).select(
709            'applicant_id', 'reg_number', 'email', 'course1')
710        if self.context.__parent__.prefix in ('special',):
711            form_fields['reg_number'].field.title = u'Identification Number'
712            return form_fields
713        return form_fields
714
715class CreateStudentPage(UtilityView, grok.View):
716    """Create a student object from applicant data.
717    """
718    grok.context(IApplicant)
719    grok.name('createstudent')
720    grok.require('waeup.createStudents')
721
722    def update(self):
723        success, msg = self.context.createStudent(view=self)
724        if success:
725            self.flash(msg)
726        else:
727            self.flash(msg, type='warning')
728        self.redirect(self.url(self.context))
729        return
730
731    def render(self):
732        return
733
734class CreateAllStudentsPage(KofaPage):
735    """Create all student objects from applicant data
736    in the root container or in a specific applicants container only.
737    Only PortalManagers or StudentCreators can do this.
738    """
739    #grok.context(IApplicantsContainer)
740    grok.name('createallstudents')
741    grok.require('waeup.createStudents')
742    label = _('Student Record Creation Report')
743
744    def update(self):
745        grok.getSite()['configuration'].maintmode_enabled_by = u'admin'
746        transaction.commit()
747        # Wait 10 seconds for all transactions to be finished.
748        # Do not wait in tests.
749        if not self.request.principal.id == 'zope.mgr':
750            sleep(10)
751        cat = getUtility(ICatalog, name='applicants_catalog')
752        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
753        created = []
754        failed = []
755        container_only = False
756        applicants_root = grok.getSite()['applicants']
757        if isinstance(self.context, ApplicantsContainer):
758            container_only = True
759        for result in results:
760            if container_only and result.__parent__ is not self.context:
761                continue
762            success, msg = result.createStudent(view=self)
763            if success:
764                created.append(result.applicant_id)
765            else:
766                failed.append(
767                    (result.applicant_id, self.url(result, 'manage'), msg))
768                ob_class = self.__implemented__.__name__.replace(
769                    'waeup.kofa.','')
770        grok.getSite()['configuration'].maintmode_enabled_by = None
771        self.successful = ', '.join(created)
772        self.failed = failed
773        return
774
775
776class ApplicationFeePaymentAddPage(UtilityView, grok.View):
777    """ Page to add an online payment ticket
778    """
779    grok.context(IApplicant)
780    grok.name('addafp')
781    grok.require('waeup.payApplicant')
782    factory = u'waeup.ApplicantOnlinePayment'
783
784    @property
785    def custom_requirements(self):
786        return ''
787
788    def update(self):
789        # Additional requirements in custom packages.
790        if self.custom_requirements:
791            self.flash(
792                self.custom_requirements,
793                type='danger')
794            self.redirect(self.url(self.context))
795            return
796        if not self.context.special:
797            for ticket in self.context.payments:
798                if ticket.p_state == 'paid':
799                      self.flash(
800                          _('This type of payment has already been made.'),
801                          type='warning')
802                      self.redirect(self.url(self.context))
803                      return
804        applicants_utils = getUtility(IApplicantsUtils)
805        container = self.context.__parent__
806        payment = createObject(self.factory)
807        failure = applicants_utils.setPaymentDetails(
808            container, payment, self.context)
809        if failure is not None:
810            self.flash(failure, type='danger')
811            self.redirect(self.url(self.context))
812            return
813        self.context[payment.p_id] = payment
814        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
815        self.flash(_('Payment ticket created.'))
816        self.redirect(self.url(payment))
817        return
818
819    def render(self):
820        return
821
822
823class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
824    """ Page to view an online payment ticket
825    """
826    grok.context(IApplicantOnlinePayment)
827    grok.name('index')
828    grok.require('waeup.viewApplication')
829    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
830    form_fields[
831        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
832    form_fields[
833        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
834    pnav = 3
835
836    @property
837    def label(self):
838        return _('${a}: Online Payment Ticket ${b}', mapping = {
839            'a':self.context.__parent__.display_fullname,
840            'b':self.context.p_id})
841
842class OnlinePaymentApprovePage(UtilityView, grok.View):
843    """ Approval view
844    """
845    grok.context(IApplicantOnlinePayment)
846    grok.name('approve')
847    grok.require('waeup.managePortal')
848
849    def update(self):
850        flashtype, msg, log = self.context.approveApplicantPayment()
851        if log is not None:
852            applicant = self.context.__parent__
853            # Add log message to applicants.log
854            applicant.writeLogMessage(self, log)
855            # Add log message to payments.log
856            self.context.logger.info(
857                '%s,%s,%s,%s,%s,,,,,,' % (
858                applicant.applicant_id,
859                self.context.p_id, self.context.p_category,
860                self.context.amount_auth, self.context.r_code))
861        self.flash(msg, type=flashtype)
862        return
863
864    def render(self):
865        self.redirect(self.url(self.context, '@@index'))
866        return
867
868class ExportPDFPaymentSlipPage(UtilityView, grok.View):
869    """Deliver a PDF slip of the context.
870    """
871    grok.context(IApplicantOnlinePayment)
872    grok.name('payment_slip.pdf')
873    grok.require('waeup.viewApplication')
874    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
875    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
876    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
877    #prefix = 'form'
878    note = None
879
880    @property
881    def title(self):
882        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
883        return translate(_('Payment Data'), 'waeup.kofa',
884            target_language=portal_language)
885
886    @property
887    def label(self):
888        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
889        return translate(_('Online Payment Slip'),
890            'waeup.kofa', target_language=portal_language) \
891            + ' %s' % self.context.p_id
892
893    @property
894    def payment_slip_download_warning(self):
895        if self.context.__parent__.state not in (
896            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
897            return _('Please submit the application form before '
898                     'trying to download payment slips.')
899        return ''
900
901    def render(self):
902        if self.payment_slip_download_warning:
903            self.flash(self.payment_slip_download_warning, type='danger')
904            self.redirect(self.url(self.context))
905            return
906        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
907            self.request)
908        students_utils = getUtility(IStudentsUtils)
909        return students_utils.renderPDF(self,'payment_slip.pdf',
910            self.context.__parent__, applicantview, note=self.note)
911
912class ExportPDFPageApplicationSlip(UtilityView, grok.View):
913    """Deliver a PDF slip of the context.
914    """
915    grok.context(IApplicant)
916    grok.name('application_slip.pdf')
917    grok.require('waeup.viewApplication')
918    #prefix = 'form'
919
920    def update(self):
921        if self.context.state in ('initialized', 'started', 'paid'):
922            self.flash(
923                _('Please pay and submit before trying to download '
924                  'the application slip.'), type='warning')
925            return self.redirect(self.url(self.context))
926        return
927
928    def render(self):
929        try:
930            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
931                view=self)
932        except IOError:
933            self.flash(
934                _('Your image file is corrupted. '
935                  'Please replace.'), type='danger')
936            return self.redirect(self.url(self.context))
937        except LayoutError, err:
938            self.flash(
939                'PDF file could not be created. Reportlab error message: %s'
940                % escape(err.message),
941                type="danger")
942            return self.redirect(self.url(self.context))
943        self.response.setHeader(
944            'Content-Type', 'application/pdf')
945        return pdfstream
946
947def handle_img_upload(upload, context, view):
948    """Handle upload of applicant image.
949
950    Returns `True` in case of success or `False`.
951
952    Please note that file pointer passed in (`upload`) most probably
953    points to end of file when leaving this function.
954    """
955    max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
956    size = file_size(upload)
957    if size > max_upload_size:
958        view.flash(_('Uploaded image is too big!'), type='danger')
959        return False
960    dummy, ext = os.path.splitext(upload.filename)
961    ext.lower()
962    if ext != '.jpg':
963        view.flash(_('jpg file extension expected.'), type='danger')
964        return False
965    upload.seek(0) # file pointer moved when determining size
966    store = getUtility(IExtFileStore)
967    file_id = IFileStoreNameChooser(context).chooseName()
968    try:
969        store.createFile(file_id, upload)
970    except IOError:
971        view.flash(_('Image file cannot be changed.'), type='danger')
972        return False
973    return True
974
975def handle_file_upload(upload, context, view, attr=None):
976    """Handle upload of applicant files.
977
978    Returns `True` in case of success or `False`.
979
980    Please note that file pointer passed in (`upload`) most probably
981    points to end of file when leaving this function.
982    """
983    size = file_size(upload)
984    max_upload_size = 1024 * getUtility(IStudentsUtils).MAX_KB
985    if size > max_upload_size:
986        view.flash(_('Uploaded file is too big!'))
987        return False
988    dummy, ext = os.path.splitext(upload.filename)
989    ext.lower()
990    if ext != '.pdf':
991        view.flash(_('pdf file extension expected.'))
992        return False
993    upload.seek(0) # file pointer moved when determining size
994    store = getUtility(IExtFileStore)
995    file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
996    store.createFile(file_id, upload)
997    return True
998
999class ApplicantManageFormPage(KofaEditFormPage):
1000    """A full edit view for applicant data.
1001    """
1002    grok.context(IApplicant)
1003    grok.name('manage')
1004    grok.require('waeup.manageApplication')
1005    grok.template('applicanteditpage')
1006    manage_applications = True
1007    pnav = 3
1008    display_actions = [[_('Save'), _('Finally Submit')],
1009        [_('Add online payment ticket'),_('Remove selected tickets')]]
1010
1011    @property
1012    def display_payments(self):
1013        if self.context.payments:
1014            return True
1015        if self.context.special:
1016            return True
1017        return getattr(self.context.__parent__, 'application_fee', None)
1018
1019    @property
1020    def display_refereereports(self):
1021        if self.context.refereereports:
1022            return True
1023        return False
1024
1025    def display_fileupload(self, filename):
1026        """This method can be used in custom packages to avoid unneccessary
1027        file uploads.
1028        """
1029        return True
1030
1031    @property
1032    def form_fields(self):
1033        if self.context.special:
1034            form_fields = grok.AutoFields(ISpecialApplicant)
1035            form_fields['applicant_id'].for_display = True
1036        else:
1037            form_fields = grok.AutoFields(IApplicant)
1038            form_fields['student_id'].for_display = True
1039            form_fields['applicant_id'].for_display = True
1040        return form_fields
1041
1042    @property
1043    def target(self):
1044        return getattr(self.context.__parent__, 'prefix', None)
1045
1046    @property
1047    def separators(self):
1048        return getUtility(IApplicantsUtils).SEPARATORS_DICT
1049
1050    @property
1051    def custom_upload_requirements(self):
1052        return ''
1053
1054    def update(self):
1055        super(ApplicantManageFormPage, self).update()
1056        max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
1057        self.wf_info = IWorkflowInfo(self.context)
1058        self.max_upload_size = string_from_bytes(max_upload_size)
1059        self.upload_success = None
1060        upload = self.request.form.get('form.passport', None)
1061        if upload:
1062            if self.custom_upload_requirements:
1063                self.flash(
1064                    self.custom_upload_requirements,
1065                    type='danger')
1066                self.redirect(self.url(self.context))
1067                return
1068            # We got a fresh upload, upload_success is
1069            # either True or False
1070            self.upload_success = handle_img_upload(
1071                upload, self.context, self)
1072            if self.upload_success:
1073                self.context.writeLogMessage(self, 'saved: passport')
1074        file_store = getUtility(IExtFileStore)
1075        self.additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
1076        for filename in self.additional_files:
1077            upload = self.request.form.get(filename[1], None)
1078            if upload:
1079                # We got a fresh file upload
1080                success = handle_file_upload(
1081                    upload, self.context, self, attr=filename[1])
1082                if success:
1083                    self.context.writeLogMessage(
1084                        self, 'saved: %s' % filename[1])
1085                else:
1086                    self.upload_success = False
1087        self.max_file_upload_size = string_from_bytes(
1088            1024*getUtility(IStudentsUtils).MAX_KB)
1089        return
1090
1091    @property
1092    def label(self):
1093        container_title = self.context.__parent__.title
1094        return _('${a} <br /> Application Form ${b}', mapping = {
1095            'a':container_title, 'b':self.context.application_number})
1096
1097    def getTransitions(self):
1098        """Return a list of dicts of allowed transition ids and titles.
1099
1100        Each list entry provides keys ``name`` and ``title`` for
1101        internal name and (human readable) title of a single
1102        transition.
1103        """
1104        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
1105            if not t[0] in ('pay', 'create')]
1106        return [dict(name='', title=_('No transition'))] +[
1107            dict(name=x, title=y) for x, y in allowed_transitions]
1108
1109    def saveCourses(self, changed_fields):
1110        """In custom packages we needed to customize the certificate
1111        select widget. We just save course1 and course2 if these customized
1112        fields appear in the form.
1113        """
1114        error = None
1115        return error, changed_fields
1116
1117    @action(_('Save'), style='primary')
1118    def save(self, **data):
1119        form = self.request.form
1120        password = form.get('password', None)
1121        password_ctl = form.get('control_password', None)
1122        if password:
1123            validator = getUtility(IPasswordValidator)
1124            errors = validator.validate_password(password, password_ctl)
1125            if errors:
1126                self.flash( ' '.join(errors), type='danger')
1127                return
1128        if self.upload_success is False:  # False is not None!
1129            # Error during image upload. Ignore other values.
1130            return
1131        changed_fields = self.applyData(self.context, **data)
1132        # Turn list of lists into single list
1133        if changed_fields:
1134            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1135        else:
1136            changed_fields = []
1137        error, changed_fields = self.saveCourses(changed_fields)
1138        if error:
1139            self.flash(error)
1140            return
1141        if password:
1142            # Now we know that the form has no errors and can set password ...
1143            IUserAccount(self.context).setPassword(password)
1144            changed_fields.append('password')
1145        fields_string = ' + '.join(changed_fields)
1146        trans_id = form.get('transition', None)
1147        if trans_id:
1148            self.wf_info.fireTransition(trans_id)
1149        self.flash(_('Form has been saved.'))
1150        if fields_string:
1151            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
1152        return
1153
1154    def unremovable(self, ticket):
1155        return False
1156
1157    # This method is also used by the ApplicantEditFormPage
1158    def delPaymentTickets(self, **data):
1159        form = self.request.form
1160        if 'val_id' in form:
1161            child_id = form['val_id']
1162        else:
1163            self.flash(_('No payment selected.'), type='warning')
1164            self.redirect(self.url(self.context))
1165            return
1166        if not isinstance(child_id, list):
1167            child_id = [child_id]
1168        deleted = []
1169        for id in child_id:
1170            # Applicants are not allowed to remove used payment tickets
1171            if not self.unremovable(self.context[id]):
1172                try:
1173                    del self.context[id]
1174                    deleted.append(id)
1175                except:
1176                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1177                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1178        if len(deleted):
1179            self.flash(_('Successfully removed: ${a}',
1180                mapping = {'a':', '.join(deleted)}))
1181            self.context.writeLogMessage(
1182                self, 'removed: % s' % ', '.join(deleted))
1183        return
1184
1185    # We explicitely want the forms to be validated before payment tickets
1186    # can be created. If no validation is requested, use
1187    # 'validator=NullValidator' in the action directive
1188    @action(_('Add online payment ticket'), style='primary')
1189    def addPaymentTicket(self, **data):
1190        self.redirect(self.url(self.context, '@@addafp'))
1191        return
1192
1193    @jsaction(_('Remove selected tickets'))
1194    def removePaymentTickets(self, **data):
1195        self.delPaymentTickets(**data)
1196        self.redirect(self.url(self.context) + '/@@manage')
1197        return
1198
1199    # Not used in base package
1200    def file_exists(self, attr):
1201        file = getUtility(IExtFileStore).getFileByContext(
1202            self.context, attr=attr)
1203        if file:
1204            return True
1205        else:
1206            return False
1207
1208class ApplicantEditFormPage(ApplicantManageFormPage):
1209    """An applicant-centered edit view for applicant data.
1210    """
1211    grok.context(IApplicantEdit)
1212    grok.name('edit')
1213    grok.require('waeup.handleApplication')
1214    grok.template('applicanteditpage')
1215    manage_applications = False
1216    submit_state = PAID
1217    mandate_days = 31
1218
1219    @property
1220    def display_refereereports(self):
1221        return False
1222
1223    @property
1224    def form_fields(self):
1225        if self.context.special:
1226            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1227                'locked', 'suspended')
1228            form_fields['applicant_id'].for_display = True
1229        else:
1230            form_fields = grok.AutoFields(IApplicantEdit).omit(
1231                'locked', 'course_admitted', 'student_id',
1232                'suspended'
1233                )
1234            form_fields['applicant_id'].for_display = True
1235            form_fields['reg_number'].for_display = True
1236        return form_fields
1237
1238    @property
1239    def display_actions(self):
1240        state = IWorkflowState(self.context).getState()
1241        # If the form is unlocked, applicants are allowed to save the form
1242        # and remove unused tickets.
1243        actions = [[_('Save')], [_('Remove selected tickets')]]
1244        # Only in state started they can also add tickets.
1245        if state == STARTED:
1246            actions = [[_('Save')],
1247                [_('Add online payment ticket'),_('Remove selected tickets')]]
1248        # In state paid, they can submit the data and further add tickets
1249        # if the application is special.
1250        elif self.context.special and state == PAID:
1251            actions = [[_('Save'), _('Finally Submit')],
1252                [_('Add online payment ticket'),_('Remove selected tickets')]]
1253        elif state == PAID:
1254            actions = [[_('Save'), _('Finally Submit')],
1255                [_('Remove selected tickets')]]
1256        return actions
1257
1258    def unremovable(self, ticket):
1259        return ticket.r_code
1260
1261    def emit_lock_message(self):
1262        self.flash(_('The requested form is locked (read-only).'),
1263                   type='warning')
1264        self.redirect(self.url(self.context))
1265        return
1266
1267    def update(self):
1268        if self.context.locked or (
1269            self.context.__parent__.expired and
1270            self.context.__parent__.strict_deadline):
1271            self.emit_lock_message()
1272            return
1273        super(ApplicantEditFormPage, self).update()
1274        return
1275
1276    def dataNotComplete(self, data):
1277        if self.context.__parent__.with_picture:
1278            store = getUtility(IExtFileStore)
1279            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1280                return _('No passport picture uploaded.')
1281            if not self.request.form.get('confirm_passport', False):
1282                return _('Passport picture confirmation box not ticked.')
1283        return False
1284
1285    # We explicitely want the forms to be validated before payment tickets
1286    # can be created. If no validation is requested, use
1287    # 'validator=NullValidator' in the action directive
1288    @action(_('Add online payment ticket'), style='primary')
1289    def addPaymentTicket(self, **data):
1290        self.redirect(self.url(self.context, '@@addafp'))
1291        return
1292
1293    @jsaction(_('Remove selected tickets'))
1294    def removePaymentTickets(self, **data):
1295        self.delPaymentTickets(**data)
1296        self.redirect(self.url(self.context) + '/@@edit')
1297        return
1298
1299    def saveCourses(self):
1300        """In custom packages we needed to customize the certificate
1301        select widget. We just save course1 and course2 if these customized
1302        fields appear in the form.
1303        """
1304        return
1305
1306    @action(_('Save'), style='primary')
1307    def save(self, **data):
1308        if self.upload_success is False:  # False is not None!
1309            # Error during image upload. Ignore other values.
1310            return
1311        self.applyData(self.context, **data)
1312        error = self.saveCourses()
1313        if error:
1314            self.flash(error)
1315            return
1316        self.flash(_('Form has been saved.'))
1317        return
1318
1319    def informReferees(self):
1320        site = grok.getSite()
1321        kofa_utils = getUtility(IKofaUtils)
1322        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1323        failed = ''
1324        emails_sent = 0
1325        for referee in self.context.referees:
1326            if referee.email_sent:
1327                continue
1328            mandate = RefereeReportMandate(days=self.mandate_days)
1329            mandate.params['name'] = referee.name
1330            mandate.params['email'] = referee.email
1331            mandate.params[
1332                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1333                    self.context.__parent__.code,
1334                    self.context.application_number)
1335            mandate.params['redirect_path2'] = ''
1336            mandate.params['applicant_id'] = self.context.applicant_id
1337            site['mandates'].addMandate(mandate)
1338            # Send invitation email
1339            args = {'mandate_id':mandate.mandate_id}
1340            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1341            url_info = u'Report link: %s' % mandate_url
1342            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1343            if success:
1344                emails_sent += 1
1345                self.context.writeLogMessage(
1346                    self, 'email sent: %s' % referee.email)
1347                referee.email_sent = True
1348            else:
1349                failed += '%s ' % referee.email
1350        return failed, emails_sent
1351
1352    @action(_('Finally Submit'), warning=WARNING)
1353    def finalsubmit(self, **data):
1354        if self.upload_success is False:  # False is not None!
1355            return # error during image upload. Ignore other values
1356        dnt = self.dataNotComplete(data)
1357        if dnt:
1358            self.flash(dnt, type='danger')
1359            return
1360        self.applyData(self.context, **data)
1361        state = IWorkflowState(self.context).getState()
1362        # This shouldn't happen, but the application officer
1363        # might have forgotten to lock the form after changing the state
1364        if state != self.submit_state:
1365            self.flash(_('The form cannot be submitted. Wrong state!'),
1366                       type='danger')
1367            return
1368        msg = _('Form has been submitted.')
1369        # Create mandates and send emails to referees
1370        if getattr(self.context, 'referees', None):
1371            failed, emails_sent = self.informReferees()
1372            if failed:
1373                self.flash(
1374                    _('Some invitation emails could not be sent:') + failed,
1375                    type='danger')
1376                return
1377            msg = _('Form has been successfully submitted and '
1378                    '${a} invitation emails were sent.',
1379                    mapping = {'a':  emails_sent})
1380        IWorkflowInfo(self.context).fireTransition('submit')
1381        # application_date is used in export files for sorting.
1382        # We can thus store utc.
1383        self.context.application_date = datetime.utcnow()
1384        self.flash(msg)
1385        self.redirect(self.url(self.context))
1386        return
1387
1388class PassportImage(grok.View):
1389    """Renders the passport image for applicants.
1390    """
1391    grok.name('passport.jpg')
1392    grok.context(IApplicant)
1393    grok.require('waeup.viewApplication')
1394
1395    def render(self):
1396        # A filename chooser turns a context into a filename suitable
1397        # for file storage.
1398        image = getUtility(IExtFileStore).getFileByContext(self.context)
1399        self.response.setHeader('Content-Type', 'image/jpeg')
1400        if image is None:
1401            # show placeholder image
1402            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1403        return image
1404
1405class PassportImageForReport(PassportImage):
1406    """Renders the passport image for applicants for referee reports.
1407    """
1408    grok.name('passport_for_report.jpg')
1409    grok.context(IApplicant)
1410    grok.require('waeup.Public')
1411
1412    def render(self):
1413        # Check mandate
1414        form = self.request.form
1415        self.mandate_id = form.get('mandate_id', None)
1416        self.mandates = grok.getSite()['mandates']
1417        mandate = self.mandates.get(self.mandate_id, None)
1418        if mandate is None:
1419            self.flash(_('No mandate.'), type='warning')
1420            self.redirect(self.application_url())
1421            return
1422        if mandate:
1423            # Check the mandate expiration date after redirect again
1424            if mandate.expires < datetime.utcnow():
1425                self.flash(_('Mandate expired.'),
1426                           type='warning')
1427                self.redirect(self.application_url())
1428                return
1429            # Check if mandate allows access
1430            if mandate.params.get('applicant_id') != self.context.applicant_id:
1431                self.flash(_('Wrong mandate.'),
1432                           type='warning')
1433                self.redirect(self.application_url())
1434                return
1435            return super(PassportImageForReport, self).render()
1436        return
1437
1438class ApplicantRegistrationPage(KofaAddFormPage):
1439    """Captcha'd registration page for applicants.
1440    """
1441    grok.context(IApplicantsContainer)
1442    grok.name('register')
1443    grok.require('waeup.Anonymous')
1444    grok.template('applicantregister')
1445
1446    @property
1447    def form_fields(self):
1448        form_fields = None
1449        if self.context.mode == 'update':
1450            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1451                'lastname','reg_number','email')
1452        else: #if self.context.mode == 'create':
1453            form_fields = grok.AutoFields(IApplicantEdit).select(
1454                'firstname', 'middlename', 'lastname', 'email', 'phone')
1455        return form_fields
1456
1457    @property
1458    def label(self):
1459        return _('Apply for ${a}',
1460            mapping = {'a':self.context.title})
1461
1462    def update(self):
1463        if self.context.expired:
1464            self.flash(_('Outside application period.'), type='warning')
1465            self.redirect(self.url(self.context))
1466            return
1467        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1468        if blocker:
1469            self.flash(_('The portal is in maintenance mode '
1470                        'and registration temporarily disabled.'),
1471                       type='warning')
1472            self.redirect(self.url(self.context))
1473            return
1474        # Handle captcha
1475        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1476        self.captcha_result = self.captcha.verify(self.request)
1477        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1478        return
1479
1480    def _redirect(self, email, password, applicant_id):
1481        # Forward only email to landing page in base package.
1482        self.redirect(self.url(self.context, 'registration_complete',
1483            data = dict(email=email)))
1484        return
1485
1486    @property
1487    def _postfix(self):
1488        """In customized packages we can add a container dependent string if
1489        applicants have been imported into several containers.
1490        """
1491        return ''
1492
1493    @action(_('Send login credentials to email address'), style='primary')
1494    def register(self, **data):
1495        if not self.captcha_result.is_valid:
1496            # Captcha will display error messages automatically.
1497            # No need to flash something.
1498            return
1499        if self.context.mode == 'create':
1500            # Check if there are unused records in this container which
1501            # can be taken
1502            applicant = self.context.first_unused
1503            if applicant is None:
1504                # Add applicant
1505                applicant = createObject(u'waeup.Applicant')
1506                self.context.addApplicant(applicant)
1507            else:
1508                applicants_root = grok.getSite()['applicants']
1509                ob_class = self.__implemented__.__name__.replace(
1510                    'waeup.kofa.','')
1511                applicants_root.logger.info('%s - used: %s' % (
1512                    ob_class, applicant.applicant_id))
1513            self.applyData(applicant, **data)
1514            # applicant.reg_number = applicant.applicant_id
1515            notify(grok.ObjectModifiedEvent(applicant))
1516        elif self.context.mode == 'update':
1517            # Update applicant
1518            reg_number = data.get('reg_number','')
1519            lastname = data.get('lastname','')
1520            cat = getUtility(ICatalog, name='applicants_catalog')
1521            searchstr = reg_number + self._postfix
1522            results = list(
1523                cat.searchResults(reg_number=(searchstr, searchstr)))
1524            if results:
1525                applicant = results[0]
1526                if getattr(applicant,'lastname',None) is None:
1527                    self.flash(_('An error occurred.'), type='danger')
1528                    return
1529                elif applicant.lastname.lower() != lastname.lower():
1530                    # Don't tell the truth here. Anonymous must not
1531                    # know that a record was found and only the lastname
1532                    # verification failed.
1533                    self.flash(
1534                        _('No application record found.'), type='warning')
1535                    return
1536                elif applicant.password is not None and \
1537                    applicant.state != INITIALIZED:
1538                    self.flash(_('Your password has already been set and used. '
1539                                 'Please proceed to the login page.'),
1540                               type='warning')
1541                    return
1542                # Store email address but nothing else.
1543                applicant.email = data['email']
1544                notify(grok.ObjectModifiedEvent(applicant))
1545            else:
1546                # No record found, this is the truth.
1547                self.flash(_('No application record found.'), type='warning')
1548                return
1549        else:
1550            # Does not happen but anyway ...
1551            return
1552        kofa_utils = getUtility(IKofaUtils)
1553        password = kofa_utils.genPassword()
1554        IUserAccount(applicant).setPassword(password)
1555        # Send email with credentials
1556        login_url = self.url(grok.getSite(), 'login')
1557        url_info = u'Login: %s' % login_url
1558        msg = _('You have successfully been registered for the')
1559        if kofa_utils.sendCredentials(IUserAccount(applicant),
1560            password, url_info, msg):
1561            email_sent = applicant.email
1562        else:
1563            email_sent = None
1564        self._redirect(email=email_sent, password=password,
1565            applicant_id=applicant.applicant_id)
1566        return
1567
1568class ApplicantRegistrationEmailSent(KofaPage):
1569    """Landing page after successful registration.
1570
1571    """
1572    grok.name('registration_complete')
1573    grok.require('waeup.Public')
1574    grok.template('applicantregemailsent')
1575    label = _('Your registration was successful.')
1576
1577    def update(self, email=None, applicant_id=None, password=None):
1578        self.email = email
1579        self.password = password
1580        self.applicant_id = applicant_id
1581        return
1582
1583class ApplicantCheckStatusPage(KofaPage):
1584    """Captcha'd status checking page for applicants.
1585    """
1586    grok.context(IApplicantsRoot)
1587    grok.name('checkstatus')
1588    grok.require('waeup.Anonymous')
1589    grok.template('applicantcheckstatus')
1590    buttonname = _('Submit')
1591    pnav = 7
1592
1593    def label(self):
1594        if self.result:
1595            return _('Admission status of ${a}',
1596                     mapping = {'a':self.applicant.applicant_id})
1597        return _('Check your admission status')
1598
1599    def update(self, SUBMIT=None):
1600        form = self.request.form
1601        self.result = False
1602        # Handle captcha
1603        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1604        self.captcha_result = self.captcha.verify(self.request)
1605        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1606        if SUBMIT:
1607            if not self.captcha_result.is_valid:
1608                # Captcha will display error messages automatically.
1609                # No need to flash something.
1610                return
1611            unique_id = form.get('unique_id', None)
1612            lastname = form.get('lastname', None)
1613            if not unique_id or not lastname:
1614                self.flash(
1615                    _('Required input missing.'), type='warning')
1616                return
1617            cat = getUtility(ICatalog, name='applicants_catalog')
1618            results = list(
1619                cat.searchResults(applicant_id=(unique_id, unique_id)))
1620            if not results:
1621                results = list(
1622                    cat.searchResults(reg_number=(unique_id, unique_id)))
1623            if results:
1624                applicant = results[0]
1625                if applicant.lastname.lower().strip() != lastname.lower():
1626                    # Don't tell the truth here. Anonymous must not
1627                    # know that a record was found and only the lastname
1628                    # verification failed.
1629                    self.flash(
1630                        _('No application record found.'), type='warning')
1631                    return
1632            else:
1633                self.flash(_('No application record found.'), type='warning')
1634                return
1635            self.applicant = applicant
1636            self.entry_session = "%s/%s" % (
1637                applicant.__parent__.year,
1638                applicant.__parent__.year+1)
1639            course_admitted = getattr(applicant, 'course_admitted', None)
1640            self.course_admitted = False
1641            if course_admitted is not None:
1642                try:
1643                    self.course_admitted = True
1644                    self.longtitle = course_admitted.longtitle
1645                    self.department = course_admitted.__parent__.__parent__.longtitle
1646                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1647                except AttributeError:
1648                    self.flash(_('Application record invalid.'), type='warning')
1649                    return
1650            self.result = True
1651            self.admitted = False
1652            self.not_admitted = False
1653            self.submitted = False
1654            self.not_submitted = False
1655            self.created = False
1656            if applicant.state in (ADMITTED, CREATED):
1657                self.admitted = True
1658            if applicant.state in (CREATED):
1659                self.created = True
1660                self.student_id = applicant.student_id
1661                self.password = applicant.application_number
1662            if applicant.state in (NOT_ADMITTED,):
1663                self.not_admitted = True
1664            if applicant.state in (SUBMITTED,):
1665                self.submitted = True
1666            if applicant.state in (INITIALIZED, STARTED, PAID):
1667                self.not_submitted = True
1668        return
1669
1670class CheckTranscriptStatus(KofaPage):
1671    """A display page for checking transcript processing status.
1672    """
1673    grok.context(IApplicantsRoot)
1674    grok.name('checktranscript')
1675    grok.require('waeup.Public')
1676    label = _('Check transcript status')
1677    buttonname = _('Check status now')
1678    pnav = 8
1679    websites = (('DemoPortal', 'https://kofa-demo.waeup.org/'),)
1680    #websites = (('DemoPortal', 'http://localhost:8080/app/'),)
1681    appl_url1 = 'https://kofa-demo.waeup.org/applicants'
1682    appl_url2 = 'https://kofa-demo.waeup.org/applicants'
1683
1684    def update(self, SUBMIT=None):
1685        form = self.request.form
1686        self.button = False
1687        # Handle captcha
1688        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1689        self.captcha_result = self.captcha.verify(self.request)
1690        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1691        if SUBMIT:
1692            self.results = []
1693            if not self.captcha_result.is_valid:
1694                # Captcha will display error messages automatically.
1695                # No need to flash something.
1696                return
1697            unique_id = form.get('unique_id', None)
1698            email = form.get('email', None)
1699            if not unique_id or not email:
1700                self.flash(
1701                    _('Required input missing.'), type='warning')
1702                return
1703            self.button = True
1704            # Call webservice of all websites
1705            for website in self.websites:
1706                server = xmlrpclib.ServerProxy(website[1])
1707                result = server.get_grad_student(unique_id, email)
1708                if not result:
1709                    continue
1710                self.results.append((result, website))
1711        return
1712
1713class ExportJobContainerOverview(KofaPage):
1714    """Page that lists active applicant data export jobs and provides links
1715    to discard or download CSV files.
1716
1717    """
1718    grok.context(VirtualApplicantsExportJobContainer)
1719    grok.require('waeup.manageApplication')
1720    grok.name('index.html')
1721    grok.template('exportjobsindex')
1722    label = _('Data Exports')
1723    pnav = 3
1724
1725    def update(self, CREATE=None, DISCARD=None, job_id=None):
1726        if CREATE:
1727            self.redirect(self.url('@@start_export'))
1728            return
1729        if DISCARD and job_id:
1730            entry = self.context.entry_from_job_id(job_id)
1731            self.context.delete_export_entry(entry)
1732            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1733            self.context.logger.info(
1734                '%s - discarded: job_id=%s' % (ob_class, job_id))
1735            self.flash(_('Discarded export') + ' %s' % job_id)
1736        self.entries = doll_up(self, user=self.request.principal.id)
1737        return
1738
1739class ExportJobContainerJobStart(UtilityView, grok.View):
1740    """View that starts three export jobs, one for applicants, a second
1741    one for applicant payments and a third for referee reports.
1742    """
1743    grok.context(VirtualApplicantsExportJobContainer)
1744    grok.require('waeup.manageApplication')
1745    grok.name('start_export')
1746
1747    def update(self):
1748        utils = queryUtility(IKofaUtils)
1749        if not utils.expensive_actions_allowed():
1750            self.flash(_(
1751                "Currently, exporters cannot be started due to high "
1752                "system load. Please try again later."), type='danger')
1753            self.entries = doll_up(self, user=None)
1754            return
1755
1756        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1757        container_code = self.context.__parent__.code
1758        # Start first exporter
1759        for exporter in ('applicants',
1760                         'applicantpayments',
1761                         'applicantrefereereports'):
1762            job_id = self.context.start_export_job(exporter,
1763                                          self.request.principal.id,
1764                                          container=container_code)
1765            self.context.logger.info(
1766                '%s - exported: %s (%s), job_id=%s'
1767                % (ob_class, exporter, container_code, job_id))
1768            # Commit transaction so that job is stored in the ZODB
1769            transaction.commit()
1770        self.flash(_('Exports started.'))
1771        self.redirect(self.url(self.context))
1772        return
1773
1774    def render(self):
1775        return
1776
1777class ExportJobContainerDownload(ExportCSVView):
1778    """Page that downloads a students export csv file.
1779
1780    """
1781    grok.context(VirtualApplicantsExportJobContainer)
1782    grok.require('waeup.manageApplication')
1783
1784class RefereesRemindPage(UtilityView, grok.View):
1785    """A display view for referee reports.
1786    """
1787    grok.context(IApplicant)
1788    grok.name('remind_referees')
1789    grok.require('waeup.manageApplication')
1790
1791    mandate_days = 31
1792
1793    def remindReferees(self):
1794        site = grok.getSite()
1795        kofa_utils = getUtility(IKofaUtils)
1796        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1797        failed = ''
1798        emails_sent = 0
1799        for referee in self.context.referees:
1800            if not referee.email_sent:
1801                continue
1802            # Check if referee has already created a report
1803            report_exists = False
1804            for report in self.context.refereereports:
1805                if report.email == referee.email:
1806                    report_exists = True
1807            if report_exists:
1808                continue
1809            # If not, create new mandate
1810            mandate = RefereeReportMandate(days=self.mandate_days)
1811            mandate.params['name'] = referee.name
1812            mandate.params['email'] = referee.email
1813            mandate.params[
1814                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1815                    self.context.__parent__.code,
1816                    self.context.application_number)
1817            mandate.params['redirect_path2'] = ''
1818            mandate.params['applicant_id'] = self.context.applicant_id
1819            site['mandates'].addMandate(mandate)
1820            # Send invitation email
1821            args = {'mandate_id':mandate.mandate_id}
1822            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1823            url_info = u'Report link: %s' % mandate_url
1824            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1825            if success:
1826                emails_sent += 1
1827                self.context.writeLogMessage(
1828                    self, 'email sent: %s' % referee.email)
1829                referee.email_sent = True
1830            else:
1831                failed += '%s ' % referee.email
1832        return failed, emails_sent
1833
1834    def update(self):
1835        if self.context.state != 'submitted':
1836            self.flash(
1837                _('Not allowed!'), type='danger')
1838            return self.redirect(self.url(self.context))
1839        failed, emails_sent = self.remindReferees()
1840        msg = _('${a} referee(s) have been reminded by email.',
1841                mapping = {'a':  emails_sent})
1842        self.flash(msg)
1843        return self.redirect(self.url(self.context))
1844
1845    def render(self):
1846        return
1847
1848class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1849    """A display view for referee reports.
1850    """
1851    grok.context(IApplicantRefereeReport)
1852    grok.name('index')
1853    grok.require('waeup.manageApplication')
1854    label = _('Referee Report')
1855    pnav = 3
1856    form_fields = grok.AutoFields(IApplicantRefereeReport)
1857    form_fields[
1858        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1859
1860class RemoveRefereeReportPage(UtilityView, grok.View):
1861    """
1862    """
1863    grok.context(IApplicantRefereeReport)
1864    grok.name('remove')
1865    grok.require('waeup.manageApplication')
1866
1867    def update(self):
1868        redirect_url = self.url(self.context.__parent__)
1869        self.context.__parent__.writeLogMessage(
1870            self, 'removed: %s' % self.context.r_id)
1871        del self.context.__parent__[self.context.r_id]
1872        self.flash(_('Referee report removed.'))
1873        self.redirect(redirect_url)
1874        return
1875
1876    def render(self):
1877        return
1878
1879class RefereeReportAddFormPage(KofaAddFormPage):
1880    """Add-form to add an referee report. This form
1881    is protected by a mandate.
1882    """
1883    grok.context(IApplicant)
1884    grok.require('waeup.Public')
1885    grok.name('addrefereereport')
1886    form_fields = grok.AutoFields(
1887        IApplicantRefereeReport).omit('creation_date')
1888    grok.template('refereereportpage')
1889    label = _('Referee Report Form')
1890    pnav = 3
1891    #doclink = DOCLINK + '/refereereports.html'
1892
1893    def update(self):
1894        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1895        if blocker:
1896            self.flash(_('The portal is in maintenance mode. '
1897                        'Referee report forms are temporarily disabled.'),
1898                       type='warning')
1899            self.redirect(self.application_url())
1900            return
1901        # Check mandate
1902        form = self.request.form
1903        self.mandate_id = form.get('mandate_id', None)
1904        self.mandates = grok.getSite()['mandates']
1905        mandate = self.mandates.get(self.mandate_id, None)
1906        if mandate is None and not self.request.form.get('form.actions.submit'):
1907            self.flash(_('No mandate.'), type='warning')
1908            self.redirect(self.application_url())
1909            return
1910        if mandate:
1911            # Check the mandate expiration date after redirect again
1912            if mandate.expires < datetime.utcnow():
1913                self.flash(_('Mandate expired.'),
1914                           type='warning')
1915                self.redirect(self.application_url())
1916                return
1917            args = {'mandate_id':mandate.mandate_id}
1918            # Check if report exists.
1919            # (1) If mandate has been used to create a report,
1920            # redirect to the pdf file.
1921            if mandate.params.get('redirect_path2'):
1922                self.redirect(
1923                    self.application_url() +
1924                    mandate.params.get('redirect_path2') +
1925                    '?%s' % urlencode(args))
1926                return
1927            # (2) Report exists but was created with another mandate.
1928            for report in self.context.refereereports:
1929                if report.email == mandate.params.get('email'):
1930                    self.flash(_('You have already created a '
1931                                 'report with another mandate.'),
1932                               type='warning')
1933                    self.redirect(self.application_url())
1934                    return
1935            # Prefill form with mandate params
1936            self.form_fields.get(
1937                'name').field.default = mandate.params['name']
1938            self.form_fields.get(
1939                'email').field.default = mandate.params['email']
1940            self.passport_url = self.url(
1941                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
1942        super(RefereeReportAddFormPage, self).update()
1943        return
1944
1945    @action(_('Submit'),
1946              warning=_('Are you really sure? '
1947                        'Reports can neither be modified or added '
1948                        'after submission.'),
1949              style='primary')
1950    def addRefereeReport(self, **data):
1951        report = createObject(u'waeup.ApplicantRefereeReport')
1952        timestamp = ("%d" % int(time()*10000))[1:]
1953        report.r_id = "r%s" % timestamp
1954        self.applyData(report, **data)
1955        self.context[report.r_id] = report
1956        # self.flash(_('Referee report has been saved. Thank you!'))
1957        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1958        # Changed on 19/04/20: We do no longer delete the mandate
1959        # but set path to redirect to the pdf file
1960        self.mandates[self.mandate_id].params[
1961            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
1962                self.context.__parent__.code,
1963                self.context.application_number,
1964                report.r_id)
1965        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
1966        args = {'mandate_id':self.mandate_id}
1967        self.flash(_('Your report has been successfully submitted. '
1968                     'Please use the report link in the email again to download '
1969                     'a pdf slip of your report.'))
1970        #self.redirect(self.url(report, 'referee_report.pdf')
1971        #              + '?%s' % urlencode(args))
1972        self.redirect(self.application_url())
1973        return
1974
1975class ExportPDFReportSlipPage(UtilityView, grok.View):
1976    """Deliver a PDF slip of the context.
1977    """
1978    grok.context(IApplicantRefereeReport)
1979    grok.name('referee_report_slip.pdf')
1980    grok.require('waeup.manageApplication')
1981    form_fields = grok.AutoFields(IApplicantRefereeReport)
1982    form_fields[
1983        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1984    #prefix = 'form'
1985    note = None
1986
1987    @property
1988    def title(self):
1989        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1990        return translate(_('Referee Report'), 'waeup.kofa',
1991            target_language=portal_language)
1992
1993    @property
1994    def label(self):
1995        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1996        return translate(_('Referee Report Slip'),
1997            'waeup.kofa', target_language=portal_language) \
1998            + ' %s' % self.context.r_id
1999
2000    def render(self):
2001        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
2002            self.request)
2003        students_utils = getUtility(IStudentsUtils)
2004        return students_utils.renderPDF(self,'referee_report_slip.pdf',
2005            self.context.__parent__, applicantview, note=self.note)
2006
2007class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
2008    """Deliver a PDF slip of the context to referees.
2009    """
2010    grok.name('referee_report.pdf')
2011    grok.require('waeup.Public')
2012
2013    def update(self):
2014        # Check mandate
2015        form = self.request.form
2016        self.mandate_id = form.get('mandate_id', None)
2017        self.mandates = grok.getSite()['mandates']
2018        mandate = self.mandates.get(self.mandate_id, None)
2019        if mandate is None:
2020            self.flash(_('No mandate.'), type='warning')
2021            self.redirect(self.application_url())
2022            return
2023        if mandate:
2024            # Check the mandate expiration date after redirect again
2025            if mandate.expires < datetime.utcnow():
2026                self.flash(_('Mandate expired.'),
2027                           type='warning')
2028                self.redirect(self.application_url())
2029                return
2030            # Check if form has really been submitted
2031            if not mandate.params.get('redirect_path2') \
2032                or mandate.params.get(
2033                    'applicant_id') != self.context.__parent__.applicant_id:
2034                self.flash(_('Wrong mandate.'),
2035                           type='warning')
2036                self.redirect(self.application_url())
2037                return
2038            super(ExportPDFReportSlipPage2, self).update()
2039        return
2040
2041class AdditionalFile(grok.View):
2042    """Renders additional pdf files for applicants.
2043    This is a baseclass.
2044    """
2045    grok.baseclass()
2046    grok.context(IApplicant)
2047    grok.require('waeup.viewApplication')
2048
2049    def render(self):
2050        pdf = getUtility(IExtFileStore).getFileByContext(
2051            self.context, attr=self.__name__)
2052        self.response.setHeader('Content-Type', 'application/pdf')
2053        return pdf
2054
2055class TestFile(AdditionalFile):
2056    """Renders testfile.pdf.
2057    """
2058    grok.name('testfile.pdf')
Note: See TracBrowser for help on using the repository browser.