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

Last change on this file since 17990 was 17985, checked in by Henrik Bettermann, 2 weeks ago

CreateAllStudentsPage: Stop creation after 1000 students to avoid write
conflicts which sill occur even though transactions have formerly been
commited.

  • Property svn:keywords set to Id
File size: 82.2 KB
Line 
1## $Id: browser.py 17985 2025-01-16 02:08:45Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23import transaction
24from cgi import escape
25from urllib import urlencode
26from datetime import datetime, date
27from time import time, sleep
28import xmlrpclib
29from zope.event import notify
30from zope.component import getUtility, queryUtility, createObject, getAdapter
31from zope.catalog.interfaces import ICatalog
32from zope.i18n import translate
33from zope.security import checkPermission
34from hurry.workflow.interfaces import (
35    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
36from reportlab.platypus.doctemplate import LayoutError
37from waeup.kofa.mandates.mandate import RefereeReportMandate
38from waeup.kofa.applicants.interfaces import (
39    IApplicant, IApplicantEdit, IApplicantsRoot,
40    IApplicantsContainer, IApplicantsContainerAdd,
41    IApplicantOnlinePayment, IApplicantsUtils,
42    IApplicantRegisterUpdate, ISpecialApplicant,
43    IApplicantRefereeReport,
44    IApplicantBalancePayment
45    )
46from waeup.kofa.utils.helpers import (html2dict,
47    string_from_bytes, file_size, get_fileformat)
48from waeup.kofa.applicants.container import (
49    ApplicantsContainer, VirtualApplicantsExportJobContainer)
50from waeup.kofa.applicants.applicant import search
51from waeup.kofa.applicants.workflow import (
52    INITIALIZED, STARTED, PAID, SUBMITTED,
53    ADMITTED, NOT_ADMITTED, CREATED, PROCESSED)
54from waeup.kofa.browser import (
55#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
56    DEFAULT_PASSPORT_IMAGE_PATH)
57from waeup.kofa.browser.layout import (
58    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
59from waeup.kofa.browser.interfaces import ICaptchaManager
60from waeup.kofa.browser.breadcrumbs import Breadcrumb
61from waeup.kofa.browser.layout import (
62    NullValidator, jsaction, action, UtilityView)
63from waeup.kofa.browser.pages import (
64    add_local_role, del_local_roles, doll_up, ExportCSVView)
65from waeup.kofa.interfaces import (
66    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF, DOCLINK,
67    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
68from waeup.kofa.interfaces import MessageFactory as _
69from waeup.kofa.permissions import get_users_with_local_roles
70from waeup.kofa.students.interfaces import IStudentsUtils
71from waeup.kofa.students.browser import ContactStudentFormPage
72from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
73from waeup.kofa.widgets.datewidget import (
74    FriendlyDateDisplayWidget,
75    FriendlyDatetimeDisplayWidget)
76
77grok.context(IKofaObject) # Make IKofaObject the default context
78
79WARNING = _('Please always save the form before final submission and note that'
80            ' you can not edit your application records after submission.'
81            ' You really want to submit?')
82
83class ApplicantsRootPage(KofaDisplayFormPage):
84    grok.context(IApplicantsRoot)
85    grok.name('index')
86    grok.require('waeup.Public')
87    form_fields = grok.AutoFields(IApplicantsRoot)
88    label = _('Applicants Section')
89    pnav = 3
90
91    def update(self):
92        super(ApplicantsRootPage, self).update()
93        return
94
95    @property
96    def introduction(self):
97        # Here we know that the cookie has been set
98        lang = self.request.cookies.get('kofa.language')
99        html = self.context.description_dict.get(lang,'')
100        if html == '':
101            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
102            html = self.context.description_dict.get(portal_language,'')
103        return html
104
105    @property
106    def containers(self):
107        if self.layout.isAuthenticated():
108            return self.context.values()
109        values = sorted([container for container in self.context.values()
110                         if not container.hidden and container.enddate],
111                        key=lambda value: value.enddate, reverse=True)
112        return values
113
114class ApplicantsSearchPage(KofaPage):
115    grok.context(IApplicantsRoot)
116    grok.name('search')
117    grok.require('waeup.viewApplication')
118    label = _('Find applicants')
119    search_button = _('Find applicant')
120    pnav = 3
121
122    def update(self, *args, **kw):
123        form = self.request.form
124        self.results = []
125        if 'searchterm' in form and form['searchterm']:
126            self.searchterm = form['searchterm']
127            self.searchtype = form['searchtype']
128        elif 'old_searchterm' in form:
129            self.searchterm = form['old_searchterm']
130            self.searchtype = form['old_searchtype']
131        else:
132            if 'search' in form:
133                self.flash(_('Empty search string'), type='warning')
134            return
135        self.results = search(query=self.searchterm,
136            searchtype=self.searchtype, view=self)
137        if not self.results:
138            self.flash(_('No applicant found.'), type='warning')
139        return
140
141class ApplicantsRootManageFormPage(KofaEditFormPage):
142    grok.context(IApplicantsRoot)
143    grok.name('manage')
144    grok.template('applicantsrootmanagepage')
145    form_fields = grok.AutoFields(IApplicantsRoot)
146    label = _('Manage applicants section')
147    pnav = 3
148    grok.require('waeup.manageApplication')
149    taboneactions = [_('Save')]
150    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
151    tabthreeactions1 = [_('Remove selected local roles')]
152    tabthreeactions2 = [_('Add local role')]
153    subunits = _('Applicants Containers')
154    doclink = DOCLINK + '/applicants.html'
155
156    def getLocalRoles(self):
157        roles = ILocalRolesAssignable(self.context)
158        return roles()
159
160    def getUsers(self):
161        return getUtility(IKofaUtils).getUsers()
162
163    #def getUsers(self):
164    #    """Get a list of all users.
165    #    """
166    #    for key, val in grok.getSite()['users'].items():
167    #        url = self.url(val)
168    #        yield(dict(url=url, name=key, val=val))
169
170    def getUsersWithLocalRoles(self):
171        return get_users_with_local_roles(self.context)
172
173    @jsaction(_('Remove selected'))
174    def delApplicantsContainers(self, **data):
175        form = self.request.form
176        if 'val_id' in form:
177            child_id = form['val_id']
178        else:
179            self.flash(_('No container selected!'), type='warning')
180            self.redirect(self.url(self.context, '@@manage')+'#tab2')
181            return
182        if not isinstance(child_id, list):
183            child_id = [child_id]
184        deleted = []
185        for id in child_id:
186            try:
187                del self.context[id]
188                deleted.append(id)
189            except:
190                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
191                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
192        if len(deleted):
193            self.flash(_('Successfully removed: ${a}',
194                mapping = {'a':', '.join(deleted)}))
195        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
196        self.context.logger.info(
197            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
198        self.redirect(self.url(self.context, '@@manage')+'#tab2')
199        return
200
201    @action(_('Add applicants container'), validator=NullValidator)
202    def addApplicantsContainer(self, **data):
203        self.redirect(self.url(self.context, '@@add'))
204        return
205
206    @action(_('Add local role'), validator=NullValidator)
207    def addLocalRole(self, **data):
208        return add_local_role(self,3, **data)
209
210    @action(_('Remove selected local roles'))
211    def delLocalRoles(self, **data):
212        return del_local_roles(self,3,**data)
213
214    @action(_('Save'), style='primary')
215    def save(self, **data):
216        self.applyData(self.context, **data)
217        description = getattr(self.context, 'description', None)
218        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
219        self.context.description_dict = html2dict(description, portal_language)
220        self.flash(_('Form has been saved.'))
221        return
222
223class ApplicantsContainerAddFormPage(KofaAddFormPage):
224    grok.context(IApplicantsRoot)
225    grok.require('waeup.manageApplication')
226    grok.name('add')
227    grok.template('applicantscontaineraddpage')
228    label = _('Add applicants container')
229    pnav = 3
230
231    form_fields = grok.AutoFields(
232        IApplicantsContainerAdd).omit('code').omit('title')
233
234    @action(_('Add applicants container'))
235    def addApplicantsContainer(self, **data):
236        year = data['year']
237        if not data['container_number'] and not data['year']:
238            self.flash(
239              _('You must select either Year of Entrance or Container Number.'),
240                type='warning')
241            return
242        if not data['container_number']:
243            code = u'%s%s' % (data['prefix'], year)
244        else:
245            code = u'%s%s' % (data['prefix'], data['container_number'])
246        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
247        title = u'%s' % apptypes_dict[data['prefix']][0]
248        if year:
249            title = u'%s %s/%s' % (title, year, year + 1)
250        if code in self.context.keys():
251            self.flash(
252              _('An applicants container for the same application '
253                'type and entrance year or container number '
254                'already exists in the database.'),
255                type='warning')
256            return
257        # Add new applicants container...
258        container = createObject(u'waeup.ApplicantsContainer')
259        self.applyData(container, **data)
260        container.code = code
261        container.title = title
262        self.context[code] = container
263        self.flash(_('Added:') + ' "%s".' % code)
264        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
265        self.context.logger.info('%s - added: %s' % (ob_class, code))
266        self.redirect(self.url(self.context, u'@@manage'))
267        return
268
269    @action(_('Cancel'), validator=NullValidator)
270    def cancel(self, **data):
271        self.redirect(self.url(self.context, '@@manage'))
272
273class ApplicantsRootBreadcrumb(Breadcrumb):
274    """A breadcrumb for applicantsroot.
275    """
276    grok.context(IApplicantsRoot)
277    title = _(u'Applicants')
278
279class ApplicantsContainerBreadcrumb(Breadcrumb):
280    """A breadcrumb for applicantscontainers.
281    """
282    grok.context(IApplicantsContainer)
283
284
285class ApplicantsExportsBreadcrumb(Breadcrumb):
286    """A breadcrumb for exports.
287    """
288    grok.context(VirtualApplicantsExportJobContainer)
289    title = _(u'Applicant Data Exports')
290    target = None
291
292class ApplicantBreadcrumb(Breadcrumb):
293    """A breadcrumb for applicants.
294    """
295    grok.context(IApplicant)
296
297    @property
298    def title(self):
299        """Get a title for a context.
300        """
301        return self.context.application_number
302
303class OnlinePaymentBreadcrumb(Breadcrumb):
304    """A breadcrumb for payments.
305    """
306    grok.context(IApplicantOnlinePayment)
307
308    @property
309    def title(self):
310        return self.context.p_id
311
312class RefereeReportBreadcrumb(Breadcrumb):
313    """A breadcrumb for referee reports.
314    """
315    grok.context(IApplicantRefereeReport)
316
317    @property
318    def title(self):
319        return self.context.r_id
320
321class ApplicantsStatisticsPage(KofaDisplayFormPage):
322    """Some statistics about applicants in a container.
323    """
324    grok.context(IApplicantsContainer)
325    grok.name('statistics')
326    grok.require('waeup.viewApplicationStatistics')
327    grok.template('applicantcontainerstatistics')
328
329    @property
330    def label(self):
331        return "%s" % self.context.title
332
333class ApplicantsContainerPage(KofaDisplayFormPage):
334    """The standard view for regular applicant containers.
335    """
336    grok.context(IApplicantsContainer)
337    grok.name('index')
338    grok.require('waeup.Public')
339    grok.template('applicantscontainerpage')
340    pnav = 3
341
342    @property
343    def form_fields(self):
344        form_fields = grok.AutoFields(IApplicantsContainer).omit(
345            'title', 'description')
346        form_fields[
347            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
348        form_fields[
349            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
350        if self.request.principal.id == 'zope.anybody':
351            form_fields = form_fields.omit(
352                'code', 'prefix', 'year', 'mode', 'hidden',
353                'strict_deadline', 'application_category',
354                'application_slip_notice', 'with_picture')
355        return form_fields
356
357    @property
358    def introduction(self):
359        # Here we know that the cookie has been set
360        lang = self.request.cookies.get('kofa.language')
361        html = self.context.description_dict.get(lang,'')
362        if html == '':
363            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
364            html = self.context.description_dict.get(portal_language,'')
365        return html
366
367    @property
368    def label(self):
369        return "%s" % self.context.title
370
371class ApplicantsContainerManageFormPage(KofaEditFormPage):
372    grok.context(IApplicantsContainer)
373    grok.name('manage')
374    grok.template('applicantscontainermanagepage')
375    form_fields = grok.AutoFields(IApplicantsContainer)
376    taboneactions = [_('Save'),_('Cancel')]
377    tabtwoactions = [_('Remove selected'),_('Cancel'),
378        _('Create students from selected')]
379    tabthreeactions1 = [_('Remove selected local roles')]
380    tabthreeactions2 = [_('Add local role')]
381    # Use friendlier date widget...
382    grok.require('waeup.manageApplication')
383    doclink = DOCLINK + '/applicants.html'
384    max_applicants = 2000
385
386    @property
387    def label(self):
388        return _('Manage applicants container')
389
390    pnav = 3
391
392    @property
393    def showApplicants(self):
394        if self.context.counts[1] < self.max_applicants:
395            return True
396        return False
397
398    def getLocalRoles(self):
399        roles = ILocalRolesAssignable(self.context)
400        return roles()
401
402    #def getUsers(self):
403    #    """Get a list of all users.
404    #    """
405    #    for key, val in grok.getSite()['users'].items():
406    #        url = self.url(val)
407    #        yield(dict(url=url, name=key, val=val))
408
409    def getUsers(self):
410        return getUtility(IKofaUtils).getUsers()
411
412    def getUsersWithLocalRoles(self):
413        return get_users_with_local_roles(self.context)
414
415    @action(_('Save'), style='primary')
416    def save(self, **data):
417        changed_fields = self.applyData(self.context, **data)
418        if changed_fields:
419            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
420        else:
421            changed_fields = []
422        description = getattr(self.context, 'description', None)
423        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
424        self.context.description_dict = html2dict(description, portal_language)
425        self.flash(_('Form has been saved.'))
426        fields_string = ' + '.join(changed_fields)
427        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
428        return
429
430    @jsaction(_('Remove selected'))
431    def delApplicant(self, **data):
432        form = self.request.form
433        if 'val_id' in form:
434            child_id = form['val_id']
435        else:
436            self.flash(_('No applicant selected!'), type='warning')
437            self.redirect(self.url(self.context, '@@manage')+'#tab2')
438            return
439        if not isinstance(child_id, list):
440            child_id = [child_id]
441        deleted = []
442        for id in child_id:
443            try:
444                del self.context[id]
445                deleted.append(id)
446            except:
447                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
448                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
449        if len(deleted):
450            self.flash(_('Successfully removed: ${a}',
451                mapping = {'a':', '.join(deleted)}))
452        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
453        return
454
455    @action(_('Create students from selected'))
456    def createStudents(self, **data):
457        if not checkPermission('waeup.createStudents', self.context):
458            self.flash(
459                _('You don\'t have permission to create student records.'),
460                type='warning')
461            self.redirect(self.url(self.context, '@@manage')+'#tab2')
462            return
463        form = self.request.form
464        if 'val_id' in form:
465            child_id = form['val_id']
466        else:
467            self.flash(_('No applicant selected!'), type='warning')
468            self.redirect(self.url(self.context, '@@manage')+'#tab2')
469            return
470        if not isinstance(child_id, list):
471            child_id = [child_id]
472        created = []
473        if len(child_id) > 10 and self.request.principal.id != 'admin':
474            self.flash(_('A maximum of 10 applicants can be selected!'),
475                       type='warning')
476            self.redirect(self.url(self.context, '@@manage')+'#tab2')
477            return
478        for id in child_id:
479            success, msg = self.context[id].createStudent(view=self)
480            if success:
481                created.append(id)
482        if len(created):
483            self.flash(_('${a} students successfully created.',
484                mapping = {'a': len(created)}))
485        else:
486            self.flash(_('No student could be created.'), type='warning')
487        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
488        return
489
490    @action(_('Cancel'), validator=NullValidator)
491    def cancel(self, **data):
492        self.redirect(self.url(self.context))
493        return
494
495    @action(_('Add local role'), validator=NullValidator)
496    def addLocalRole(self, **data):
497        return add_local_role(self,3, **data)
498
499    @action(_('Remove selected local roles'))
500    def delLocalRoles(self, **data):
501        return del_local_roles(self,3,**data)
502
503class ApplicantAddFormPage(KofaAddFormPage):
504    """Add-form to add an applicant.
505    """
506    grok.context(IApplicantsContainer)
507    grok.require('waeup.manageApplication')
508    grok.name('addapplicant')
509    #grok.template('applicantaddpage')
510    form_fields = grok.AutoFields(IApplicant).select(
511        'firstname', 'middlename', 'lastname',
512        'email', 'phone')
513    label = _('Add applicant')
514    pnav = 3
515    doclink = DOCLINK + '/applicants.html'
516
517    @action(_('Create application record'))
518    def addApplicant(self, **data):
519        applicant = createObject(u'waeup.Applicant')
520        self.applyData(applicant, **data)
521        self.context.addApplicant(applicant)
522        self.flash(_('Application record created.'))
523        self.redirect(
524            self.url(self.context[applicant.application_number], 'index'))
525        return
526
527class ApplicantsContainerPrefillFormPage(KofaAddFormPage):
528    """Form to pre-fill applicants containers.
529    """
530    grok.context(IApplicantsContainer)
531    grok.require('waeup.manageApplication')
532    grok.name('prefill')
533    grok.template('prefillcontainer')
534    label = _('Pre-fill container')
535    pnav = 3
536    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
537
538    def update(self):
539        if self.context.mode == 'update':
540            self.flash(_('Container must be in create mode to be pre-filled.'),
541                type='danger')
542            self.redirect(self.url(self.context))
543            return
544        super(ApplicantsContainerPrefillFormPage, self).update()
545        return
546
547    @action(_('Pre-fill now'), style='primary')
548    def addApplicants(self):
549        form = self.request.form
550        if 'number' in form and form['number']:
551            number = int(form['number'])
552        for i in range(number):
553            applicant = createObject(u'waeup.Applicant')
554            self.context.addApplicant(applicant)
555        self.flash(_('%s application records created.' % number))
556        self.context.writeLogMessage(self, '%s applicants created' % (number))
557        self.redirect(self.url(self.context, 'index'))
558        return
559
560    @action(_('Cancel'), validator=NullValidator)
561    def cancel(self, **data):
562        self.redirect(self.url(self.context))
563        return
564
565class ApplicantsContainerPurgeFormPage(KofaEditFormPage):
566    """Form to purge applicants containers.
567    """
568    grok.context(IApplicantsContainer)
569    grok.require('waeup.manageApplication')
570    grok.name('purge')
571    grok.template('purgecontainer')
572    label = _('Purge container')
573    pnav = 3
574    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
575
576    @action(_('Remove initialized records'),
577              tooltip=_('Don\'t use if application is in progress!'),
578              warning=_('Are you really sure?'),
579              style='primary')
580    def purgeInitialized(self):
581        form = self.request.form
582        purged = 0
583        keys = [key for key in self.context.keys()]
584        for key in keys:
585            if self.context[key].state == 'initialized':
586                del self.context[key]
587                purged += 1
588        self.flash(_('%s application records purged.' % purged))
589        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
590        self.redirect(self.url(self.context, 'index'))
591        return
592
593    @action(_('Cancel'), validator=NullValidator)
594    def cancel(self, **data):
595        self.redirect(self.url(self.context))
596        return
597
598class ApplicantDisplayFormPage(KofaDisplayFormPage):
599    """A display view for applicant data.
600    """
601    grok.context(IApplicant)
602    grok.name('index')
603    grok.require('waeup.viewApplication')
604    grok.template('applicantdisplaypage')
605    label = _('Applicant')
606    pnav = 3
607    hide_hint = False
608
609    @property
610    def display_refereereports(self):
611        if self.context.refereereports:
612            return True
613        return False
614
615    @property
616    def file_links(self):
617        html = ''
618        file_store = getUtility(IExtFileStore)
619        additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
620        for filename in additional_files:
621            pdf = getUtility(IExtFileStore).getFileByContext(
622                self.context, attr=filename[1])
623            if pdf:
624                html += '<a href="%s">%s</a>, ' % (self.url(
625                    self.context, filename[1]), filename[0])
626        html = html.strip(', ')
627        return html
628
629    @property
630    def display_payments(self):
631        if self.context.payments:
632            return True
633        if self.context.special:
634            return True
635        return getattr(self.context.__parent__, 'application_fee', None)
636
637    @property
638    def form_fields(self):
639        if self.context.special:
640            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
641        else:
642            form_fields = grok.AutoFields(IApplicant).omit(
643                'locked', 'course_admitted', 'password', 'suspended')
644        return form_fields
645
646    @property
647    def target(self):
648        return getattr(self.context.__parent__, 'prefix', None)
649
650    @property
651    def separators(self):
652        return getUtility(IApplicantsUtils).SEPARATORS_DICT
653
654    def update(self):
655        self.passport_url = self.url(self.context, 'passport.jpg')
656        # Mark application as started if applicant logs in for the first time
657        usertype = getattr(self.request.principal, 'user_type', None)
658        if usertype == 'applicant' and \
659            IWorkflowState(self.context).getState() == INITIALIZED:
660            IWorkflowInfo(self.context).fireTransition('start')
661        if usertype == 'applicant' and self.context.state == 'created':
662            session = '%s/%s' % (self.context.__parent__.year,
663                                 self.context.__parent__.year+1)
664            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
665            msg = _(
666                '\n <strong>Congratulations!</strong>' +
667                ' You have been offered provisional admission into the' +
668                ' ${c} Academic Session of ${d}.'
669                ' Your student record has been created for you.' +
670                ' Please, logout again and proceed to the' +
671                ' login page of the portal.'
672                ' Then enter your new student credentials:' +
673                ' user name= ${a}, password = ${b}.' +
674                ' Change your password when you have logged in.',
675                mapping = {
676                    'a':self.context.student_id,
677                    'b':self.context.application_number,
678                    'c':session,
679                    'd':title}
680                )
681            self.flash(msg)
682        return
683
684    @property
685    def hasPassword(self):
686        if self.context.password:
687            return _('set')
688        return _('unset')
689
690    @property
691    def label(self):
692        container_title = self.context.__parent__.title
693        return _('${a} <br /> Application Record ${b}', mapping = {
694            'a':container_title, 'b':self.context.application_number})
695
696    def getCourseAdmitted(self):
697        """Return link, title and code in html format to the certificate
698           admitted.
699        """
700        course_admitted = self.context.course_admitted
701        if getattr(course_admitted, '__parent__',None):
702            url = self.url(course_admitted)
703            title = course_admitted.title
704            code = course_admitted.code
705            return '<a href="%s">%s - %s</a>' %(url,code,title)
706        return ''
707
708class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
709    grok.context(IApplicant)
710    grok.name('base')
711
712    @property
713    def form_fields(self):
714        form_fields = grok.AutoFields(IApplicant).select(
715            'applicant_id', 'reg_number', 'email', 'course1')
716        if self.context.__parent__.prefix in ('special',):
717            form_fields['reg_number'].field.title = u'Identification Number'
718            return form_fields
719        return form_fields
720
721class ContactApplicantFormPage(ContactStudentFormPage):
722    grok.context(IApplicant)
723    grok.name('contactapplicant')
724    grok.require('waeup.viewApplication')
725    pnav = 3
726
727class CreateStudentPage(UtilityView, grok.View):
728    """Create a student object from applicant data.
729    """
730    grok.context(IApplicant)
731    grok.name('createstudent')
732    grok.require('waeup.createStudents')
733
734    def update(self):
735        success, msg = self.context.createStudent(view=self, send_email=True)
736        if success:
737            self.flash(msg)
738        else:
739            self.flash(msg, type='warning')
740        self.redirect(self.url(self.context))
741        return
742
743    def render(self):
744        return
745
746class CreateAllStudentsPage(KofaPage):
747    """Create all student objects from applicant data
748    in the root container or in a specific applicants container only.
749    Only PortalManagers or StudentCreators can do this.
750    """
751    #grok.context(IApplicantsContainer)
752    grok.name('createallstudents')
753    grok.require('waeup.createStudents')
754    label = _('Student Record Creation Report')
755
756    def update(self):
757        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
758        applicants_root = grok.getSite()['applicants']
759        grok.getSite()['configuration'].maintmode_enabled_by = u'admin'
760        applicants_root.logger.info('%s - started in %s' % (
761            ob_class, self.context.__name__))
762        transaction.commit()
763        # Wait 10 seconds for all transactions to be finished.
764        # Do not wait in tests.
765        if not self.request.principal.id == 'zope.mgr':
766            sleep(10)
767        cat = getUtility(ICatalog, name='applicants_catalog')
768        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
769        created = []
770        failed = []
771        container_only = False
772        applicants_root = grok.getSite()['applicants']
773        if isinstance(self.context, ApplicantsContainer):
774            container_only = True
775        n = 0
776        for result in results:
777            if container_only and result.__parent__ is not self.context:
778                continue
779            n += 1
780            success, msg = result.createStudent(view=self)
781            if success:
782                created.append(result.applicant_id)
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
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(self.request)
1581        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1582        return
1583
1584    def _redirect(self, email, password, applicant_id):
1585        # Forward only email to landing page in base package.
1586        self.redirect(self.url(self.context, 'registration_complete',
1587            data = dict(email=email)))
1588        return
1589
1590    @property
1591    def _postfix(self):
1592        """In customized packages we can add a container dependent string if
1593        applicants have been imported into several containers.
1594        """
1595        return ''
1596
1597    @action(_('Send login credentials to email address'), style='primary')
1598    def register(self, **data):
1599        if not self.captcha_result.is_valid:
1600            # Captcha will display error messages automatically.
1601            # No need to flash something.
1602            return
1603        if self.context.mode == 'create':
1604            # Check if there are unused records in this container which
1605            # can be taken
1606            applicant = self.context.first_unused
1607            if applicant is None:
1608                # Add applicant
1609                applicant = createObject(u'waeup.Applicant')
1610                self.context.addApplicant(applicant)
1611            else:
1612                applicants_root = grok.getSite()['applicants']
1613                ob_class = self.__implemented__.__name__.replace(
1614                    'waeup.kofa.','')
1615                applicants_root.logger.info('%s - used: %s' % (
1616                    ob_class, applicant.applicant_id))
1617            self.applyData(applicant, **data)
1618            # applicant.reg_number = applicant.applicant_id
1619            notify(grok.ObjectModifiedEvent(applicant))
1620        elif self.context.mode == 'update':
1621            # Update applicant
1622            reg_number = data.get('reg_number','')
1623            lastname = data.get('lastname','')
1624            cat = getUtility(ICatalog, name='applicants_catalog')
1625            searchstr = reg_number + self._postfix
1626            results = list(
1627                cat.searchResults(reg_number=(searchstr, searchstr)))
1628            if results:
1629                applicant = results[0]
1630                if getattr(applicant,'lastname',None) is None:
1631                    self.flash(_('An error occurred.'), type='danger')
1632                    return
1633                elif applicant.lastname.lower() != lastname.lower():
1634                    # Don't tell the truth here. Anonymous must not
1635                    # know that a record was found and only the lastname
1636                    # verification failed.
1637                    self.flash(
1638                        _('No application record found.'), type='warning')
1639                    return
1640                elif applicant.password is not None and \
1641                    applicant.state != INITIALIZED:
1642                    self.flash(_('Your password has already been set and used. '
1643                                 'Please proceed to the login page.'),
1644                               type='warning')
1645                    return
1646                # Store email address but nothing else.
1647                applicant.email = data['email']
1648                notify(grok.ObjectModifiedEvent(applicant))
1649            else:
1650                # No record found, this is the truth.
1651                self.flash(_('No application record found.'), type='warning')
1652                return
1653        else:
1654            # Does not happen but anyway ...
1655            return
1656        kofa_utils = getUtility(IKofaUtils)
1657        password = kofa_utils.genPassword()
1658        IUserAccount(applicant).setPassword(password)
1659        # Send email with credentials
1660        args = {'login':applicant.applicant_id, 'password':password}
1661        login_url = self.url(grok.getSite()) + '/login?%s' % urlencode(args)
1662        url_info = u'Login: %s' % login_url
1663        msg = _('You have successfully been registered for the')
1664        if kofa_utils.sendCredentials(IUserAccount(applicant),
1665            password, url_info, msg):
1666            email_sent = applicant.email
1667        else:
1668            email_sent = None
1669        self._redirect(email=email_sent, password=password,
1670            applicant_id=applicant.applicant_id)
1671        return
1672
1673class ApplicantRegistrationEmailSent(KofaPage):
1674    """Landing page after successful registration.
1675
1676    """
1677    grok.name('registration_complete')
1678    grok.require('waeup.Public')
1679    grok.template('applicantregemailsent')
1680    label = _('Your registration was successful.')
1681
1682    def update(self, email=None, applicant_id=None, password=None):
1683        self.email = email
1684        self.password = password
1685        self.applicant_id = applicant_id
1686        return
1687
1688class ApplicantCheckStatusPage(KofaPage):
1689    """Captcha'd status checking page for applicants.
1690    """
1691    grok.context(IApplicantsRoot)
1692    grok.name('checkstatus')
1693    grok.require('waeup.Anonymous')
1694    grok.template('applicantcheckstatus')
1695    buttonname = _('Submit')
1696    pnav = 7
1697
1698    def label(self):
1699        if self.result:
1700            return _('Admission status of ${a}',
1701                     mapping = {'a':self.applicant.applicant_id})
1702        return _('Check your admission status')
1703
1704    def update(self, SUBMIT=None):
1705        form = self.request.form
1706        self.result = False
1707        # Handle captcha
1708        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1709        self.captcha_result = self.captcha.verify(self.request)
1710        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1711        if SUBMIT:
1712            if not self.captcha_result.is_valid:
1713                # Captcha will display error messages automatically.
1714                # No need to flash something.
1715                return
1716            unique_id = form.get('unique_id', None)
1717            lastname = form.get('lastname', None)
1718            if not unique_id or not lastname:
1719                self.flash(
1720                    _('Required input missing.'), type='warning')
1721                return
1722            cat = getUtility(ICatalog, name='applicants_catalog')
1723            results = list(
1724                cat.searchResults(applicant_id=(unique_id, unique_id)))
1725            if not results:
1726                results = list(
1727                    cat.searchResults(reg_number=(unique_id, unique_id)))
1728            if results:
1729                applicant = results[0]
1730                if applicant.lastname.lower().strip() != lastname.lower():
1731                    # Don't tell the truth here. Anonymous must not
1732                    # know that a record was found and only the lastname
1733                    # verification failed.
1734                    self.flash(
1735                        _('No application record found.'), type='warning')
1736                    return
1737            else:
1738                self.flash(_('No application record found.'), type='warning')
1739                return
1740            self.applicant = applicant
1741            self.entry_session = "%s/%s" % (
1742                applicant.__parent__.year,
1743                applicant.__parent__.year+1)
1744            course_admitted = getattr(applicant, 'course_admitted', None)
1745            self.course_admitted = False
1746            if course_admitted is not None:
1747                try:
1748                    self.course_admitted = True
1749                    self.longtitle = course_admitted.longtitle
1750                    self.department = course_admitted.__parent__.__parent__.longtitle
1751                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1752                except AttributeError:
1753                    self.flash(_('Application record invalid.'), type='warning')
1754                    return
1755            self.result = True
1756            self.admitted = False
1757            self.not_admitted = False
1758            self.submitted = False
1759            self.not_submitted = False
1760            self.created = False
1761            if applicant.state in (ADMITTED, CREATED):
1762                self.admitted = True
1763            if applicant.state in (CREATED):
1764                self.created = True
1765                self.student_id = applicant.student_id
1766                self.password = applicant.application_number
1767            if applicant.state in (NOT_ADMITTED,):
1768                self.not_admitted = True
1769            if applicant.state in (SUBMITTED,):
1770                self.submitted = True
1771            if applicant.state in (INITIALIZED, STARTED, PAID):
1772                self.not_submitted = True
1773        return
1774
1775class CheckTranscriptStatus(KofaPage):
1776    """A display page for checking transcript processing status.
1777    """
1778    grok.context(IApplicantsRoot)
1779    grok.name('checktranscript')
1780    grok.require('waeup.Public')
1781    label = _('Check transcript status')
1782    buttonname = _('Check status now')
1783    pnav = 8
1784    websites = (('DemoPortal', 'https://kofa-demo.waeup.org/'),)
1785    #websites = (('DemoPortal', 'http://localhost:8080/app/'),)
1786    appl_url1 = 'https://kofa-demo.waeup.org/applicants'
1787    appl_url2 = 'https://kofa-demo.waeup.org/applicants'
1788
1789    def update(self, SUBMIT=None):
1790        form = self.request.form
1791        self.button = False
1792        # Handle captcha
1793        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1794        self.captcha_result = self.captcha.verify(self.request)
1795        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1796        if SUBMIT:
1797            self.results = []
1798            if not self.captcha_result.is_valid:
1799                # Captcha will display error messages automatically.
1800                # No need to flash something.
1801                return
1802            unique_id = form.get('unique_id', None)
1803            email = form.get('email', None)
1804            if not unique_id or not email:
1805                self.flash(
1806                    _('Required input missing.'), type='warning')
1807                return
1808            self.button = True
1809            # Call webservice of all websites
1810            for website in self.websites:
1811                server = xmlrpclib.ServerProxy(website[1])
1812                result = server.get_grad_student(unique_id, email)
1813                if not result:
1814                    continue
1815                self.results.append((result, website))
1816        return
1817
1818class ExportJobContainerOverview(KofaPage):
1819    """Page that lists active applicant data export jobs and provides links
1820    to discard or download CSV files.
1821
1822    """
1823    grok.context(VirtualApplicantsExportJobContainer)
1824    grok.require('waeup.exportApplication')
1825    grok.name('index.html')
1826    grok.template('exportjobsindex')
1827    label = _('Data Exports')
1828    pnav = 3
1829
1830    def update(self, CREATE=None, DISCARD=None, job_id=None):
1831        if CREATE:
1832            self.redirect(self.url('@@start_export'))
1833            return
1834        if DISCARD and job_id:
1835            entry = self.context.entry_from_job_id(job_id)
1836            self.context.delete_export_entry(entry)
1837            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1838            self.context.logger.info(
1839                '%s - discarded: job_id=%s' % (ob_class, job_id))
1840            self.flash(_('Discarded export') + ' %s' % job_id)
1841        self.entries = doll_up(self, user=self.request.principal.id)
1842        return
1843
1844class ExportJobContainerJobStart(UtilityView, grok.View):
1845    """View that starts three export jobs, one for applicants, a second
1846    one for applicant payments and a third for referee reports.
1847    """
1848    grok.context(VirtualApplicantsExportJobContainer)
1849    grok.require('waeup.exportApplication')
1850    grok.name('start_export')
1851
1852    EXPORTER_LIST = ('applicants',
1853                   'applicantpayments',
1854                   'applicantrefereereports')
1855
1856    def update(self):
1857        utils = queryUtility(IKofaUtils)
1858        if not utils.expensive_actions_allowed():
1859            self.flash(_(
1860                "Currently, exporters cannot be started due to high "
1861                "system load. Please try again later."), type='danger')
1862            self.entries = doll_up(self, user=None)
1863            return
1864
1865        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1866        container_code = self.context.__parent__.code
1867        # Start first exporter
1868        for exporter in self.EXPORTER_LIST:
1869            job_id = self.context.start_export_job(exporter,
1870                                          self.request.principal.id,
1871                                          container=container_code)
1872            self.context.logger.info(
1873                '%s - exported: %s (%s), job_id=%s'
1874                % (ob_class, exporter, container_code, job_id))
1875            # Commit transaction so that job is stored in the ZODB
1876            transaction.commit()
1877        self.flash(_('Exports started.'))
1878        self.redirect(self.url(self.context))
1879        return
1880
1881    def render(self):
1882        return
1883
1884class ExportJobContainerDownload(ExportCSVView):
1885    """Page that downloads a students export csv file.
1886
1887    """
1888    grok.context(VirtualApplicantsExportJobContainer)
1889    grok.require('waeup.exportApplication')
1890
1891class RefereesRemindPage(UtilityView, grok.View):
1892    """A display view for referee reports.
1893    """
1894    grok.context(IApplicant)
1895    grok.name('remind_referees')
1896    grok.require('waeup.manageApplication')
1897
1898    mandate_days = 31
1899
1900    def remindReferees(self):
1901        site = grok.getSite()
1902        kofa_utils = getUtility(IKofaUtils)
1903        failed = ''
1904        emails_sent = 0
1905        for referee in self.context.referees:
1906            #if not referee.email_sent:
1907            #    continue
1908            # Check if referee has already created a report
1909            report_exists = False
1910            for report in self.context.refereereports:
1911                if report.email == referee.email:
1912                    report_exists = True
1913            if report_exists:
1914                continue
1915            # If not, create new mandate
1916            mandate = RefereeReportMandate(days=self.mandate_days)
1917            mandate.params['name'] = referee.name
1918            mandate.params['email'] = referee.email
1919            mandate.params[
1920                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1921                    self.context.__parent__.code,
1922                    self.context.application_number)
1923            mandate.params['redirect_path2'] = ''
1924            mandate.params['applicant_id'] = self.context.applicant_id
1925            site['mandates'].addMandate(mandate)
1926            # Send invitation email
1927            args = {'mandate_id':mandate.mandate_id}
1928            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1929            url_info = u'Report link: %s' % mandate_url
1930            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1931            if success:
1932                emails_sent += 1
1933                self.context.writeLogMessage(
1934                    self, 'email sent: %s' % referee.email)
1935                referee.email_sent = True
1936            else:
1937                failed += '%s ' % referee.email
1938        return failed, emails_sent
1939
1940    def update(self):
1941        if self.context.state != 'submitted':
1942            self.flash(
1943                _('Not allowed!'), type='danger')
1944            return self.redirect(self.url(self.context))
1945        failed, emails_sent = self.remindReferees()
1946        msg = _('${a} referee(s) have been reminded by email.',
1947                mapping = {'a':  emails_sent})
1948        self.flash(msg)
1949        return self.redirect(self.url(self.context))
1950
1951    def render(self):
1952        return
1953
1954class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1955    """A display view for referee reports.
1956    """
1957    grok.context(IApplicantRefereeReport)
1958    grok.name('index')
1959    grok.require('waeup.manageApplication')
1960    label = _('Referee Report')
1961    pnav = 3
1962    form_fields = grok.AutoFields(IApplicantRefereeReport)
1963    form_fields[
1964        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1965
1966class RefereeReportManageFormPage(KofaEditFormPage):
1967    """A displaymanage for referee reports.
1968    """
1969    grok.context(IApplicantRefereeReport)
1970    grok.name('manage')
1971    grok.require('waeup.managePortal')
1972    label = _('Manage Referee Report')
1973    pnav = 3
1974    form_fields = grok.AutoFields(IApplicantRefereeReport).omit('creation_date')
1975
1976    @action(_('Save'), style='primary')
1977    def save(self, **data):
1978        changed_fields = self.applyData(self.context, **data)
1979        # Turn list of lists into single list
1980        if changed_fields:
1981            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1982        else:
1983            changed_fields = []
1984        fields_string = ' + '.join(changed_fields)
1985        self.flash(_('Form has been saved.'))
1986        if fields_string:
1987            self.context.__parent__.writeLogMessage(
1988                self, '%s - saved: %s' % (self.context.r_id, fields_string))
1989        return
1990
1991class RemoveRefereeReportPage(UtilityView, grok.View):
1992    """
1993    """
1994    grok.context(IApplicantRefereeReport)
1995    grok.name('remove')
1996    grok.require('waeup.manageApplication')
1997
1998    def update(self):
1999        redirect_url = self.url(self.context.__parent__)
2000        self.context.__parent__.writeLogMessage(
2001            self, 'removed: %s' % self.context.r_id)
2002        del self.context.__parent__[self.context.r_id]
2003        self.flash(_('Referee report removed.'))
2004        self.redirect(redirect_url)
2005        return
2006
2007    def render(self):
2008        return
2009
2010class RefereeReportAddFormPage(KofaAddFormPage):
2011    """Add-form to add an referee report. This form
2012    is protected by a mandate.
2013    """
2014    grok.context(IApplicant)
2015    grok.require('waeup.Public')
2016    grok.name('addrefereereport')
2017    form_fields = grok.AutoFields(
2018        IApplicantRefereeReport).omit('creation_date')
2019    grok.template('refereereportpage')
2020    label = _('Referee Report Form')
2021    pnav = 3
2022    #doclink = DOCLINK + '/refereereports.html'
2023
2024    def update(self):
2025        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2026        if blocker:
2027            self.flash(_('The portal is in maintenance mode. '
2028                        'Referee report forms are temporarily disabled.'),
2029                       type='warning')
2030            self.redirect(self.application_url())
2031            return
2032        # Check mandate
2033        form = self.request.form
2034        self.mandate_id = form.get('mandate_id', None)
2035        self.mandates = grok.getSite()['mandates']
2036        mandate = self.mandates.get(self.mandate_id, None)
2037        if mandate is None and not self.request.form.get('form.actions.submit'):
2038            self.flash(_('No mandate.'), type='warning')
2039            self.redirect(self.application_url())
2040            return
2041        if mandate:
2042            # Check the mandate expiration date after redirect again
2043            if mandate.expires < datetime.utcnow():
2044                self.flash(_('Mandate expired.'),
2045                           type='warning')
2046                self.redirect(self.application_url())
2047                return
2048            args = {'mandate_id':mandate.mandate_id}
2049            # Check if report exists.
2050            # (1) If mandate has been used to create a report,
2051            # redirect to the pdf file.
2052            if mandate.params.get('redirect_path2'):
2053                self.redirect(
2054                    self.application_url() +
2055                    mandate.params.get('redirect_path2') +
2056                    '?%s' % urlencode(args))
2057                return
2058            # (2) Report exists but was created with another mandate.
2059            for report in self.context.refereereports:
2060                if report.email == mandate.params.get('email'):
2061                    self.flash(_('You have already created a '
2062                                 'report with another mandate.'),
2063                               type='warning')
2064                    self.redirect(self.application_url())
2065                    return
2066            # Prefill form with mandate params
2067            self.form_fields.get(
2068                'name').field.default = mandate.params['name']
2069            self.form_fields.get(
2070                'email_pref').field.default = mandate.params['email']
2071            self.passport_url = self.url(
2072                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
2073        super(RefereeReportAddFormPage, self).update()
2074        return
2075
2076    @action(_('Submit'),
2077              warning=_('Are you really sure? '
2078                        'Reports can neither be modified or added '
2079                        'after submission.'),
2080              style='primary')
2081    def addRefereeReport(self, **data):
2082        report = createObject(u'waeup.ApplicantRefereeReport')
2083        timestamp = ("%d" % int(time()*10000))[1:]
2084        report.r_id = "r%s" % timestamp
2085        report.email = self.mandates[self.mandate_id].params['email']
2086        self.applyData(report, **data)
2087        self.context[report.r_id] = report
2088        # self.flash(_('Referee report has been saved. Thank you!'))
2089        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
2090        # Changed on 19/04/20: We do no longer delete the mandate
2091        # but set path to redirect to the pdf file
2092        self.mandates[self.mandate_id].params[
2093            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
2094                self.context.__parent__.code,
2095                self.context.application_number,
2096                report.r_id)
2097        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
2098        args = {'mandate_id':self.mandate_id}
2099        self.flash(_('Your report has been successfully submitted. '
2100                     'Please use the report link in the email again to download '
2101                     'a pdf slip of your report.'))
2102        #self.redirect(self.url(report, 'referee_report.pdf')
2103        #              + '?%s' % urlencode(args))
2104        self.redirect(self.application_url())
2105        return
2106
2107class ExportPDFReportSlipPage(UtilityView, grok.View):
2108    """Deliver a PDF slip of the context.
2109    """
2110    grok.context(IApplicantRefereeReport)
2111    grok.name('referee_report_slip.pdf')
2112    grok.require('waeup.manageApplication')
2113    form_fields = grok.AutoFields(IApplicantRefereeReport)
2114    form_fields[
2115        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2116    #prefix = 'form'
2117    note = None
2118
2119    @property
2120    def title(self):
2121        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2122        return translate(_('Referee Report'), 'waeup.kofa',
2123            target_language=portal_language)
2124
2125    @property
2126    def label(self):
2127        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2128        return translate(_('Referee Report Slip'),
2129            'waeup.kofa', target_language=portal_language) \
2130            + ' %s' % self.context.r_id
2131
2132    def render(self):
2133        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
2134            self.request)
2135        students_utils = getUtility(IStudentsUtils)
2136        return students_utils.renderPDF(self,'referee_report_slip.pdf',
2137            self.context.__parent__, applicantview, note=self.note)
2138
2139class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
2140    """Deliver a PDF slip of the context to referees.
2141    """
2142    grok.name('referee_report.pdf')
2143    grok.require('waeup.Public')
2144
2145    def update(self):
2146        # Check mandate
2147        form = self.request.form
2148        self.mandate_id = form.get('mandate_id', None)
2149        self.mandates = grok.getSite()['mandates']
2150        mandate = self.mandates.get(self.mandate_id, None)
2151        if mandate is None:
2152            self.flash(_('No mandate.'), type='warning')
2153            self.redirect(self.application_url())
2154            return
2155        if mandate:
2156            # Check the mandate expiration date after redirect again
2157            if mandate.expires < datetime.utcnow():
2158                self.flash(_('Mandate expired.'),
2159                           type='warning')
2160                self.redirect(self.application_url())
2161                return
2162            # Check if form has really been submitted
2163            if not mandate.params.get('redirect_path2') \
2164                or mandate.params.get(
2165                    'applicant_id') != self.context.__parent__.applicant_id:
2166                self.flash(_('Wrong mandate.'),
2167                           type='warning')
2168                self.redirect(self.application_url())
2169                return
2170            super(ExportPDFReportSlipPage2, self).update()
2171        return
2172
2173class AdditionalFile(grok.View):
2174    """Renders additional files for applicants.
2175    This is a baseclass.
2176    """
2177    grok.baseclass()
2178    grok.context(IApplicant)
2179    grok.require('waeup.viewApplication')
2180
2181    def render(self):
2182        #image = getUtility(IExtFileStore).getFileByContext(
2183        #    self.context, attr=self.download_name)
2184        file = getUtility(IExtFileStore).getFileByContext(
2185            self.context, attr=self.__name__)
2186        dummy,ext = os.path.splitext(file.name)
2187        if ext == '.jpg':
2188            self.response.setHeader('Content-Type', 'image/jpeg')
2189        elif ext == '.pdf':
2190            self.response.setHeader('Content-Type', 'application/pdf')
2191        return file
2192
2193class TestFile(AdditionalFile):
2194    """Renders testfile.
2195    """
2196    grok.name('testfile')
Note: See TracBrowser for help on using the repository browser.