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

Last change on this file since 14256 was 14256, checked in by Henrik Bettermann, 8 years ago

Catch Reportlab LayoutError.

  • Property svn:keywords set to Id
File size: 60.4 KB
RevLine 
[5273]1## $Id: browser.py 14256 2016-11-04 05:23:01Z 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 = []
442        for id in child_id:
443            success, msg = self.context[id].createStudent(view=self)
444            if success:
445                created.append(id)
446        if len(created):
447            self.flash(_('${a} students successfully created.',
448                mapping = {'a': len(created)}))
449        else:
[11254]450            self.flash(_('No student could be created.'), type='warning')
451        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
[8314]452        return
453
[7710]454    @action(_('Cancel'), validator=NullValidator)
[5837]455    def cancel(self, **data):
456        self.redirect(self.url(self.context))
457        return
[5886]458
[7710]459    @action(_('Add local role'), validator=NullValidator)
[6184]460    def addLocalRole(self, **data):
461        return add_local_role(self,3, **data)
[6105]462
[7710]463    @action(_('Remove selected local roles'))
[6184]464    def delLocalRoles(self, **data):
465        return del_local_roles(self,3,**data)
466
[7819]467class ApplicantAddFormPage(KofaAddFormPage):
[6622]468    """Add-form to add an applicant.
[6327]469    """
470    grok.context(IApplicantsContainer)
[7136]471    grok.require('waeup.manageApplication')
[6327]472    grok.name('addapplicant')
[7240]473    #grok.template('applicantaddpage')
474    form_fields = grok.AutoFields(IApplicant).select(
[7356]475        'firstname', 'middlename', 'lastname',
[7240]476        'email', 'phone')
[7714]477    label = _('Add applicant')
[6327]478    pnav = 3
[13177]479    doclink = DOCLINK + '/applicants.html'
[6327]480
[7714]481    @action(_('Create application record'))
[6327]482    def addApplicant(self, **data):
[8008]483        applicant = createObject(u'waeup.Applicant')
[7240]484        self.applyData(applicant, **data)
485        self.context.addApplicant(applicant)
[13073]486        self.flash(_('Application record created.'))
[7363]487        self.redirect(
488            self.url(self.context[applicant.application_number], 'index'))
[6327]489        return
490
[13217]491class ApplicantsContainerPrefillFormPage(KofaAddFormPage):
[13218]492    """Form to pre-fill applicants containers.
[13217]493    """
494    grok.context(IApplicantsContainer)
495    grok.require('waeup.manageApplication')
496    grok.name('prefill')
[13218]497    grok.template('prefillcontainer')
[13217]498    label = _('Pre-fill container')
499    pnav = 3
[13232]500    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
[13217]501
502    def update(self):
503        if self.context.mode == 'update':
504            self.flash(_('Container must be in create mode to be pre-filled.'),
505                type='danger')
506            self.redirect(self.url(self.context))
507            return
508        super(ApplicantsContainerPrefillFormPage, self).update()
509        return
510
511    @action(_('Pre-fill now'), style='primary')
[13218]512    def addApplicants(self):
[13217]513        form = self.request.form
514        if 'number' in form and form['number']:
515            number = int(form['number'])
516        for i in range(number):
517            applicant = createObject(u'waeup.Applicant')
518            self.context.addApplicant(applicant)
519        self.flash(_('%s application records created.' % number))
520        self.context.writeLogMessage(self, '%s applicants created' % (number))
521        self.redirect(self.url(self.context, 'index'))
522        return
523
524    @action(_('Cancel'), validator=NullValidator)
525    def cancel(self, **data):
526        self.redirect(self.url(self.context))
527        return
528
[13218]529class ApplicantsContainerPurgeFormPage(KofaEditFormPage):
[14109]530    """Form to purge applicants containers.
[13218]531    """
532    grok.context(IApplicantsContainer)
533    grok.require('waeup.manageApplication')
534    grok.name('purge')
535    grok.template('purgecontainer')
536    label = _('Purge container')
537    pnav = 3
[13232]538    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
[13218]539
[13232]540    @action(_('Remove initialized records'),
541              tooltip=_('Don\'t use if application is in progress!'),
542              warning=_('Are you really sure?'),
543              style='primary')
[13218]544    def purgeInitialized(self):
545        form = self.request.form
546        purged = 0
547        keys = [key for key in self.context.keys()]
548        for key in keys:
549            if self.context[key].state == 'initialized':
550                del self.context[key]
551                purged += 1
552        self.flash(_('%s application records purged.' % purged))
553        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
554        self.redirect(self.url(self.context, 'index'))
555        return
556
557    @action(_('Cancel'), validator=NullValidator)
558    def cancel(self, **data):
559        self.redirect(self.url(self.context))
560        return
561
[7819]562class ApplicantDisplayFormPage(KofaDisplayFormPage):
[8014]563    """A display view for applicant data.
564    """
[5273]565    grok.context(IApplicant)
566    grok.name('index')
[7113]567    grok.require('waeup.viewApplication')
[7200]568    grok.template('applicantdisplaypage')
[7714]569    label = _('Applicant')
[5843]570    pnav = 3
[8922]571    hide_hint = False
[5273]572
[8046]573    @property
[13886]574    def display_payments(self):
575        if self.context.special:
576            return True
577        return getattr(self.context.__parent__, 'application_fee', None)
578
579    @property
[10831]580    def form_fields(self):
581        if self.context.special:
[11599]582            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
[10831]583        else:
584            form_fields = grok.AutoFields(IApplicant).omit(
[10845]585                'locked', 'course_admitted', 'password', 'suspended')
[10831]586        return form_fields
587
588    @property
[10534]589    def target(self):
590        return getattr(self.context.__parent__, 'prefix', None)
591
592    @property
[8046]593    def separators(self):
594        return getUtility(IApplicantsUtils).SEPARATORS_DICT
595
[7063]596    def update(self):
597        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]598        # Mark application as started if applicant logs in for the first time
[7272]599        usertype = getattr(self.request.principal, 'user_type', None)
600        if usertype == 'applicant' and \
601            IWorkflowState(self.context).getState() == INITIALIZED:
[7240]602            IWorkflowInfo(self.context).fireTransition('start')
[10895]603        if usertype == 'applicant' and self.context.state == 'created':
[10908]604            session = '%s/%s' % (self.context.__parent__.year,
605                                 self.context.__parent__.year+1)
606            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
[10895]607            msg = _(
608                '\n <strong>Congratulations!</strong>' +
[10933]609                ' You have been offered provisional admission into the' +
[10908]610                ' ${c} Academic Session of ${d}.'
611                ' Your student record has been created for you.' +
612                ' Please, logout again and proceed to the' +
613                ' login page of the portal.'
[10895]614                ' Then enter your new student credentials:' +
615                ' user name= ${a}, password = ${b}.' +
616                ' Change your password when you have logged in.',
617                mapping = {
618                    'a':self.context.student_id,
[10908]619                    'b':self.context.application_number,
620                    'c':session,
621                    'd':title}
[10895]622                )
623            self.flash(msg)
[7063]624        return
625
[6196]626    @property
[7240]627    def hasPassword(self):
628        if self.context.password:
[7714]629            return _('set')
630        return _('unset')
[7240]631
632    @property
[6196]633    def label(self):
634        container_title = self.context.__parent__.title
[8096]635        return _('${a} <br /> Application Record ${b}', mapping = {
[7714]636            'a':container_title, 'b':self.context.application_number})
[6196]637
[7347]638    def getCourseAdmitted(self):
639        """Return link, title and code in html format to the certificate
640           admitted.
641        """
642        course_admitted = self.context.course_admitted
[7351]643        if getattr(course_admitted, '__parent__',None):
[7347]644            url = self.url(course_admitted)
645            title = course_admitted.title
646            code = course_admitted.code
647            return '<a href="%s">%s - %s</a>' %(url,code,title)
648        return ''
[6254]649
[7259]650class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
651    grok.context(IApplicant)
652    grok.name('base')
653    form_fields = grok.AutoFields(IApplicant).select(
[9141]654        'applicant_id','email', 'course1')
[7259]655
[7459]656class CreateStudentPage(UtilityView, grok.View):
[8636]657    """Create a student object from applicant data.
[7341]658    """
659    grok.context(IApplicant)
660    grok.name('createstudent')
661    grok.require('waeup.manageStudent')
662
663    def update(self):
[8314]664        msg = self.context.createStudent(view=self)[1]
[11254]665        self.flash(msg, type='warning')
[7341]666        self.redirect(self.url(self.context))
667        return
668
669    def render(self):
670        return
671
[8636]672class CreateAllStudentsPage(UtilityView, grok.View):
673    """Create all student objects from applicant data
[11874]674    in the root container or in a specific  applicants container only.
[11826]675    Only PortalManagers can do this.
[8636]676    """
[9900]677    #grok.context(IApplicantsContainer)
[8636]678    grok.name('createallstudents')
679    grok.require('waeup.managePortal')
680
681    def update(self):
682        cat = getUtility(ICatalog, name='applicants_catalog')
683        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
684        created = []
[9900]685        container_only = False
686        applicants_root = grok.getSite()['applicants']
687        if isinstance(self.context, ApplicantsContainer):
688            container_only = True
[8636]689        for result in results:
[9900]690            if container_only and result.__parent__ is not self.context:
[8636]691                continue
692            success, msg = result.createStudent(view=self)
693            if success:
694                created.append(result.applicant_id)
695            else:
[12893]696                ob_class = self.__implemented__.__name__.replace(
697                    'waeup.kofa.','')
[9900]698                applicants_root.logger.info(
[8742]699                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
[8636]700        if len(created):
701            self.flash(_('${a} students successfully created.',
702                mapping = {'a': len(created)}))
703        else:
[11254]704            self.flash(_('No student could be created.'), type='warning')
[9900]705        self.redirect(self.url(self.context))
[8636]706        return
707
708    def render(self):
709        return
710
[8260]711class ApplicationFeePaymentAddPage(UtilityView, grok.View):
[7250]712    """ Page to add an online payment ticket
713    """
714    grok.context(IApplicant)
715    grok.name('addafp')
716    grok.require('waeup.payApplicant')
[8243]717    factory = u'waeup.ApplicantOnlinePayment'
[7250]718
[11726]719    @property
720    def custom_requirements(self):
721        return ''
722
[7250]723    def update(self):
[11726]724        # Additional requirements in custom packages.
725        if self.custom_requirements:
726            self.flash(
727                self.custom_requirements,
728                type='danger')
729            self.redirect(self.url(self.context))
[11727]730            return
[11575]731        if not self.context.special:
732            for key in self.context.keys():
733                ticket = self.context[key]
734                if ticket.p_state == 'paid':
735                      self.flash(
736                          _('This type of payment has already been made.'),
737                          type='warning')
738                      self.redirect(self.url(self.context))
739                      return
[8524]740        applicants_utils = getUtility(IApplicantsUtils)
741        container = self.context.__parent__
[8243]742        payment = createObject(self.factory)
[11254]743        failure = applicants_utils.setPaymentDetails(
[10831]744            container, payment, self.context)
[11254]745        if failure is not None:
[13123]746            self.flash(failure, type='danger')
[8524]747            self.redirect(self.url(self.context))
748            return
[7250]749        self.context[payment.p_id] = payment
[12893]750        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
[7714]751        self.flash(_('Payment ticket created.'))
[8280]752        self.redirect(self.url(payment))
[7250]753        return
754
755    def render(self):
756        return
757
758
[7819]759class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[7250]760    """ Page to view an online payment ticket
761    """
762    grok.context(IApplicantOnlinePayment)
763    grok.name('index')
764    grok.require('waeup.viewApplication')
[9984]765    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8170]766    form_fields[
767        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
768    form_fields[
769        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]770    pnav = 3
771
772    @property
773    def label(self):
[7714]774        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8170]775            'a':self.context.__parent__.display_fullname,
776            'b':self.context.p_id})
[7250]777
[8420]778class OnlinePaymentApprovePage(UtilityView, grok.View):
779    """ Approval view
[7250]780    """
781    grok.context(IApplicantOnlinePayment)
[8420]782    grok.name('approve')
783    grok.require('waeup.managePortal')
[7250]784
785    def update(self):
[11580]786        flashtype, msg, log = self.context.approveApplicantPayment()
[8428]787        if log is not None:
[9771]788            applicant = self.context.__parent__
789            # Add log message to applicants.log
790            applicant.writeLogMessage(self, log)
791            # Add log message to payments.log
792            self.context.logger.info(
[9795]793                '%s,%s,%s,%s,%s,,,,,,' % (
[9771]794                applicant.applicant_id,
795                self.context.p_id, self.context.p_category,
796                self.context.amount_auth, self.context.r_code))
[11580]797        self.flash(msg, type=flashtype)
[7250]798        return
799
800    def render(self):
801        self.redirect(self.url(self.context, '@@index'))
802        return
803
[7459]804class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7250]805    """Deliver a PDF slip of the context.
806    """
807    grok.context(IApplicantOnlinePayment)
[8262]808    grok.name('payment_slip.pdf')
[7250]809    grok.require('waeup.viewApplication')
[9984]810    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8173]811    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
812    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]813    prefix = 'form'
[8258]814    note = None
[7250]815
816    @property
[7714]817    def title(self):
[7819]818        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]819        return translate(_('Payment Data'), 'waeup.kofa',
[7714]820            target_language=portal_language)
821
822    @property
[7250]823    def label(self):
[7819]824        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8262]825        return translate(_('Online Payment Slip'),
[7811]826            'waeup.kofa', target_language=portal_language) \
[7714]827            + ' %s' % self.context.p_id
[7250]828
[11754]829    @property
830    def payment_slip_download_warning(self):
831        if self.context.__parent__.state != SUBMITTED:
832            return _('Please submit the application form before '
833                     'trying to download payment slips.')
834        return ''
835
[7250]836    def render(self):
[11754]837        if self.payment_slip_download_warning:
838            self.flash(self.payment_slip_download_warning, type='danger')
[11599]839            self.redirect(self.url(self.context))
840            return
[7259]841        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
[7250]842            self.request)
843        students_utils = getUtility(IStudentsUtils)
[8262]844        return students_utils.renderPDF(self,'payment_slip.pdf',
[8258]845            self.context.__parent__, applicantview, note=self.note)
[7250]846
[10571]847class ExportPDFPageApplicationSlip(UtilityView, grok.View):
[6358]848    """Deliver a PDF slip of the context.
849    """
850    grok.context(IApplicant)
851    grok.name('application_slip.pdf')
[7136]852    grok.require('waeup.viewApplication')
[6358]853    prefix = 'form'
854
[8666]855    def update(self):
[9051]856        if self.context.state in ('initialized', 'started', 'paid'):
[8666]857            self.flash(
[11254]858                _('Please pay and submit before trying to download '
859                  'the application slip.'), type='warning')
[8666]860            return self.redirect(self.url(self.context))
861        return
862
[6358]863    def render(self):
[12395]864        try:
865            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
866                view=self)
867        except IOError:
868            self.flash(
869                _('Your image file is corrupted. '
870                  'Please replace.'), type='danger')
871            return self.redirect(self.url(self.context))
[14256]872        except LayoutError, err:
873            view.flash(
874                'PDF file could not be created. Reportlab error message: %s'
875                % escape(err.message),
876                type="danger")
877            return self.redirect(self.url(self.context))
[6358]878        self.response.setHeader(
879            'Content-Type', 'application/pdf')
[7392]880        return pdfstream
[6358]881
[7081]882def handle_img_upload(upload, context, view):
[7063]883    """Handle upload of applicant image.
[7081]884
885    Returns `True` in case of success or `False`.
886
887    Please note that file pointer passed in (`upload`) most probably
888    points to end of file when leaving this function.
[7063]889    """
[7081]890    size = file_size(upload)
891    if size > MAX_UPLOAD_SIZE:
[11254]892        view.flash(_('Uploaded image is too big!'), type='danger')
[7081]893        return False
[7247]894    dummy, ext = os.path.splitext(upload.filename)
895    ext.lower()
896    if ext != '.jpg':
[11254]897        view.flash(_('jpg file extension expected.'), type='danger')
[7247]898        return False
[7081]899    upload.seek(0) # file pointer moved when determining size
[7063]900    store = getUtility(IExtFileStore)
901    file_id = IFileStoreNameChooser(context).chooseName()
[14001]902    try:
903        store.createFile(file_id, upload)
904    except IOError:
905        view.flash(_('Image file cannot be changed.'), type='danger')
906        return False
[7081]907    return True
[7063]908
[7819]909class ApplicantManageFormPage(KofaEditFormPage):
[6196]910    """A full edit view for applicant data.
911    """
912    grok.context(IApplicant)
[7200]913    grok.name('manage')
[7136]914    grok.require('waeup.manageApplication')
[7200]915    grok.template('applicanteditpage')
[6322]916    manage_applications = True
[6196]917    pnav = 3
[12664]918    display_actions = [[_('Save'), _('Finally Submit')],
[7714]919        [_('Add online payment ticket'),_('Remove selected tickets')]]
[6196]920
[8046]921    @property
[13886]922    def display_payments(self):
923        if self.context.special:
924            return True
925        return getattr(self.context.__parent__, 'application_fee', None)
926
927    @property
[13976]928    def display_refereereports(self):
929        if self.context.refereereports:
930            return True
931        return False
932
933    @property
[10831]934    def form_fields(self):
935        if self.context.special:
[10845]936            form_fields = grok.AutoFields(ISpecialApplicant)
937            form_fields['applicant_id'].for_display = True
[10831]938        else:
939            form_fields = grok.AutoFields(IApplicant)
940            form_fields['student_id'].for_display = True
941            form_fields['applicant_id'].for_display = True
942        return form_fields
943
944    @property
[10534]945    def target(self):
946        return getattr(self.context.__parent__, 'prefix', None)
947
948    @property
[8046]949    def separators(self):
950        return getUtility(IApplicantsUtils).SEPARATORS_DICT
951
[11733]952    @property
953    def custom_upload_requirements(self):
954        return ''
955
[6196]956    def update(self):
[7200]957        super(ApplicantManageFormPage, self).update()
[6353]958        self.wf_info = IWorkflowInfo(self.context)
[7081]959        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[10090]960        self.upload_success = None
[6598]961        upload = self.request.form.get('form.passport', None)
962        if upload:
[11733]963            if self.custom_upload_requirements:
964                self.flash(
965                    self.custom_upload_requirements,
966                    type='danger')
967                self.redirect(self.url(self.context))
968                return
[10090]969            # We got a fresh upload, upload_success is
970            # either True or False
971            self.upload_success = handle_img_upload(
[7084]972                upload, self.context, self)
[10090]973            if self.upload_success:
[10095]974                self.context.writeLogMessage(self, 'saved: passport')
[6196]975        return
976
977    @property
978    def label(self):
979        container_title = self.context.__parent__.title
[8096]980        return _('${a} <br /> Application Form ${b}', mapping = {
[7714]981            'a':container_title, 'b':self.context.application_number})
[6196]982
[6303]983    def getTransitions(self):
[6351]984        """Return a list of dicts of allowed transition ids and titles.
[6353]985
986        Each list entry provides keys ``name`` and ``title`` for
987        internal name and (human readable) title of a single
988        transition.
[6349]989        """
[8434]990        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
[11482]991            if not t[0] in ('pay', 'create')]
[7687]992        return [dict(name='', title=_('No transition'))] +[
[6355]993            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]994
[7714]995    @action(_('Save'), style='primary')
[6196]996    def save(self, **data):
[7240]997        form = self.request.form
998        password = form.get('password', None)
999        password_ctl = form.get('control_password', None)
1000        if password:
1001            validator = getUtility(IPasswordValidator)
1002            errors = validator.validate_password(password, password_ctl)
1003            if errors:
[11254]1004                self.flash( ' '.join(errors), type='danger')
[7240]1005                return
[10090]1006        if self.upload_success is False:  # False is not None!
1007            # Error during image upload. Ignore other values.
1008            return
[6475]1009        changed_fields = self.applyData(self.context, **data)
[7199]1010        # Turn list of lists into single list
1011        if changed_fields:
1012            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]1013        else:
1014            changed_fields = []
1015        if password:
1016            # Now we know that the form has no errors and can set password ...
1017            IUserAccount(self.context).setPassword(password)
1018            changed_fields.append('password')
[7199]1019        fields_string = ' + '.join(changed_fields)
[7085]1020        trans_id = form.get('transition', None)
1021        if trans_id:
1022            self.wf_info.fireTransition(trans_id)
[7714]1023        self.flash(_('Form has been saved.'))
[6644]1024        if fields_string:
[12892]1025            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
[6196]1026        return
1027
[7250]1028    def unremovable(self, ticket):
[7330]1029        return False
[7250]1030
1031    # This method is also used by the ApplicantEditFormPage
1032    def delPaymentTickets(self, **data):
1033        form = self.request.form
[9701]1034        if 'val_id' in form:
[7250]1035            child_id = form['val_id']
1036        else:
[11254]1037            self.flash(_('No payment selected.'), type='warning')
[7250]1038            self.redirect(self.url(self.context))
1039            return
1040        if not isinstance(child_id, list):
1041            child_id = [child_id]
1042        deleted = []
1043        for id in child_id:
1044            # Applicants are not allowed to remove used payment tickets
1045            if not self.unremovable(self.context[id]):
1046                try:
1047                    del self.context[id]
1048                    deleted.append(id)
1049                except:
[7714]1050                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[11254]1051                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
[7250]1052        if len(deleted):
[7741]1053            self.flash(_('Successfully removed: ${a}',
[7738]1054                mapping = {'a':', '.join(deleted)}))
[8742]1055            self.context.writeLogMessage(
1056                self, 'removed: % s' % ', '.join(deleted))
[7250]1057        return
1058
[7252]1059    # We explicitely want the forms to be validated before payment tickets
1060    # can be created. If no validation is requested, use
[7459]1061    # 'validator=NullValidator' in the action directive
[11578]1062    @action(_('Add online payment ticket'), style='primary')
[7250]1063    def addPaymentTicket(self, **data):
1064        self.redirect(self.url(self.context, '@@addafp'))
[7252]1065        return
[7250]1066
[7714]1067    @jsaction(_('Remove selected tickets'))
[7250]1068    def removePaymentTickets(self, **data):
1069        self.delPaymentTickets(**data)
1070        self.redirect(self.url(self.context) + '/@@manage')
1071        return
1072
[10094]1073    # Not used in base package
1074    def file_exists(self, attr):
1075        file = getUtility(IExtFileStore).getFileByContext(
1076            self.context, attr=attr)
1077        if file:
1078            return True
1079        else:
1080            return False
1081
[7200]1082class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]1083    """An applicant-centered edit view for applicant data.
1084    """
[6196]1085    grok.context(IApplicantEdit)
[5273]1086    grok.name('edit')
[6198]1087    grok.require('waeup.handleApplication')
[7200]1088    grok.template('applicanteditpage')
[6322]1089    manage_applications = False
[10358]1090    submit_state = PAID
[5484]1091
[7250]1092    @property
[13976]1093    def display_refereereports(self):
1094        return False
1095
1096    @property
[10831]1097    def form_fields(self):
1098        if self.context.special:
[11657]1099            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1100                'locked', 'suspended')
[10845]1101            form_fields['applicant_id'].for_display = True
[10831]1102        else:
1103            form_fields = grok.AutoFields(IApplicantEdit).omit(
1104                'locked', 'course_admitted', 'student_id',
[10845]1105                'suspended'
[10831]1106                )
1107            form_fields['applicant_id'].for_display = True
1108            form_fields['reg_number'].for_display = True
1109        return form_fields
1110
1111    @property
[7250]1112    def display_actions(self):
[8286]1113        state = IWorkflowState(self.context).getState()
[13100]1114        # If the form is unlocked, applicants are allowed to save the form
1115        # and remove unused tickets.
1116        actions = [[_('Save')], [_('Remove selected tickets')]]
1117        # Only in state started they can also add tickets.
[10358]1118        if state == STARTED:
[7714]1119            actions = [[_('Save')],
1120                [_('Add online payment ticket'),_('Remove selected tickets')]]
[13100]1121        # In state paid, they can submit the data and further add tickets
1122        # if the application is special.
[11599]1123        elif self.context.special and state == PAID:
[12664]1124            actions = [[_('Save'), _('Finally Submit')],
[11599]1125                [_('Add online payment ticket'),_('Remove selected tickets')]]
[8286]1126        elif state == PAID:
[12664]1127            actions = [[_('Save'), _('Finally Submit')],
[7714]1128                [_('Remove selected tickets')]]
[7250]1129        return actions
1130
[7330]1131    def unremovable(self, ticket):
[13100]1132        return ticket.r_code
[7330]1133
[7145]1134    def emit_lock_message(self):
[11254]1135        self.flash(_('The requested form is locked (read-only).'),
1136                   type='warning')
[5941]1137        self.redirect(self.url(self.context))
1138        return
[6078]1139
[5686]1140    def update(self):
[8665]1141        if self.context.locked or (
1142            self.context.__parent__.expired and
1143            self.context.__parent__.strict_deadline):
[7145]1144            self.emit_lock_message()
[5941]1145            return
[7200]1146        super(ApplicantEditFormPage, self).update()
[5686]1147        return
[5952]1148
[6196]1149    def dataNotComplete(self):
[7252]1150        store = getUtility(IExtFileStore)
1151        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
[7714]1152            return _('No passport picture uploaded.')
[6322]1153        if not self.request.form.get('confirm_passport', False):
[7714]1154            return _('Passport picture confirmation box not ticked.')
[6196]1155        return False
[5952]1156
[7252]1157    # We explicitely want the forms to be validated before payment tickets
1158    # can be created. If no validation is requested, use
[7459]1159    # 'validator=NullValidator' in the action directive
[11578]1160    @action(_('Add online payment ticket'), style='primary')
[7250]1161    def addPaymentTicket(self, **data):
1162        self.redirect(self.url(self.context, '@@addafp'))
[7252]1163        return
[7250]1164
[7714]1165    @jsaction(_('Remove selected tickets'))
[7250]1166    def removePaymentTickets(self, **data):
1167        self.delPaymentTickets(**data)
1168        self.redirect(self.url(self.context) + '/@@edit')
1169        return
1170
[7996]1171    @action(_('Save'), style='primary')
[5273]1172    def save(self, **data):
[10090]1173        if self.upload_success is False:  # False is not None!
1174            # Error during image upload. Ignore other values.
1175            return
[5273]1176        self.applyData(self.context, **data)
[10210]1177        self.flash(_('Form has been saved.'))
[5273]1178        return
1179
[14014]1180    def informReferees(self):
1181        site = grok.getSite()
1182        kofa_utils = getUtility(IKofaUtils)
1183        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1184        failed = ''
[14016]1185        emails_sent = 0
[14014]1186        for referee in self.context.referees:
1187            if referee.email_sent:
1188                continue
1189            mandate = RefereeReportMandate()
1190            mandate.params['name'] = referee.name
1191            mandate.params['email'] = referee.email
1192            mandate.params[
1193                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1194                    self.context.__parent__.code,
1195                    self.context.application_number)
1196            site['mandates'].addMandate(mandate)
1197            # Send invitation email
1198            args = {'mandate_id':mandate.mandate_id}
1199            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1200            url_info = u'Report link: %s' % mandate_url
1201            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1202            if success:
[14016]1203                emails_sent += 1
[14014]1204                self.context.writeLogMessage(
1205                    self, 'email sent: %s' % referee.email)
1206                referee.email_sent = True
1207            else:
1208                failed += '%s ' % referee.email
[14016]1209        return failed, emails_sent
[14014]1210
[14025]1211    @action(_('Finally Submit'), warning=WARNING)
[5484]1212    def finalsubmit(self, **data):
[10090]1213        if self.upload_success is False:  # False is not None!
[7084]1214            return # error during image upload. Ignore other values
[6196]1215        if self.dataNotComplete():
[11254]1216            self.flash(self.dataNotComplete(), type='danger')
[5941]1217            return
[7252]1218        self.applyData(self.context, **data)
[8286]1219        state = IWorkflowState(self.context).getState()
[6322]1220        # This shouldn't happen, but the application officer
1221        # might have forgotten to lock the form after changing the state
[10358]1222        if state != self.submit_state:
[11254]1223            self.flash(_('The form cannot be submitted. Wrong state!'),
1224                       type='danger')
[6303]1225            return
[14014]1226        msg = _('Form has been submitted.')
1227        # Create mandates and send emails to referees
1228        if getattr(self.context, 'referees', None):
[14016]1229            failed, emails_sent = self.informReferees()
[14014]1230            if failed:
1231                self.flash(
1232                    _('Some invitation emails could not be sent:') + failed,
1233                    type='danger')
1234                return
1235            msg = _('Form has been successfully submitted and '
[14016]1236                    '${a} invitation emails were sent.',
1237                    mapping = {'a':  emails_sent})
[6303]1238        IWorkflowInfo(self.context).fireTransition('submit')
[8589]1239        # application_date is used in export files for sorting.
1240        # We can thus store utc.
[8194]1241        self.context.application_date = datetime.utcnow()
[14014]1242        self.flash(msg)
[6196]1243        self.redirect(self.url(self.context))
[5273]1244        return
[5941]1245
[7063]1246class PassportImage(grok.View):
1247    """Renders the passport image for applicants.
1248    """
1249    grok.name('passport.jpg')
1250    grok.context(IApplicant)
[7113]1251    grok.require('waeup.viewApplication')
[7063]1252
1253    def render(self):
1254        # A filename chooser turns a context into a filename suitable
1255        # for file storage.
1256        image = getUtility(IExtFileStore).getFileByContext(self.context)
1257        self.response.setHeader(
1258            'Content-Type', 'image/jpeg')
1259        if image is None:
1260            # show placeholder image
[7089]1261            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1262        return image
[7363]1263
[7819]1264class ApplicantRegistrationPage(KofaAddFormPage):
[7363]1265    """Captcha'd registration page for applicants.
1266    """
1267    grok.context(IApplicantsContainer)
1268    grok.name('register')
[7373]1269    grok.require('waeup.Anonymous')
[7363]1270    grok.template('applicantregister')
1271
[7368]1272    @property
[8033]1273    def form_fields(self):
1274        form_fields = None
[8128]1275        if self.context.mode == 'update':
1276            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
[11738]1277                'lastname','reg_number','email')
[8128]1278        else: #if self.context.mode == 'create':
[8033]1279            form_fields = grok.AutoFields(IApplicantEdit).select(
1280                'firstname', 'middlename', 'lastname', 'email', 'phone')
1281        return form_fields
1282
1283    @property
[7368]1284    def label(self):
[8078]1285        return _('Apply for ${a}',
[7714]1286            mapping = {'a':self.context.title})
[7368]1287
[7363]1288    def update(self):
[8665]1289        if self.context.expired:
[11254]1290            self.flash(_('Outside application period.'), type='warning')
[7368]1291            self.redirect(self.url(self.context))
1292            return
[13394]1293        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1294        if blocker:
1295            self.flash(_('The portal is in maintenance mode '
1296                        'and registration temporarily disabled.'),
1297                       type='warning')
1298            self.redirect(self.url(self.context))
1299            return
[7368]1300        # Handle captcha
[7363]1301        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1302        self.captcha_result = self.captcha.verify(self.request)
1303        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1304        return
1305
[8629]1306    def _redirect(self, email, password, applicant_id):
1307        # Forward only email to landing page in base package.
1308        self.redirect(self.url(self.context, 'registration_complete',
1309            data = dict(email=email)))
1310        return
1311
[9178]1312    @action(_('Send login credentials to email address'), style='primary')
[7363]1313    def register(self, **data):
1314        if not self.captcha_result.is_valid:
[8037]1315            # Captcha will display error messages automatically.
[7363]1316            # No need to flash something.
1317            return
[8033]1318        if self.context.mode == 'create':
[13217]1319            # Check if there are unused records in this container which
1320            # can be taken
1321            applicant = self.context.first_unused
1322            if applicant is None:
[13215]1323                # Add applicant
1324                applicant = createObject(u'waeup.Applicant')
1325                self.context.addApplicant(applicant)
[14110]1326            else:
1327                applicants_root = grok.getSite()['applicants']
1328                ob_class = self.__implemented__.__name__.replace(
1329                    'waeup.kofa.','')
1330                applicants_root.logger.info('%s - used: %s' % (
1331                    ob_class, applicant.applicant_id))
[8033]1332            self.applyData(applicant, **data)
[8042]1333            applicant.reg_number = applicant.applicant_id
1334            notify(grok.ObjectModifiedEvent(applicant))
[8033]1335        elif self.context.mode == 'update':
1336            # Update applicant
[8037]1337            reg_number = data.get('reg_number','')
[11738]1338            lastname = data.get('lastname','')
[8033]1339            cat = getUtility(ICatalog, name='applicants_catalog')
1340            results = list(
1341                cat.searchResults(reg_number=(reg_number, reg_number)))
1342            if results:
1343                applicant = results[0]
[11738]1344                if getattr(applicant,'lastname',None) is None:
[11254]1345                    self.flash(_('An error occurred.'), type='danger')
[8037]1346                    return
[11738]1347                elif applicant.lastname.lower() != lastname.lower():
[8042]1348                    # Don't tell the truth here. Anonymous must not
[11738]1349                    # know that a record was found and only the lastname
[8042]1350                    # verification failed.
[13099]1351                    self.flash(
1352                        _('No application record found.'), type='warning')
[8037]1353                    return
[8627]1354                elif applicant.password is not None and \
1355                    applicant.state != INITIALIZED:
1356                    self.flash(_('Your password has already been set and used. '
[11254]1357                                 'Please proceed to the login page.'),
1358                               type='warning')
[8042]1359                    return
1360                # Store email address but nothing else.
[8033]1361                applicant.email = data['email']
[8042]1362                notify(grok.ObjectModifiedEvent(applicant))
[8033]1363            else:
[8042]1364                # No record found, this is the truth.
[11254]1365                self.flash(_('No application record found.'), type='warning')
[8033]1366                return
1367        else:
[8042]1368            # Does not happen but anyway ...
[8033]1369            return
[7819]1370        kofa_utils = getUtility(IKofaUtils)
[7811]1371        password = kofa_utils.genPassword()
[7380]1372        IUserAccount(applicant).setPassword(password)
[7365]1373        # Send email with credentials
[7399]1374        login_url = self.url(grok.getSite(), 'login')
[8853]1375        url_info = u'Login: %s' % login_url
[7714]1376        msg = _('You have successfully been registered for the')
[7811]1377        if kofa_utils.sendCredentials(IUserAccount(applicant),
[8853]1378            password, url_info, msg):
[8629]1379            email_sent = applicant.email
[7380]1380        else:
[8629]1381            email_sent = None
1382        self._redirect(email=email_sent, password=password,
1383            applicant_id=applicant.applicant_id)
[7380]1384        return
1385
[7819]1386class ApplicantRegistrationEmailSent(KofaPage):
[7380]1387    """Landing page after successful registration.
[8629]1388
[7380]1389    """
1390    grok.name('registration_complete')
1391    grok.require('waeup.Public')
1392    grok.template('applicantregemailsent')
[7714]1393    label = _('Your registration was successful.')
[7380]1394
[8629]1395    def update(self, email=None, applicant_id=None, password=None):
[7380]1396        self.email = email
[8629]1397        self.password = password
1398        self.applicant_id = applicant_id
[7380]1399        return
[10655]1400
[13254]1401class ApplicantCheckStatusPage(KofaPage):
1402    """Captcha'd status checking page for applicants.
1403    """
1404    grok.context(IApplicantsRoot)
1405    grok.name('checkstatus')
1406    grok.require('waeup.Anonymous')
1407    grok.template('applicantcheckstatus')
1408    buttonname = _('Submit')
1409
1410    def label(self):
1411        if self.result:
[13429]1412            return _('Admission status of ${a}',
[13254]1413                     mapping = {'a':self.applicant.applicant_id})
[13428]1414        return _('Check your admission status')
[13254]1415
1416    def update(self, SUBMIT=None):
1417        form = self.request.form
1418        self.result = False
1419        # Handle captcha
1420        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1421        self.captcha_result = self.captcha.verify(self.request)
1422        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1423        if SUBMIT:
1424            if not self.captcha_result.is_valid:
1425                # Captcha will display error messages automatically.
1426                # No need to flash something.
1427                return
[13363]1428            unique_id = form.get('unique_id', None)
[13254]1429            lastname = form.get('lastname', None)
[13363]1430            if not unique_id or not lastname:
[13254]1431                self.flash(
1432                    _('Required input missing.'), type='warning')
1433                return
1434            cat = getUtility(ICatalog, name='applicants_catalog')
1435            results = list(
[13363]1436                cat.searchResults(applicant_id=(unique_id, unique_id)))
1437            if not results:
1438                results = list(
1439                    cat.searchResults(reg_number=(unique_id, unique_id)))
[13254]1440            if results:
1441                applicant = results[0]
[13282]1442                if applicant.lastname.lower().strip() != lastname.lower():
[13254]1443                    # Don't tell the truth here. Anonymous must not
1444                    # know that a record was found and only the lastname
1445                    # verification failed.
1446                    self.flash(
1447                        _('No application record found.'), type='warning')
1448                    return
1449            else:
1450                self.flash(_('No application record found.'), type='warning')
1451                return
1452            self.applicant = applicant
1453            self.entry_session = "%s/%s" % (
1454                applicant.__parent__.year,
1455                applicant.__parent__.year+1)
1456            course_admitted = getattr(applicant, 'course_admitted', None)
1457            self.course_admitted = False
1458            if course_admitted is not None:
1459                self.course_admitted = True
1460                self.longtitle = course_admitted.longtitle
1461                self.department = course_admitted.__parent__.__parent__.longtitle
1462                self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1463            self.result = True
1464            self.admitted = False
1465            self.not_admitted = False
1466            self.submitted = False
1467            self.not_submitted = False
[13356]1468            self.created = False
[13254]1469            if applicant.state in (ADMITTED, CREATED):
1470                self.admitted = True
[13356]1471            if applicant.state in (CREATED):
1472                self.created = True
[13365]1473                self.student_id = applicant.student_id
1474                self.password = applicant.application_number
[13254]1475            if applicant.state in (NOT_ADMITTED,):
1476                self.not_admitted = True
1477            if applicant.state in (SUBMITTED,):
1478                self.submitted = True
1479            if applicant.state in (INITIALIZED, STARTED, PAID):
1480                self.not_submitted = True
1481        return
1482
[10655]1483class ExportJobContainerOverview(KofaPage):
1484    """Page that lists active applicant data export jobs and provides links
1485    to discard or download CSV files.
1486
1487    """
1488    grok.context(VirtualApplicantsExportJobContainer)
1489    grok.require('waeup.manageApplication')
1490    grok.name('index.html')
1491    grok.template('exportjobsindex')
[11254]1492    label = _('Data Exports')
[10655]1493    pnav = 3
1494
1495    def update(self, CREATE=None, DISCARD=None, job_id=None):
1496        if CREATE:
1497            self.redirect(self.url('@@start_export'))
1498            return
1499        if DISCARD and job_id:
1500            entry = self.context.entry_from_job_id(job_id)
1501            self.context.delete_export_entry(entry)
1502            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1503            self.context.logger.info(
1504                '%s - discarded: job_id=%s' % (ob_class, job_id))
1505            self.flash(_('Discarded export') + ' %s' % job_id)
1506        self.entries = doll_up(self, user=self.request.principal.id)
1507        return
1508
[13950]1509class ExportJobContainerJobStart(UtilityView, grok.View):
1510    """View that starts two export jobs, one for applicants and a second
1511    one for applicant payments.
[10655]1512    """
1513    grok.context(VirtualApplicantsExportJobContainer)
1514    grok.require('waeup.manageApplication')
1515    grok.name('start_export')
1516
1517    def update(self):
[13152]1518        utils = queryUtility(IKofaUtils)
1519        if not utils.expensive_actions_allowed():
1520            self.flash(_(
1521                "Currently, exporters cannot be started due to high "
1522                "system load. Please try again later."), type='danger')
1523            self.entries = doll_up(self, user=None)
1524            return
[13950]1525
1526        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1527        container_code = self.context.__parent__.code
1528        # Start first exporter
[10655]1529        exporter = 'applicants'
1530        job_id = self.context.start_export_job(exporter,
1531                                      self.request.principal.id,
1532                                      container=container_code)
1533        self.context.logger.info(
1534            '%s - exported: %s (%s), job_id=%s'
1535            % (ob_class, exporter, container_code, job_id))
[13950]1536        # Commit transaction so that job is stored in the ZODB
1537        transaction.commit()
1538        # Start second exporter
1539        exporter = 'applicantpayments'
1540        job_id = self.context.start_export_job(exporter,
1541                                      self.request.principal.id,
1542                                      container=container_code)
1543        self.context.logger.info(
1544            '%s - exported: %s (%s), job_id=%s'
1545            % (ob_class, exporter, container_code, job_id))
1546
1547        self.flash(_('Exports started.'))
[10655]1548        self.redirect(self.url(self.context))
1549        return
1550
1551    def render(self):
1552        return
1553
1554class ExportJobContainerDownload(ExportCSVView):
1555    """Page that downloads a students export csv file.
1556
1557    """
1558    grok.context(VirtualApplicantsExportJobContainer)
[11253]1559    grok.require('waeup.manageApplication')
[13976]1560
1561class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1562    """A display view for referee reports.
1563    """
1564    grok.context(IApplicantRefereeReport)
1565    grok.name('index')
1566    grok.require('waeup.manageApplication')
1567    label = _('Referee Report')
1568    pnav = 3
1569
1570class RefereeReportAddFormPage(KofaAddFormPage):
1571    """Add-form to add an referee report. This form
[13992]1572    is protected by a mandate.
[13976]1573    """
1574    grok.context(IApplicant)
[13991]1575    grok.require('waeup.Public')
[13976]1576    grok.name('addrefereereport')
1577    form_fields = grok.AutoFields(
1578        IApplicantRefereeReport).omit('creation_date')
[13992]1579    grok.template('refereereportpage')
[13976]1580    label = _('Add referee report')
1581    pnav = 3
1582    #doclink = DOCLINK + '/refereereports.html'
1583
1584    def update(self):
[13991]1585        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1586        if blocker:
1587            self.flash(_('The portal is in maintenance mode. '
1588                        'Referee report forms are temporarily disabled.'),
1589                       type='warning')
1590            self.redirect(self.application_url())
1591            return
1592        # Check mandate
1593        form = self.request.form
[13992]1594        self.mandate_id = form.get('mandate_id', None)
1595        self.mandates = grok.getSite()['mandates']
1596        mandate = self.mandates.get(self.mandate_id, None)
[13991]1597        if mandate is None and not self.request.form.get('form.actions.submit'):
1598            self.flash(_('No mandate.'), type='warning')
1599            self.redirect(self.application_url())
1600            return
1601        if mandate:
1602            # Prefill form with mandate params
1603            self.form_fields.get(
1604                'name').field.default = mandate.params['name']
1605            self.form_fields.get(
1606                'email').field.default = mandate.params['email']
[13976]1607        super(RefereeReportAddFormPage, self).update()
1608        return
1609
1610    @action(_('Submit'),
1611              warning=_('Are you really sure? '
1612                        'Reports can neither be modified or added '
1613                        'after submission.'),
1614              style='primary')
1615    def addRefereeReport(self, **data):
1616        report = createObject(u'waeup.ApplicantRefereeReport')
1617        timestamp = ("%d" % int(time()*10000))[1:]
1618        report.r_id = "r%s" % timestamp
1619        self.applyData(report, **data)
1620        self.context[report.r_id] = report
[13991]1621        self.flash(_('Referee report has been saved. Thank you!'))
[13976]1622        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
[13992]1623        # Delete mandate
1624        del self.mandates[self.mandate_id]
[13991]1625        self.redirect(self.application_url())
[13976]1626        return
Note: See TracBrowser for help on using the repository browser.