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

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

Add tests for acceptance fee payment.

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