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

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

See last revisions. Do the same for hostels.

  • Property svn:keywords set to Id
File size: 37.4 KB
Line 
1## $Id: browser.py 7332 2011-12-11 14:12:12Z 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, jsaction, JSAction
40from waeup.sirp.browser.pages import add_local_role, del_local_roles
41from waeup.sirp.browser.resources import datepicker, tabs, datatable, warning
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, SUBMITTED
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        warning.need()
101        return super(ApplicantsRootManageFormPage, self).update()
102
103    def getLocalRoles(self):
104        roles = ILocalRolesAssignable(self.context)
105        return roles()
106
107    def getUsers(self):
108        """Get a list of all users.
109        """
110        for key, val in grok.getSite()['users'].items():
111            url = self.url(val)
112            yield(dict(url=url, name=key, val=val))
113
114    def getUsersWithLocalRoles(self):
115        return get_users_with_local_roles(self.context)
116
117    @jsaction('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        warning.need()
342        datatable.need()  # Enable jQurey datatables for contents listing
343        return super(ApplicantsContainerManageFormPage, self).update()
344
345    def getLocalRoles(self):
346        roles = ILocalRolesAssignable(self.context)
347        return roles()
348
349    def getUsers(self):
350        """Get a list of all users.
351        """
352        for key, val in grok.getSite()['users'].items():
353            url = self.url(val)
354            yield(dict(url=url, name=key, val=val))
355
356    def getUsersWithLocalRoles(self):
357        return get_users_with_local_roles(self.context)
358
359    @grok.action('Save')
360    def apply(self, **data):
361        self.applyData(self.context, **data)
362        self.flash('Data saved.')
363        return
364
365    @jsaction('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        warning.need()
823        super(ApplicantManageFormPage, self).update()
824        self.wf_info = IWorkflowInfo(self.context)
825        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
826        self.passport_changed = None
827        upload = self.request.form.get('form.passport', None)
828        if upload:
829            # We got a fresh upload
830            self.passport_changed = handle_img_upload(
831                upload, self.context, self)
832        return
833
834    @property
835    def title(self):
836        return 'Application Record %s' % self.context.application_number
837
838    @property
839    def label(self):
840        container_title = self.context.__parent__.title
841        return '%s Application Form %s' % (
842            container_title, self.context.application_number)
843
844    def getTransitions(self):
845        """Return a list of dicts of allowed transition ids and titles.
846
847        Each list entry provides keys ``name`` and ``title`` for
848        internal name and (human readable) title of a single
849        transition.
850        """
851        allowed_transitions = self.wf_info.getManualTransitions()
852        return [dict(name='', title='No transition')] +[
853            dict(name=x, title=y) for x, y in allowed_transitions]
854
855    @grok.action('Save')
856    def save(self, **data):
857        form = self.request.form
858        password = form.get('password', None)
859        password_ctl = form.get('control_password', None)
860        if password:
861            validator = getUtility(IPasswordValidator)
862            errors = validator.validate_password(password, password_ctl)
863            if errors:
864                self.flash( ' '.join(errors))
865                return
866        if self.passport_changed is False:  # False is not None!
867            return # error during image upload. Ignore other values
868        changed_fields = self.applyData(self.context, **data)
869        # Turn list of lists into single list
870        if changed_fields:
871            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
872        else:
873            changed_fields = []
874        if self.passport_changed:
875            changed_fields.append('passport')
876        if password:
877            # Now we know that the form has no errors and can set password ...
878            IUserAccount(self.context).setPassword(password)
879            changed_fields.append('password')
880        fields_string = ' + '.join(changed_fields)
881        trans_id = form.get('transition', None)
882        if trans_id:
883            self.wf_info.fireTransition(trans_id)
884        self.flash('Form has been saved.')
885        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
886        if fields_string:
887            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
888        return
889
890    def unremovable(self, ticket):
891        return False
892
893    # This method is also used by the ApplicantEditFormPage
894    def delPaymentTickets(self, **data):
895        form = self.request.form
896        if form.has_key('val_id'):
897            child_id = form['val_id']
898        else:
899            self.flash('No payment selected.')
900            self.redirect(self.url(self.context))
901            return
902        if not isinstance(child_id, list):
903            child_id = [child_id]
904        deleted = []
905        for id in child_id:
906            # Applicants are not allowed to remove used payment tickets
907            if not self.unremovable(self.context[id]):
908                try:
909                    del self.context[id]
910                    deleted.append(id)
911                except:
912                    self.flash('Could not delete %s: %s: %s' % (
913                            id, sys.exc_info()[0], sys.exc_info()[1]))
914        if len(deleted):
915            self.flash('Successfully removed: %s' % ', '.join(deleted))
916            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
917            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
918        return
919
920    # We explicitely want the forms to be validated before payment tickets
921    # can be created. If no validation is requested, use
922    # 'validator=NullValidator' in the grok.action directive
923    @grok.action('Add online payment ticket')
924    def addPaymentTicket(self, **data):
925        self.redirect(self.url(self.context, '@@addafp'))
926        return
927
928    @jsaction('Remove selected tickets')
929    def removePaymentTickets(self, **data):
930        self.delPaymentTickets(**data)
931        self.redirect(self.url(self.context) + '/@@manage')
932        return
933
934class ApplicantEditFormPage(ApplicantManageFormPage):
935    """An applicant-centered edit view for applicant data.
936    """
937    grok.context(IApplicantEdit)
938    grok.name('edit')
939    grok.require('waeup.handleApplication')
940    form_fields = grok.AutoFields(IApplicantEdit).omit(
941        'locked', 'course_admitted', 'student_id',
942        'screening_score', 'applicant_id', 'reg_number'
943        )
944    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
945    grok.template('applicanteditpage')
946    manage_applications = False
947    title = u'Your Application Form'
948
949    @property
950    def display_actions(self):
951        state = IWorkflowState(self.context).getState()
952        if state == INITIALIZED:
953            actions = [[],[]]
954        elif state == STARTED:
955            actions = [['Save'],
956                       ['Add online payment ticket','Remove selected tickets']]
957        elif state == PAID:
958            actions = [['Save', 'Final Submit'],
959                       ['Remove selected tickets']]
960        elif state == SUBMITTED:
961            actions = [[],[]]
962        return actions
963
964    def unremovable(self, ticket):
965        state = IWorkflowState(self.context).getState()
966        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
967
968    def emit_lock_message(self):
969        self.flash('The requested form is locked (read-only).')
970        self.redirect(self.url(self.context))
971        return
972
973    def update(self):
974        if self.context.locked:
975            self.emit_lock_message()
976            return
977        super(ApplicantEditFormPage, self).update()
978        return
979
980    def dataNotComplete(self):
981        store = getUtility(IExtFileStore)
982        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
983            return 'No passport picture uploaded.'
984        if not self.request.form.get('confirm_passport', False):
985            return 'Passport picture confirmation box not ticked.'
986        return False
987
988    # We explicitely want the forms to be validated before payment tickets
989    # can be created. If no validation is requested, use
990    # 'validator=NullValidator' in the grok.action directive
991    @grok.action('Add online payment ticket')
992    def addPaymentTicket(self, **data):
993        self.redirect(self.url(self.context, '@@addafp'))
994        return
995
996    @jsaction('Remove selected tickets')
997    def removePaymentTickets(self, **data):
998        self.delPaymentTickets(**data)
999        self.redirect(self.url(self.context) + '/@@edit')
1000        return
1001
1002    @grok.action('Save')
1003    def save(self, **data):
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.