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

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

Roll back r8282. Also in students the registration state is simply called state.

  • Property svn:keywords set to Id
File size: 35.0 KB
RevLine 
[5273]1## $Id: browser.py 8286 2012-04-26 13:58:25Z 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.'))
[8280]464            self.redirect(self.url(self.context))
[7250]465            return
466        timestamp = "%d" % int(time()*1000)
467        for key in self.context.keys():
468            ticket = self.context[key]
469            if ticket.p_state == 'paid':
470                  self.flash(
[7714]471                      _('This type of payment has already been made.'))
[7250]472                  self.redirect(self.url(self.context))
473                  return
[8243]474        payment = createObject(self.factory)
[7250]475        payment.p_id = "p%s" % timestamp
476        payment.p_item = self.context.__parent__.title
[8245]477        payment.p_session = self.context.__parent__.year
[7250]478        payment.p_category = p_category
[8260]479        payment.amount_auth = session_config.application_fee
[8245]480        payment = self._fillCustomFields(payment, session_config)
[7250]481        self.context[payment.p_id] = payment
[7714]482        self.flash(_('Payment ticket created.'))
[8280]483        self.redirect(self.url(payment))
[7250]484        return
485
486    def render(self):
487        return
488
489
[7819]490class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[7250]491    """ Page to view an online payment ticket
492    """
493    grok.context(IApplicantOnlinePayment)
494    grok.name('index')
495    grok.require('waeup.viewApplication')
496    form_fields = grok.AutoFields(IApplicantOnlinePayment)
[8170]497    form_fields[
498        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
499    form_fields[
500        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]501    pnav = 3
502
503    @property
504    def label(self):
[7714]505        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8170]506            'a':self.context.__parent__.display_fullname,
507            'b':self.context.p_id})
[7250]508
[7459]509class OnlinePaymentCallbackPage(UtilityView, grok.View):
[7250]510    """ Callback view
511    """
512    grok.context(IApplicantOnlinePayment)
[7997]513    grok.name('simulate_callback')
[7250]514    grok.require('waeup.payApplicant')
515
516    # This update method simulates a valid callback und must be
[7997]517    # neutralized in the customization package.
[7250]518    def update(self):
[7322]519        self.wf_info = IWorkflowInfo(self.context.__parent__)
520        try:
521            self.wf_info.fireTransition('pay')
522        except InvalidTransitionError:
523            self.flash('Error: %s' % sys.exc_info()[1])
[7250]524            return
525        self.context.r_amount_approved = self.context.amount_auth
526        self.context.r_code = u'00'
527        self.context.p_state = 'paid'
[8194]528        self.context.payment_date = datetime.utcnow()
[7811]529        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
[7250]530        self.context.__parent__.loggerInfo(
531            ob_class, 'valid callback: %s' % self.context.p_id)
[7714]532        self.flash(_('Valid callback received.'))
[7250]533        return
534
535    def render(self):
536        self.redirect(self.url(self.context, '@@index'))
537        return
538
[7459]539class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7250]540    """Deliver a PDF slip of the context.
541    """
542    grok.context(IApplicantOnlinePayment)
[8262]543    grok.name('payment_slip.pdf')
[7250]544    grok.require('waeup.viewApplication')
545    form_fields = grok.AutoFields(IApplicantOnlinePayment)
[8173]546    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
547    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7250]548    prefix = 'form'
[8258]549    note = None
[7250]550
551    @property
[7714]552    def title(self):
[7819]553        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]554        return translate(_('Payment Data'), 'waeup.kofa',
[7714]555            target_language=portal_language)
556
557    @property
[7250]558    def label(self):
[7819]559        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8262]560        return translate(_('Online Payment Slip'),
[7811]561            'waeup.kofa', target_language=portal_language) \
[7714]562            + ' %s' % self.context.p_id
[7250]563
564    def render(self):
[8262]565        #if self.context.p_state != 'paid':
566        #    self.flash(_('Ticket not yet paid.'))
567        #    self.redirect(self.url(self.context))
568        #    return
[7259]569        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
[7250]570            self.request)
571        students_utils = getUtility(IStudentsUtils)
[8262]572        return students_utils.renderPDF(self,'payment_slip.pdf',
[8258]573            self.context.__parent__, applicantview, note=self.note)
[7250]574
[7459]575class ExportPDFPage(UtilityView, grok.View):
[6358]576    """Deliver a PDF slip of the context.
577    """
578    grok.context(IApplicant)
579    grok.name('application_slip.pdf')
[7136]580    grok.require('waeup.viewApplication')
[6358]581    prefix = 'form'
582
583    def render(self):
[7392]584        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
585            view=self)
[6358]586        self.response.setHeader(
587            'Content-Type', 'application/pdf')
[7392]588        return pdfstream
[6358]589
[7081]590def handle_img_upload(upload, context, view):
[7063]591    """Handle upload of applicant image.
[7081]592
593    Returns `True` in case of success or `False`.
594
595    Please note that file pointer passed in (`upload`) most probably
596    points to end of file when leaving this function.
[7063]597    """
[7081]598    size = file_size(upload)
599    if size > MAX_UPLOAD_SIZE:
[7714]600        view.flash(_('Uploaded image is too big!'))
[7081]601        return False
[7247]602    dummy, ext = os.path.splitext(upload.filename)
603    ext.lower()
604    if ext != '.jpg':
[7714]605        view.flash(_('jpg file extension expected.'))
[7247]606        return False
[7081]607    upload.seek(0) # file pointer moved when determining size
[7063]608    store = getUtility(IExtFileStore)
609    file_id = IFileStoreNameChooser(context).chooseName()
610    store.createFile(file_id, upload)
[7081]611    return True
[7063]612
[7819]613class ApplicantManageFormPage(KofaEditFormPage):
[6196]614    """A full edit view for applicant data.
615    """
616    grok.context(IApplicant)
[7200]617    grok.name('manage')
[7136]618    grok.require('waeup.manageApplication')
[6476]619    form_fields = grok.AutoFields(IApplicant)
[7351]620    form_fields['student_id'].for_display = True
[7378]621    form_fields['applicant_id'].for_display = True
[7200]622    grok.template('applicanteditpage')
[6322]623    manage_applications = True
[6196]624    pnav = 3
[7714]625    display_actions = [[_('Save'), _('Final Submit')],
626        [_('Add online payment ticket'),_('Remove selected tickets')]]
[6196]627
[8046]628    @property
629    def separators(self):
630        return getUtility(IApplicantsUtils).SEPARATORS_DICT
631
[6196]632    def update(self):
633        datepicker.need() # Enable jQuery datepicker in date fields.
[7330]634        warning.need()
[7200]635        super(ApplicantManageFormPage, self).update()
[6353]636        self.wf_info = IWorkflowInfo(self.context)
[7081]637        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]638        self.passport_changed = None
[6598]639        upload = self.request.form.get('form.passport', None)
640        if upload:
641            # We got a fresh upload
[7084]642            self.passport_changed = handle_img_upload(
643                upload, self.context, self)
[6196]644        return
645
646    @property
647    def label(self):
648        container_title = self.context.__parent__.title
[8096]649        return _('${a} <br /> Application Form ${b}', mapping = {
[7714]650            'a':container_title, 'b':self.context.application_number})
[6196]651
[6303]652    def getTransitions(self):
[6351]653        """Return a list of dicts of allowed transition ids and titles.
[6353]654
655        Each list entry provides keys ``name`` and ``title`` for
656        internal name and (human readable) title of a single
657        transition.
[6349]658        """
[6353]659        allowed_transitions = self.wf_info.getManualTransitions()
[7687]660        return [dict(name='', title=_('No transition'))] +[
[6355]661            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]662
[7714]663    @action(_('Save'), style='primary')
[6196]664    def save(self, **data):
[7240]665        form = self.request.form
666        password = form.get('password', None)
667        password_ctl = form.get('control_password', None)
668        if password:
669            validator = getUtility(IPasswordValidator)
670            errors = validator.validate_password(password, password_ctl)
671            if errors:
672                self.flash( ' '.join(errors))
673                return
[7084]674        if self.passport_changed is False:  # False is not None!
675            return # error during image upload. Ignore other values
[6475]676        changed_fields = self.applyData(self.context, **data)
[7199]677        # Turn list of lists into single list
678        if changed_fields:
679            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]680        else:
681            changed_fields = []
682        if self.passport_changed:
683            changed_fields.append('passport')
684        if password:
685            # Now we know that the form has no errors and can set password ...
686            IUserAccount(self.context).setPassword(password)
687            changed_fields.append('password')
[7199]688        fields_string = ' + '.join(changed_fields)
[7085]689        trans_id = form.get('transition', None)
690        if trans_id:
691            self.wf_info.fireTransition(trans_id)
[7714]692        self.flash(_('Form has been saved.'))
[7811]693        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
[6644]694        if fields_string:
695            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6196]696        return
697
[7250]698    def unremovable(self, ticket):
[7330]699        return False
[7250]700
701    # This method is also used by the ApplicantEditFormPage
702    def delPaymentTickets(self, **data):
703        form = self.request.form
704        if form.has_key('val_id'):
705            child_id = form['val_id']
706        else:
[7714]707            self.flash(_('No payment selected.'))
[7250]708            self.redirect(self.url(self.context))
709            return
710        if not isinstance(child_id, list):
711            child_id = [child_id]
712        deleted = []
713        for id in child_id:
714            # Applicants are not allowed to remove used payment tickets
715            if not self.unremovable(self.context[id]):
716                try:
717                    del self.context[id]
718                    deleted.append(id)
719                except:
[7714]720                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
[7250]721                            id, sys.exc_info()[0], sys.exc_info()[1]))
722        if len(deleted):
[7741]723            self.flash(_('Successfully removed: ${a}',
[7738]724                mapping = {'a':', '.join(deleted)}))
[7811]725            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
[7363]726            self.context.loggerInfo(
727                ob_class, 'removed: % s' % ', '.join(deleted))
[7250]728        return
729
[7252]730    # We explicitely want the forms to be validated before payment tickets
731    # can be created. If no validation is requested, use
[7459]732    # 'validator=NullValidator' in the action directive
[7714]733    @action(_('Add online payment ticket'))
[7250]734    def addPaymentTicket(self, **data):
735        self.redirect(self.url(self.context, '@@addafp'))
[7252]736        return
[7250]737
[7714]738    @jsaction(_('Remove selected tickets'))
[7250]739    def removePaymentTickets(self, **data):
740        self.delPaymentTickets(**data)
741        self.redirect(self.url(self.context) + '/@@manage')
742        return
743
[7200]744class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]745    """An applicant-centered edit view for applicant data.
746    """
[6196]747    grok.context(IApplicantEdit)
[5273]748    grok.name('edit')
[6198]749    grok.require('waeup.handleApplication')
[6459]750    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]751        'locked', 'course_admitted', 'student_id',
[8039]752        'screening_score',
[6459]753        )
[7459]754    form_fields['applicant_id'].for_display = True
[8039]755    form_fields['reg_number'].for_display = True
[7200]756    grok.template('applicanteditpage')
[6322]757    manage_applications = False
[5484]758
[7250]759    @property
760    def display_actions(self):
[8286]761        state = IWorkflowState(self.context).getState()
762        if state == INITIALIZED:
[7250]763            actions = [[],[]]
[8286]764        elif state == STARTED:
[7714]765            actions = [[_('Save')],
766                [_('Add online payment ticket'),_('Remove selected tickets')]]
[8286]767        elif state == PAID:
[7714]768            actions = [[_('Save'), _('Final Submit')],
769                [_('Remove selected tickets')]]
[7351]770        else:
[7250]771            actions = [[],[]]
772        return actions
773
[7330]774    def unremovable(self, ticket):
[8286]775        state = IWorkflowState(self.context).getState()
776        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
[7330]777
[7145]778    def emit_lock_message(self):
[7714]779        self.flash(_('The requested form is locked (read-only).'))
[5941]780        self.redirect(self.url(self.context))
781        return
[6078]782
[5686]783    def update(self):
[5941]784        if self.context.locked:
[7145]785            self.emit_lock_message()
[5941]786            return
[7200]787        super(ApplicantEditFormPage, self).update()
[5686]788        return
[5952]789
[6196]790    def dataNotComplete(self):
[7252]791        store = getUtility(IExtFileStore)
792        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
[7714]793            return _('No passport picture uploaded.')
[6322]794        if not self.request.form.get('confirm_passport', False):
[7714]795            return _('Passport picture confirmation box not ticked.')
[6196]796        return False
[5952]797
[7252]798    # We explicitely want the forms to be validated before payment tickets
799    # can be created. If no validation is requested, use
[7459]800    # 'validator=NullValidator' in the action directive
[7714]801    @action(_('Add online payment ticket'))
[7250]802    def addPaymentTicket(self, **data):
803        self.redirect(self.url(self.context, '@@addafp'))
[7252]804        return
[7250]805
[7714]806    @jsaction(_('Remove selected tickets'))
[7250]807    def removePaymentTickets(self, **data):
808        self.delPaymentTickets(**data)
809        self.redirect(self.url(self.context) + '/@@edit')
810        return
811
[7996]812    @action(_('Save'), style='primary')
[5273]813    def save(self, **data):
[7084]814        if self.passport_changed is False:  # False is not None!
815            return # error during image upload. Ignore other values
[5273]816        self.applyData(self.context, **data)
[6196]817        self.flash('Form has been saved.')
[5273]818        return
819
[7714]820    @action(_('Final Submit'))
[5484]821    def finalsubmit(self, **data):
[7084]822        if self.passport_changed is False:  # False is not None!
823            return # error during image upload. Ignore other values
[6196]824        if self.dataNotComplete():
825            self.flash(self.dataNotComplete())
[5941]826            return
[7252]827        self.applyData(self.context, **data)
[8286]828        state = IWorkflowState(self.context).getState()
[6322]829        # This shouldn't happen, but the application officer
830        # might have forgotten to lock the form after changing the state
[8286]831        if state != PAID:
[7714]832            self.flash(_('This form cannot be submitted. Wrong state!'))
[6303]833            return
834        IWorkflowInfo(self.context).fireTransition('submit')
[8194]835        self.context.application_date = datetime.utcnow()
[5941]836        self.context.locked = True
[7714]837        self.flash(_('Form has been submitted.'))
[6196]838        self.redirect(self.url(self.context))
[5273]839        return
[5941]840
[7063]841class PassportImage(grok.View):
842    """Renders the passport image for applicants.
843    """
844    grok.name('passport.jpg')
845    grok.context(IApplicant)
[7113]846    grok.require('waeup.viewApplication')
[7063]847
848    def render(self):
849        # A filename chooser turns a context into a filename suitable
850        # for file storage.
851        image = getUtility(IExtFileStore).getFileByContext(self.context)
852        self.response.setHeader(
853            'Content-Type', 'image/jpeg')
854        if image is None:
855            # show placeholder image
[7089]856            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]857        return image
[7363]858
[7819]859class ApplicantRegistrationPage(KofaAddFormPage):
[7363]860    """Captcha'd registration page for applicants.
861    """
862    grok.context(IApplicantsContainer)
863    grok.name('register')
[7373]864    grok.require('waeup.Anonymous')
[7363]865    grok.template('applicantregister')
866
[7368]867    @property
[8033]868    def form_fields(self):
869        form_fields = None
[8128]870        if self.context.mode == 'update':
871            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
872                'firstname','reg_number','email')
873        else: #if self.context.mode == 'create':
[8033]874            form_fields = grok.AutoFields(IApplicantEdit).select(
875                'firstname', 'middlename', 'lastname', 'email', 'phone')
876        return form_fields
877
878    @property
[7368]879    def label(self):
[8078]880        return _('Apply for ${a}',
[7714]881            mapping = {'a':self.context.title})
[7368]882
[7363]883    def update(self):
[7368]884        # Check if application has started ...
[8200]885        if not self.context.startdate or (
886            self.context.startdate > datetime.now(pytz.utc)):
[7714]887            self.flash(_('Application has not yet started.'))
[7368]888            self.redirect(self.url(self.context))
889            return
890        # ... or ended
[8200]891        if not self.context.enddate or (
892            self.context.enddate < datetime.now(pytz.utc)):
[7714]893            self.flash(_('Application has ended.'))
[7368]894            self.redirect(self.url(self.context))
895            return
896        # Handle captcha
[7363]897        self.captcha = getUtility(ICaptchaManager).getCaptcha()
898        self.captcha_result = self.captcha.verify(self.request)
899        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
900        return
901
[7714]902    @action(_('Get login credentials'), style='primary')
[7363]903    def register(self, **data):
904        if not self.captcha_result.is_valid:
[8037]905            # Captcha will display error messages automatically.
[7363]906            # No need to flash something.
907            return
[8033]908        if self.context.mode == 'create':
909            # Add applicant
910            applicant = createObject(u'waeup.Applicant')
911            self.applyData(applicant, **data)
912            self.context.addApplicant(applicant)
[8042]913            applicant.reg_number = applicant.applicant_id
914            notify(grok.ObjectModifiedEvent(applicant))
[8033]915        elif self.context.mode == 'update':
916            # Update applicant
[8037]917            reg_number = data.get('reg_number','')
918            firstname = data.get('firstname','')
[8033]919            cat = getUtility(ICatalog, name='applicants_catalog')
920            results = list(
921                cat.searchResults(reg_number=(reg_number, reg_number)))
922            if results:
923                applicant = results[0]
[8042]924                if getattr(applicant,'firstname',None) is None:
[8037]925                    self.flash(_('An error occurred.'))
926                    return
927                elif applicant.firstname.lower() != firstname.lower():
[8042]928                    # Don't tell the truth here. Anonymous must not
929                    # know that a record was found and only the firstname
930                    # verification failed.
[8037]931                    self.flash(_('No application record found.'))
932                    return
[8042]933                elif applicant.password is not None:
934                    self.flash(_('Your password has already been set. '
935                                 'Please proceed to the login page.'))
936                    return
937                # Store email address but nothing else.
[8033]938                applicant.email = data['email']
[8042]939                notify(grok.ObjectModifiedEvent(applicant))
[8033]940            else:
[8042]941                # No record found, this is the truth.
[8033]942                self.flash(_('No application record found.'))
943                return
944        else:
[8042]945            # Does not happen but anyway ...
[8033]946            return
[7819]947        kofa_utils = getUtility(IKofaUtils)
[7811]948        password = kofa_utils.genPassword()
[7380]949        IUserAccount(applicant).setPassword(password)
[7365]950        # Send email with credentials
[7399]951        login_url = self.url(grok.getSite(), 'login')
[7714]952        msg = _('You have successfully been registered for the')
[7811]953        if kofa_utils.sendCredentials(IUserAccount(applicant),
[7407]954            password, login_url, msg):
[7380]955            self.redirect(self.url(self.context, 'registration_complete',
956                                   data = dict(email=applicant.email)))
957            return
958        else:
[7714]959            self.flash(_('Email could not been sent. Please retry later.'))
[7380]960        return
961
[7819]962class ApplicantRegistrationEmailSent(KofaPage):
[7380]963    """Landing page after successful registration.
964    """
965    grok.name('registration_complete')
966    grok.require('waeup.Public')
967    grok.template('applicantregemailsent')
[7714]968    label = _('Your registration was successful.')
[7380]969
970    def update(self, email=None):
971        self.email = email
972        return
Note: See TracBrowser for help on using the repository browser.