source: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py @ 7253

Last change on this file since 7253 was 7252, checked in by Henrik Bettermann, 14 years ago

Add tests for acceptance fee payment.

  • Property svn:keywords set to Id
File size: 37.8 KB
RevLine 
[5273]1## $Id: browser.py 7252 2011-12-02 21:11:44Z 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
[6816]24from datetime import datetime, date
25from zope.authentication.interfaces import ILogout, IAuthentication
[7240]26from zope.component import getUtility, createObject
[6081]27from zope.formlib.widget import CustomWidgetFactory
[6358]28from zope.formlib.form import setUpEditWidgets
[5937]29from zope.securitypolicy.interfaces import IPrincipalRoleManager
[6153]30from zope.traversing.browser import absoluteURL
[6081]31
[6303]32from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[6357]33from reportlab.pdfgen import canvas
[6364]34from reportlab.lib.units import cm
[6390]35from reportlab.lib.pagesizes import A4
[6364]36from reportlab.lib.styles import getSampleStyleSheet
37from reportlab.platypus import (Frame, Paragraph, Image,
38    Table, Spacer)
39from reportlab.platypus.tables import TableStyle
[6357]40
[6469]41from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
42from waeup.sirp.accesscodes.workflow import USED
[5273]43from waeup.sirp.browser import (
[6321]44    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
[6081]45from waeup.sirp.browser.breadcrumbs import Breadcrumb
[6103]46from waeup.sirp.browser.layout import NullValidator
[6321]47from waeup.sirp.browser.pages import add_local_role, del_local_roles
[6013]48from waeup.sirp.browser.resources import datepicker, tabs, datatable
[6153]49from waeup.sirp.browser.viewlets import (
50    ManageActionButton, PrimaryNavTab, LeftSidebarLink
51    )
[7063]52from waeup.sirp.interfaces import (
[7240]53    IWAeUPObject, ILocalRolesAssignable, IExtFileStore,
54    IFileStoreNameChooser, IPasswordValidator, IUserAccount)
[6321]55from waeup.sirp.permissions import get_users_with_local_roles
[7094]56from waeup.sirp.browser import DEFAULT_PASSPORT_IMAGE_PATH
[6254]57from waeup.sirp.university.interfaces import ICertificate
[7081]58from waeup.sirp.utils.helpers import string_from_bytes, file_size
[6054]59from waeup.sirp.widgets.datewidget import (
[7250]60    FriendlyDateWidget, FriendlyDateDisplayWidget,
61    FriendlyDatetimeDisplayWidget)
[6084]62from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
[5303]63from waeup.sirp.widgets.objectwidget import (
[5301]64    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
[7240]65from waeup.sirp.applicants import Applicant, get_applicant_data
[6081]66from waeup.sirp.applicants.interfaces import (
[7240]67    IApplicant, IApplicantEdit, IApplicantsRoot,
[7063]68    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
[7250]69    MAX_UPLOAD_SIZE, IApplicantOnlinePayment,
[5686]70    )
[7250]71from waeup.sirp.applicants.workflow import INITIALIZED, STARTED, PAID
[7240]72from waeup.sirp.students.viewlets import PrimaryStudentNavTab
[7250]73from waeup.sirp.students.interfaces import IStudentsUtils
[5320]74
[7240]75grok.context(IWAeUPObject) # Make IWAeUPObject the default context
[5273]76
[6067]77class ApplicantsRootPage(WAeUPPage):
[5822]78    grok.context(IApplicantsRoot)
79    grok.name('index')
[6153]80    grok.require('waeup.Public')
[5822]81    title = 'Applicants'
[6069]82    label = 'Application Section'
[5843]83    pnav = 3
[6012]84
85    def update(self):
[6067]86        super(ApplicantsRootPage, self).update()
[6012]87        datatable.need()
88        return
89
[5828]90class ManageApplicantsRootActionButton(ManageActionButton):
91    grok.context(IApplicantsRoot)
[6067]92    grok.view(ApplicantsRootPage)
[7136]93    grok.require('waeup.manageApplication')
[6069]94    text = 'Manage application section'
[5828]95
[6069]96class ApplicantsRootManageFormPage(WAeUPEditFormPage):
[5828]97    grok.context(IApplicantsRoot)
98    grok.name('manage')
[6107]99    grok.template('applicantsrootmanagepage')
[6069]100    title = 'Applicants'
101    label = 'Manage application section'
[5843]102    pnav = 3
[7136]103    grok.require('waeup.manageApplication')
[6069]104    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
[6184]105    tabtwoactions1 = ['Remove selected local roles']
106    tabtwoactions2 = ['Add local role']
[6069]107    subunits = 'Applicants Containers'
[6078]108
[6069]109    def update(self):
110        tabs.need()
[6108]111        datatable.need()
[6069]112        return super(ApplicantsRootManageFormPage, self).update()
[5828]113
[6184]114    def getLocalRoles(self):
115        roles = ILocalRolesAssignable(self.context)
116        return roles()
117
118    def getUsers(self):
119        """Get a list of all users.
120        """
121        for key, val in grok.getSite()['users'].items():
122            url = self.url(val)
123            yield(dict(url=url, name=key, val=val))
124
125    def getUsersWithLocalRoles(self):
126        return get_users_with_local_roles(self.context)
127
[6069]128    # ToDo: Show warning message before deletion
129    @grok.action('Remove selected')
130    def delApplicantsContainers(self, **data):
131        form = self.request.form
132        child_id = form['val_id']
133        if not isinstance(child_id, list):
134            child_id = [child_id]
135        deleted = []
136        for id in child_id:
137            try:
138                del self.context[id]
139                deleted.append(id)
140            except:
141                self.flash('Could not delete %s: %s: %s' % (
142                        id, sys.exc_info()[0], sys.exc_info()[1]))
143        if len(deleted):
144            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6078]145        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
146        return
[5828]147
[6069]148    @grok.action('Add applicants container', validator=NullValidator)
149    def addApplicantsContainer(self, **data):
150        self.redirect(self.url(self.context, '@@add'))
[6078]151        return
152
[6069]153    @grok.action('Cancel', validator=NullValidator)
154    def cancel(self, **data):
155        self.redirect(self.url(self.context))
[6078]156        return
157
[6184]158    @grok.action('Add local role', validator=NullValidator)
159    def addLocalRole(self, **data):
160        return add_local_role(self,2, **data)
161
162    @grok.action('Remove selected local roles')
163    def delLocalRoles(self, **data):
164        return del_local_roles(self,2,**data)
165
[6069]166class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
[5822]167    grok.context(IApplicantsRoot)
[7136]168    grok.require('waeup.manageApplication')
[5822]169    grok.name('add')
[6107]170    grok.template('applicantscontaineraddpage')
[6069]171    title = 'Applicants'
172    label = 'Add applicants container'
[5843]173    pnav = 3
[6078]174
[6103]175    form_fields = grok.AutoFields(
176        IApplicantsContainerAdd).omit('code').omit('title')
[6083]177    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
178    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[6078]179
[6083]180    def update(self):
181        datepicker.need() # Enable jQuery datepicker in date fields.
182        return super(ApplicantsContainerAddFormPage, self).update()
183
[6069]184    @grok.action('Add applicants container')
185    def addApplicantsContainer(self, **data):
[6103]186        year = data['year']
187        code = u'%s%s' % (data['prefix'], year)
188        prefix = application_types_vocab.getTerm(data['prefix'])
189        title = u'%s %s/%s' % (prefix.title, year, year + 1)
[6087]190        if code in self.context.keys():
[6105]191            self.flash(
192                'An applicants container for the same application '
193                'type and entrance year exists already in the database.')
[5822]194            return
195        # Add new applicants container...
[6083]196        provider = data['provider'][1]
[5822]197        container = provider.factory()
[6069]198        self.applyData(container, **data)
[6087]199        container.code = code
200        container.title = title
201        self.context[code] = container
[6105]202        self.flash('Added: "%s".' % code)
[6069]203        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
[5822]204        return
[6078]205
[6103]206    @grok.action('Cancel', validator=NullValidator)
[6069]207    def cancel(self, **data):
[6103]208        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
[6078]209
[5845]210class ApplicantsRootBreadcrumb(Breadcrumb):
211    """A breadcrumb for applicantsroot.
212    """
213    grok.context(IApplicantsRoot)
[6654]214    title = u'Applicants'
[6078]215
[5845]216class ApplicantsContainerBreadcrumb(Breadcrumb):
217    """A breadcrumb for applicantscontainers.
218    """
219    grok.context(IApplicantsContainer)
[6319]220
[6153]221class ApplicantBreadcrumb(Breadcrumb):
222    """A breadcrumb for applicants.
223    """
224    grok.context(IApplicant)
[6319]225
[6153]226    @property
227    def title(self):
228        """Get a title for a context.
229        """
[7240]230        return self.context.application_number
[5828]231
[7250]232class OnlinePaymentBreadcrumb(Breadcrumb):
233    """A breadcrumb for payments.
234    """
235    grok.context(IApplicantOnlinePayment)
236
237    @property
238    def title(self):
239        return self.context.p_id
240
[7184]241class ApplicantsAuthTab(PrimaryNavTab):
[6153]242    """Applicants tab in primary navigation.
[5828]243    """
244    grok.context(IWAeUPObject)
245    grok.order(3)
[7250]246    grok.require('waeup.viewApplicantsTab')
[5843]247    pnav = 3
[5828]248    tab_title = u'Applicants'
249
250    @property
251    def link_target(self):
252        return self.view.application_url('applicants')
253
[7184]254class ApplicantsAnonTab(ApplicantsAuthTab):
255    """Applicants tab in primary navigation.
256
257    Display tab only for anonymous. Authenticated users can call the
258    form from the user navigation bar.
259    """
260    grok.require('waeup.Anonymous')
261    tab_title = u'Application'
262
263    # Also zope.manager has role Anonymous.
[7243]264    # To avoid displaying this tab, we have to check the principal id too.
265    @property
266    def link_target(self):
267        if self.request.principal.id == 'zope.anybody':
268            return self.view.application_url('applicants')
269        return
[7184]270
[7240]271class MyApplicationDataTab(PrimaryStudentNavTab):
272    """MyData-tab in primary navigation.
273    """
274    grok.order(3)
275    grok.require('waeup.viewMyApplicationDataTab')
276    pnav = 3
277    tab_title = u'My Data'
278
279    @property
280    def link_target(self):
281        try:
282            container, application_number = self.request.principal.id.split('_')
283        except ValueError:
284            return
285        rel_link = '/applicants/%s/%s' % (container, application_number)
286        return self.view.application_url() + rel_link
287
[6029]288class ApplicantsContainerPage(WAeUPDisplayFormPage):
[5830]289    """The standard view for regular applicant containers.
290    """
291    grok.context(IApplicantsContainer)
292    grok.name('index')
[6153]293    grok.require('waeup.Public')
[6029]294    grok.template('applicantscontainerpage')
[5850]295    pnav = 3
[6053]296
[6105]297    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
[6054]298    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
299    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
[6084]300    form_fields['description'].custom_widget = ReSTDisplayWidget
[6053]301
[5837]302    @property
303    def title(self):
[6087]304        return "Applicants Container: %s" % self.context.title
[5837]305
306    @property
307    def label(self):
[6087]308        return self.context.title
[5830]309
[6107]310class ApplicantsContainerManageActionButton(ManageActionButton):
[6336]311    grok.order(1)
[5832]312    grok.context(IApplicantsContainer)
313    grok.view(ApplicantsContainerPage)
[7136]314    grok.require('waeup.manageApplication')
[6070]315    text = 'Manage applicants container'
[5832]316
[7240]317#class ApplicantLoginActionButton(ManageActionButton):
318#    grok.order(2)
319#    grok.context(IApplicantsContainer)
320#    grok.view(ApplicantsContainerPage)
321#    grok.require('waeup.Anonymous')
322#    icon = 'login.png'
323#    text = 'Login for applicants'
324#    target = 'login'
[5832]325
[6107]326class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
[5837]327    grok.context(IApplicantsContainer)
[5850]328    grok.name('manage')
[6107]329    grok.template('applicantscontainermanagepage')
[6105]330    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
331    taboneactions = ['Save','Cancel']
332    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
[6184]333    tabthreeactions1 = ['Remove selected local roles']
334    tabthreeactions2 = ['Add local role']
[5844]335    # Use friendlier date widget...
[6054]336    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
337    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[7136]338    grok.require('waeup.manageApplication')
[5850]339
340    @property
341    def title(self):
[6087]342        return "Applicants Container: %s" % self.context.title
[6078]343
[5850]344    @property
345    def label(self):
[6087]346        return 'Manage applicants container'
[5850]347
[5845]348    pnav = 3
[5837]349
350    def update(self):
[5850]351        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]352        tabs.need()
[6015]353        datatable.need()  # Enable jQurey datatables for contents listing
[6107]354        return super(ApplicantsContainerManageFormPage, self).update()
[5837]355
[6184]356    def getLocalRoles(self):
357        roles = ILocalRolesAssignable(self.context)
358        return roles()
359
360    def getUsers(self):
361        """Get a list of all users.
362        """
363        for key, val in grok.getSite()['users'].items():
364            url = self.url(val)
365            yield(dict(url=url, name=key, val=val))
366
367    def getUsersWithLocalRoles(self):
368        return get_users_with_local_roles(self.context)
369
[5850]370    @grok.action('Save')
[5837]371    def apply(self, **data):
372        self.applyData(self.context, **data)
373        self.flash('Data saved.')
374        return
[6078]375
[6105]376    # ToDo: Show warning message before deletion
377    @grok.action('Remove selected')
378    def delApplicant(self, **data):
[6189]379        form = self.request.form
380        if form.has_key('val_id'):
381            child_id = form['val_id']
382        else:
383            self.flash('No applicant selected!')
384            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
385            return
386        if not isinstance(child_id, list):
387            child_id = [child_id]
388        deleted = []
389        for id in child_id:
390            try:
391                del self.context[id]
392                deleted.append(id)
393            except:
394                self.flash('Could not delete %s: %s: %s' % (
395                        id, sys.exc_info()[0], sys.exc_info()[1]))
396        if len(deleted):
397            self.flash('Successfully removed: %s' % ', '.join(deleted))
398        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
399        return
[6105]400
401    @grok.action('Add applicant', validator=NullValidator)
402    def addApplicant(self, **data):
[6327]403        self.redirect(self.url(self.context, 'addapplicant'))
404        return
[6105]405
406    @grok.action('Cancel', validator=NullValidator)
[5837]407    def cancel(self, **data):
408        self.redirect(self.url(self.context))
409        return
[5886]410
[6184]411    @grok.action('Add local role', validator=NullValidator)
412    def addLocalRole(self, **data):
413        return add_local_role(self,3, **data)
[6105]414
[6184]415    @grok.action('Remove selected local roles')
416    def delLocalRoles(self, **data):
417        return del_local_roles(self,3,**data)
418
[6327]419class ApplicantAddFormPage(WAeUPAddFormPage):
[6622]420    """Add-form to add an applicant.
[6327]421    """
422    grok.context(IApplicantsContainer)
[7136]423    grok.require('waeup.manageApplication')
[6327]424    grok.name('addapplicant')
[7240]425    #grok.template('applicantaddpage')
426    form_fields = grok.AutoFields(IApplicant).select(
427        'firstname', 'middlenames', 'lastname',
428        'email', 'phone')
[6327]429    title = 'Applicants'
430    label = 'Add applicant'
431    pnav = 3
432
433    @property
434    def title(self):
435        return "Applicants Container: %s" % self.context.title
436
437    @grok.action('Create application record')
438    def addApplicant(self, **data):
[7240]439        applicant = createObject(u'waeup.Applicant', container = self.context)
440        self.applyData(applicant, **data)
441        self.context.addApplicant(applicant)
442        self.flash('Applicant record created.')
443        self.redirect(self.url(self.context[applicant.application_number], 'index'))
[6327]444        return
445
[7200]446class ApplicantDisplayFormPage(WAeUPDisplayFormPage):
[5273]447    grok.context(IApplicant)
448    grok.name('index')
[7113]449    grok.require('waeup.viewApplication')
[7200]450    grok.template('applicantdisplaypage')
[6320]451    form_fields = grok.AutoFields(IApplicant).omit(
[7240]452        'locked', 'course_admitted', 'password')
[6054]453    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[5273]454    label = 'Applicant'
[5843]455    pnav = 3
[5273]456
[7063]457    def update(self):
458        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]459        # Mark application as started if applicant logs in for the first time
460        if IWorkflowState(self.context).getState() == INITIALIZED:
461            IWorkflowInfo(self.context).fireTransition('start')
[7063]462        return
463
[6196]464    @property
[7240]465    def hasPassword(self):
466        if self.context.password:
467            return 'set'
468        return 'unset'
469
470    @property
[6196]471    def title(self):
[7240]472        return 'Application Record %s' % self.context.application_number
[6196]473
474    @property
475    def label(self):
476        container_title = self.context.__parent__.title
[7240]477        return '%s Application Record %s' % (
478            container_title, self.context.application_number)
[6196]479
[6254]480    def getCourseAdmitted(self):
[6355]481        """Return link, title and code in html format to the certificate
482           admitted.
[6351]483        """
[6254]484        course_admitted = self.context.course_admitted
485        if ICertificate.providedBy(course_admitted):
486            url = self.url(course_admitted)
487            title = course_admitted.title
488            code = course_admitted.code
[6366]489            return '<a href="%s">%s - %s</a>' %(url,code,title)
[6457]490        return ''
[6254]491
[7250]492    def formatDatetime(self,datetimeobj):
493        if isinstance(datetimeobj, datetime):
494            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
495        else:
496            return None
497
498class AcceptanceFeePaymentAddPage(grok.View):
499    """ Page to add an online payment ticket
500    """
501    grok.context(IApplicant)
502    grok.name('addafp')
503    grok.require('waeup.payApplicant')
504
505    def update(self):
506        p_category = 'acceptance'
507        d = {}
508        session = str(self.context.__parent__.year)
509        try:
510            academic_session = grok.getSite()['configuration'][session]
511        except KeyError:
512            self.flash('Session configuration object is not available.')
513            return
514        timestamp = "%d" % int(time()*1000)
515        #order_id = "%s%s" % (student_id[1:],timestamp)
516        for key in self.context.keys():
517            ticket = self.context[key]
518            if ticket.p_state == 'paid':
519                  self.flash(
520                      'This type of payment has already been made.')
521                  self.redirect(self.url(self.context))
522                  return
523        payment = createObject(u'waeup.ApplicantOnlinePayment')
524        payment.p_id = "p%s" % timestamp
525        payment.p_item = self.context.__parent__.title
526        payment.p_year = self.context.__parent__.year
527        payment.p_category = p_category
528        payment.amount_auth = academic_session.acceptance_fee
529        payment.surcharge_1 = academic_session.surcharge_1
530        payment.surcharge_2 = academic_session.surcharge_2
531        payment.surcharge_3 = academic_session.surcharge_3
532        self.context[payment.p_id] = payment
533        self.flash('Payment ticket created.')
534        return
535
536    def render(self):
537        usertype = getattr(self.request.principal, 'user_type', None)
538        if usertype == 'applicant':
539            self.redirect(self.url(self.context, '@@edit'))
540            return
541        self.redirect(self.url(self.context, '@@manage'))
542        return
543
544
545class OnlinePaymentDisplayFormPage(WAeUPDisplayFormPage):
546    """ Page to view an online payment ticket
547    """
548    grok.context(IApplicantOnlinePayment)
549    grok.name('index')
550    grok.require('waeup.viewApplication')
551    form_fields = grok.AutoFields(IApplicantOnlinePayment)
552    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
553    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
554    pnav = 3
555
556    @property
557    def title(self):
558        return 'Online Payment Ticket %s' % self.context.p_id
559
560    @property
561    def label(self):
562        return '%s: Online Payment Ticket %s' % (
563            self.context.__parent__.fullname,self.context.p_id)
564
565class PaymentReceiptActionButton(ManageActionButton):
566    grok.order(1)
567    grok.context(IApplicantOnlinePayment)
568    grok.view(OnlinePaymentDisplayFormPage)
569    grok.require('waeup.viewApplication')
570    icon = 'actionicon_pdf.png'
571    text = 'Download payment receipt'
572    target = 'payment_receipt.pdf'
573
574    @property
575    def target_url(self):
576        if self.context.p_state != 'paid':
577            return ''
578        return self.view.url(self.view.context, self.target)
579
580class RequestCallbackActionButton(ManageActionButton):
581    grok.order(2)
582    grok.context(IApplicantOnlinePayment)
583    grok.view(OnlinePaymentDisplayFormPage)
584    grok.require('waeup.payApplicant')
585    icon = 'actionicon_call.png'
586    text = 'Request callback'
587    target = 'callback'
588
589    @property
590    def target_url(self):
591        if self.context.p_state != 'unpaid':
592            return ''
593        return self.view.url(self.view.context, self.target)
594
595class OnlinePaymentCallbackPage(grok.View):
596    """ Callback view
597    """
598    grok.context(IApplicantOnlinePayment)
599    grok.name('callback')
600    grok.require('waeup.payApplicant')
601
602    # This update method simulates a valid callback und must be
603    # specified in the customization package. The parameters must be taken
604    # from the incoming request.
605    def update(self):
606        if self.context.p_state == 'paid':
607            self.flash('This ticket has already been paid.')
608            return
609        self.context.r_amount_approved = self.context.amount_auth
610        self.context.r_card_num = u'0000'
611        self.context.r_code = u'00'
612        self.context.p_state = 'paid'
613        self.context.payment_date = datetime.now()
614        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
615        self.context.__parent__.loggerInfo(
616            ob_class, 'valid callback: %s' % self.context.p_id)
617        self.wf_info = IWorkflowInfo(self.context.__parent__)
618        self.wf_info.fireTransition('pay')
619        self.flash('Valid callback received.')
620        return
621
622    def render(self):
623        self.redirect(self.url(self.context, '@@index'))
624        return
625
626class ExportPDFPaymentSlipPage(grok.View):
627    """Deliver a PDF slip of the context.
628    """
629    grok.context(IApplicantOnlinePayment)
630    grok.name('payment_receipt.pdf')
631    grok.require('waeup.viewApplication')
632    form_fields = grok.AutoFields(IApplicantOnlinePayment)
633    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
634    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
635    prefix = 'form'
636
637    @property
638    def label(self):
639        return 'Online Payment Receipt %s' % self.context.p_id
640
641    def render(self):
642        if self.context.p_state != 'paid':
643            self.flash('Ticket not yet paid.')
644            self.redirect(self.url(self.context))
645            return
646        applicantview = ApplicantDisplayFormPage(self.context.__parent__,
647            self.request)
648        students_utils = getUtility(IStudentsUtils)
649        return students_utils.renderPDF(self,'Payment', 'payment_receipt.pdf',
650            self.context.__parent__, applicantview)
651
[6358]652class PDFActionButton(ManageActionButton):
653    grok.context(IApplicant)
[7136]654    grok.require('waeup.viewApplication')
[6358]655    icon = 'actionicon_pdf.png'
[6367]656    text = 'Download application slip'
[6358]657    target = 'application_slip.pdf'
658
659class ExportPDFPage(grok.View):
660    """Deliver a PDF slip of the context.
661    """
662    grok.context(IApplicant)
663    grok.name('application_slip.pdf')
[7136]664    grok.require('waeup.viewApplication')
[6358]665    form_fields = grok.AutoFields(IApplicant).omit(
[7240]666        'locked', 'course_admitted')
[6358]667    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
668    prefix = 'form'
669
[6363]670    @property
671    def label(self):
672        container_title = self.context.__parent__.title
[7240]673        return '%s Application Record %s' % (
674            container_title, self.context.application_number)
[6363]675
[6358]676    def getCourseAdmitted(self):
677        """Return title and code in html format to the certificate
678           admitted.
679        """
680        course_admitted = self.context.course_admitted
681        if ICertificate.providedBy(course_admitted):
682            title = course_admitted.title
683            code = course_admitted.code
[6365]684            return '%s - %s' %(code,title)
[6462]685        return ''
[6358]686
687    def setUpWidgets(self, ignore_request=False):
688        self.adapters = {}
689        self.widgets = setUpEditWidgets(
690            self.form_fields, self.prefix, self.context, self.request,
691            adapters=self.adapters, for_display=True,
692            ignore_request=ignore_request
693            )
694
695    def render(self):
[6364]696        SLIP_STYLE = TableStyle(
697            [('VALIGN',(0,0),(-1,-1),'TOP')]
698            )
[6358]699
700        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
[6364]701        pdf.setTitle(self.label)
702        pdf.setSubject('Application')
703        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
704            self.request.principal.id))
705        pdf.setCreator('WAeUP SIRP')
[6358]706        width, height = A4
707        style = getSampleStyleSheet()
[6365]708        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
[6363]709
[6358]710        story = []
[6365]711        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
[6363]712        header_title = getattr(grok.getSite(), 'name', u'Sample University')
713        story.append(Paragraph(header_title, style["Heading1"]))
714        frame_header.addFromList(story,pdf)
715
716        story = []
[6365]717        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
[6364]718        story.append(Paragraph(self.label, style["Heading2"]))
719        story.append(Spacer(1, 18))
720        for msg in self.context.history.messages:
721            f_msg = '<font face="Courier" size=10>%s</font>' % msg
722            story.append(Paragraph(f_msg, style["Normal"]))
[6363]723        story.append(Spacer(1, 24))
[7063]724
[7068]725
[7063]726        # insert passport photograph
727        img = getUtility(IExtFileStore).getFileByContext(self.context)
728        if img is None:
[7089]729            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7063]730        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
[7068]731        story.append(doc_img)
732        story.append(Spacer(1, 18))
[7063]733
734        # render widget fields
[7068]735        data = []
[6358]736        self.setUpWidgets()
737        for widget in self.widgets:
[6462]738            f_label = '<font size=12>%s</font>:' % widget.label.strip()
[6363]739            f_label = Paragraph(f_label, style["Normal"])
[7063]740            f_text = '<font size=12>%s</font>' % widget()
741            f_text = Paragraph(f_text, style["Normal"])
742            data.append([f_label,f_text])
[6462]743        f_label = '<font size=12>Admitted Course of Study:</font>'
[6363]744        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
745        f_label = Paragraph(f_label, style["Normal"])
746        f_text = Paragraph(f_text, style["Normal"])
747        data.append([f_label,f_text])
[6364]748        table = Table(data,style=SLIP_STYLE)
[6363]749        story.append(table)
750        frame_body.addFromList(story,pdf)
751
[6364]752        story = []
[6365]753        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
[6364]754        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
755        f_text = '<font size=10>%s</font>' % timestamp
756        story.append(Paragraph(f_text, style["Normal"]))
757        frame_footer.addFromList(story,pdf)
758
[6358]759        self.response.setHeader(
760            'Content-Type', 'application/pdf')
761        return pdf.getpdfdata()
762
[6383]763class ApplicantManageActionButton(ManageActionButton):
[6198]764    grok.context(IApplicant)
[7200]765    grok.view(ApplicantDisplayFormPage)
[7136]766    grok.require('waeup.manageApplication')
[6383]767    text = 'Manage application record'
[7200]768    target = 'manage'
[6198]769
[7240]770class ApplicantEditActionButton(ManageActionButton):
771    grok.context(IApplicant)
772    grok.view(ApplicantDisplayFormPage)
773    grok.require('waeup.handleApplication')
774    text = 'Edit application record'
775    target ='edit'
[7081]776
[7240]777    @property
778    def target_url(self):
779        """Get a URL to the target...
780        """
781        if self.context.locked:
782            return
783        return self.view.url(self.view.context, self.target)
784
[7081]785def handle_img_upload(upload, context, view):
[7063]786    """Handle upload of applicant image.
[7081]787
788    Returns `True` in case of success or `False`.
789
790    Please note that file pointer passed in (`upload`) most probably
791    points to end of file when leaving this function.
[7063]792    """
[7081]793    size = file_size(upload)
794    if size > MAX_UPLOAD_SIZE:
795        view.flash('Uploaded image is too big!')
796        return False
[7247]797    dummy, ext = os.path.splitext(upload.filename)
798    ext.lower()
799    if ext != '.jpg':
800        view.flash('jpg file extension expected.')
801        return False
[7081]802    upload.seek(0) # file pointer moved when determining size
[7063]803    store = getUtility(IExtFileStore)
804    file_id = IFileStoreNameChooser(context).chooseName()
805    store.createFile(file_id, upload)
[7081]806    return True
[7063]807
[7200]808class ApplicantManageFormPage(WAeUPEditFormPage):
[6196]809    """A full edit view for applicant data.
810    """
811    grok.context(IApplicant)
[7200]812    grok.name('manage')
[7136]813    grok.require('waeup.manageApplication')
[6476]814    form_fields = grok.AutoFields(IApplicant)
[6196]815    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]816    grok.template('applicanteditpage')
[6322]817    manage_applications = True
[6196]818    pnav = 3
[7250]819    display_actions = [['Save', 'Final Submit'],
820                       ['Add online payment ticket','Remove selected tickets']]
[6196]821
822    def update(self):
823        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]824        super(ApplicantManageFormPage, self).update()
[6353]825        self.wf_info = IWorkflowInfo(self.context)
[7081]826        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]827        self.passport_changed = None
[6598]828        upload = self.request.form.get('form.passport', None)
829        if upload:
830            # We got a fresh upload
[7084]831            self.passport_changed = handle_img_upload(
832                upload, self.context, self)
[6196]833        return
834
835    @property
836    def title(self):
[7240]837        return 'Application Record %s' % self.context.application_number
[6196]838
839    @property
840    def label(self):
841        container_title = self.context.__parent__.title
[7240]842        return '%s Application Form %s' % (
843            container_title, self.context.application_number)
[6196]844
[6303]845    def getTransitions(self):
[6351]846        """Return a list of dicts of allowed transition ids and titles.
[6353]847
848        Each list entry provides keys ``name`` and ``title`` for
849        internal name and (human readable) title of a single
850        transition.
[6349]851        """
[6353]852        allowed_transitions = self.wf_info.getManualTransitions()
[6355]853        return [dict(name='', title='No transition')] +[
854            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]855
[6196]856    @grok.action('Save')
857    def save(self, **data):
[7240]858        form = self.request.form
859        password = form.get('password', None)
860        password_ctl = form.get('control_password', None)
861        if password:
862            validator = getUtility(IPasswordValidator)
863            errors = validator.validate_password(password, password_ctl)
864            if errors:
865                self.flash( ' '.join(errors))
866                return
[7084]867        if self.passport_changed is False:  # False is not None!
868            return # error during image upload. Ignore other values
[6475]869        changed_fields = self.applyData(self.context, **data)
[7199]870        # Turn list of lists into single list
871        if changed_fields:
872            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]873        else:
874            changed_fields = []
875        if self.passport_changed:
876            changed_fields.append('passport')
877        if password:
878            # Now we know that the form has no errors and can set password ...
879            IUserAccount(self.context).setPassword(password)
880            changed_fields.append('password')
[7199]881        fields_string = ' + '.join(changed_fields)
[7085]882        trans_id = form.get('transition', None)
883        if trans_id:
884            self.wf_info.fireTransition(trans_id)
[6196]885        self.flash('Form has been saved.')
[6475]886        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[6644]887        if fields_string:
888            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6196]889        return
890
[7250]891    def formatDatetime(self,datetimeobj):
892        if isinstance(datetimeobj, datetime):
893            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
894        else:
895            return None
896
897    def unremovable(self, ticket):
898        usertype = getattr(self.request.principal, 'user_type', None)
899        if not usertype:
900            return False
901        return self.request.principal.user_type == 'applicant' and ticket.r_code
902
903    # This method is also used by the ApplicantEditFormPage
904    def delPaymentTickets(self, **data):
905        form = self.request.form
906        if form.has_key('val_id'):
907            child_id = form['val_id']
908        else:
909            self.flash('No payment selected.')
910            self.redirect(self.url(self.context))
911            return
912        if not isinstance(child_id, list):
913            child_id = [child_id]
914        deleted = []
915        for id in child_id:
916            # Applicants are not allowed to remove used payment tickets
917            if not self.unremovable(self.context[id]):
918                try:
919                    del self.context[id]
920                    deleted.append(id)
921                except:
922                    self.flash('Could not delete %s: %s: %s' % (
923                            id, sys.exc_info()[0], sys.exc_info()[1]))
924        if len(deleted):
925            self.flash('Successfully removed: %s' % ', '.join(deleted))
926            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
927            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
928        return
929
[7252]930    # We explicitely want the forms to be validated before payment tickets
931    # can be created. If no validation is requested, use
932    # 'validator=NullValidator' in the grok.action directive
[7250]933    @grok.action('Add online payment ticket')
934    def addPaymentTicket(self, **data):
935        self.redirect(self.url(self.context, '@@addafp'))
[7252]936        return
[7250]937
938    @grok.action('Remove selected tickets')
939    def removePaymentTickets(self, **data):
940        self.delPaymentTickets(**data)
941        self.redirect(self.url(self.context) + '/@@manage')
942        return
943
[7200]944class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]945    """An applicant-centered edit view for applicant data.
946    """
[6196]947    grok.context(IApplicantEdit)
[5273]948    grok.name('edit')
[6198]949    grok.require('waeup.handleApplication')
[6459]950    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]951        'locked', 'course_admitted', 'student_id',
[7240]952        'screening_score', 'applicant_id'
[6459]953        )
[6054]954    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]955    grok.template('applicanteditpage')
[6322]956    manage_applications = False
[6465]957    title = u'Your Application Form'
[5484]958
[7250]959    @property
960    def display_actions(self):
961        state = IWorkflowState(self.context).getState()
962        if state == INITIALIZED:
963            actions = [[],[]]
964        elif state == STARTED:
965            actions = [['Save'],
966                       ['Add online payment ticket','Remove selected tickets']]
967        elif state == PAID:
968            actions = [['Save', 'Final Submit'],
969                       ['Remove selected tickets']]
970        elif state == SUBMITTED:
971            actions = [[],[]]
972        return actions
973
[7145]974    def emit_lock_message(self):
[6105]975        self.flash('The requested form is locked (read-only).')
[5941]976        self.redirect(self.url(self.context))
977        return
[6078]978
[5686]979    def update(self):
[5941]980        if self.context.locked:
[7145]981            self.emit_lock_message()
[5941]982            return
[6040]983        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]984        super(ApplicantEditFormPage, self).update()
[5686]985        return
[5952]986
[6196]987    def dataNotComplete(self):
[7252]988        store = getUtility(IExtFileStore)
989        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
990            return 'No passport picture uploaded.'
[6322]991        if not self.request.form.get('confirm_passport', False):
[7252]992            return 'Passport picture confirmation box not ticked.'
[6196]993        return False
[5952]994
[7252]995    # We explicitely want the forms to be validated before payment tickets
996    # can be created. If no validation is requested, use
997    # 'validator=NullValidator' in the grok.action directive
[7250]998    @grok.action('Add online payment ticket')
999    def addPaymentTicket(self, **data):
1000        self.redirect(self.url(self.context, '@@addafp'))
[7252]1001        return
[7250]1002
1003    @grok.action('Remove selected tickets')
1004    def removePaymentTickets(self, **data):
1005        self.delPaymentTickets(**data)
1006        self.redirect(self.url(self.context) + '/@@edit')
1007        return
1008
[5273]1009    @grok.action('Save')
1010    def save(self, **data):
[7252]1011        #import pdb; pdb.set_trace()
[7084]1012        if self.passport_changed is False:  # False is not None!
1013            return # error during image upload. Ignore other values
[5273]1014        self.applyData(self.context, **data)
[6196]1015        self.flash('Form has been saved.')
[5273]1016        return
1017
[5484]1018    @grok.action('Final Submit')
1019    def finalsubmit(self, **data):
[7084]1020        if self.passport_changed is False:  # False is not None!
1021            return # error during image upload. Ignore other values
[6196]1022        if self.dataNotComplete():
1023            self.flash(self.dataNotComplete())
[5941]1024            return
[7252]1025        self.applyData(self.context, **data)
[6303]1026        state = IWorkflowState(self.context).getState()
[6322]1027        # This shouldn't happen, but the application officer
1028        # might have forgotten to lock the form after changing the state
[7250]1029        if state != PAID:
[6322]1030            self.flash('This form cannot be submitted. Wrong state!')
[6303]1031            return
1032        IWorkflowInfo(self.context).fireTransition('submit')
[6476]1033        self.context.application_date = datetime.now()
[5941]1034        self.context.locked = True
[6196]1035        self.flash('Form has been submitted.')
1036        self.redirect(self.url(self.context))
[5273]1037        return
[5941]1038
[6367]1039class ApplicantViewActionButton(ManageActionButton):
1040    grok.context(IApplicant)
[7200]1041    grok.view(ApplicantManageFormPage)
[7240]1042    grok.require('waeup.viewApplication')
[6383]1043    icon = 'actionicon_view.png'
[6367]1044    text = 'View application record'
[6598]1045    target = 'index'
[7063]1046
1047class PassportImage(grok.View):
1048    """Renders the passport image for applicants.
1049    """
1050    grok.name('passport.jpg')
1051    grok.context(IApplicant)
[7113]1052    grok.require('waeup.viewApplication')
[7063]1053
1054    def render(self):
1055        # A filename chooser turns a context into a filename suitable
1056        # for file storage.
1057        image = getUtility(IExtFileStore).getFileByContext(self.context)
1058        self.response.setHeader(
1059            'Content-Type', 'image/jpeg')
1060        if image is None:
1061            # show placeholder image
[7089]1062            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1063        return image
Note: See TracBrowser for help on using the repository browser.