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

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

Improve RefereeReportAddFormPage? and add tests.

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