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

Last change on this file since 17664 was 17550, checked in by Henrik Bettermann, 15 months ago

Add logging messages before after creating students from applicants.

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