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

Last change on this file since 10016 was 9984, checked in by Henrik Bettermann, 12 years ago

FCEOkene wants to hide the bed coordinates if maintenance fee is not yet paid. Thus we need additional property attributes which return the p_item (payment tickets) and the bed_coordinates attributes (bed tickets) by default and can be easily customized to hide this information in certain cases. bed_coordinates and p_item must be omitted on forms. The new display_ attributes are displayed instead.

All packages must now be adjusted.

The 'cost-benefit ratio' of these kinds of customizations is quite bad and we should think about declining such customization requests. However, I started customization and these are the changed made in the base package.

  • Property svn:keywords set to Id
File size: 40.9 KB
RevLine 
[5273]1## $Id: browser.py 9984 2013-02-24 08:29:24Z 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
[7370]23from datetime import datetime, date
[8042]24from zope.event import notify
[7392]25from zope.component import getUtility, createObject, getAdapter
[8033]26from zope.catalog.interfaces import ICatalog
[7714]27from zope.i18n import translate
[7322]28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
[7811]30from waeup.kofa.applicants.interfaces import (
[7363]31    IApplicant, IApplicantEdit, IApplicantsRoot,
[7683]32    IApplicantsContainer, IApplicantsContainerAdd,
[8033]33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
[8037]34    IApplicantRegisterUpdate
[7363]35    )
[9900]36from waeup.kofa.applicants.container import ApplicantsContainer
[8404]37from waeup.kofa.applicants.applicant import search
[8636]38from waeup.kofa.applicants.workflow import (
39    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
[7811]40from waeup.kofa.browser import (
[9217]41#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
[7363]42    DEFAULT_PASSPORT_IMAGE_PATH)
[9217]43from waeup.kofa.browser.layout import (
44    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
[7811]45from waeup.kofa.browser.interfaces import ICaptchaManager
46from waeup.kofa.browser.breadcrumbs import Breadcrumb
[8314]47from waeup.kofa.browser.resources import toggleall
[7811]48from waeup.kofa.browser.layout import (
[8550]49    NullValidator, jsaction, action, UtilityView, JSAction)
[7811]50from waeup.kofa.browser.pages import add_local_role, del_local_roles
51from waeup.kofa.browser.resources import datepicker, tabs, datatable, warning
52from waeup.kofa.interfaces import (
[7819]53    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
54    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
[7811]55from waeup.kofa.interfaces import MessageFactory as _
56from waeup.kofa.permissions import get_users_with_local_roles
57from waeup.kofa.students.interfaces import IStudentsUtils
[8186]58from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
[8170]59from waeup.kofa.widgets.datewidget import (
60    FriendlyDateDisplayWidget, FriendlyDateDisplayWidget,
61    FriendlyDatetimeDisplayWidget)
[8365]62from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
[5320]63
[7819]64grok.context(IKofaObject) # Make IKofaObject the default context
[5273]65
[8550]66class SubmitJSAction(JSAction):
67
68    msg = _('\'You can not edit your application records after final submission.'
69            ' You really want to submit?\'')
70
71class submitaction(grok.action):
72
73    def __call__(self, success):
74        action = SubmitJSAction(self.label, success=success, **self.options)
75        self.actions.append(action)
76        return action
77
[8388]78class ApplicantsRootPage(KofaDisplayFormPage):
[5822]79    grok.context(IApplicantsRoot)
80    grok.name('index')
[6153]81    grok.require('waeup.Public')
[8388]82    form_fields = grok.AutoFields(IApplicantsRoot)
83    form_fields['description'].custom_widget = HTMLDisplayWidget
[7710]84    label = _('Application Section')
[8404]85    search_button = _('Search')
[5843]86    pnav = 3
[6012]87
88    def update(self):
[6067]89        super(ApplicantsRootPage, self).update()
[6012]90        return
91
[8388]92    @property
93    def introduction(self):
94        # Here we know that the cookie has been set
95        lang = self.request.cookies.get('kofa.language')
96        html = self.context.description_dict.get(lang,'')
97        if html == '':
98            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
99            html = self.context.description_dict.get(portal_language,'')
100        return html
101
[8404]102class ApplicantsSearchPage(KofaPage):
103    grok.context(IApplicantsRoot)
104    grok.name('search')
105    grok.require('waeup.viewApplication')
106    label = _('Search applicants')
107    search_button = _('Search')
108    pnav = 3
109
110    def update(self, *args, **kw):
111        datatable.need()
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:
122                self.flash(_('Empty search string'))
123            return
124        self.results = search(query=self.searchterm,
125            searchtype=self.searchtype, view=self)
126        if not self.results:
127            self.flash(_('No applicant found.'))
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)
[7710]135    label = _('Manage application 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')
[6078]143
[6069]144    def update(self):
145        tabs.need()
[6108]146        datatable.need()
[7330]147        warning.need()
[8388]148        self.tab1 = self.tab2 = self.tab3 = ''
149        qs = self.request.get('QUERY_STRING', '')
150        if not qs:
151            qs = 'tab1'
152        setattr(self, qs, 'active')
[6069]153        return super(ApplicantsRootManageFormPage, self).update()
[5828]154
[6184]155    def getLocalRoles(self):
156        roles = ILocalRolesAssignable(self.context)
157        return roles()
158
159    def getUsers(self):
160        """Get a list of all users.
161        """
162        for key, val in grok.getSite()['users'].items():
163            url = self.url(val)
164            yield(dict(url=url, name=key, val=val))
165
166    def getUsersWithLocalRoles(self):
167        return get_users_with_local_roles(self.context)
168
[7710]169    @jsaction(_('Remove selected'))
[6069]170    def delApplicantsContainers(self, **data):
171        form = self.request.form
[9701]172        if 'val_id' in form:
[8388]173            child_id = form['val_id']
174        else:
175            self.flash(_('No container selected!'))
176            self.redirect(self.url(self.context, '@@manage')+'?tab2')
177            return
[6069]178        if not isinstance(child_id, list):
179            child_id = [child_id]
180        deleted = []
181        for id in child_id:
182            try:
183                del self.context[id]
184                deleted.append(id)
185            except:
[7710]186                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[6069]187                        id, sys.exc_info()[0], sys.exc_info()[1]))
188        if len(deleted):
[7738]189            self.flash(_('Successfully removed: ${a}',
190                mapping = {'a':', '.join(deleted)}))
[8388]191        self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6078]192        return
[5828]193
[7710]194    @action(_('Add applicants container'), validator=NullValidator)
[6069]195    def addApplicantsContainer(self, **data):
196        self.redirect(self.url(self.context, '@@add'))
[6078]197        return
198
[7710]199    @action(_('Add local role'), validator=NullValidator)
[6184]200    def addLocalRole(self, **data):
[7484]201        return add_local_role(self,3, **data)
[6184]202
[7710]203    @action(_('Remove selected local roles'))
[6184]204    def delLocalRoles(self, **data):
[7484]205        return del_local_roles(self,3,**data)
[6184]206
[8388]207    def _description(self):
208        view = ApplicantsRootPage(
209            self.context,self.request)
210        view.setUpWidgets()
211        return view.widgets['description']()
212
213    @action(_('Save'), style='primary')
214    def save(self, **data):
215        self.applyData(self.context, **data)
216        self.context.description_dict = self._description()
[8390]217        self.flash(_('Form has been saved.'))
[8388]218        return
219
[7819]220class ApplicantsContainerAddFormPage(KofaAddFormPage):
[5822]221    grok.context(IApplicantsRoot)
[7136]222    grok.require('waeup.manageApplication')
[5822]223    grok.name('add')
[6107]224    grok.template('applicantscontaineraddpage')
[7710]225    label = _('Add applicants container')
[5843]226    pnav = 3
[6078]227
[6103]228    form_fields = grok.AutoFields(
[7903]229        IApplicantsContainerAdd).omit('code').omit('title')
[6078]230
[6083]231    def update(self):
232        datepicker.need() # Enable jQuery datepicker in date fields.
233        return super(ApplicantsContainerAddFormPage, self).update()
234
[7710]235    @action(_('Add applicants container'))
[6069]236    def addApplicantsContainer(self, **data):
[6103]237        year = data['year']
238        code = u'%s%s' % (data['prefix'], year)
[9529]239        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
240        title = apptypes_dict[data['prefix']][0]
[7685]241        title = u'%s %s/%s' % (title, year, year + 1)
[6087]242        if code in self.context.keys():
[6105]243            self.flash(
[7710]244                _('An applicants container for the same application type and entrance year exists already in the database.'))
[5822]245            return
246        # Add new applicants container...
[8009]247        container = createObject(u'waeup.ApplicantsContainer')
[6069]248        self.applyData(container, **data)
[6087]249        container.code = code
250        container.title = title
251        self.context[code] = container
[7710]252        self.flash(_('Added:') + ' "%s".' % code)
[7484]253        self.redirect(self.url(self.context, u'@@manage'))
[5822]254        return
[6078]255
[7710]256    @action(_('Cancel'), validator=NullValidator)
[6069]257    def cancel(self, **data):
[7484]258        self.redirect(self.url(self.context, '@@manage'))
[6078]259
[5845]260class ApplicantsRootBreadcrumb(Breadcrumb):
261    """A breadcrumb for applicantsroot.
262    """
263    grok.context(IApplicantsRoot)
[7710]264    title = _(u'Applicants')
[6078]265
[5845]266class ApplicantsContainerBreadcrumb(Breadcrumb):
267    """A breadcrumb for applicantscontainers.
268    """
269    grok.context(IApplicantsContainer)
[6319]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
[8563]291class ApplicantsStatisticsPage(KofaDisplayFormPage):
292    """Some statistics about applicants in a container.
293    """
294    grok.context(IApplicantsContainer)
295    grok.name('statistics')
[8565]296    grok.require('waeup.viewApplicationStatistics')
[8563]297    grok.template('applicantcontainerstatistics')
298
299    @property
300    def label(self):
301        return "%s" % self.context.title
302
[7819]303class ApplicantsContainerPage(KofaDisplayFormPage):
[5830]304    """The standard view for regular applicant containers.
305    """
306    grok.context(IApplicantsContainer)
307    grok.name('index')
[6153]308    grok.require('waeup.Public')
[6029]309    grok.template('applicantscontainerpage')
[5850]310    pnav = 3
[6053]311
[9078]312    @property
313    def form_fields(self):
314        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
315        form_fields['description'].custom_widget = HTMLDisplayWidget
316        form_fields[
317            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
318        form_fields[
319            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
320        if self.request.principal.id == 'zope.anybody':
321            form_fields = form_fields.omit(
322                'code', 'prefix', 'year', 'mode',
323                'strict_deadline', 'application_category')
324        return form_fields
[6053]325
[5837]326    @property
[7708]327    def introduction(self):
[7833]328        # Here we know that the cookie has been set
329        lang = self.request.cookies.get('kofa.language')
[7708]330        html = self.context.description_dict.get(lang,'')
[8388]331        if html == '':
[7833]332            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7708]333            html = self.context.description_dict.get(portal_language,'')
[8388]334        return html
[7708]335
336    @property
[7467]337    def label(self):
[7493]338        return "%s" % self.context.title
[5837]339
[7819]340class ApplicantsContainerManageFormPage(KofaEditFormPage):
[5837]341    grok.context(IApplicantsContainer)
[5850]342    grok.name('manage')
[6107]343    grok.template('applicantscontainermanagepage')
[7903]344    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
[7710]345    taboneactions = [_('Save'),_('Cancel')]
[8684]346    tabtwoactions = [_('Remove selected'),_('Cancel'),
[8314]347        _('Create students from selected')]
[7710]348    tabthreeactions1 = [_('Remove selected local roles')]
349    tabthreeactions2 = [_('Add local role')]
[5844]350    # Use friendlier date widget...
[7136]351    grok.require('waeup.manageApplication')
[5850]352
353    @property
354    def label(self):
[7710]355        return _('Manage applicants container')
[5850]356
[5845]357    pnav = 3
[5837]358
[8547]359    @property
360    def showApplicants(self):
361        if len(self.context) < 5000:
362            return True
363        return False
364
[5837]365    def update(self):
[5850]366        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]367        tabs.need()
[8314]368        toggleall.need()
[7484]369        self.tab1 = self.tab2 = self.tab3 = ''
370        qs = self.request.get('QUERY_STRING', '')
371        if not qs:
372            qs = 'tab1'
373        setattr(self, qs, 'active')
[7330]374        warning.need()
[6015]375        datatable.need()  # Enable jQurey datatables for contents listing
[6107]376        return super(ApplicantsContainerManageFormPage, self).update()
[5837]377
[6184]378    def getLocalRoles(self):
379        roles = ILocalRolesAssignable(self.context)
380        return roles()
381
382    def getUsers(self):
383        """Get a list of all users.
384        """
385        for key, val in grok.getSite()['users'].items():
386            url = self.url(val)
387            yield(dict(url=url, name=key, val=val))
388
389    def getUsersWithLocalRoles(self):
390        return get_users_with_local_roles(self.context)
391
[7708]392    def _description(self):
393        view = ApplicantsContainerPage(
394            self.context,self.request)
395        view.setUpWidgets()
396        return view.widgets['description']()
397
[7714]398    @action(_('Save'), style='primary')
[7489]399    def save(self, **data):
[9531]400        changed_fields = self.applyData(self.context, **data)
401        if changed_fields:
402            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
403        else:
404            changed_fields = []
[7708]405        self.context.description_dict = self._description()
[8562]406        # Always refresh title. So we can change titles
407        # if APP_TYPES_DICT has been edited.
[9529]408        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
409        title = apptypes_dict[self.context.prefix][0]
[8562]410        self.context.title = u'%s %s/%s' % (
411            title, self.context.year, self.context.year + 1)
[7710]412        self.flash(_('Form has been saved.'))
[9531]413        fields_string = ' + '.join(changed_fields)
414        self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[5837]415        return
[6078]416
[7710]417    @jsaction(_('Remove selected'))
[6105]418    def delApplicant(self, **data):
[6189]419        form = self.request.form
[9701]420        if 'val_id' in form:
[6189]421            child_id = form['val_id']
422        else:
[7710]423            self.flash(_('No applicant selected!'))
[7484]424            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6189]425            return
426        if not isinstance(child_id, list):
427            child_id = [child_id]
428        deleted = []
429        for id in child_id:
430            try:
431                del self.context[id]
432                deleted.append(id)
433            except:
[7710]434                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[6189]435                        id, sys.exc_info()[0], sys.exc_info()[1]))
436        if len(deleted):
[7741]437            self.flash(_('Successfully removed: ${a}',
[7738]438                mapping = {'a':', '.join(deleted)}))
[7484]439        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6189]440        return
[6105]441
[8314]442    @action(_('Create students from selected'))
443    def createStudents(self, **data):
444        form = self.request.form
[9701]445        if 'val_id' in form:
[8314]446            child_id = form['val_id']
447        else:
448            self.flash(_('No applicant selected!'))
449            self.redirect(self.url(self.context, '@@manage')+'?tab2')
450            return
451        if not isinstance(child_id, list):
452            child_id = [child_id]
453        created = []
454        for id in child_id:
455            success, msg = self.context[id].createStudent(view=self)
456            if success:
457                created.append(id)
458        if len(created):
459            self.flash(_('${a} students successfully created.',
460                mapping = {'a': len(created)}))
461        else:
462            self.flash(_('No student could be created.'))
463        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
464        return
465
[7710]466    @action(_('Cancel'), validator=NullValidator)
[5837]467    def cancel(self, **data):
468        self.redirect(self.url(self.context))
469        return
[5886]470
[7710]471    @action(_('Add local role'), validator=NullValidator)
[6184]472    def addLocalRole(self, **data):
473        return add_local_role(self,3, **data)
[6105]474
[7710]475    @action(_('Remove selected local roles'))
[6184]476    def delLocalRoles(self, **data):
477        return del_local_roles(self,3,**data)
478
[7819]479class ApplicantAddFormPage(KofaAddFormPage):
[6622]480    """Add-form to add an applicant.
[6327]481    """
482    grok.context(IApplicantsContainer)
[7136]483    grok.require('waeup.manageApplication')
[6327]484    grok.name('addapplicant')
[7240]485    #grok.template('applicantaddpage')
486    form_fields = grok.AutoFields(IApplicant).select(
[7356]487        'firstname', 'middlename', 'lastname',
[7240]488        'email', 'phone')
[7714]489    label = _('Add applicant')
[6327]490    pnav = 3
491
[7714]492    @action(_('Create application record'))
[6327]493    def addApplicant(self, **data):
[8008]494        applicant = createObject(u'waeup.Applicant')
[7240]495        self.applyData(applicant, **data)
496        self.context.addApplicant(applicant)
[7714]497        self.flash(_('Applicant record created.'))
[7363]498        self.redirect(
499            self.url(self.context[applicant.application_number], 'index'))
[6327]500        return
501
[7819]502class ApplicantDisplayFormPage(KofaDisplayFormPage):
[8014]503    """A display view for applicant data.
504    """
[5273]505    grok.context(IApplicant)
506    grok.name('index')
[7113]507    grok.require('waeup.viewApplication')
[7200]508    grok.template('applicantdisplaypage')
[6320]509    form_fields = grok.AutoFields(IApplicant).omit(
[8983]510        'locked', 'course_admitted', 'password', 'suspended')
[7714]511    label = _('Applicant')
[5843]512    pnav = 3
[8922]513    hide_hint = False
[5273]514
[8046]515    @property
516    def separators(self):
517        return getUtility(IApplicantsUtils).SEPARATORS_DICT
518
[7063]519    def update(self):
520        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]521        # Mark application as started if applicant logs in for the first time
[7272]522        usertype = getattr(self.request.principal, 'user_type', None)
523        if usertype == 'applicant' and \
524            IWorkflowState(self.context).getState() == INITIALIZED:
[7240]525            IWorkflowInfo(self.context).fireTransition('start')
[7063]526        return
527
[6196]528    @property
[7240]529    def hasPassword(self):
530        if self.context.password:
[7714]531            return _('set')
532        return _('unset')
[7240]533
534    @property
[6196]535    def label(self):
536        container_title = self.context.__parent__.title
[8096]537        return _('${a} <br /> Application Record ${b}', mapping = {
[7714]538            'a':container_title, 'b':self.context.application_number})
[6196]539
[7347]540    def getCourseAdmitted(self):
541        """Return link, title and code in html format to the certificate
542           admitted.
543        """
544        course_admitted = self.context.course_admitted
[7351]545        if getattr(course_admitted, '__parent__',None):
[7347]546            url = self.url(course_admitted)
547            title = course_admitted.title
548            code = course_admitted.code
549            return '<a href="%s">%s - %s</a>' %(url,code,title)
550        return ''
[6254]551
[7259]552class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
553    grok.context(IApplicant)
554    grok.name('base')
555    form_fields = grok.AutoFields(IApplicant).select(
[9141]556        'applicant_id','email', 'course1')
[7259]557
[7459]558class CreateStudentPage(UtilityView, grok.View):
[8636]559    """Create a student object from applicant data.
[7341]560    """
561    grok.context(IApplicant)
562    grok.name('createstudent')
563    grok.require('waeup.manageStudent')
564
565    def update(self):
[8314]566        msg = self.context.createStudent(view=self)[1]
[7341]567        self.flash(msg)
568        self.redirect(self.url(self.context))
569        return
570
571    def render(self):
572        return
573
[8636]574class CreateAllStudentsPage(UtilityView, grok.View):
575    """Create all student objects from applicant data
576    in a container.
577
578    This is a hidden page, no link or button will
579    be provided and only PortalManagers can do this.
580    """
[9900]581    #grok.context(IApplicantsContainer)
[8636]582    grok.name('createallstudents')
583    grok.require('waeup.managePortal')
584
585    def update(self):
586        cat = getUtility(ICatalog, name='applicants_catalog')
587        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
588        created = []
[9900]589        container_only = False
590        applicants_root = grok.getSite()['applicants']
591        if isinstance(self.context, ApplicantsContainer):
592            container_only = True
[8636]593        for result in results:
[9900]594            if container_only and result.__parent__ is not self.context:
[8636]595                continue
596            success, msg = result.createStudent(view=self)
597            if success:
598                created.append(result.applicant_id)
599            else:
600                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
[9900]601                applicants_root.logger.info(
[8742]602                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
[8636]603        if len(created):
604            self.flash(_('${a} students successfully created.',
605                mapping = {'a': len(created)}))
606        else:
607            self.flash(_('No student could be created.'))
[9900]608        self.redirect(self.url(self.context))
[8636]609        return
610
611    def render(self):
612        return
613
[8260]614class ApplicationFeePaymentAddPage(UtilityView, grok.View):
[7250]615    """ Page to add an online payment ticket
616    """
617    grok.context(IApplicant)
618    grok.name('addafp')
619    grok.require('waeup.payApplicant')
[8243]620    factory = u'waeup.ApplicantOnlinePayment'
[7250]621
622    def update(self):
623        for key in self.context.keys():
624            ticket = self.context[key]
625            if ticket.p_state == 'paid':
626                  self.flash(
[7714]627                      _('This type of payment has already been made.'))
[7250]628                  self.redirect(self.url(self.context))
629                  return
[8524]630        applicants_utils = getUtility(IApplicantsUtils)
631        container = self.context.__parent__
[8243]632        payment = createObject(self.factory)
[8524]633        error = applicants_utils.setPaymentDetails(container, payment)
634        if error is not None:
635            self.flash(error)
636            self.redirect(self.url(self.context))
637            return
[7250]638        self.context[payment.p_id] = payment
[7714]639        self.flash(_('Payment ticket created.'))
[8280]640        self.redirect(self.url(payment))
[7250]641        return
642
643    def render(self):
644        return
645
646
[7819]647class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[7250]648    """ Page to view an online payment ticket
649    """
650    grok.context(IApplicantOnlinePayment)
651    grok.name('index')
652    grok.require('waeup.viewApplication')
[9984]653    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8170]654    form_fields[
655        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
656    form_fields[
657        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]658    pnav = 3
659
660    @property
661    def label(self):
[7714]662        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8170]663            'a':self.context.__parent__.display_fullname,
664            'b':self.context.p_id})
[7250]665
[8420]666class OnlinePaymentApprovePage(UtilityView, grok.View):
667    """ Approval view
[7250]668    """
669    grok.context(IApplicantOnlinePayment)
[8420]670    grok.name('approve')
671    grok.require('waeup.managePortal')
[7250]672
673    def update(self):
[8428]674        success, msg, log = self.context.approveApplicantPayment()
675        if log is not None:
[9771]676            applicant = self.context.__parent__
677            # Add log message to applicants.log
678            applicant.writeLogMessage(self, log)
679            # Add log message to payments.log
680            self.context.logger.info(
[9795]681                '%s,%s,%s,%s,%s,,,,,,' % (
[9771]682                applicant.applicant_id,
683                self.context.p_id, self.context.p_category,
684                self.context.amount_auth, self.context.r_code))
[8422]685        self.flash(msg)
[7250]686        return
687
688    def render(self):
689        self.redirect(self.url(self.context, '@@index'))
690        return
691
[7459]692class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7250]693    """Deliver a PDF slip of the context.
694    """
695    grok.context(IApplicantOnlinePayment)
[8262]696    grok.name('payment_slip.pdf')
[7250]697    grok.require('waeup.viewApplication')
[9984]698    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
[8173]699    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
700    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]701    prefix = 'form'
[8258]702    note = None
[7250]703
704    @property
[7714]705    def title(self):
[7819]706        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]707        return translate(_('Payment Data'), 'waeup.kofa',
[7714]708            target_language=portal_language)
709
710    @property
[7250]711    def label(self):
[7819]712        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8262]713        return translate(_('Online Payment Slip'),
[7811]714            'waeup.kofa', target_language=portal_language) \
[7714]715            + ' %s' % self.context.p_id
[7250]716
717    def render(self):
[8262]718        #if self.context.p_state != 'paid':
719        #    self.flash(_('Ticket not yet paid.'))
720        #    self.redirect(self.url(self.context))
721        #    return
[7259]722        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
[7250]723            self.request)
724        students_utils = getUtility(IStudentsUtils)
[8262]725        return students_utils.renderPDF(self,'payment_slip.pdf',
[8258]726            self.context.__parent__, applicantview, note=self.note)
[7250]727
[7459]728class ExportPDFPage(UtilityView, grok.View):
[6358]729    """Deliver a PDF slip of the context.
730    """
731    grok.context(IApplicant)
732    grok.name('application_slip.pdf')
[7136]733    grok.require('waeup.viewApplication')
[6358]734    prefix = 'form'
735
[8666]736    def update(self):
[9051]737        if self.context.state in ('initialized', 'started', 'paid'):
[8666]738            self.flash(
[9051]739                _('Please pay and submit before trying to download the application slip.'))
[8666]740            return self.redirect(self.url(self.context))
741        return
742
[6358]743    def render(self):
[7392]744        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
745            view=self)
[6358]746        self.response.setHeader(
747            'Content-Type', 'application/pdf')
[7392]748        return pdfstream
[6358]749
[7081]750def handle_img_upload(upload, context, view):
[7063]751    """Handle upload of applicant image.
[7081]752
753    Returns `True` in case of success or `False`.
754
755    Please note that file pointer passed in (`upload`) most probably
756    points to end of file when leaving this function.
[7063]757    """
[7081]758    size = file_size(upload)
759    if size > MAX_UPLOAD_SIZE:
[7714]760        view.flash(_('Uploaded image is too big!'))
[7081]761        return False
[7247]762    dummy, ext = os.path.splitext(upload.filename)
763    ext.lower()
764    if ext != '.jpg':
[7714]765        view.flash(_('jpg file extension expected.'))
[7247]766        return False
[7081]767    upload.seek(0) # file pointer moved when determining size
[7063]768    store = getUtility(IExtFileStore)
769    file_id = IFileStoreNameChooser(context).chooseName()
770    store.createFile(file_id, upload)
[7081]771    return True
[7063]772
[7819]773class ApplicantManageFormPage(KofaEditFormPage):
[6196]774    """A full edit view for applicant data.
775    """
776    grok.context(IApplicant)
[7200]777    grok.name('manage')
[7136]778    grok.require('waeup.manageApplication')
[6476]779    form_fields = grok.AutoFields(IApplicant)
[7351]780    form_fields['student_id'].for_display = True
[7378]781    form_fields['applicant_id'].for_display = True
[7200]782    grok.template('applicanteditpage')
[6322]783    manage_applications = True
[6196]784    pnav = 3
[7714]785    display_actions = [[_('Save'), _('Final Submit')],
786        [_('Add online payment ticket'),_('Remove selected tickets')]]
[6196]787
[8046]788    @property
789    def separators(self):
790        return getUtility(IApplicantsUtils).SEPARATORS_DICT
791
[6196]792    def update(self):
793        datepicker.need() # Enable jQuery datepicker in date fields.
[7330]794        warning.need()
[7200]795        super(ApplicantManageFormPage, self).update()
[6353]796        self.wf_info = IWorkflowInfo(self.context)
[7081]797        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]798        self.passport_changed = None
[6598]799        upload = self.request.form.get('form.passport', None)
800        if upload:
801            # We got a fresh upload
[7084]802            self.passport_changed = handle_img_upload(
803                upload, self.context, self)
[6196]804        return
805
806    @property
807    def label(self):
808        container_title = self.context.__parent__.title
[8096]809        return _('${a} <br /> Application Form ${b}', mapping = {
[7714]810            'a':container_title, 'b':self.context.application_number})
[6196]811
[6303]812    def getTransitions(self):
[6351]813        """Return a list of dicts of allowed transition ids and titles.
[6353]814
815        Each list entry provides keys ``name`` and ``title`` for
816        internal name and (human readable) title of a single
817        transition.
[6349]818        """
[8434]819        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
820            if not t[0] == 'pay']
[7687]821        return [dict(name='', title=_('No transition'))] +[
[6355]822            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]823
[7714]824    @action(_('Save'), style='primary')
[6196]825    def save(self, **data):
[7240]826        form = self.request.form
827        password = form.get('password', None)
828        password_ctl = form.get('control_password', None)
829        if password:
830            validator = getUtility(IPasswordValidator)
831            errors = validator.validate_password(password, password_ctl)
832            if errors:
833                self.flash( ' '.join(errors))
834                return
[7084]835        if self.passport_changed is False:  # False is not None!
836            return # error during image upload. Ignore other values
[6475]837        changed_fields = self.applyData(self.context, **data)
[7199]838        # Turn list of lists into single list
839        if changed_fields:
840            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]841        else:
842            changed_fields = []
843        if self.passport_changed:
844            changed_fields.append('passport')
845        if password:
846            # Now we know that the form has no errors and can set password ...
847            IUserAccount(self.context).setPassword(password)
848            changed_fields.append('password')
[7199]849        fields_string = ' + '.join(changed_fields)
[7085]850        trans_id = form.get('transition', None)
851        if trans_id:
852            self.wf_info.fireTransition(trans_id)
[7714]853        self.flash(_('Form has been saved.'))
[6644]854        if fields_string:
[8742]855            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[6196]856        return
857
[7250]858    def unremovable(self, ticket):
[7330]859        return False
[7250]860
861    # This method is also used by the ApplicantEditFormPage
862    def delPaymentTickets(self, **data):
863        form = self.request.form
[9701]864        if 'val_id' in form:
[7250]865            child_id = form['val_id']
866        else:
[7714]867            self.flash(_('No payment selected.'))
[7250]868            self.redirect(self.url(self.context))
869            return
870        if not isinstance(child_id, list):
871            child_id = [child_id]
872        deleted = []
873        for id in child_id:
874            # Applicants are not allowed to remove used payment tickets
875            if not self.unremovable(self.context[id]):
876                try:
877                    del self.context[id]
878                    deleted.append(id)
879                except:
[7714]880                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[7250]881                            id, sys.exc_info()[0], sys.exc_info()[1]))
882        if len(deleted):
[7741]883            self.flash(_('Successfully removed: ${a}',
[7738]884                mapping = {'a':', '.join(deleted)}))
[8742]885            self.context.writeLogMessage(
886                self, 'removed: % s' % ', '.join(deleted))
[7250]887        return
888
[7252]889    # We explicitely want the forms to be validated before payment tickets
890    # can be created. If no validation is requested, use
[7459]891    # 'validator=NullValidator' in the action directive
[7714]892    @action(_('Add online payment ticket'))
[7250]893    def addPaymentTicket(self, **data):
894        self.redirect(self.url(self.context, '@@addafp'))
[7252]895        return
[7250]896
[7714]897    @jsaction(_('Remove selected tickets'))
[7250]898    def removePaymentTickets(self, **data):
899        self.delPaymentTickets(**data)
900        self.redirect(self.url(self.context) + '/@@manage')
901        return
902
[7200]903class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]904    """An applicant-centered edit view for applicant data.
905    """
[6196]906    grok.context(IApplicantEdit)
[5273]907    grok.name('edit')
[6198]908    grok.require('waeup.handleApplication')
[6459]909    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]910        'locked', 'course_admitted', 'student_id',
[9047]911        'suspended'
[6459]912        )
[7459]913    form_fields['applicant_id'].for_display = True
[8039]914    form_fields['reg_number'].for_display = True
[7200]915    grok.template('applicanteditpage')
[6322]916    manage_applications = False
[5484]917
[7250]918    @property
919    def display_actions(self):
[8286]920        state = IWorkflowState(self.context).getState()
921        if state == INITIALIZED:
[7250]922            actions = [[],[]]
[8286]923        elif state == STARTED:
[7714]924            actions = [[_('Save')],
925                [_('Add online payment ticket'),_('Remove selected tickets')]]
[8286]926        elif state == PAID:
[7714]927            actions = [[_('Save'), _('Final Submit')],
928                [_('Remove selected tickets')]]
[7351]929        else:
[7250]930            actions = [[],[]]
931        return actions
932
[7330]933    def unremovable(self, ticket):
[8286]934        state = IWorkflowState(self.context).getState()
935        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
[7330]936
[7145]937    def emit_lock_message(self):
[7714]938        self.flash(_('The requested form is locked (read-only).'))
[5941]939        self.redirect(self.url(self.context))
940        return
[6078]941
[5686]942    def update(self):
[8665]943        if self.context.locked or (
944            self.context.__parent__.expired and
945            self.context.__parent__.strict_deadline):
[7145]946            self.emit_lock_message()
[5941]947            return
[7200]948        super(ApplicantEditFormPage, self).update()
[5686]949        return
[5952]950
[6196]951    def dataNotComplete(self):
[7252]952        store = getUtility(IExtFileStore)
953        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
[7714]954            return _('No passport picture uploaded.')
[6322]955        if not self.request.form.get('confirm_passport', False):
[7714]956            return _('Passport picture confirmation box not ticked.')
[6196]957        return False
[5952]958
[7252]959    # We explicitely want the forms to be validated before payment tickets
960    # can be created. If no validation is requested, use
[7459]961    # 'validator=NullValidator' in the action directive
[7714]962    @action(_('Add online payment ticket'))
[7250]963    def addPaymentTicket(self, **data):
964        self.redirect(self.url(self.context, '@@addafp'))
[7252]965        return
[7250]966
[7714]967    @jsaction(_('Remove selected tickets'))
[7250]968    def removePaymentTickets(self, **data):
969        self.delPaymentTickets(**data)
970        self.redirect(self.url(self.context) + '/@@edit')
971        return
972
[7996]973    @action(_('Save'), style='primary')
[5273]974    def save(self, **data):
[7084]975        if self.passport_changed is False:  # False is not None!
976            return # error during image upload. Ignore other values
[5273]977        self.applyData(self.context, **data)
[6196]978        self.flash('Form has been saved.')
[5273]979        return
980
[8550]981    @submitaction(_('Final Submit'))
[5484]982    def finalsubmit(self, **data):
[7084]983        if self.passport_changed is False:  # False is not None!
984            return # error during image upload. Ignore other values
[6196]985        if self.dataNotComplete():
986            self.flash(self.dataNotComplete())
[5941]987            return
[7252]988        self.applyData(self.context, **data)
[8286]989        state = IWorkflowState(self.context).getState()
[6322]990        # This shouldn't happen, but the application officer
991        # might have forgotten to lock the form after changing the state
[8286]992        if state != PAID:
[8589]993            self.flash(_('The form cannot be submitted. Wrong state!'))
[6303]994            return
995        IWorkflowInfo(self.context).fireTransition('submit')
[8589]996        # application_date is used in export files for sorting.
997        # We can thus store utc.
[8194]998        self.context.application_date = datetime.utcnow()
[7714]999        self.flash(_('Form has been submitted.'))
[6196]1000        self.redirect(self.url(self.context))
[5273]1001        return
[5941]1002
[7063]1003class PassportImage(grok.View):
1004    """Renders the passport image for applicants.
1005    """
1006    grok.name('passport.jpg')
1007    grok.context(IApplicant)
[7113]1008    grok.require('waeup.viewApplication')
[7063]1009
1010    def render(self):
1011        # A filename chooser turns a context into a filename suitable
1012        # for file storage.
1013        image = getUtility(IExtFileStore).getFileByContext(self.context)
1014        self.response.setHeader(
1015            'Content-Type', 'image/jpeg')
1016        if image is None:
1017            # show placeholder image
[7089]1018            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1019        return image
[7363]1020
[7819]1021class ApplicantRegistrationPage(KofaAddFormPage):
[7363]1022    """Captcha'd registration page for applicants.
1023    """
1024    grok.context(IApplicantsContainer)
1025    grok.name('register')
[7373]1026    grok.require('waeup.Anonymous')
[7363]1027    grok.template('applicantregister')
1028
[7368]1029    @property
[8033]1030    def form_fields(self):
1031        form_fields = None
[8128]1032        if self.context.mode == 'update':
1033            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1034                'firstname','reg_number','email')
1035        else: #if self.context.mode == 'create':
[8033]1036            form_fields = grok.AutoFields(IApplicantEdit).select(
1037                'firstname', 'middlename', 'lastname', 'email', 'phone')
1038        return form_fields
1039
1040    @property
[7368]1041    def label(self):
[8078]1042        return _('Apply for ${a}',
[7714]1043            mapping = {'a':self.context.title})
[7368]1044
[7363]1045    def update(self):
[8665]1046        if self.context.expired:
1047            self.flash(_('Outside application period.'))
[7368]1048            self.redirect(self.url(self.context))
1049            return
1050        # Handle captcha
[7363]1051        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1052        self.captcha_result = self.captcha.verify(self.request)
1053        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1054        return
1055
[8629]1056    def _redirect(self, email, password, applicant_id):
1057        # Forward only email to landing page in base package.
1058        self.redirect(self.url(self.context, 'registration_complete',
1059            data = dict(email=email)))
1060        return
1061
[9178]1062    @action(_('Send login credentials to email address'), style='primary')
[7363]1063    def register(self, **data):
1064        if not self.captcha_result.is_valid:
[8037]1065            # Captcha will display error messages automatically.
[7363]1066            # No need to flash something.
1067            return
[8033]1068        if self.context.mode == 'create':
1069            # Add applicant
1070            applicant = createObject(u'waeup.Applicant')
1071            self.applyData(applicant, **data)
1072            self.context.addApplicant(applicant)
[8042]1073            applicant.reg_number = applicant.applicant_id
1074            notify(grok.ObjectModifiedEvent(applicant))
[8033]1075        elif self.context.mode == 'update':
1076            # Update applicant
[8037]1077            reg_number = data.get('reg_number','')
1078            firstname = data.get('firstname','')
[8033]1079            cat = getUtility(ICatalog, name='applicants_catalog')
1080            results = list(
1081                cat.searchResults(reg_number=(reg_number, reg_number)))
1082            if results:
1083                applicant = results[0]
[8042]1084                if getattr(applicant,'firstname',None) is None:
[8037]1085                    self.flash(_('An error occurred.'))
1086                    return
1087                elif applicant.firstname.lower() != firstname.lower():
[8042]1088                    # Don't tell the truth here. Anonymous must not
1089                    # know that a record was found and only the firstname
1090                    # verification failed.
[8037]1091                    self.flash(_('No application record found.'))
1092                    return
[8627]1093                elif applicant.password is not None and \
1094                    applicant.state != INITIALIZED:
1095                    self.flash(_('Your password has already been set and used. '
[8042]1096                                 'Please proceed to the login page.'))
1097                    return
1098                # Store email address but nothing else.
[8033]1099                applicant.email = data['email']
[8042]1100                notify(grok.ObjectModifiedEvent(applicant))
[8033]1101            else:
[8042]1102                # No record found, this is the truth.
[8033]1103                self.flash(_('No application record found.'))
1104                return
1105        else:
[8042]1106            # Does not happen but anyway ...
[8033]1107            return
[7819]1108        kofa_utils = getUtility(IKofaUtils)
[7811]1109        password = kofa_utils.genPassword()
[7380]1110        IUserAccount(applicant).setPassword(password)
[7365]1111        # Send email with credentials
[7399]1112        login_url = self.url(grok.getSite(), 'login')
[8853]1113        url_info = u'Login: %s' % login_url
[7714]1114        msg = _('You have successfully been registered for the')
[7811]1115        if kofa_utils.sendCredentials(IUserAccount(applicant),
[8853]1116            password, url_info, msg):
[8629]1117            email_sent = applicant.email
[7380]1118        else:
[8629]1119            email_sent = None
1120        self._redirect(email=email_sent, password=password,
1121            applicant_id=applicant.applicant_id)
[7380]1122        return
1123
[7819]1124class ApplicantRegistrationEmailSent(KofaPage):
[7380]1125    """Landing page after successful registration.
[8629]1126
[7380]1127    """
1128    grok.name('registration_complete')
1129    grok.require('waeup.Public')
1130    grok.template('applicantregemailsent')
[7714]1131    label = _('Your registration was successful.')
[7380]1132
[8629]1133    def update(self, email=None, applicant_id=None, password=None):
[7380]1134        self.email = email
[8629]1135        self.password = password
1136        self.applicant_id = applicant_id
[7380]1137        return
Note: See TracBrowser for help on using the repository browser.