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

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

Add log entry if pre-filled application record is used.

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