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

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

Catch InvalidTransitionError? and flash error message.

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