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

Last change on this file since 17850 was 17850, checked in by Henrik Bettermann, 5 months ago

Add ExportApplication permission which allows bursary officers
to access the application section and export application data.

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