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

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

Do not log error messages.
Improve Student Creation Report.
Adjust tests.

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