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

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

Store utc without tzinfo in persistent datetime objects. Localisation will be done in views only.

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