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

Last change on this file since 14842 was 14682, checked in by Henrik Bettermann, 7 years ago

Do not allow to create more than 10 students with a single request to
avoid a timeout of Nginx/Apache?.

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