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

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

Add tests for applicant batch importer.

Make reg_no filed unique.

Two tests still fail because the importer only accepts the application_number as location field and does not yet search for registration numbers.

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