source: main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/applicants/browser.py @ 18083

Last change on this file since 18083 was 18083, checked in by uli, 5 days ago

Support Google Recaptcha v3.

  • Property svn:keywords set to Id
File size: 84.0 KB
Line 
1## $Id: browser.py 18083 2025-06-07 02:06:59Z uli $
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        n = 1
776        for result in results:
777            if container_only and result.__parent__ is not self.context:
778                continue
779            success, msg = result.createStudent(view=self)
780            if success:
781                created.append(result.applicant_id)
782                n += 1
783            else:
784                failed.append(
785                    (result.applicant_id, self.url(result, 'manage'), msg))
786            if not n % 1000:
787                # Stop after 1000 students to avoid write conflicts which sill
788                # occur even though transactions have formerly been commited.
789                break
790        self.flash(_('Finished.'))
791        grok.getSite()['configuration'].maintmode_enabled_by = None
792        self.n = n-1
793        self.successful = ', '.join(created)
794        self.failed = failed
795        applicants_root.logger.info('%s - created in %s: %s' % (
796            ob_class, self.context.__name__, self.successful))
797        return
798
799
800class ApplicationFeePaymentAddPage(UtilityView, grok.View):
801    """ Page to add an online payment ticket
802    """
803    grok.context(IApplicant)
804    grok.name('addafp')
805    grok.require('waeup.payApplicant')
806    factory = u'waeup.ApplicantOnlinePayment'
807
808    @property
809    def custom_requirements(self):
810        return ''
811
812    def update(self):
813        # Additional requirements in custom packages.
814        if self.custom_requirements:
815            self.flash(
816                self.custom_requirements,
817                type='danger')
818            self.redirect(self.url(self.context))
819            return
820        if not self.context.special:
821            for ticket in self.context.payments:
822                if ticket.p_state == 'paid':
823                      self.flash(
824                          _('This type of payment has already been made.'),
825                          type='warning')
826                      self.redirect(self.url(self.context))
827                      return
828        applicants_utils = getUtility(IApplicantsUtils)
829        container = self.context.__parent__
830        payment = createObject(self.factory)
831        failure = applicants_utils.setPaymentDetails(
832            container, payment, self.context)
833        if failure is not None:
834            self.flash(failure, type='danger')
835            self.redirect(self.url(self.context))
836            return
837        self.context[payment.p_id] = payment
838        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
839        self.flash(_('Payment ticket created.'))
840        self.redirect(self.url(payment))
841        return
842
843    def render(self):
844        return
845
846class BalancePaymentAddFormPage(KofaAddFormPage):
847    """ Page to add an online payment which can balance s previous session
848    payment.
849    """
850    grok.context(IApplicant)
851    grok.name('addbp')
852    grok.template('balancepaymentaddform')
853    grok.require('waeup.manageApplication')
854    form_fields = grok.AutoFields(IApplicantBalancePayment)
855    label = _('Add balance')
856    #pnav = 4
857
858    @property
859    def selectable_payment_options(self):
860        options = getUtility(
861            IKofaUtils).selectable_payment_options(self.context)
862        return sorted(options.items(), key=lambda value: value[1])
863
864    @action(_('Create ticket'), style='primary')
865    def createTicket(self, **data):
866        p_category = data['p_category']
867        form = self.request.form
868        p_option = form.get('form.p_option', None)
869        balance_amount = data.get('balance_amount', None)
870        applicants_utils = getUtility(IApplicantsUtils)
871        error, payment = applicants_utils.setBalanceDetails(
872            self.context, p_category, balance_amount)
873        if error is not None:
874            self.flash(error, type="danger")
875            return
876        if p_option:
877            payment.p_option = p_option
878        self.context[payment.p_id] = payment
879        self.flash(_('Payment ticket created.'))
880        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
881        self.redirect(self.url(payment))
882        return
883
884    @action(_('Cancel'), validator=NullValidator)
885    def cancel(self, **data):
886        self.redirect(self.url(self.context))
887
888
889class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
890    """ Page to view an online payment ticket
891    """
892    grok.context(IApplicantOnlinePayment)
893    grok.name('index')
894    grok.require('waeup.viewApplication')
895    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
896    form_fields[
897        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
898    form_fields[
899        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
900    pnav = 3
901
902    @property
903    def label(self):
904        return _('${a}: Online Payment Ticket ${b}', mapping = {
905            'a':self.context.__parent__.display_fullname,
906            'b':self.context.p_id})
907
908class OnlinePaymentApprovePage(UtilityView, grok.View):
909    """ Approval view
910    """
911    grok.context(IApplicantOnlinePayment)
912    grok.name('approve')
913    grok.require('waeup.managePortal')
914
915    def update(self):
916        flashtype, msg, log = self.context.approveApplicantPayment()
917        if log is not None:
918            applicant = self.context.__parent__
919            # Add log message to applicants.log
920            applicant.writeLogMessage(self, log)
921            # Add log message to payments.log
922            self.context.logger.info(
923                '%s,%s,%s,%s,%s,,,,,,' % (
924                applicant.applicant_id,
925                self.context.p_id, self.context.p_category,
926                self.context.amount_auth, self.context.r_code))
927        self.flash(msg, type=flashtype)
928        return
929
930    def render(self):
931        self.redirect(self.url(self.context, '@@index'))
932        return
933
934class ExportPDFPaymentSlipPage(UtilityView, grok.View):
935    """Deliver a PDF slip of the context.
936    """
937    grok.context(IApplicantOnlinePayment)
938    grok.name('payment_slip.pdf')
939    grok.require('waeup.viewApplication')
940    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
941    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
942    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
943    #prefix = 'form'
944    note = None
945
946    @property
947    def title(self):
948        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
949        return translate(_('Payment Data'), 'waeup.kofa',
950            target_language=portal_language)
951
952    @property
953    def label(self):
954        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
955        return translate(_('Online Payment Slip'),
956            'waeup.kofa', target_language=portal_language) \
957            + ' %s' % self.context.p_id
958
959    @property
960    def payment_slip_download_warning(self):
961        if self.context.__parent__.state not in (
962            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
963            return _('Please submit the application form before '
964                     'trying to download payment slips.')
965        return ''
966
967    def render(self):
968        if self.payment_slip_download_warning:
969            self.flash(self.payment_slip_download_warning, type='danger')
970            self.redirect(self.url(self.context))
971            return
972        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
973            self.request)
974        students_utils = getUtility(IStudentsUtils)
975        return students_utils.renderPDF(self,'payment_slip.pdf',
976            self.context.__parent__, applicantview, note=self.note)
977
978class ExportPDFPageApplicationSlip(UtilityView, grok.View):
979    """Deliver a PDF slip of the context.
980    """
981    grok.context(IApplicant)
982    grok.name('application_slip.pdf')
983    grok.require('waeup.viewApplication')
984    #prefix = 'form'
985
986    def update(self):
987        if self.context.state in ('initialized', 'started', 'paid'):
988            self.flash(
989                _('Please pay and submit before trying to download '
990                  'the application slip.'), type='warning')
991            return self.redirect(self.url(self.context))
992        return
993
994    def render(self):
995        try:
996            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
997                view=self)
998        except IOError:
999            self.flash(
1000                _('Your image file is corrupted. '
1001                  'Please replace.'), type='danger')
1002            return self.redirect(self.url(self.context))
1003        except LayoutError, err:
1004            self.flash(
1005                'PDF file could not be created. Reportlab error message: %s'
1006                % escape(err.message),
1007                type="danger")
1008            return self.redirect(self.url(self.context))
1009        self.response.setHeader(
1010            'Content-Type', 'application/pdf')
1011        return pdfstream
1012
1013def handle_img_upload(upload, context, view):
1014    """Handle upload of applicant image.
1015
1016    Returns `True` in case of success or `False`.
1017
1018    Please note that file pointer passed in (`upload`) most probably
1019    points to end of file when leaving this function.
1020    """
1021    max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
1022    size = file_size(upload)
1023    if size > max_upload_size:
1024        view.flash(_('Uploaded image is too big!'), type='danger')
1025        return False
1026    dummy, ext = os.path.splitext(upload.filename)
1027    ext.lower()
1028    if ext != '.jpg':
1029        view.flash(_('jpg file extension expected.'), type='danger')
1030        return False
1031    upload.seek(0) # file pointer moved when determining size
1032    store = getUtility(IExtFileStore)
1033    file_id = IFileStoreNameChooser(context).chooseName()
1034    try:
1035        store.createFile(file_id, upload)
1036    except IOError:
1037        view.flash(_('Image file cannot be changed.'), type='danger')
1038        return False
1039    return True
1040
1041def handle_file_upload(upload, context, view, attr=None):
1042    """Handle upload of applicant files.
1043
1044    Returns `True` in case of success or `False`.
1045
1046    Please note that file pointer passed in (`upload`) most probably
1047    points to end of file when leaving this function.
1048    """
1049    size = file_size(upload)
1050    max_upload_size = 1024 * getUtility(IStudentsUtils).MAX_KB
1051    if size > max_upload_size:
1052        view.flash(_('Uploaded file is too big!'))
1053        return False
1054    upload.seek(0)  # file pointer moved when determining size
1055    dummy,ext = os.path.splitext(upload.filename)
1056    file_format = get_fileformat(None, upload.read(512))
1057    upload.seek(0)  # same here
1058    if file_format is None:
1059        view.flash(_('Could not determine file type.'), type="danger")
1060        return False
1061    ext.lower()
1062    if ext not in ('.pdf', '.jpg'):
1063        view.flash(_('pdf or jpg file extension expected.'), type='danger')
1064        return False
1065    download_name = attr + '.' + file_format
1066    store = getUtility(IExtFileStore)
1067    file_id = IFileStoreNameChooser(context).chooseName(attr=download_name)
1068    store.createFile(file_id, upload)
1069    return True
1070
1071class ApplicantManageFormPage(KofaEditFormPage):
1072    """A full edit view for applicant data.
1073    """
1074    grok.context(IApplicant)
1075    grok.name('manage')
1076    grok.require('waeup.manageApplication')
1077    grok.template('applicanteditpage')
1078    manage_applications = True
1079    pnav = 3
1080
1081    @property
1082    def display_actions(self):
1083        actions = [[_('Save'), _('Finally Submit')],
1084                   [_('Add online payment ticket'),
1085                    _('Add balance payment ticket'),
1086                    _('Remove selected tickets')]]
1087        applicants_utils = getUtility(IApplicantsUtils)
1088        if self.context.state not in applicants_utils.BALANCE_PAYMENT_STATES:
1089            actions[1].pop(1)
1090        return actions
1091
1092    @property
1093    def display_payments(self):
1094        if self.context.payments:
1095            return True
1096        if self.context.special:
1097            return True
1098        return getattr(self.context.__parent__, 'application_fee', None)
1099
1100    @property
1101    def display_refereereports(self):
1102        if self.context.refereereports:
1103            return True
1104        return False
1105
1106    def display_fileupload(self, filename):
1107        """This method can be used in custom packages to avoid unneccessary
1108        file uploads.
1109        """
1110        return True
1111
1112    @property
1113    def form_fields(self):
1114        if self.context.special:
1115            form_fields = grok.AutoFields(ISpecialApplicant)
1116            form_fields['applicant_id'].for_display = True
1117        else:
1118            form_fields = grok.AutoFields(IApplicant)
1119            form_fields['student_id'].for_display = True
1120            form_fields['applicant_id'].for_display = True
1121        return form_fields
1122
1123    @property
1124    def target(self):
1125        return getattr(self.context.__parent__, 'prefix', None)
1126
1127    @property
1128    def separators(self):
1129        return getUtility(IApplicantsUtils).SEPARATORS_DICT
1130
1131    @property
1132    def custom_upload_requirements(self):
1133        return ''
1134
1135    def update(self):
1136        super(ApplicantManageFormPage, self).update()
1137        max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
1138        self.wf_info = IWorkflowInfo(self.context)
1139        self.max_upload_size = string_from_bytes(max_upload_size)
1140        self.upload_success = None
1141        upload = self.request.form.get('form.passport', None)
1142        if upload:
1143            if self.custom_upload_requirements:
1144                self.flash(
1145                    self.custom_upload_requirements,
1146                    type='danger')
1147                self.redirect(self.url(self.context))
1148                return
1149            # We got a fresh upload, upload_success is
1150            # either True or False
1151            self.upload_success = handle_img_upload(
1152                upload, self.context, self)
1153            if self.upload_success:
1154                self.context.writeLogMessage(self, 'saved: passport')
1155        file_store = getUtility(IExtFileStore)
1156        self.additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
1157        for filename in self.additional_files:
1158            upload = self.request.form.get(filename[1], None)
1159            if upload:
1160                # We got a fresh file upload
1161                success = handle_file_upload(
1162                    upload, self.context, self, attr=filename[1])
1163                if success:
1164                    self.context.writeLogMessage(
1165                        self, 'saved: %s' % filename[1])
1166                else:
1167                    self.upload_success = False
1168        self.max_file_upload_size = string_from_bytes(
1169            1024*getUtility(IStudentsUtils).MAX_KB)
1170        return
1171
1172    @property
1173    def label(self):
1174        container_title = self.context.__parent__.title
1175        return _('${a} <br /> Application Form ${b}', mapping = {
1176            'a':container_title, 'b':self.context.application_number})
1177
1178    def getTransitions(self):
1179        """Return a list of dicts of allowed transition ids and titles.
1180
1181        Each list entry provides keys ``name`` and ``title`` for
1182        internal name and (human readable) title of a single
1183        transition.
1184        """
1185        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
1186            if not t[0] in ('pay', 'create')]
1187        return [dict(name='', title=_('No transition'))] +[
1188            dict(name=x, title=y) for x, y in allowed_transitions]
1189
1190    def saveCourses(self):
1191        """In custom packages we needed to customize the certificate
1192        select widget. We just save course1 and course2 if these customized
1193        fields appear in the form.
1194        """
1195        return None, []
1196
1197    @action(_('Save'), style='primary')
1198    def save(self, **data):
1199        error, changed_courses = self.saveCourses()
1200        if error:
1201            self.flash(error, type='danger')
1202            return
1203        form = self.request.form
1204        password = form.get('password', None)
1205        password_ctl = form.get('control_password', None)
1206        if password:
1207            validator = getUtility(IPasswordValidator)
1208            errors = validator.validate_password(password, password_ctl)
1209            if errors:
1210                self.flash( ' '.join(errors), type='danger')
1211                return
1212        if self.upload_success is False:  # False is not None!
1213            # Error during image upload. Ignore other values.
1214            return
1215        changed_fields = self.applyData(self.context, **data)
1216        # Turn list of lists into single list
1217        if changed_fields:
1218            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1219        else:
1220            changed_fields = []
1221        changed_fields += changed_courses
1222        if password:
1223            # Now we know that the form has no errors and can set password ...
1224            IUserAccount(self.context).setPassword(password)
1225            changed_fields.append('password')
1226        fields_string = ' + '.join(changed_fields)
1227        trans_id = form.get('transition', None)
1228        if trans_id:
1229            self.wf_info.fireTransition(trans_id)
1230        self.flash(_('Form has been saved.'))
1231        if fields_string:
1232            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
1233        return
1234
1235    def unremovable(self, ticket):
1236        return False
1237
1238    @property
1239    def picture_editable(self):
1240        return self.context.__parent__.with_picture
1241
1242    # This method is also used by the ApplicantEditFormPage
1243    def delPaymentTickets(self, **data):
1244        form = self.request.form
1245        if 'val_id' in form:
1246            child_id = form['val_id']
1247        else:
1248            self.flash(_('No payment selected.'), type='warning')
1249            self.redirect(self.url(self.context))
1250            return
1251        if not isinstance(child_id, list):
1252            child_id = [child_id]
1253        deleted = []
1254        for id in child_id:
1255            # Applicants are not allowed to remove used payment tickets
1256            if not self.unremovable(self.context[id]):
1257                try:
1258                    del self.context[id]
1259                    deleted.append(id)
1260                except:
1261                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1262                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1263        if len(deleted):
1264            self.flash(_('Successfully removed: ${a}',
1265                mapping = {'a':', '.join(deleted)}))
1266            self.context.writeLogMessage(
1267                self, 'removed: % s' % ', '.join(deleted))
1268        return
1269
1270    # We explicitely want the forms to be validated before payment tickets
1271    # can be created. If no validation is requested, use
1272    # 'validator=NullValidator' in the action directive
1273    @action(_('Add online payment ticket'), style='primary')
1274    def addPaymentTicket(self, **data):
1275        self.redirect(self.url(self.context, '@@addafp'))
1276        return
1277
1278    @action(_('Add balance payment ticket'), style='primary')
1279    def addBalancePaymentTicket(self, **data):
1280        self.redirect(self.url(self.context, '@@addbp'))
1281        return
1282
1283    @jsaction(_('Remove selected tickets'))
1284    def removePaymentTickets(self, **data):
1285        self.delPaymentTickets(**data)
1286        self.redirect(self.url(self.context) + '/@@manage')
1287        return
1288
1289    # Not used in base package
1290    def file_exists(self, attr):
1291        file = getUtility(IExtFileStore).getFileByContext(
1292            self.context, attr=attr)
1293        if file:
1294            return True
1295        else:
1296            return False
1297
1298class ApplicantEditFormPage(ApplicantManageFormPage):
1299    """An applicant-centered edit view for applicant data.
1300    """
1301    grok.context(IApplicantEdit)
1302    grok.name('edit')
1303    grok.require('waeup.handleApplication')
1304    grok.template('applicanteditpage')
1305    manage_applications = False
1306    submit_state = PAID
1307    mandate_days = 31
1308
1309    @property
1310    def display_refereereports(self):
1311        return False
1312
1313    @property
1314    def form_fields(self):
1315        if self.context.special:
1316            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1317                'locked', 'suspended')
1318            form_fields['applicant_id'].for_display = True
1319        else:
1320            form_fields = grok.AutoFields(IApplicantEdit).omit(
1321                'locked', 'course_admitted', 'student_id',
1322                'suspended'
1323                )
1324            form_fields['applicant_id'].for_display = True
1325            form_fields['reg_number'].for_display = True
1326        return form_fields
1327
1328    @property
1329    def display_actions(self):
1330        state = IWorkflowState(self.context).getState()
1331        # If the form is unlocked, applicants are allowed to save the form
1332        # and remove unused tickets.
1333        actions = [[_('Save')], [_('Remove selected tickets')]]
1334        # Only in state started they can also add tickets.
1335        if state == STARTED:
1336            actions = [[_('Save')],
1337                [_('Add online payment ticket'),_('Remove selected tickets')]]
1338        # In state paid, they can submit the data and further add tickets
1339        # if the application is special.
1340        elif self.context.special and state == PAID:
1341            actions = [[_('Save'), _('Finally Submit')],
1342                [_('Add online payment ticket'),_('Remove selected tickets')]]
1343        elif state == PAID:
1344            actions = [[_('Save'), _('Finally Submit')],
1345                [_('Remove selected tickets')]]
1346        applicants_utils = getUtility(IApplicantsUtils)
1347        if self.context.state in applicants_utils.BALANCE_PAYMENT_STATES:
1348            actions[1].append(_('Add balance payment ticket'))
1349        return actions
1350
1351    @property
1352    def picture_editable(self):
1353        return self.context.__parent__.picture_editable
1354
1355    def unremovable(self, ticket):
1356        return ticket.r_code
1357
1358    def emit_lock_message(self):
1359        self.flash(_('The requested form is locked (read-only).'),
1360                   type='warning')
1361        self.redirect(self.url(self.context))
1362        return
1363
1364    def update(self):
1365        if self.context.locked or (
1366            self.context.__parent__.expired and
1367            self.context.__parent__.strict_deadline):
1368            self.emit_lock_message()
1369            return
1370        super(ApplicantEditFormPage, self).update()
1371        return
1372
1373    def dataNotComplete(self, data):
1374        if self.picture_editable:
1375            store = getUtility(IExtFileStore)
1376            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1377                return _('No passport picture uploaded.')
1378            if not self.request.form.get('confirm_passport', False):
1379                return _('Passport picture confirmation box not ticked.')
1380        return False
1381
1382    # We explicitely want the forms to be validated before payment tickets
1383    # can be created. If no validation is requested, use
1384    # 'validator=NullValidator' in the action directive
1385    @action(_('Add online payment ticket'), style='primary')
1386    def addPaymentTicket(self, **data):
1387        self.redirect(self.url(self.context, '@@addafp'))
1388        return
1389
1390    @action(_('Add balance payment ticket'), style='primary')
1391    def addBalancePaymentTicket(self, **data):
1392        self.redirect(self.url(self.context, '@@addbp'))
1393        return
1394
1395    @jsaction(_('Remove selected tickets'))
1396    def removePaymentTickets(self, **data):
1397        self.delPaymentTickets(**data)
1398        self.redirect(self.url(self.context) + '/@@edit')
1399        return
1400
1401    @action(_('Save'), style='primary')
1402    def save(self, **data):
1403        if self.upload_success is False:  # False is not None!
1404            # Error during image upload. Ignore other values.
1405            return
1406        self.applyData(self.context, **data)
1407        error, dummy = self.saveCourses()
1408        if error:
1409            self.flash(error, type='danger')
1410            return
1411        self.flash(_('Form has been saved.'))
1412        return
1413
1414    def informReferees(self):
1415        site = grok.getSite()
1416        kofa_utils = getUtility(IKofaUtils)
1417        failed = ''
1418        emails_sent = 0
1419        for referee in self.context.referees:
1420            if referee.email_sent:
1421                continue
1422            mandate = RefereeReportMandate(days=self.mandate_days)
1423            mandate.params['name'] = referee.name
1424            mandate.params['email'] = referee.email
1425            mandate.params[
1426                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1427                    self.context.__parent__.code,
1428                    self.context.application_number)
1429            mandate.params['redirect_path2'] = ''
1430            mandate.params['applicant_id'] = self.context.applicant_id
1431            site['mandates'].addMandate(mandate)
1432            # Send invitation email
1433            args = {'mandate_id':mandate.mandate_id}
1434            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1435            url_info = u'Report link: %s' % mandate_url
1436            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1437            if success:
1438                emails_sent += 1
1439                self.context.writeLogMessage(
1440                    self, 'email sent: %s' % referee.email)
1441                referee.email_sent = True
1442            else:
1443                failed += '%s ' % referee.email
1444        return failed, emails_sent
1445
1446    @property
1447    def _finalsubmit_msg(self):
1448        return _('Form has been submitted.')
1449
1450    @action(_('Finally Submit'), warning=WARNING)
1451    def finalsubmit(self, **data):
1452        if self.upload_success is False:  # False is not None!
1453            return # error during image upload. Ignore other values
1454        dnt = self.dataNotComplete(data)
1455        if dnt:
1456            self.flash(dnt, type='danger')
1457            return
1458        self.applyData(self.context, **data)
1459        error, dummy = self.saveCourses()
1460        if error:
1461            self.flash(error, type='danger')
1462            return
1463        state = IWorkflowState(self.context).getState()
1464        # This shouldn't happen, but the application officer
1465        # might have forgotten to lock the form after changing the state
1466        if state != self.submit_state:
1467            self.flash(_('The form cannot be submitted. Wrong state!'),
1468                       type='danger')
1469            return
1470        msg = self._finalsubmit_msg
1471        # Create mandates and send emails to referees
1472        if getattr(self.context, 'referees', None):
1473            failed, emails_sent = self.informReferees()
1474            if failed:
1475                self.flash(
1476                    _('Some invitation emails could not be sent:') + failed,
1477                    type='danger')
1478                return
1479            msg = _('Form has been successfully submitted and '
1480                    '${a} invitation emails were sent.',
1481                    mapping = {'a':  emails_sent})
1482        IWorkflowInfo(self.context).fireTransition('submit')
1483        # Send confirmation email
1484        getUtility(IKofaUtils).informApplicant(self.context)
1485        # application_date is used in export files for sorting.
1486        # We can thus store utc.
1487        self.context.application_date = datetime.utcnow()
1488        self.flash(msg)
1489        self.redirect(self.url(self.context))
1490        return
1491
1492class PassportImage(grok.View):
1493    """Renders the passport image for applicants.
1494    """
1495    grok.name('passport.jpg')
1496    grok.context(IApplicant)
1497    grok.require('waeup.viewApplication')
1498
1499    def render(self):
1500        # A filename chooser turns a context into a filename suitable
1501        # for file storage.
1502        image = getUtility(IExtFileStore).getFileByContext(self.context)
1503        self.response.setHeader('Content-Type', 'image/jpeg')
1504        if image is None:
1505            # show placeholder image
1506            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1507        return image
1508
1509class PassportImageForReport(PassportImage):
1510    """Renders the passport image for applicants for referee reports.
1511    """
1512    grok.name('passport_for_report.jpg')
1513    grok.context(IApplicant)
1514    grok.require('waeup.Public')
1515
1516    def render(self):
1517        # Check mandate
1518        form = self.request.form
1519        self.mandate_id = form.get('mandate_id', None)
1520        self.mandates = grok.getSite()['mandates']
1521        mandate = self.mandates.get(self.mandate_id, None)
1522        if mandate is None:
1523            self.flash(_('No mandate.'), type='warning')
1524            self.redirect(self.application_url())
1525            return
1526        if mandate:
1527            # Check the mandate expiration date after redirect again
1528            if mandate.expires < datetime.utcnow():
1529                self.flash(_('Mandate expired.'),
1530                           type='warning')
1531                self.redirect(self.application_url())
1532                return
1533            # Check if mandate allows access
1534            if mandate.params.get('applicant_id') != self.context.applicant_id:
1535                self.flash(_('Wrong mandate.'),
1536                           type='warning')
1537                self.redirect(self.application_url())
1538                return
1539            return super(PassportImageForReport, self).render()
1540        return
1541
1542class ApplicantRegistrationPage(KofaAddFormPage):
1543    """Captcha'd registration page for applicants.
1544    """
1545    grok.context(IApplicantsContainer)
1546    grok.name('register')
1547    grok.require('waeup.Anonymous')
1548    grok.template('applicantregister')
1549
1550    @property
1551    def form_fields(self):
1552        form_fields = None
1553        if self.context.mode == 'update':
1554            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1555                'lastname','reg_number','email')
1556        else: #if self.context.mode == 'create':
1557            form_fields = grok.AutoFields(IApplicantEdit).select(
1558                'firstname', 'middlename', 'lastname', 'email', 'phone')
1559        return form_fields
1560
1561    @property
1562    def label(self):
1563        return _('Apply for ${a}',
1564            mapping = {'a':self.context.title})
1565
1566    def update(self):
1567        if self.context.expired:
1568            self.flash(_('Outside application period.'), type='warning')
1569            self.redirect(self.url(self.context))
1570            return
1571        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1572        if blocker:
1573            self.flash(_('The portal is in maintenance mode '
1574                        'and registration temporarily disabled.'),
1575                       type='warning')
1576            self.redirect(self.url(self.context))
1577            return
1578        # Handle captcha
1579        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1580        self.captcha_result = self.captcha.verify(
1581            self.request, 'applicant_register')
1582        self.captcha_code = self.captcha.display(
1583            self.captcha_result.error_code, 'applicant_register')
1584        return
1585
1586    def _redirect(self, email, password, applicant_id):
1587        # Forward only email to landing page in base package.
1588        self.redirect(self.url(self.context, 'registration_complete',
1589            data = dict(email=email)))
1590        return
1591
1592    @property
1593    def _postfix(self):
1594        """In customized packages we can add a container dependent string if
1595        applicants have been imported into several containers.
1596        """
1597        return ''
1598
1599    @action(_('Send login credentials to email address'), style='primary')
1600    def register(self, **data):
1601        if not self.captcha_result.is_valid:
1602            # Captcha will display error messages automatically.
1603            # No need to flash something.
1604            return
1605        if self.context.mode == 'create':
1606            # Check if there are unused records in this container which
1607            # can be taken
1608            applicant = self.context.first_unused
1609            if applicant is None:
1610                # Add applicant
1611                applicant = createObject(u'waeup.Applicant')
1612                self.context.addApplicant(applicant)
1613            else:
1614                applicants_root = grok.getSite()['applicants']
1615                ob_class = self.__implemented__.__name__.replace(
1616                    'waeup.kofa.','')
1617                applicants_root.logger.info('%s - used: %s' % (
1618                    ob_class, applicant.applicant_id))
1619            self.applyData(applicant, **data)
1620            # applicant.reg_number = applicant.applicant_id
1621            notify(grok.ObjectModifiedEvent(applicant))
1622        elif self.context.mode == 'update':
1623            # Update applicant
1624            reg_number = data.get('reg_number','')
1625            lastname = data.get('lastname','')
1626            cat = getUtility(ICatalog, name='applicants_catalog')
1627            searchstr = reg_number + self._postfix
1628            results = list(
1629                cat.searchResults(reg_number=(searchstr, searchstr)))
1630            if results:
1631                applicant = results[0]
1632                if getattr(applicant,'lastname',None) is None:
1633                    self.flash(_('An error occurred.'), type='danger')
1634                    return
1635                elif applicant.lastname.lower() != lastname.lower():
1636                    # Don't tell the truth here. Anonymous must not
1637                    # know that a record was found and only the lastname
1638                    # verification failed.
1639                    self.flash(
1640                        _('No application record found.'), type='warning')
1641                    return
1642                elif applicant.password is not None and \
1643                    applicant.state != INITIALIZED:
1644                    self.flash(_('Your password has already been set and used. '
1645                                 'Please proceed to the login page.'),
1646                               type='warning')
1647                    return
1648                # Store email address but nothing else.
1649                applicant.email = data['email']
1650                notify(grok.ObjectModifiedEvent(applicant))
1651            else:
1652                # No record found, this is the truth.
1653                self.flash(_('No application record found.'), type='warning')
1654                return
1655        else:
1656            # Does not happen but anyway ...
1657            return
1658        kofa_utils = getUtility(IKofaUtils)
1659        password = kofa_utils.genPassword()
1660        IUserAccount(applicant).setPassword(password)
1661        # Send email with credentials
1662        args = {'login':applicant.applicant_id, 'password':password}
1663        login_url = self.url(grok.getSite()) + '/login?%s' % urlencode(args)
1664        url_info = u'Login: %s' % login_url
1665        msg = _('You have successfully been registered for the')
1666        if kofa_utils.sendCredentials(IUserAccount(applicant),
1667            password, url_info, msg):
1668            email_sent = applicant.email
1669        else:
1670            email_sent = None
1671        self._redirect(email=email_sent, password=password,
1672            applicant_id=applicant.applicant_id)
1673        return
1674
1675class ApplicantRegistrationEmailSent(KofaPage):
1676    """Landing page after successful registration.
1677
1678    """
1679    grok.name('registration_complete')
1680    grok.require('waeup.Public')
1681    grok.template('applicantregemailsent')
1682    label = _('Your registration was successful.')
1683
1684    def update(self, email=None, applicant_id=None, password=None):
1685        self.email = email
1686        self.password = password
1687        self.applicant_id = applicant_id
1688        return
1689
1690class ApplicantCheckStatusPage(KofaPage):
1691    """Captcha'd status checking page for applicants.
1692    """
1693    grok.context(IApplicantsRoot)
1694    grok.name('checkstatus')
1695    grok.require('waeup.Anonymous')
1696    grok.template('applicantcheckstatus')
1697    buttonname = _('Submit')
1698    pnav = 7
1699
1700    def label(self):
1701        if self.result:
1702            return _('Admission status of ${a}',
1703                     mapping = {'a':self.applicant.applicant_id})
1704        return _('Check your admission status')
1705
1706    def update(self, SUBMIT=None):
1707        form = self.request.form
1708        self.result = False
1709        # Handle captcha
1710        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1711        self.captcha_result = self.captcha.verify(
1712            self.request, 'check_appl_status')
1713        self.captcha_code = self.captcha.display(
1714            self.captcha_result.error_code, 'check_appl_status')
1715        if SUBMIT:
1716            if not self.captcha_result.is_valid:
1717                # Captcha will display error messages automatically.
1718                # No need to flash something.
1719                return
1720            unique_id = form.get('unique_id', None)
1721            lastname = form.get('lastname', None)
1722            if not unique_id or not lastname:
1723                self.flash(
1724                    _('Required input missing.'), type='warning')
1725                return
1726            cat = getUtility(ICatalog, name='applicants_catalog')
1727            results = list(
1728                cat.searchResults(applicant_id=(unique_id, unique_id)))
1729            if not results:
1730                results = list(
1731                    cat.searchResults(reg_number=(unique_id, unique_id)))
1732            if results:
1733                applicant = results[0]
1734                if applicant.lastname.lower().strip() != lastname.lower():
1735                    # Don't tell the truth here. Anonymous must not
1736                    # know that a record was found and only the lastname
1737                    # verification failed.
1738                    self.flash(
1739                        _('No application record found.'), type='warning')
1740                    return
1741            else:
1742                self.flash(_('No application record found.'), type='warning')
1743                return
1744            self.applicant = applicant
1745            self.entry_session = "%s/%s" % (
1746                applicant.__parent__.year,
1747                applicant.__parent__.year+1)
1748            course_admitted = getattr(applicant, 'course_admitted', None)
1749            self.course_admitted = False
1750            if course_admitted is not None:
1751                try:
1752                    self.course_admitted = True
1753                    self.longtitle = course_admitted.longtitle
1754                    self.department = course_admitted.__parent__.__parent__.longtitle
1755                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1756                except AttributeError:
1757                    self.flash(_('Application record invalid.'), type='warning')
1758                    return
1759            self.result = True
1760            self.admitted = False
1761            self.not_admitted = False
1762            self.submitted = False
1763            self.not_submitted = False
1764            self.created = False
1765            if applicant.state in (ADMITTED, CREATED):
1766                self.admitted = True
1767            if applicant.state in (CREATED):
1768                self.created = True
1769                self.student_id = applicant.student_id
1770                self.password = applicant.application_number
1771            if applicant.state in (NOT_ADMITTED,):
1772                self.not_admitted = True
1773            if applicant.state in (SUBMITTED,):
1774                self.submitted = True
1775            if applicant.state in (INITIALIZED, STARTED, PAID):
1776                self.not_submitted = True
1777        return
1778
1779class CheckTranscriptStatus(KofaPage):
1780    """A display page for checking transcript processing status.
1781    """
1782    grok.context(IApplicantsRoot)
1783    grok.name('checktranscript')
1784    grok.require('waeup.Public')
1785    label = _('Check transcript status')
1786    buttonname = _('Check status now')
1787    pnav = 8
1788    websites = (('DemoPortal', 'https://kofa-demo.waeup.org/'),)
1789    #websites = (('DemoPortal', 'http://localhost:8080/app/'),)
1790    appl_url1 = 'https://kofa-demo.waeup.org/applicants'
1791    appl_url2 = 'https://kofa-demo.waeup.org/applicants'
1792
1793    def update(self, SUBMIT=None):
1794        form = self.request.form
1795        self.button = False
1796        # Handle captcha
1797        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1798        self.captcha_result = self.captcha.verify(
1799            self.request, 'check_trans_state')
1800        self.captcha_code = self.captcha.display(
1801            self.captcha_result.error_code, 'check_trans_state')
1802        if SUBMIT:
1803            self.results = []
1804            if not self.captcha_result.is_valid:
1805                # Captcha will display error messages automatically.
1806                # No need to flash something.
1807                return
1808            unique_id = form.get('unique_id', None)
1809            email = form.get('email', None)
1810            if not unique_id or not email:
1811                self.flash(
1812                    _('Required input missing.'), type='warning')
1813                return
1814            self.button = True
1815            # Call webservice of all websites
1816            for website in self.websites:
1817                server = xmlrpclib.ServerProxy(website[1])
1818                result = server.get_grad_student(unique_id, email)
1819                if not result:
1820                    continue
1821                self.results.append((result, website))
1822        return
1823
1824class ExportJobContainerOverview(KofaPage):
1825    """Page that lists active applicant data export jobs and provides links
1826    to discard or download CSV files.
1827
1828    """
1829    grok.context(VirtualApplicantsExportJobContainer)
1830    grok.require('waeup.exportApplication')
1831    grok.name('index.html')
1832    grok.template('exportjobsindex')
1833    label = _('Data Exports')
1834    pnav = 3
1835
1836    def update(self, CREATE=None, CREATE_PAYMENTS=None, payments_start=None,
1837               payments_end=None, DISCARD=None, job_id=None):
1838        if CREATE:
1839            self.redirect(self.url('@@start_export'))
1840            return
1841        if CREATE_PAYMENTS:
1842            args = {'payments_start':payments_start, 'payments_end':payments_end}
1843            self.redirect(self.url('@@start_export_payments') + '?%s' % urlencode(args))
1844            return
1845        if DISCARD and job_id:
1846            entry = self.context.entry_from_job_id(job_id)
1847            self.context.delete_export_entry(entry)
1848            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1849            self.context.logger.info(
1850                '%s - discarded: job_id=%s' % (ob_class, job_id))
1851            self.flash(_('Discarded export') + ' %s' % job_id)
1852        self.entries = doll_up(self, user=self.request.principal.id)
1853        return
1854
1855class ExportJobContainerJobStart(UtilityView, grok.View):
1856    """View that starts three export jobs, one for applicants, a second
1857    one for applicant payments and a third for referee reports.
1858    """
1859    grok.context(VirtualApplicantsExportJobContainer)
1860    grok.require('waeup.exportApplication')
1861    grok.name('start_export')
1862
1863    EXPORTER_LIST = ('applicants',
1864                   'applicantpayments',
1865                   'applicantrefereereports')
1866
1867    def update(self):
1868        utils = queryUtility(IKofaUtils)
1869        if not utils.expensive_actions_allowed():
1870            self.flash(_(
1871                "Currently, exporters cannot be started due to high "
1872                "system load. Please try again later."), type='danger')
1873            self.entries = doll_up(self, user=None)
1874            return
1875
1876        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1877        container_code = self.context.__parent__.code
1878        # Start first exporter
1879        for exporter in self.EXPORTER_LIST:
1880            job_id = self.context.start_export_job(exporter,
1881                                          self.request.principal.id,
1882                                          container=container_code)
1883            self.context.logger.info(
1884                '%s - exported: %s (%s), job_id=%s'
1885                % (ob_class, exporter, container_code, job_id))
1886            # Commit transaction so that job is stored in the ZODB
1887            transaction.commit()
1888        self.flash(_('Exports started.'))
1889        self.redirect(self.url(self.context))
1890        return
1891
1892    def render(self):
1893        return
1894
1895
1896class PaymentsExportJobContainerJobStart(UtilityView, grok.View):
1897    """View that starts only payment export jobs.
1898    """
1899    grok.context(VirtualApplicantsExportJobContainer)
1900    grok.require('waeup.exportApplication')
1901    grok.name('start_export_payments')
1902
1903    def update(self, payments_start=None, payments_end= None):
1904        utils = queryUtility(IKofaUtils)
1905        if not utils.expensive_actions_allowed():
1906            self.flash(_(
1907                "Currently, exporters cannot be started due to high "
1908                "system load. Please try again later."), type='danger')
1909            self.entries = doll_up(self, user=None)
1910            return
1911
1912        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1913        container_code = self.context.__parent__.code
1914        job_id = self.context.start_export_job('applicantpayments',
1915                                      self.request.principal.id,
1916                                      container=container_code,
1917                                      p_start=payments_start,
1918                                      p_end=payments_end)
1919        self.context.logger.info(
1920            '%s - exported: %s (%s), job_id=%s'
1921            % (ob_class, 'applicantpayments', container_code, job_id))
1922        self.flash(_('Exports started.'))
1923        self.redirect(self.url(self.context))
1924        return
1925
1926    def render(self):
1927        return
1928
1929class ExportJobContainerDownload(ExportCSVView):
1930    """Page that downloads an applicants export csv file.
1931
1932    """
1933    grok.context(VirtualApplicantsExportJobContainer)
1934    grok.require('waeup.exportApplication')
1935
1936class RefereesRemindPage(UtilityView, grok.View):
1937    """A display view for referee reports.
1938    """
1939    grok.context(IApplicant)
1940    grok.name('remind_referees')
1941    grok.require('waeup.manageApplication')
1942
1943    mandate_days = 31
1944
1945    def remindReferees(self):
1946        site = grok.getSite()
1947        kofa_utils = getUtility(IKofaUtils)
1948        failed = ''
1949        emails_sent = 0
1950        for referee in self.context.referees:
1951            #if not referee.email_sent:
1952            #    continue
1953            # Check if referee has already created a report
1954            report_exists = False
1955            for report in self.context.refereereports:
1956                if report.email == referee.email:
1957                    report_exists = True
1958            if report_exists:
1959                continue
1960            # If not, create new mandate
1961            mandate = RefereeReportMandate(days=self.mandate_days)
1962            mandate.params['name'] = referee.name
1963            mandate.params['email'] = referee.email
1964            mandate.params[
1965                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1966                    self.context.__parent__.code,
1967                    self.context.application_number)
1968            mandate.params['redirect_path2'] = ''
1969            mandate.params['applicant_id'] = self.context.applicant_id
1970            site['mandates'].addMandate(mandate)
1971            # Send invitation email
1972            args = {'mandate_id':mandate.mandate_id}
1973            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1974            url_info = u'Report link: %s' % mandate_url
1975            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1976            if success:
1977                emails_sent += 1
1978                self.context.writeLogMessage(
1979                    self, 'email sent: %s' % referee.email)
1980                referee.email_sent = True
1981            else:
1982                failed += '%s ' % referee.email
1983        return failed, emails_sent
1984
1985    def update(self):
1986        if self.context.state != 'submitted':
1987            self.flash(
1988                _('Not allowed!'), type='danger')
1989            return self.redirect(self.url(self.context))
1990        failed, emails_sent = self.remindReferees()
1991        msg = _('${a} referee(s) have been reminded by email.',
1992                mapping = {'a':  emails_sent})
1993        self.flash(msg)
1994        return self.redirect(self.url(self.context))
1995
1996    def render(self):
1997        return
1998
1999class RefereeReportDisplayFormPage(KofaDisplayFormPage):
2000    """A display view for referee reports.
2001    """
2002    grok.context(IApplicantRefereeReport)
2003    grok.name('index')
2004    grok.require('waeup.manageApplication')
2005    label = _('Referee Report')
2006    pnav = 3
2007    form_fields = grok.AutoFields(IApplicantRefereeReport)
2008    form_fields[
2009        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2010
2011class RefereeReportManageFormPage(KofaEditFormPage):
2012    """A displaymanage for referee reports.
2013    """
2014    grok.context(IApplicantRefereeReport)
2015    grok.name('manage')
2016    grok.require('waeup.managePortal')
2017    label = _('Manage Referee Report')
2018    pnav = 3
2019    form_fields = grok.AutoFields(IApplicantRefereeReport).omit('creation_date')
2020
2021    @action(_('Save'), style='primary')
2022    def save(self, **data):
2023        changed_fields = self.applyData(self.context, **data)
2024        # Turn list of lists into single list
2025        if changed_fields:
2026            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
2027        else:
2028            changed_fields = []
2029        fields_string = ' + '.join(changed_fields)
2030        self.flash(_('Form has been saved.'))
2031        if fields_string:
2032            self.context.__parent__.writeLogMessage(
2033                self, '%s - saved: %s' % (self.context.r_id, fields_string))
2034        return
2035
2036class RemoveRefereeReportPage(UtilityView, grok.View):
2037    """
2038    """
2039    grok.context(IApplicantRefereeReport)
2040    grok.name('remove')
2041    grok.require('waeup.manageApplication')
2042
2043    def update(self):
2044        redirect_url = self.url(self.context.__parent__)
2045        self.context.__parent__.writeLogMessage(
2046            self, 'removed: %s' % self.context.r_id)
2047        del self.context.__parent__[self.context.r_id]
2048        self.flash(_('Referee report removed.'))
2049        self.redirect(redirect_url)
2050        return
2051
2052    def render(self):
2053        return
2054
2055class RefereeReportAddFormPage(KofaAddFormPage):
2056    """Add-form to add an referee report. This form
2057    is protected by a mandate.
2058    """
2059    grok.context(IApplicant)
2060    grok.require('waeup.Public')
2061    grok.name('addrefereereport')
2062    form_fields = grok.AutoFields(
2063        IApplicantRefereeReport).omit('creation_date')
2064    grok.template('refereereportpage')
2065    label = _('Referee Report Form')
2066    pnav = 3
2067    #doclink = DOCLINK + '/refereereports.html'
2068
2069    def update(self):
2070        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2071        if blocker:
2072            self.flash(_('The portal is in maintenance mode. '
2073                        'Referee report forms are temporarily disabled.'),
2074                       type='warning')
2075            self.redirect(self.application_url())
2076            return
2077        # Check mandate
2078        form = self.request.form
2079        self.mandate_id = form.get('mandate_id', None)
2080        self.mandates = grok.getSite()['mandates']
2081        mandate = self.mandates.get(self.mandate_id, None)
2082        if mandate is None and not self.request.form.get('form.actions.submit'):
2083            self.flash(_('No mandate.'), type='warning')
2084            self.redirect(self.application_url())
2085            return
2086        if mandate:
2087            # Check the mandate expiration date after redirect again
2088            if mandate.expires < datetime.utcnow():
2089                self.flash(_('Mandate expired.'),
2090                           type='warning')
2091                self.redirect(self.application_url())
2092                return
2093            args = {'mandate_id':mandate.mandate_id}
2094            # Check if report exists.
2095            # (1) If mandate has been used to create a report,
2096            # redirect to the pdf file.
2097            if mandate.params.get('redirect_path2'):
2098                self.redirect(
2099                    self.application_url() +
2100                    mandate.params.get('redirect_path2') +
2101                    '?%s' % urlencode(args))
2102                return
2103            # (2) Report exists but was created with another mandate.
2104            for report in self.context.refereereports:
2105                if report.email == mandate.params.get('email'):
2106                    self.flash(_('You have already created a '
2107                                 'report with another mandate.'),
2108                               type='warning')
2109                    self.redirect(self.application_url())
2110                    return
2111            # Prefill form with mandate params
2112            self.form_fields.get(
2113                'name').field.default = mandate.params['name']
2114            self.form_fields.get(
2115                'email_pref').field.default = mandate.params['email']
2116            self.passport_url = self.url(
2117                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
2118        super(RefereeReportAddFormPage, self).update()
2119        return
2120
2121    @action(_('Submit'),
2122              warning=_('Are you really sure? '
2123                        'Reports can neither be modified or added '
2124                        'after submission.'),
2125              style='primary')
2126    def addRefereeReport(self, **data):
2127        report = createObject(u'waeup.ApplicantRefereeReport')
2128        timestamp = ("%d" % int(time()*10000))[1:]
2129        report.r_id = "r%s" % timestamp
2130        report.email = self.mandates[self.mandate_id].params['email']
2131        self.applyData(report, **data)
2132        self.context[report.r_id] = report
2133        # self.flash(_('Referee report has been saved. Thank you!'))
2134        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
2135        # Changed on 19/04/20: We do no longer delete the mandate
2136        # but set path to redirect to the pdf file
2137        self.mandates[self.mandate_id].params[
2138            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
2139                self.context.__parent__.code,
2140                self.context.application_number,
2141                report.r_id)
2142        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
2143        args = {'mandate_id':self.mandate_id}
2144        self.flash(_('Your report has been successfully submitted. '
2145                     'Please use the report link in the email again to download '
2146                     'a pdf slip of your report.'))
2147        #self.redirect(self.url(report, 'referee_report.pdf')
2148        #              + '?%s' % urlencode(args))
2149        self.redirect(self.application_url())
2150        return
2151
2152class ExportPDFReportSlipPage(UtilityView, grok.View):
2153    """Deliver a PDF slip of the context.
2154    """
2155    grok.context(IApplicantRefereeReport)
2156    grok.name('referee_report_slip.pdf')
2157    grok.require('waeup.manageApplication')
2158    form_fields = grok.AutoFields(IApplicantRefereeReport)
2159    form_fields[
2160        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2161    #prefix = 'form'
2162    note = None
2163
2164    @property
2165    def title(self):
2166        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2167        return translate(_('Referee Report'), 'waeup.kofa',
2168            target_language=portal_language)
2169
2170    @property
2171    def label(self):
2172        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2173        return translate(_('Referee Report Slip'),
2174            'waeup.kofa', target_language=portal_language) \
2175            + ' %s' % self.context.r_id
2176
2177    def render(self):
2178        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
2179            self.request)
2180        students_utils = getUtility(IStudentsUtils)
2181        return students_utils.renderPDF(self,'referee_report_slip.pdf',
2182            self.context.__parent__, applicantview, note=self.note)
2183
2184class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
2185    """Deliver a PDF slip of the context to referees.
2186    """
2187    grok.name('referee_report.pdf')
2188    grok.require('waeup.Public')
2189
2190    def update(self):
2191        # Check mandate
2192        form = self.request.form
2193        self.mandate_id = form.get('mandate_id', None)
2194        self.mandates = grok.getSite()['mandates']
2195        mandate = self.mandates.get(self.mandate_id, None)
2196        if mandate is None:
2197            self.flash(_('No mandate.'), type='warning')
2198            self.redirect(self.application_url())
2199            return
2200        if mandate:
2201            # Check the mandate expiration date after redirect again
2202            if mandate.expires < datetime.utcnow():
2203                self.flash(_('Mandate expired.'),
2204                           type='warning')
2205                self.redirect(self.application_url())
2206                return
2207            # Check if form has really been submitted
2208            if not mandate.params.get('redirect_path2') \
2209                or mandate.params.get(
2210                    'applicant_id') != self.context.__parent__.applicant_id:
2211                self.flash(_('Wrong mandate.'),
2212                           type='warning')
2213                self.redirect(self.application_url())
2214                return
2215            super(ExportPDFReportSlipPage2, self).update()
2216        return
2217
2218class AdditionalFile(grok.View):
2219    """Renders additional files for applicants.
2220    This is a baseclass.
2221    """
2222    grok.baseclass()
2223    grok.context(IApplicant)
2224    grok.require('waeup.viewApplication')
2225
2226    def render(self):
2227        #image = getUtility(IExtFileStore).getFileByContext(
2228        #    self.context, attr=self.download_name)
2229        file = getUtility(IExtFileStore).getFileByContext(
2230            self.context, attr=self.__name__)
2231        dummy,ext = os.path.splitext(file.name)
2232        if ext == '.jpg':
2233            self.response.setHeader('Content-Type', 'image/jpeg')
2234        elif ext == '.pdf':
2235            self.response.setHeader('Content-Type', 'application/pdf')
2236        return file
2237
2238class TestFile(AdditionalFile):
2239    """Renders testfile.
2240    """
2241    grok.name('testfile')
Note: See TracBrowser for help on using the repository browser.