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

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

It seems that font attributes can't be defined in Tables if Paragraphs are being used. The latter have to be used to render the widgets properly.

Reorganise slip pages in students section.

Sort course lists by semester and course code.

  • Property svn:keywords set to Id
File size: 37.5 KB
Line 
1## $Id: browser.py 7318 2011-12-09 12:34:30Z 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    title = 'Payment Data'
626
627    @property
628    def label(self):
629        return 'Online Payment Receipt %s' % self.context.p_id
630
631    def render(self):
632        if self.context.p_state != 'paid':
633            self.flash('Ticket not yet paid.')
634            self.redirect(self.url(self.context))
635            return
636        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
637            self.request)
638        students_utils = getUtility(IStudentsUtils)
639        return students_utils.renderPDF(self,'payment_receipt.pdf',
640            self.context.__parent__, applicantview)
641
642class PDFActionButton(ManageActionButton):
643    grok.context(IApplicant)
644    grok.require('waeup.viewApplication')
645    icon = 'actionicon_pdf.png'
646    text = 'Download application slip'
647    target = 'application_slip.pdf'
648
649class ExportPDFPage(grok.View):
650    """Deliver a PDF slip of the context.
651    """
652    grok.context(IApplicant)
653    grok.name('application_slip.pdf')
654    grok.require('waeup.viewApplication')
655    form_fields = grok.AutoFields(IApplicant).omit(
656        'locked', 'course_admitted')
657    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
658    prefix = 'form'
659
660    @property
661    def label(self):
662        container_title = self.context.__parent__.title
663        return '%s Application Record %s' % (
664            container_title, self.context.application_number)
665
666    def getCourseAdmitted(self):
667        """Return title and code in html format to the certificate
668           admitted.
669        """
670        course_admitted = self.context.course_admitted
671        if ICertificate.providedBy(course_admitted):
672            title = course_admitted.title
673            code = course_admitted.code
674            return '%s - %s' %(code,title)
675        return ''
676
677    def setUpWidgets(self, ignore_request=False):
678        self.adapters = {}
679        self.widgets = setUpEditWidgets(
680            self.form_fields, self.prefix, self.context, self.request,
681            adapters=self.adapters, for_display=True,
682            ignore_request=ignore_request
683            )
684
685    def render(self):
686        # To recall the table coordinate system:
687        # (0,0),(-1,-1) = whole table
688        # (0,0),(0,-1) = first column
689        # (-1,0),(-1,-1) = last column
690        # (0,0),(-1,0) = first row
691        # (0,-1),(-1,-1) = last row
692
693        SLIP_STYLE = TableStyle(
694            [('VALIGN',(0,0),(-1,-1),'TOP')]
695            )
696
697        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
698        pdf.setTitle(self.label)
699        pdf.setSubject('Application')
700        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
701            self.request.principal.id))
702        pdf.setCreator('WAeUP SIRP')
703        width, height = A4
704        style = getSampleStyleSheet()
705        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
706
707        story = []
708        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
709        header_title = getattr(grok.getSite(), 'name', u'Sample University')
710        story.append(Paragraph(header_title, style["Heading1"]))
711        frame_header.addFromList(story,pdf)
712
713        story = []
714        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
715        story.append(Paragraph(self.label, style["Heading2"]))
716        story.append(Spacer(1, 18))
717        for msg in self.context.history.messages:
718            f_msg = '<font face="Courier" size=10>%s</font>' % msg
719            story.append(Paragraph(f_msg, style["Normal"]))
720        story.append(Spacer(1, 24))
721        # Setup table data
722        data = []
723        # Insert passport photograph
724        img = getUtility(IExtFileStore).getFileByContext(self.context)
725        if img is None:
726            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
727        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
728        data.append([doc_img])
729        data.append([Spacer(1, 18)])
730        # Render widget fields
731        self.setUpWidgets()
732        for widget in self.widgets:
733            f_label = '<font size=12>%s</font>:' % widget.label.strip()
734            f_label = Paragraph(f_label, style["Normal"])
735            f_text = '<font size=12>%s</font>' % widget()
736            f_text = Paragraph(f_text, style["Normal"])
737            data.append([f_label,f_text])
738        f_label = '<font size=12>Admitted Course of Study:</font>'
739        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
740        f_label = Paragraph(f_label, style["Normal"])
741        f_text = Paragraph(f_text, style["Normal"])
742        data.append([f_label,f_text])
743        # Create table
744        table = Table(data,style=SLIP_STYLE)
745        story.append(table)
746        frame_body.addFromList(story,pdf)
747
748        story = []
749        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
750        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
751        f_text = '<font size=10>%s</font>' % timestamp
752        story.append(Paragraph(f_text, style["Normal"]))
753        frame_footer.addFromList(story,pdf)
754
755        self.response.setHeader(
756            'Content-Type', 'application/pdf')
757        return pdf.getpdfdata()
758
759class ApplicantManageActionButton(ManageActionButton):
760    grok.context(IApplicant)
761    grok.view(ApplicantDisplayFormPage)
762    grok.require('waeup.manageApplication')
763    text = 'Manage application record'
764    target = 'manage'
765
766class ApplicantEditActionButton(ManageActionButton):
767    grok.context(IApplicant)
768    grok.view(ApplicantDisplayFormPage)
769    grok.require('waeup.handleApplication')
770    text = 'Edit application record'
771    target ='edit'
772
773    @property
774    def target_url(self):
775        """Get a URL to the target...
776        """
777        if self.context.locked:
778            return
779        return self.view.url(self.view.context, self.target)
780
781def handle_img_upload(upload, context, view):
782    """Handle upload of applicant image.
783
784    Returns `True` in case of success or `False`.
785
786    Please note that file pointer passed in (`upload`) most probably
787    points to end of file when leaving this function.
788    """
789    size = file_size(upload)
790    if size > MAX_UPLOAD_SIZE:
791        view.flash('Uploaded image is too big!')
792        return False
793    dummy, ext = os.path.splitext(upload.filename)
794    ext.lower()
795    if ext != '.jpg':
796        view.flash('jpg file extension expected.')
797        return False
798    upload.seek(0) # file pointer moved when determining size
799    store = getUtility(IExtFileStore)
800    file_id = IFileStoreNameChooser(context).chooseName()
801    store.createFile(file_id, upload)
802    return True
803
804class ApplicantManageFormPage(WAeUPEditFormPage):
805    """A full edit view for applicant data.
806    """
807    grok.context(IApplicant)
808    grok.name('manage')
809    grok.require('waeup.manageApplication')
810    form_fields = grok.AutoFields(IApplicant)
811    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
812    grok.template('applicanteditpage')
813    manage_applications = True
814    pnav = 3
815    display_actions = [['Save', 'Final Submit'],
816                       ['Add online payment ticket','Remove selected tickets']]
817
818    def update(self):
819        datepicker.need() # Enable jQuery datepicker in date fields.
820        super(ApplicantManageFormPage, self).update()
821        self.wf_info = IWorkflowInfo(self.context)
822        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
823        self.passport_changed = None
824        upload = self.request.form.get('form.passport', None)
825        if upload:
826            # We got a fresh upload
827            self.passport_changed = handle_img_upload(
828                upload, self.context, self)
829        return
830
831    @property
832    def title(self):
833        return 'Application Record %s' % self.context.application_number
834
835    @property
836    def label(self):
837        container_title = self.context.__parent__.title
838        return '%s Application Form %s' % (
839            container_title, self.context.application_number)
840
841    def getTransitions(self):
842        """Return a list of dicts of allowed transition ids and titles.
843
844        Each list entry provides keys ``name`` and ``title`` for
845        internal name and (human readable) title of a single
846        transition.
847        """
848        allowed_transitions = self.wf_info.getManualTransitions()
849        return [dict(name='', title='No transition')] +[
850            dict(name=x, title=y) for x, y in allowed_transitions]
851
852    @grok.action('Save')
853    def save(self, **data):
854        form = self.request.form
855        password = form.get('password', None)
856        password_ctl = form.get('control_password', None)
857        if password:
858            validator = getUtility(IPasswordValidator)
859            errors = validator.validate_password(password, password_ctl)
860            if errors:
861                self.flash( ' '.join(errors))
862                return
863        if self.passport_changed is False:  # False is not None!
864            return # error during image upload. Ignore other values
865        changed_fields = self.applyData(self.context, **data)
866        # Turn list of lists into single list
867        if changed_fields:
868            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
869        else:
870            changed_fields = []
871        if self.passport_changed:
872            changed_fields.append('passport')
873        if password:
874            # Now we know that the form has no errors and can set password ...
875            IUserAccount(self.context).setPassword(password)
876            changed_fields.append('password')
877        fields_string = ' + '.join(changed_fields)
878        trans_id = form.get('transition', None)
879        if trans_id:
880            self.wf_info.fireTransition(trans_id)
881        self.flash('Form has been saved.')
882        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
883        if fields_string:
884            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
885        return
886
887    def unremovable(self, ticket):
888        usertype = getattr(self.request.principal, 'user_type', None)
889        if not usertype:
890            return False
891        return self.request.principal.user_type == 'applicant' and ticket.r_code
892
893    # This method is also used by the ApplicantEditFormPage
894    def delPaymentTickets(self, **data):
895        form = self.request.form
896        if form.has_key('val_id'):
897            child_id = form['val_id']
898        else:
899            self.flash('No payment selected.')
900            self.redirect(self.url(self.context))
901            return
902        if not isinstance(child_id, list):
903            child_id = [child_id]
904        deleted = []
905        for id in child_id:
906            # Applicants are not allowed to remove used payment tickets
907            if not self.unremovable(self.context[id]):
908                try:
909                    del self.context[id]
910                    deleted.append(id)
911                except:
912                    self.flash('Could not delete %s: %s: %s' % (
913                            id, sys.exc_info()[0], sys.exc_info()[1]))
914        if len(deleted):
915            self.flash('Successfully removed: %s' % ', '.join(deleted))
916            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
917            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
918        return
919
920    # We explicitely want the forms to be validated before payment tickets
921    # can be created. If no validation is requested, use
922    # 'validator=NullValidator' in the grok.action directive
923    @grok.action('Add online payment ticket')
924    def addPaymentTicket(self, **data):
925        self.redirect(self.url(self.context, '@@addafp'))
926        return
927
928    @grok.action('Remove selected tickets')
929    def removePaymentTickets(self, **data):
930        self.delPaymentTickets(**data)
931        self.redirect(self.url(self.context) + '/@@manage')
932        return
933
934class ApplicantEditFormPage(ApplicantManageFormPage):
935    """An applicant-centered edit view for applicant data.
936    """
937    grok.context(IApplicantEdit)
938    grok.name('edit')
939    grok.require('waeup.handleApplication')
940    form_fields = grok.AutoFields(IApplicantEdit).omit(
941        'locked', 'course_admitted', 'student_id',
942        'screening_score', 'applicant_id', 'reg_number'
943        )
944    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
945    grok.template('applicanteditpage')
946    manage_applications = False
947    title = u'Your Application Form'
948
949    @property
950    def display_actions(self):
951        state = IWorkflowState(self.context).getState()
952        if state == INITIALIZED:
953            actions = [[],[]]
954        elif state == STARTED:
955            actions = [['Save'],
956                       ['Add online payment ticket','Remove selected tickets']]
957        elif state == PAID:
958            actions = [['Save', 'Final Submit'],
959                       ['Remove selected tickets']]
960        elif state == SUBMITTED:
961            actions = [[],[]]
962        return actions
963
964    def emit_lock_message(self):
965        self.flash('The requested form is locked (read-only).')
966        self.redirect(self.url(self.context))
967        return
968
969    def update(self):
970        if self.context.locked:
971            self.emit_lock_message()
972            return
973        datepicker.need() # Enable jQuery datepicker in date fields.
974        super(ApplicantEditFormPage, self).update()
975        return
976
977    def dataNotComplete(self):
978        store = getUtility(IExtFileStore)
979        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
980            return 'No passport picture uploaded.'
981        if not self.request.form.get('confirm_passport', False):
982            return 'Passport picture confirmation box not ticked.'
983        return False
984
985    # We explicitely want the forms to be validated before payment tickets
986    # can be created. If no validation is requested, use
987    # 'validator=NullValidator' in the grok.action directive
988    @grok.action('Add online payment ticket')
989    def addPaymentTicket(self, **data):
990        self.redirect(self.url(self.context, '@@addafp'))
991        return
992
993    @grok.action('Remove selected tickets')
994    def removePaymentTickets(self, **data):
995        self.delPaymentTickets(**data)
996        self.redirect(self.url(self.context) + '/@@edit')
997        return
998
999    @grok.action('Save')
1000    def save(self, **data):
1001        #import pdb; pdb.set_trace()
1002        if self.passport_changed is False:  # False is not None!
1003            return # error during image upload. Ignore other values
1004        self.applyData(self.context, **data)
1005        self.flash('Form has been saved.')
1006        return
1007
1008    @grok.action('Final Submit')
1009    def finalsubmit(self, **data):
1010        if self.passport_changed is False:  # False is not None!
1011            return # error during image upload. Ignore other values
1012        if self.dataNotComplete():
1013            self.flash(self.dataNotComplete())
1014            return
1015        self.applyData(self.context, **data)
1016        state = IWorkflowState(self.context).getState()
1017        # This shouldn't happen, but the application officer
1018        # might have forgotten to lock the form after changing the state
1019        if state != PAID:
1020            self.flash('This form cannot be submitted. Wrong state!')
1021            return
1022        IWorkflowInfo(self.context).fireTransition('submit')
1023        self.context.application_date = datetime.now()
1024        self.context.locked = True
1025        self.flash('Form has been submitted.')
1026        self.redirect(self.url(self.context))
1027        return
1028
1029class ApplicantViewActionButton(ManageActionButton):
1030    grok.context(IApplicant)
1031    grok.view(ApplicantManageFormPage)
1032    grok.require('waeup.viewApplication')
1033    icon = 'actionicon_view.png'
1034    text = 'View application record'
1035    target = 'index'
1036
1037class PassportImage(grok.View):
1038    """Renders the passport image for applicants.
1039    """
1040    grok.name('passport.jpg')
1041    grok.context(IApplicant)
1042    grok.require('waeup.viewApplication')
1043
1044    def render(self):
1045        # A filename chooser turns a context into a filename suitable
1046        # for file storage.
1047        image = getUtility(IExtFileStore).getFileByContext(self.context)
1048        self.response.setHeader(
1049            'Content-Type', 'image/jpeg')
1050        if image is None:
1051            # show placeholder image
1052            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1053        return image
Note: See TracBrowser for help on using the repository browser.