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

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

Save email address provided by mandate when referee report
is created. Add RefereeReportManageFormPage (no button available).

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