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

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

Don't check if emails have already been sent.

  • Property svn:keywords set to Id
File size: 76.9 KB
Line 
1## $Id: browser.py 16221 2020-08-29 06:12:41Z 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):
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        return None, []
1115
1116    @action(_('Save'), style='primary')
1117    def save(self, **data):
1118        error, changed_courses = self.saveCourses()
1119        if error:
1120            self.flash(error, type='danger')
1121            return
1122        form = self.request.form
1123        password = form.get('password', None)
1124        password_ctl = form.get('control_password', None)
1125        if password:
1126            validator = getUtility(IPasswordValidator)
1127            errors = validator.validate_password(password, password_ctl)
1128            if errors:
1129                self.flash( ' '.join(errors), type='danger')
1130                return
1131        if self.upload_success is False:  # False is not None!
1132            # Error during image upload. Ignore other values.
1133            return
1134        changed_fields = self.applyData(self.context, **data)
1135        # Turn list of lists into single list
1136        if changed_fields:
1137            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1138        else:
1139            changed_fields = []
1140        changed_fields += changed_courses
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    @action(_('Save'), style='primary')
1300    def save(self, **data):
1301        if self.upload_success is False:  # False is not None!
1302            # Error during image upload. Ignore other values.
1303            return
1304        self.applyData(self.context, **data)
1305        error, dummy = self.saveCourses()
1306        if error:
1307            self.flash(error, type='danger')
1308            return
1309        self.flash(_('Form has been saved.'))
1310        return
1311
1312    def informReferees(self):
1313        site = grok.getSite()
1314        kofa_utils = getUtility(IKofaUtils)
1315        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1316        failed = ''
1317        emails_sent = 0
1318        for referee in self.context.referees:
1319            if referee.email_sent:
1320                continue
1321            mandate = RefereeReportMandate(days=self.mandate_days)
1322            mandate.params['name'] = referee.name
1323            mandate.params['email'] = referee.email
1324            mandate.params[
1325                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1326                    self.context.__parent__.code,
1327                    self.context.application_number)
1328            mandate.params['redirect_path2'] = ''
1329            mandate.params['applicant_id'] = self.context.applicant_id
1330            site['mandates'].addMandate(mandate)
1331            # Send invitation email
1332            args = {'mandate_id':mandate.mandate_id}
1333            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1334            url_info = u'Report link: %s' % mandate_url
1335            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1336            if success:
1337                emails_sent += 1
1338                self.context.writeLogMessage(
1339                    self, 'email sent: %s' % referee.email)
1340                referee.email_sent = True
1341            else:
1342                failed += '%s ' % referee.email
1343        return failed, emails_sent
1344
1345    @action(_('Finally Submit'), warning=WARNING)
1346    def finalsubmit(self, **data):
1347        if self.upload_success is False:  # False is not None!
1348            return # error during image upload. Ignore other values
1349        dnt = self.dataNotComplete(data)
1350        if dnt:
1351            self.flash(dnt, type='danger')
1352            return
1353        self.applyData(self.context, **data)
1354        error, dummy = self.saveCourses()
1355        if error:
1356            self.flash(error, type='danger')
1357            return
1358        state = IWorkflowState(self.context).getState()
1359        # This shouldn't happen, but the application officer
1360        # might have forgotten to lock the form after changing the state
1361        if state != self.submit_state:
1362            self.flash(_('The form cannot be submitted. Wrong state!'),
1363                       type='danger')
1364            return
1365        msg = _('Form has been submitted.')
1366        # Create mandates and send emails to referees
1367        if getattr(self.context, 'referees', None):
1368            failed, emails_sent = self.informReferees()
1369            if failed:
1370                self.flash(
1371                    _('Some invitation emails could not be sent:') + failed,
1372                    type='danger')
1373                return
1374            msg = _('Form has been successfully submitted and '
1375                    '${a} invitation emails were sent.',
1376                    mapping = {'a':  emails_sent})
1377        IWorkflowInfo(self.context).fireTransition('submit')
1378        # application_date is used in export files for sorting.
1379        # We can thus store utc.
1380        self.context.application_date = datetime.utcnow()
1381        self.flash(msg)
1382        self.redirect(self.url(self.context))
1383        return
1384
1385class PassportImage(grok.View):
1386    """Renders the passport image for applicants.
1387    """
1388    grok.name('passport.jpg')
1389    grok.context(IApplicant)
1390    grok.require('waeup.viewApplication')
1391
1392    def render(self):
1393        # A filename chooser turns a context into a filename suitable
1394        # for file storage.
1395        image = getUtility(IExtFileStore).getFileByContext(self.context)
1396        self.response.setHeader('Content-Type', 'image/jpeg')
1397        if image is None:
1398            # show placeholder image
1399            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1400        return image
1401
1402class PassportImageForReport(PassportImage):
1403    """Renders the passport image for applicants for referee reports.
1404    """
1405    grok.name('passport_for_report.jpg')
1406    grok.context(IApplicant)
1407    grok.require('waeup.Public')
1408
1409    def render(self):
1410        # Check mandate
1411        form = self.request.form
1412        self.mandate_id = form.get('mandate_id', None)
1413        self.mandates = grok.getSite()['mandates']
1414        mandate = self.mandates.get(self.mandate_id, None)
1415        if mandate is None:
1416            self.flash(_('No mandate.'), type='warning')
1417            self.redirect(self.application_url())
1418            return
1419        if mandate:
1420            # Check the mandate expiration date after redirect again
1421            if mandate.expires < datetime.utcnow():
1422                self.flash(_('Mandate expired.'),
1423                           type='warning')
1424                self.redirect(self.application_url())
1425                return
1426            # Check if mandate allows access
1427            if mandate.params.get('applicant_id') != self.context.applicant_id:
1428                self.flash(_('Wrong mandate.'),
1429                           type='warning')
1430                self.redirect(self.application_url())
1431                return
1432            return super(PassportImageForReport, self).render()
1433        return
1434
1435class ApplicantRegistrationPage(KofaAddFormPage):
1436    """Captcha'd registration page for applicants.
1437    """
1438    grok.context(IApplicantsContainer)
1439    grok.name('register')
1440    grok.require('waeup.Anonymous')
1441    grok.template('applicantregister')
1442
1443    @property
1444    def form_fields(self):
1445        form_fields = None
1446        if self.context.mode == 'update':
1447            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1448                'lastname','reg_number','email')
1449        else: #if self.context.mode == 'create':
1450            form_fields = grok.AutoFields(IApplicantEdit).select(
1451                'firstname', 'middlename', 'lastname', 'email', 'phone')
1452        return form_fields
1453
1454    @property
1455    def label(self):
1456        return _('Apply for ${a}',
1457            mapping = {'a':self.context.title})
1458
1459    def update(self):
1460        if self.context.expired:
1461            self.flash(_('Outside application period.'), type='warning')
1462            self.redirect(self.url(self.context))
1463            return
1464        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1465        if blocker:
1466            self.flash(_('The portal is in maintenance mode '
1467                        'and registration temporarily disabled.'),
1468                       type='warning')
1469            self.redirect(self.url(self.context))
1470            return
1471        # Handle captcha
1472        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1473        self.captcha_result = self.captcha.verify(self.request)
1474        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1475        return
1476
1477    def _redirect(self, email, password, applicant_id):
1478        # Forward only email to landing page in base package.
1479        self.redirect(self.url(self.context, 'registration_complete',
1480            data = dict(email=email)))
1481        return
1482
1483    @property
1484    def _postfix(self):
1485        """In customized packages we can add a container dependent string if
1486        applicants have been imported into several containers.
1487        """
1488        return ''
1489
1490    @action(_('Send login credentials to email address'), style='primary')
1491    def register(self, **data):
1492        if not self.captcha_result.is_valid:
1493            # Captcha will display error messages automatically.
1494            # No need to flash something.
1495            return
1496        if self.context.mode == 'create':
1497            # Check if there are unused records in this container which
1498            # can be taken
1499            applicant = self.context.first_unused
1500            if applicant is None:
1501                # Add applicant
1502                applicant = createObject(u'waeup.Applicant')
1503                self.context.addApplicant(applicant)
1504            else:
1505                applicants_root = grok.getSite()['applicants']
1506                ob_class = self.__implemented__.__name__.replace(
1507                    'waeup.kofa.','')
1508                applicants_root.logger.info('%s - used: %s' % (
1509                    ob_class, applicant.applicant_id))
1510            self.applyData(applicant, **data)
1511            # applicant.reg_number = applicant.applicant_id
1512            notify(grok.ObjectModifiedEvent(applicant))
1513        elif self.context.mode == 'update':
1514            # Update applicant
1515            reg_number = data.get('reg_number','')
1516            lastname = data.get('lastname','')
1517            cat = getUtility(ICatalog, name='applicants_catalog')
1518            searchstr = reg_number + self._postfix
1519            results = list(
1520                cat.searchResults(reg_number=(searchstr, searchstr)))
1521            if results:
1522                applicant = results[0]
1523                if getattr(applicant,'lastname',None) is None:
1524                    self.flash(_('An error occurred.'), type='danger')
1525                    return
1526                elif applicant.lastname.lower() != lastname.lower():
1527                    # Don't tell the truth here. Anonymous must not
1528                    # know that a record was found and only the lastname
1529                    # verification failed.
1530                    self.flash(
1531                        _('No application record found.'), type='warning')
1532                    return
1533                elif applicant.password is not None and \
1534                    applicant.state != INITIALIZED:
1535                    self.flash(_('Your password has already been set and used. '
1536                                 'Please proceed to the login page.'),
1537                               type='warning')
1538                    return
1539                # Store email address but nothing else.
1540                applicant.email = data['email']
1541                notify(grok.ObjectModifiedEvent(applicant))
1542            else:
1543                # No record found, this is the truth.
1544                self.flash(_('No application record found.'), type='warning')
1545                return
1546        else:
1547            # Does not happen but anyway ...
1548            return
1549        kofa_utils = getUtility(IKofaUtils)
1550        password = kofa_utils.genPassword()
1551        IUserAccount(applicant).setPassword(password)
1552        # Send email with credentials
1553        login_url = self.url(grok.getSite(), 'login')
1554        url_info = u'Login: %s' % login_url
1555        msg = _('You have successfully been registered for the')
1556        if kofa_utils.sendCredentials(IUserAccount(applicant),
1557            password, url_info, msg):
1558            email_sent = applicant.email
1559        else:
1560            email_sent = None
1561        self._redirect(email=email_sent, password=password,
1562            applicant_id=applicant.applicant_id)
1563        return
1564
1565class ApplicantRegistrationEmailSent(KofaPage):
1566    """Landing page after successful registration.
1567
1568    """
1569    grok.name('registration_complete')
1570    grok.require('waeup.Public')
1571    grok.template('applicantregemailsent')
1572    label = _('Your registration was successful.')
1573
1574    def update(self, email=None, applicant_id=None, password=None):
1575        self.email = email
1576        self.password = password
1577        self.applicant_id = applicant_id
1578        return
1579
1580class ApplicantCheckStatusPage(KofaPage):
1581    """Captcha'd status checking page for applicants.
1582    """
1583    grok.context(IApplicantsRoot)
1584    grok.name('checkstatus')
1585    grok.require('waeup.Anonymous')
1586    grok.template('applicantcheckstatus')
1587    buttonname = _('Submit')
1588    pnav = 7
1589
1590    def label(self):
1591        if self.result:
1592            return _('Admission status of ${a}',
1593                     mapping = {'a':self.applicant.applicant_id})
1594        return _('Check your admission status')
1595
1596    def update(self, SUBMIT=None):
1597        form = self.request.form
1598        self.result = False
1599        # Handle captcha
1600        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1601        self.captcha_result = self.captcha.verify(self.request)
1602        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1603        if SUBMIT:
1604            if not self.captcha_result.is_valid:
1605                # Captcha will display error messages automatically.
1606                # No need to flash something.
1607                return
1608            unique_id = form.get('unique_id', None)
1609            lastname = form.get('lastname', None)
1610            if not unique_id or not lastname:
1611                self.flash(
1612                    _('Required input missing.'), type='warning')
1613                return
1614            cat = getUtility(ICatalog, name='applicants_catalog')
1615            results = list(
1616                cat.searchResults(applicant_id=(unique_id, unique_id)))
1617            if not results:
1618                results = list(
1619                    cat.searchResults(reg_number=(unique_id, unique_id)))
1620            if results:
1621                applicant = results[0]
1622                if applicant.lastname.lower().strip() != lastname.lower():
1623                    # Don't tell the truth here. Anonymous must not
1624                    # know that a record was found and only the lastname
1625                    # verification failed.
1626                    self.flash(
1627                        _('No application record found.'), type='warning')
1628                    return
1629            else:
1630                self.flash(_('No application record found.'), type='warning')
1631                return
1632            self.applicant = applicant
1633            self.entry_session = "%s/%s" % (
1634                applicant.__parent__.year,
1635                applicant.__parent__.year+1)
1636            course_admitted = getattr(applicant, 'course_admitted', None)
1637            self.course_admitted = False
1638            if course_admitted is not None:
1639                try:
1640                    self.course_admitted = True
1641                    self.longtitle = course_admitted.longtitle
1642                    self.department = course_admitted.__parent__.__parent__.longtitle
1643                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1644                except AttributeError:
1645                    self.flash(_('Application record invalid.'), type='warning')
1646                    return
1647            self.result = True
1648            self.admitted = False
1649            self.not_admitted = False
1650            self.submitted = False
1651            self.not_submitted = False
1652            self.created = False
1653            if applicant.state in (ADMITTED, CREATED):
1654                self.admitted = True
1655            if applicant.state in (CREATED):
1656                self.created = True
1657                self.student_id = applicant.student_id
1658                self.password = applicant.application_number
1659            if applicant.state in (NOT_ADMITTED,):
1660                self.not_admitted = True
1661            if applicant.state in (SUBMITTED,):
1662                self.submitted = True
1663            if applicant.state in (INITIALIZED, STARTED, PAID):
1664                self.not_submitted = True
1665        return
1666
1667class CheckTranscriptStatus(KofaPage):
1668    """A display page for checking transcript processing status.
1669    """
1670    grok.context(IApplicantsRoot)
1671    grok.name('checktranscript')
1672    grok.require('waeup.Public')
1673    label = _('Check transcript status')
1674    buttonname = _('Check status now')
1675    pnav = 8
1676    websites = (('DemoPortal', 'https://kofa-demo.waeup.org/'),)
1677    #websites = (('DemoPortal', 'http://localhost:8080/app/'),)
1678    appl_url1 = 'https://kofa-demo.waeup.org/applicants'
1679    appl_url2 = 'https://kofa-demo.waeup.org/applicants'
1680
1681    def update(self, SUBMIT=None):
1682        form = self.request.form
1683        self.button = False
1684        # Handle captcha
1685        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1686        self.captcha_result = self.captcha.verify(self.request)
1687        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1688        if SUBMIT:
1689            self.results = []
1690            if not self.captcha_result.is_valid:
1691                # Captcha will display error messages automatically.
1692                # No need to flash something.
1693                return
1694            unique_id = form.get('unique_id', None)
1695            email = form.get('email', None)
1696            if not unique_id or not email:
1697                self.flash(
1698                    _('Required input missing.'), type='warning')
1699                return
1700            self.button = True
1701            # Call webservice of all websites
1702            for website in self.websites:
1703                server = xmlrpclib.ServerProxy(website[1])
1704                result = server.get_grad_student(unique_id, email)
1705                if not result:
1706                    continue
1707                self.results.append((result, website))
1708        return
1709
1710class ExportJobContainerOverview(KofaPage):
1711    """Page that lists active applicant data export jobs and provides links
1712    to discard or download CSV files.
1713
1714    """
1715    grok.context(VirtualApplicantsExportJobContainer)
1716    grok.require('waeup.manageApplication')
1717    grok.name('index.html')
1718    grok.template('exportjobsindex')
1719    label = _('Data Exports')
1720    pnav = 3
1721
1722    def update(self, CREATE=None, DISCARD=None, job_id=None):
1723        if CREATE:
1724            self.redirect(self.url('@@start_export'))
1725            return
1726        if DISCARD and job_id:
1727            entry = self.context.entry_from_job_id(job_id)
1728            self.context.delete_export_entry(entry)
1729            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1730            self.context.logger.info(
1731                '%s - discarded: job_id=%s' % (ob_class, job_id))
1732            self.flash(_('Discarded export') + ' %s' % job_id)
1733        self.entries = doll_up(self, user=self.request.principal.id)
1734        return
1735
1736class ExportJobContainerJobStart(UtilityView, grok.View):
1737    """View that starts three export jobs, one for applicants, a second
1738    one for applicant payments and a third for referee reports.
1739    """
1740    grok.context(VirtualApplicantsExportJobContainer)
1741    grok.require('waeup.manageApplication')
1742    grok.name('start_export')
1743
1744    def update(self):
1745        utils = queryUtility(IKofaUtils)
1746        if not utils.expensive_actions_allowed():
1747            self.flash(_(
1748                "Currently, exporters cannot be started due to high "
1749                "system load. Please try again later."), type='danger')
1750            self.entries = doll_up(self, user=None)
1751            return
1752
1753        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1754        container_code = self.context.__parent__.code
1755        # Start first exporter
1756        for exporter in ('applicants',
1757                         'applicantpayments',
1758                         'applicantrefereereports'):
1759            job_id = self.context.start_export_job(exporter,
1760                                          self.request.principal.id,
1761                                          container=container_code)
1762            self.context.logger.info(
1763                '%s - exported: %s (%s), job_id=%s'
1764                % (ob_class, exporter, container_code, job_id))
1765            # Commit transaction so that job is stored in the ZODB
1766            transaction.commit()
1767        self.flash(_('Exports started.'))
1768        self.redirect(self.url(self.context))
1769        return
1770
1771    def render(self):
1772        return
1773
1774class ExportJobContainerDownload(ExportCSVView):
1775    """Page that downloads a students export csv file.
1776
1777    """
1778    grok.context(VirtualApplicantsExportJobContainer)
1779    grok.require('waeup.manageApplication')
1780
1781class RefereesRemindPage(UtilityView, grok.View):
1782    """A display view for referee reports.
1783    """
1784    grok.context(IApplicant)
1785    grok.name('remind_referees')
1786    grok.require('waeup.manageApplication')
1787
1788    mandate_days = 31
1789
1790    def remindReferees(self):
1791        site = grok.getSite()
1792        kofa_utils = getUtility(IKofaUtils)
1793        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1794        failed = ''
1795        emails_sent = 0
1796        for referee in self.context.referees:
1797            #if not referee.email_sent:
1798            #    continue
1799            # Check if referee has already created a report
1800            report_exists = False
1801            for report in self.context.refereereports:
1802                if report.email == referee.email:
1803                    report_exists = True
1804            if report_exists:
1805                continue
1806            # If not, create new mandate
1807            mandate = RefereeReportMandate(days=self.mandate_days)
1808            mandate.params['name'] = referee.name
1809            mandate.params['email'] = referee.email
1810            mandate.params[
1811                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1812                    self.context.__parent__.code,
1813                    self.context.application_number)
1814            mandate.params['redirect_path2'] = ''
1815            mandate.params['applicant_id'] = self.context.applicant_id
1816            site['mandates'].addMandate(mandate)
1817            # Send invitation email
1818            args = {'mandate_id':mandate.mandate_id}
1819            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1820            url_info = u'Report link: %s' % mandate_url
1821            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1822            if success:
1823                emails_sent += 1
1824                self.context.writeLogMessage(
1825                    self, 'email sent: %s' % referee.email)
1826                referee.email_sent = True
1827            else:
1828                failed += '%s ' % referee.email
1829        return failed, emails_sent
1830
1831    def update(self):
1832        if self.context.state != 'submitted':
1833            self.flash(
1834                _('Not allowed!'), type='danger')
1835            return self.redirect(self.url(self.context))
1836        failed, emails_sent = self.remindReferees()
1837        msg = _('${a} referee(s) have been reminded by email.',
1838                mapping = {'a':  emails_sent})
1839        self.flash(msg)
1840        return self.redirect(self.url(self.context))
1841
1842    def render(self):
1843        return
1844
1845class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1846    """A display view for referee reports.
1847    """
1848    grok.context(IApplicantRefereeReport)
1849    grok.name('index')
1850    grok.require('waeup.manageApplication')
1851    label = _('Referee Report')
1852    pnav = 3
1853    form_fields = grok.AutoFields(IApplicantRefereeReport)
1854    form_fields[
1855        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1856
1857class RemoveRefereeReportPage(UtilityView, grok.View):
1858    """
1859    """
1860    grok.context(IApplicantRefereeReport)
1861    grok.name('remove')
1862    grok.require('waeup.manageApplication')
1863
1864    def update(self):
1865        redirect_url = self.url(self.context.__parent__)
1866        self.context.__parent__.writeLogMessage(
1867            self, 'removed: %s' % self.context.r_id)
1868        del self.context.__parent__[self.context.r_id]
1869        self.flash(_('Referee report removed.'))
1870        self.redirect(redirect_url)
1871        return
1872
1873    def render(self):
1874        return
1875
1876class RefereeReportAddFormPage(KofaAddFormPage):
1877    """Add-form to add an referee report. This form
1878    is protected by a mandate.
1879    """
1880    grok.context(IApplicant)
1881    grok.require('waeup.Public')
1882    grok.name('addrefereereport')
1883    form_fields = grok.AutoFields(
1884        IApplicantRefereeReport).omit('creation_date')
1885    grok.template('refereereportpage')
1886    label = _('Referee Report Form')
1887    pnav = 3
1888    #doclink = DOCLINK + '/refereereports.html'
1889
1890    def update(self):
1891        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1892        if blocker:
1893            self.flash(_('The portal is in maintenance mode. '
1894                        'Referee report forms are temporarily disabled.'),
1895                       type='warning')
1896            self.redirect(self.application_url())
1897            return
1898        # Check mandate
1899        form = self.request.form
1900        self.mandate_id = form.get('mandate_id', None)
1901        self.mandates = grok.getSite()['mandates']
1902        mandate = self.mandates.get(self.mandate_id, None)
1903        if mandate is None and not self.request.form.get('form.actions.submit'):
1904            self.flash(_('No mandate.'), type='warning')
1905            self.redirect(self.application_url())
1906            return
1907        if mandate:
1908            # Check the mandate expiration date after redirect again
1909            if mandate.expires < datetime.utcnow():
1910                self.flash(_('Mandate expired.'),
1911                           type='warning')
1912                self.redirect(self.application_url())
1913                return
1914            args = {'mandate_id':mandate.mandate_id}
1915            # Check if report exists.
1916            # (1) If mandate has been used to create a report,
1917            # redirect to the pdf file.
1918            if mandate.params.get('redirect_path2'):
1919                self.redirect(
1920                    self.application_url() +
1921                    mandate.params.get('redirect_path2') +
1922                    '?%s' % urlencode(args))
1923                return
1924            # (2) Report exists but was created with another mandate.
1925            for report in self.context.refereereports:
1926                if report.email == mandate.params.get('email'):
1927                    self.flash(_('You have already created a '
1928                                 'report with another mandate.'),
1929                               type='warning')
1930                    self.redirect(self.application_url())
1931                    return
1932            # Prefill form with mandate params
1933            self.form_fields.get(
1934                'name').field.default = mandate.params['name']
1935            self.form_fields.get(
1936                'email').field.default = mandate.params['email']
1937            self.passport_url = self.url(
1938                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
1939        super(RefereeReportAddFormPage, self).update()
1940        return
1941
1942    @action(_('Submit'),
1943              warning=_('Are you really sure? '
1944                        'Reports can neither be modified or added '
1945                        'after submission.'),
1946              style='primary')
1947    def addRefereeReport(self, **data):
1948        report = createObject(u'waeup.ApplicantRefereeReport')
1949        timestamp = ("%d" % int(time()*10000))[1:]
1950        report.r_id = "r%s" % timestamp
1951        self.applyData(report, **data)
1952        self.context[report.r_id] = report
1953        # self.flash(_('Referee report has been saved. Thank you!'))
1954        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1955        # Changed on 19/04/20: We do no longer delete the mandate
1956        # but set path to redirect to the pdf file
1957        self.mandates[self.mandate_id].params[
1958            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
1959                self.context.__parent__.code,
1960                self.context.application_number,
1961                report.r_id)
1962        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
1963        args = {'mandate_id':self.mandate_id}
1964        self.flash(_('Your report has been successfully submitted. '
1965                     'Please use the report link in the email again to download '
1966                     'a pdf slip of your report.'))
1967        #self.redirect(self.url(report, 'referee_report.pdf')
1968        #              + '?%s' % urlencode(args))
1969        self.redirect(self.application_url())
1970        return
1971
1972class ExportPDFReportSlipPage(UtilityView, grok.View):
1973    """Deliver a PDF slip of the context.
1974    """
1975    grok.context(IApplicantRefereeReport)
1976    grok.name('referee_report_slip.pdf')
1977    grok.require('waeup.manageApplication')
1978    form_fields = grok.AutoFields(IApplicantRefereeReport)
1979    form_fields[
1980        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1981    #prefix = 'form'
1982    note = None
1983
1984    @property
1985    def title(self):
1986        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1987        return translate(_('Referee Report'), 'waeup.kofa',
1988            target_language=portal_language)
1989
1990    @property
1991    def label(self):
1992        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1993        return translate(_('Referee Report Slip'),
1994            'waeup.kofa', target_language=portal_language) \
1995            + ' %s' % self.context.r_id
1996
1997    def render(self):
1998        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
1999            self.request)
2000        students_utils = getUtility(IStudentsUtils)
2001        return students_utils.renderPDF(self,'referee_report_slip.pdf',
2002            self.context.__parent__, applicantview, note=self.note)
2003
2004class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
2005    """Deliver a PDF slip of the context to referees.
2006    """
2007    grok.name('referee_report.pdf')
2008    grok.require('waeup.Public')
2009
2010    def update(self):
2011        # Check mandate
2012        form = self.request.form
2013        self.mandate_id = form.get('mandate_id', None)
2014        self.mandates = grok.getSite()['mandates']
2015        mandate = self.mandates.get(self.mandate_id, None)
2016        if mandate is None:
2017            self.flash(_('No mandate.'), type='warning')
2018            self.redirect(self.application_url())
2019            return
2020        if mandate:
2021            # Check the mandate expiration date after redirect again
2022            if mandate.expires < datetime.utcnow():
2023                self.flash(_('Mandate expired.'),
2024                           type='warning')
2025                self.redirect(self.application_url())
2026                return
2027            # Check if form has really been submitted
2028            if not mandate.params.get('redirect_path2') \
2029                or mandate.params.get(
2030                    'applicant_id') != self.context.__parent__.applicant_id:
2031                self.flash(_('Wrong mandate.'),
2032                           type='warning')
2033                self.redirect(self.application_url())
2034                return
2035            super(ExportPDFReportSlipPage2, self).update()
2036        return
2037
2038class AdditionalFile(grok.View):
2039    """Renders additional pdf files for applicants.
2040    This is a baseclass.
2041    """
2042    grok.baseclass()
2043    grok.context(IApplicant)
2044    grok.require('waeup.viewApplication')
2045
2046    def render(self):
2047        pdf = getUtility(IExtFileStore).getFileByContext(
2048            self.context, attr=self.__name__)
2049        self.response.setHeader('Content-Type', 'application/pdf')
2050        return pdf
2051
2052class TestFile(AdditionalFile):
2053    """Renders testfile.pdf.
2054    """
2055    grok.name('testfile.pdf')
Note: See TracBrowser for help on using the repository browser.