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

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

Align passport image with data table.

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