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

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

Don't check if emails have already been sent.

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