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

Last change on this file since 8275 was 8262, checked in by Henrik Bettermann, 13 years ago

The payment receipt is now called payment slip and can be downloaded even if not paid. This is necessary for eTranzact payments.

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