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

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

In customized packages we have to add a container dependent string to reg_number if
applicants have been imported into several containers.

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