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

Last change on this file since 15554 was 15548, checked in by Henrik Bettermann, 5 years ago

Allow to add applicants containers with a number instead of entrance year
in container code.

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