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

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

See last revision. Do the same for applicants.

Do not show select box for payment ticket removal if state doesn't allow to remove any ticket.

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