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

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

Catch traceback.

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