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

Last change on this file since 7290 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
Line 
1## $Id: browser.py 7276 2011-12-06 07:34:26Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
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.
8##
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.
13##
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##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from time import time
24from datetime import datetime
25from zope.component import getUtility, createObject
26from zope.formlib.form import setUpEditWidgets
27from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
28from reportlab.pdfgen import canvas
29from reportlab.lib.units import cm
30from reportlab.lib.pagesizes import A4
31from reportlab.lib.styles import getSampleStyleSheet
32from reportlab.platypus import (Frame, Paragraph, Image,
33    Table, Spacer)
34from reportlab.platypus.tables import TableStyle
35from waeup.sirp.browser import (
36    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
37from waeup.sirp.browser.breadcrumbs import Breadcrumb
38from waeup.sirp.browser.layout import NullValidator
39from waeup.sirp.browser.pages import add_local_role, del_local_roles
40from waeup.sirp.browser.resources import datepicker, tabs, datatable
41from waeup.sirp.browser.viewlets import ManageActionButton, PrimaryNavTab
42from waeup.sirp.interfaces import (
43    IWAeUPObject, ILocalRolesAssignable, IExtFileStore,
44    IFileStoreNameChooser, IPasswordValidator, IUserAccount)
45from waeup.sirp.permissions import get_users_with_local_roles
46from waeup.sirp.browser import DEFAULT_PASSPORT_IMAGE_PATH
47from waeup.sirp.university.interfaces import ICertificate
48from waeup.sirp.utils.helpers import string_from_bytes, file_size
49from waeup.sirp.widgets.datewidget import (
50    FriendlyDateWidget, FriendlyDateDisplayWidget,
51    FriendlyDatetimeDisplayWidget)
52from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
53from waeup.sirp.applicants.interfaces import (
54    IApplicant, IApplicantEdit, IApplicantsRoot,
55    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
56    MAX_UPLOAD_SIZE, IApplicantOnlinePayment,
57    )
58from waeup.sirp.applicants.workflow import INITIALIZED, STARTED, PAID
59from waeup.sirp.students.viewlets import PrimaryStudentNavTab
60from waeup.sirp.students.interfaces import IStudentsUtils
61
62grok.context(IWAeUPObject) # Make IWAeUPObject the default context
63
64class ApplicantsRootPage(WAeUPPage):
65    grok.context(IApplicantsRoot)
66    grok.name('index')
67    grok.require('waeup.Public')
68    title = 'Applicants'
69    label = 'Application Section'
70    pnav = 3
71
72    def update(self):
73        super(ApplicantsRootPage, self).update()
74        datatable.need()
75        return
76
77class ManageApplicantsRootActionButton(ManageActionButton):
78    grok.context(IApplicantsRoot)
79    grok.view(ApplicantsRootPage)
80    grok.require('waeup.manageApplication')
81    text = 'Manage application section'
82
83class ApplicantsRootManageFormPage(WAeUPEditFormPage):
84    grok.context(IApplicantsRoot)
85    grok.name('manage')
86    grok.template('applicantsrootmanagepage')
87    title = 'Applicants'
88    label = 'Manage application section'
89    pnav = 3
90    grok.require('waeup.manageApplication')
91    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
92    tabtwoactions1 = ['Remove selected local roles']
93    tabtwoactions2 = ['Add local role']
94    subunits = 'Applicants Containers'
95
96    def update(self):
97        tabs.need()
98        datatable.need()
99        return super(ApplicantsRootManageFormPage, self).update()
100
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
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))
132        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
133        return
134
135    @grok.action('Add applicants container', validator=NullValidator)
136    def addApplicantsContainer(self, **data):
137        self.redirect(self.url(self.context, '@@add'))
138        return
139
140    @grok.action('Cancel', validator=NullValidator)
141    def cancel(self, **data):
142        self.redirect(self.url(self.context))
143        return
144
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
153class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
154    grok.context(IApplicantsRoot)
155    grok.require('waeup.manageApplication')
156    grok.name('add')
157    grok.template('applicantscontaineraddpage')
158    title = 'Applicants'
159    label = 'Add applicants container'
160    pnav = 3
161
162    form_fields = grok.AutoFields(
163        IApplicantsContainerAdd).omit('code').omit('title')
164    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
165    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
166
167    def update(self):
168        datepicker.need() # Enable jQuery datepicker in date fields.
169        return super(ApplicantsContainerAddFormPage, self).update()
170
171    @grok.action('Add applicants container')
172    def addApplicantsContainer(self, **data):
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)
177        if code in self.context.keys():
178            self.flash(
179                'An applicants container for the same application '
180                'type and entrance year exists already in the database.')
181            return
182        # Add new applicants container...
183        provider = data['provider'][1]
184        container = provider.factory()
185        self.applyData(container, **data)
186        container.code = code
187        container.title = title
188        self.context[code] = container
189        self.flash('Added: "%s".' % code)
190        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
191        return
192
193    @grok.action('Cancel', validator=NullValidator)
194    def cancel(self, **data):
195        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
196
197class ApplicantsRootBreadcrumb(Breadcrumb):
198    """A breadcrumb for applicantsroot.
199    """
200    grok.context(IApplicantsRoot)
201    title = u'Applicants'
202
203class ApplicantsContainerBreadcrumb(Breadcrumb):
204    """A breadcrumb for applicantscontainers.
205    """
206    grok.context(IApplicantsContainer)
207
208class ApplicantBreadcrumb(Breadcrumb):
209    """A breadcrumb for applicants.
210    """
211    grok.context(IApplicant)
212
213    @property
214    def title(self):
215        """Get a title for a context.
216        """
217        return self.context.application_number
218
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
228class ApplicantsAuthTab(PrimaryNavTab):
229    """Applicants tab in primary navigation.
230    """
231    grok.context(IWAeUPObject)
232    grok.order(3)
233    grok.require('waeup.viewApplicantsTab')
234    pnav = 3
235    tab_title = u'Applicants'
236
237    @property
238    def link_target(self):
239        return self.view.application_url('applicants')
240
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.
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
257
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
275class ApplicantsContainerPage(WAeUPDisplayFormPage):
276    """The standard view for regular applicant containers.
277    """
278    grok.context(IApplicantsContainer)
279    grok.name('index')
280    grok.require('waeup.Public')
281    grok.template('applicantscontainerpage')
282    pnav = 3
283
284    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
285    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
286    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
287    form_fields['description'].custom_widget = ReSTDisplayWidget
288
289    @property
290    def title(self):
291        return "Applicants Container: %s" % self.context.title
292
293    @property
294    def label(self):
295        return self.context.title
296
297class ApplicantsContainerManageActionButton(ManageActionButton):
298    grok.order(1)
299    grok.context(IApplicantsContainer)
300    grok.view(ApplicantsContainerPage)
301    grok.require('waeup.manageApplication')
302    text = 'Manage applicants container'
303
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'
312
313class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
314    grok.context(IApplicantsContainer)
315    grok.name('manage')
316    grok.template('applicantscontainermanagepage')
317    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
318    taboneactions = ['Save','Cancel']
319    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
320    tabthreeactions1 = ['Remove selected local roles']
321    tabthreeactions2 = ['Add local role']
322    # Use friendlier date widget...
323    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
324    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
325    grok.require('waeup.manageApplication')
326
327    @property
328    def title(self):
329        return "Applicants Container: %s" % self.context.title
330
331    @property
332    def label(self):
333        return 'Manage applicants container'
334
335    pnav = 3
336
337    def update(self):
338        datepicker.need() # Enable jQuery datepicker in date fields.
339        tabs.need()
340        datatable.need()  # Enable jQurey datatables for contents listing
341        return super(ApplicantsContainerManageFormPage, self).update()
342
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
357    @grok.action('Save')
358    def apply(self, **data):
359        self.applyData(self.context, **data)
360        self.flash('Data saved.')
361        return
362
363    # ToDo: Show warning message before deletion
364    @grok.action('Remove selected')
365    def delApplicant(self, **data):
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
387
388    @grok.action('Add applicant', validator=NullValidator)
389    def addApplicant(self, **data):
390        self.redirect(self.url(self.context, 'addapplicant'))
391        return
392
393    @grok.action('Cancel', validator=NullValidator)
394    def cancel(self, **data):
395        self.redirect(self.url(self.context))
396        return
397
398    @grok.action('Add local role', validator=NullValidator)
399    def addLocalRole(self, **data):
400        return add_local_role(self,3, **data)
401
402    @grok.action('Remove selected local roles')
403    def delLocalRoles(self, **data):
404        return del_local_roles(self,3,**data)
405
406class ApplicantAddFormPage(WAeUPAddFormPage):
407    """Add-form to add an applicant.
408    """
409    grok.context(IApplicantsContainer)
410    grok.require('waeup.manageApplication')
411    grok.name('addapplicant')
412    #grok.template('applicantaddpage')
413    form_fields = grok.AutoFields(IApplicant).select(
414        'firstname', 'middlenames', 'lastname',
415        'email', 'phone')
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):
426        applicant = createObject(u'waeup.Applicant')
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'))
431        return
432
433class ApplicantDisplayFormPage(WAeUPDisplayFormPage):
434    grok.context(IApplicant)
435    grok.name('index')
436    grok.require('waeup.viewApplication')
437    grok.template('applicantdisplaypage')
438    form_fields = grok.AutoFields(IApplicant).omit(
439        'locked', 'course_admitted', 'password')
440    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
441    label = 'Applicant'
442    pnav = 3
443
444    def update(self):
445        self.passport_url = self.url(self.context, 'passport.jpg')
446        # Mark application as started if applicant logs in for the first time
447        usertype = getattr(self.request.principal, 'user_type', None)
448        if usertype == 'applicant' and \
449            IWorkflowState(self.context).getState() == INITIALIZED:
450            IWorkflowInfo(self.context).fireTransition('start')
451        return
452
453    @property
454    def hasPassword(self):
455        if self.context.password:
456            return 'set'
457        return 'unset'
458
459    @property
460    def title(self):
461        return 'Application Record %s' % self.context.application_number
462
463    @property
464    def label(self):
465        container_title = self.context.__parent__.title
466        return '%s Application Record %s' % (
467            container_title, self.context.application_number)
468
469    def getCourseAdmitted(self):
470        """Return link, title and code in html format to the certificate
471           admitted.
472        """
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
478            return '<a href="%s">%s - %s</a>' %(url,code,title)
479        return ''
480
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
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
635        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
636            self.request)
637        students_utils = getUtility(IStudentsUtils)
638        return students_utils.renderPDF(self,'Payment', 'payment_receipt.pdf',
639            self.context.__parent__, applicantview)
640
641class PDFActionButton(ManageActionButton):
642    grok.context(IApplicant)
643    grok.require('waeup.viewApplication')
644    icon = 'actionicon_pdf.png'
645    text = 'Download application slip'
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')
653    grok.require('waeup.viewApplication')
654    form_fields = grok.AutoFields(IApplicant).omit(
655        'locked', 'course_admitted')
656    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
657    prefix = 'form'
658
659    @property
660    def label(self):
661        container_title = self.context.__parent__.title
662        return '%s Application Record %s' % (
663            container_title, self.context.application_number)
664
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
673            return '%s - %s' %(code,title)
674        return ''
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):
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
692        SLIP_STYLE = TableStyle(
693            [('VALIGN',(0,0),(-1,-1),'TOP')]
694            )
695
696        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
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')
702        width, height = A4
703        style = getSampleStyleSheet()
704        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
705
706        story = []
707        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
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 = []
713        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
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"]))
719        story.append(Spacer(1, 24))
720        # Setup table data
721        data = []
722        # Insert passport photograph
723        img = getUtility(IExtFileStore).getFileByContext(self.context)
724        if img is None:
725            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
726        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
727        data.append([doc_img])
728        data.append([Spacer(1, 18)])
729        # Render widget fields
730        self.setUpWidgets()
731        for widget in self.widgets:
732            f_label = '<font size=12>%s</font>:' % widget.label.strip()
733            f_label = Paragraph(f_label, style["Normal"])
734            f_text = '<font size=12>%s</font>' % widget()
735            f_text = Paragraph(f_text, style["Normal"])
736            data.append([f_label,f_text])
737        f_label = '<font size=12>Admitted Course of Study:</font>'
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])
742        # Create table
743        table = Table(data,style=SLIP_STYLE)
744        story.append(table)
745        frame_body.addFromList(story,pdf)
746
747        story = []
748        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
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
754        self.response.setHeader(
755            'Content-Type', 'application/pdf')
756        return pdf.getpdfdata()
757
758class ApplicantManageActionButton(ManageActionButton):
759    grok.context(IApplicant)
760    grok.view(ApplicantDisplayFormPage)
761    grok.require('waeup.manageApplication')
762    text = 'Manage application record'
763    target = 'manage'
764
765class ApplicantEditActionButton(ManageActionButton):
766    grok.context(IApplicant)
767    grok.view(ApplicantDisplayFormPage)
768    grok.require('waeup.handleApplication')
769    text = 'Edit application record'
770    target ='edit'
771
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
780def handle_img_upload(upload, context, view):
781    """Handle upload of applicant image.
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.
787    """
788    size = file_size(upload)
789    if size > MAX_UPLOAD_SIZE:
790        view.flash('Uploaded image is too big!')
791        return False
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
797    upload.seek(0) # file pointer moved when determining size
798    store = getUtility(IExtFileStore)
799    file_id = IFileStoreNameChooser(context).chooseName()
800    store.createFile(file_id, upload)
801    return True
802
803class ApplicantManageFormPage(WAeUPEditFormPage):
804    """A full edit view for applicant data.
805    """
806    grok.context(IApplicant)
807    grok.name('manage')
808    grok.require('waeup.manageApplication')
809    form_fields = grok.AutoFields(IApplicant)
810    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
811    grok.template('applicanteditpage')
812    manage_applications = True
813    pnav = 3
814    display_actions = [['Save', 'Final Submit'],
815                       ['Add online payment ticket','Remove selected tickets']]
816
817    def update(self):
818        datepicker.need() # Enable jQuery datepicker in date fields.
819        super(ApplicantManageFormPage, self).update()
820        self.wf_info = IWorkflowInfo(self.context)
821        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
822        self.passport_changed = None
823        upload = self.request.form.get('form.passport', None)
824        if upload:
825            # We got a fresh upload
826            self.passport_changed = handle_img_upload(
827                upload, self.context, self)
828        return
829
830    @property
831    def title(self):
832        return 'Application Record %s' % self.context.application_number
833
834    @property
835    def label(self):
836        container_title = self.context.__parent__.title
837        return '%s Application Form %s' % (
838            container_title, self.context.application_number)
839
840    def getTransitions(self):
841        """Return a list of dicts of allowed transition ids and titles.
842
843        Each list entry provides keys ``name`` and ``title`` for
844        internal name and (human readable) title of a single
845        transition.
846        """
847        allowed_transitions = self.wf_info.getManualTransitions()
848        return [dict(name='', title='No transition')] +[
849            dict(name=x, title=y) for x, y in allowed_transitions]
850
851    @grok.action('Save')
852    def save(self, **data):
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
862        if self.passport_changed is False:  # False is not None!
863            return # error during image upload. Ignore other values
864        changed_fields = self.applyData(self.context, **data)
865        # Turn list of lists into single list
866        if changed_fields:
867            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
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')
876        fields_string = ' + '.join(changed_fields)
877        trans_id = form.get('transition', None)
878        if trans_id:
879            self.wf_info.fireTransition(trans_id)
880        self.flash('Form has been saved.')
881        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
882        if fields_string:
883            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
884        return
885
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
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
922    @grok.action('Add online payment ticket')
923    def addPaymentTicket(self, **data):
924        self.redirect(self.url(self.context, '@@addafp'))
925        return
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
933class ApplicantEditFormPage(ApplicantManageFormPage):
934    """An applicant-centered edit view for applicant data.
935    """
936    grok.context(IApplicantEdit)
937    grok.name('edit')
938    grok.require('waeup.handleApplication')
939    form_fields = grok.AutoFields(IApplicantEdit).omit(
940        'locked', 'course_admitted', 'student_id',
941        'screening_score', 'applicant_id', 'reg_number'
942        )
943    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
944    grok.template('applicanteditpage')
945    manage_applications = False
946    title = u'Your Application Form'
947
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
963    def emit_lock_message(self):
964        self.flash('The requested form is locked (read-only).')
965        self.redirect(self.url(self.context))
966        return
967
968    def update(self):
969        if self.context.locked:
970            self.emit_lock_message()
971            return
972        datepicker.need() # Enable jQuery datepicker in date fields.
973        super(ApplicantEditFormPage, self).update()
974        return
975
976    def dataNotComplete(self):
977        store = getUtility(IExtFileStore)
978        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
979            return 'No passport picture uploaded.'
980        if not self.request.form.get('confirm_passport', False):
981            return 'Passport picture confirmation box not ticked.'
982        return False
983
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
987    @grok.action('Add online payment ticket')
988    def addPaymentTicket(self, **data):
989        self.redirect(self.url(self.context, '@@addafp'))
990        return
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
998    @grok.action('Save')
999    def save(self, **data):
1000        #import pdb; pdb.set_trace()
1001        if self.passport_changed is False:  # False is not None!
1002            return # error during image upload. Ignore other values
1003        self.applyData(self.context, **data)
1004        self.flash('Form has been saved.')
1005        return
1006
1007    @grok.action('Final Submit')
1008    def finalsubmit(self, **data):
1009        if self.passport_changed is False:  # False is not None!
1010            return # error during image upload. Ignore other values
1011        if self.dataNotComplete():
1012            self.flash(self.dataNotComplete())
1013            return
1014        self.applyData(self.context, **data)
1015        state = IWorkflowState(self.context).getState()
1016        # This shouldn't happen, but the application officer
1017        # might have forgotten to lock the form after changing the state
1018        if state != PAID:
1019            self.flash('This form cannot be submitted. Wrong state!')
1020            return
1021        IWorkflowInfo(self.context).fireTransition('submit')
1022        self.context.application_date = datetime.now()
1023        self.context.locked = True
1024        self.flash('Form has been submitted.')
1025        self.redirect(self.url(self.context))
1026        return
1027
1028class ApplicantViewActionButton(ManageActionButton):
1029    grok.context(IApplicant)
1030    grok.view(ApplicantManageFormPage)
1031    grok.require('waeup.viewApplication')
1032    icon = 'actionicon_view.png'
1033    text = 'View application record'
1034    target = 'index'
1035
1036class PassportImage(grok.View):
1037    """Renders the passport image for applicants.
1038    """
1039    grok.name('passport.jpg')
1040    grok.context(IApplicant)
1041    grok.require('waeup.viewApplication')
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
1051            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1052        return image
Note: See TracBrowser for help on using the repository browser.