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

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

Start registration only if applicant visits the display view for the first time.

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