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

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

The StudentApplication? class is deprecated. We want to store the application_slip pdf file file instead.

Prepare everything in the students package for downloading such a pdf file.

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