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

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

Managers do not 'pay' fees for applicants and students, they approve payments made.

Add respective transitions.

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