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

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

First part of acceptance fee payment integration (under construction).

  • Property svn:keywords set to Id
File size: 37.2 KB
Line 
1## $Id: browser.py 7250 2011-12-02 12:46:36Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from time import time
24from datetime import datetime, 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        #import pdb; pdb.set_trace()
534        self.flash('Payment ticket created.')
535        return
536
537    def render(self):
538        usertype = getattr(self.request.principal, 'user_type', None)
539        if usertype == 'applicant':
540            self.redirect(self.url(self.context, '@@edit'))
541            return
542        self.redirect(self.url(self.context, '@@manage'))
543        return
544
545
546class OnlinePaymentDisplayFormPage(WAeUPDisplayFormPage):
547    """ Page to view an online payment ticket
548    """
549    grok.context(IApplicantOnlinePayment)
550    grok.name('index')
551    grok.require('waeup.viewApplication')
552    form_fields = grok.AutoFields(IApplicantOnlinePayment)
553    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
554    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
555    pnav = 3
556
557    @property
558    def title(self):
559        return 'Online Payment Ticket %s' % self.context.p_id
560
561    @property
562    def label(self):
563        return '%s: Online Payment Ticket %s' % (
564            self.context.__parent__.fullname,self.context.p_id)
565
566class PaymentReceiptActionButton(ManageActionButton):
567    grok.order(1)
568    grok.context(IApplicantOnlinePayment)
569    grok.view(OnlinePaymentDisplayFormPage)
570    grok.require('waeup.viewApplication')
571    icon = 'actionicon_pdf.png'
572    text = 'Download payment receipt'
573    target = 'payment_receipt.pdf'
574
575    @property
576    def target_url(self):
577        if self.context.p_state != 'paid':
578            return ''
579        return self.view.url(self.view.context, self.target)
580
581class RequestCallbackActionButton(ManageActionButton):
582    grok.order(2)
583    grok.context(IApplicantOnlinePayment)
584    grok.view(OnlinePaymentDisplayFormPage)
585    grok.require('waeup.payApplicant')
586    icon = 'actionicon_call.png'
587    text = 'Request callback'
588    target = 'callback'
589
590    @property
591    def target_url(self):
592        if self.context.p_state != 'unpaid':
593            return ''
594        return self.view.url(self.view.context, self.target)
595
596class OnlinePaymentCallbackPage(grok.View):
597    """ Callback view
598    """
599    grok.context(IApplicantOnlinePayment)
600    grok.name('callback')
601    grok.require('waeup.payApplicant')
602
603    # This update method simulates a valid callback und must be
604    # specified in the customization package. The parameters must be taken
605    # from the incoming request.
606    def update(self):
607        if self.context.p_state == 'paid':
608            self.flash('This ticket has already been paid.')
609            return
610        self.context.r_amount_approved = self.context.amount_auth
611        self.context.r_card_num = u'0000'
612        self.context.r_code = u'00'
613        self.context.p_state = 'paid'
614        self.context.payment_date = datetime.now()
615        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
616        self.context.__parent__.loggerInfo(
617            ob_class, 'valid callback: %s' % self.context.p_id)
618        self.wf_info = IWorkflowInfo(self.context.__parent__)
619        self.wf_info.fireTransition('pay')
620        self.flash('Valid callback received.')
621        return
622
623    def render(self):
624        self.redirect(self.url(self.context, '@@index'))
625        return
626
627class ExportPDFPaymentSlipPage(grok.View):
628    """Deliver a PDF slip of the context.
629    """
630    grok.context(IApplicantOnlinePayment)
631    grok.name('payment_receipt.pdf')
632    grok.require('waeup.viewApplication')
633    form_fields = grok.AutoFields(IApplicantOnlinePayment)
634    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
635    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
636    prefix = 'form'
637
638    @property
639    def label(self):
640        return 'Online Payment Receipt %s' % self.context.p_id
641
642    def render(self):
643        if self.context.p_state != 'paid':
644            self.flash('Ticket not yet paid.')
645            self.redirect(self.url(self.context))
646            return
647        applicantview = ApplicantDisplayFormPage(self.context.__parent__,
648            self.request)
649        students_utils = getUtility(IStudentsUtils)
650        return students_utils.renderPDF(self,'Payment', 'payment_receipt.pdf',
651            self.context.__parent__, applicantview)
652
653class PDFActionButton(ManageActionButton):
654    grok.context(IApplicant)
655    grok.require('waeup.viewApplication')
656    icon = 'actionicon_pdf.png'
657    text = 'Download application slip'
658    target = 'application_slip.pdf'
659
660class ExportPDFPage(grok.View):
661    """Deliver a PDF slip of the context.
662    """
663    grok.context(IApplicant)
664    grok.name('application_slip.pdf')
665    grok.require('waeup.viewApplication')
666    form_fields = grok.AutoFields(IApplicant).omit(
667        'locked', 'course_admitted')
668    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
669    prefix = 'form'
670
671    @property
672    def label(self):
673        container_title = self.context.__parent__.title
674        return '%s Application Record %s' % (
675            container_title, self.context.application_number)
676
677    def getCourseAdmitted(self):
678        """Return title and code in html format to the certificate
679           admitted.
680        """
681        course_admitted = self.context.course_admitted
682        if ICertificate.providedBy(course_admitted):
683            title = course_admitted.title
684            code = course_admitted.code
685            return '%s - %s' %(code,title)
686        return ''
687
688    def setUpWidgets(self, ignore_request=False):
689        self.adapters = {}
690        self.widgets = setUpEditWidgets(
691            self.form_fields, self.prefix, self.context, self.request,
692            adapters=self.adapters, for_display=True,
693            ignore_request=ignore_request
694            )
695
696    def render(self):
697        SLIP_STYLE = TableStyle(
698            [('VALIGN',(0,0),(-1,-1),'TOP')]
699            )
700
701        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
702        pdf.setTitle(self.label)
703        pdf.setSubject('Application')
704        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
705            self.request.principal.id))
706        pdf.setCreator('WAeUP SIRP')
707        width, height = A4
708        style = getSampleStyleSheet()
709        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
710
711        story = []
712        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
713        header_title = getattr(grok.getSite(), 'name', u'Sample University')
714        story.append(Paragraph(header_title, style["Heading1"]))
715        frame_header.addFromList(story,pdf)
716
717        story = []
718        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
719        story.append(Paragraph(self.label, style["Heading2"]))
720        story.append(Spacer(1, 18))
721        for msg in self.context.history.messages:
722            f_msg = '<font face="Courier" size=10>%s</font>' % msg
723            story.append(Paragraph(f_msg, style["Normal"]))
724        story.append(Spacer(1, 24))
725
726
727        # insert passport photograph
728        img = getUtility(IExtFileStore).getFileByContext(self.context)
729        if img is None:
730            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
731        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
732        story.append(doc_img)
733        story.append(Spacer(1, 18))
734
735        # render widget fields
736        data = []
737        self.setUpWidgets()
738        for widget in self.widgets:
739            f_label = '<font size=12>%s</font>:' % widget.label.strip()
740            f_label = Paragraph(f_label, style["Normal"])
741            f_text = '<font size=12>%s</font>' % widget()
742            f_text = Paragraph(f_text, style["Normal"])
743            data.append([f_label,f_text])
744        f_label = '<font size=12>Admitted Course of Study:</font>'
745        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
746        f_label = Paragraph(f_label, style["Normal"])
747        f_text = Paragraph(f_text, style["Normal"])
748        data.append([f_label,f_text])
749        table = Table(data,style=SLIP_STYLE)
750        story.append(table)
751        frame_body.addFromList(story,pdf)
752
753        story = []
754        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
755        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
756        f_text = '<font size=10>%s</font>' % timestamp
757        story.append(Paragraph(f_text, style["Normal"]))
758        frame_footer.addFromList(story,pdf)
759
760        self.response.setHeader(
761            'Content-Type', 'application/pdf')
762        return pdf.getpdfdata()
763
764class ApplicantManageActionButton(ManageActionButton):
765    grok.context(IApplicant)
766    grok.view(ApplicantDisplayFormPage)
767    grok.require('waeup.manageApplication')
768    text = 'Manage application record'
769    target = 'manage'
770
771class ApplicantEditActionButton(ManageActionButton):
772    grok.context(IApplicant)
773    grok.view(ApplicantDisplayFormPage)
774    grok.require('waeup.handleApplication')
775    text = 'Edit application record'
776    target ='edit'
777
778    @property
779    def target_url(self):
780        """Get a URL to the target...
781        """
782        if self.context.locked:
783            return
784        return self.view.url(self.view.context, self.target)
785
786def handle_img_upload(upload, context, view):
787    """Handle upload of applicant image.
788
789    Returns `True` in case of success or `False`.
790
791    Please note that file pointer passed in (`upload`) most probably
792    points to end of file when leaving this function.
793    """
794    size = file_size(upload)
795    if size > MAX_UPLOAD_SIZE:
796        view.flash('Uploaded image is too big!')
797        return False
798    dummy, ext = os.path.splitext(upload.filename)
799    ext.lower()
800    if ext != '.jpg':
801        view.flash('jpg file extension expected.')
802        return False
803    upload.seek(0) # file pointer moved when determining size
804    store = getUtility(IExtFileStore)
805    file_id = IFileStoreNameChooser(context).chooseName()
806    store.createFile(file_id, upload)
807    return True
808
809class ApplicantManageFormPage(WAeUPEditFormPage):
810    """A full edit view for applicant data.
811    """
812    grok.context(IApplicant)
813    grok.name('manage')
814    grok.require('waeup.manageApplication')
815    form_fields = grok.AutoFields(IApplicant)
816    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
817    grok.template('applicanteditpage')
818    manage_applications = True
819    pnav = 3
820    display_actions = [['Save', 'Final Submit'],
821                       ['Add online payment ticket','Remove selected tickets']]
822
823    def update(self):
824        datepicker.need() # Enable jQuery datepicker in date fields.
825        super(ApplicantManageFormPage, self).update()
826        self.wf_info = IWorkflowInfo(self.context)
827        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
828        self.passport_changed = None
829        upload = self.request.form.get('form.passport', None)
830        if upload:
831            # We got a fresh upload
832            self.passport_changed = handle_img_upload(
833                upload, self.context, self)
834        return
835
836    @property
837    def title(self):
838        return 'Application Record %s' % self.context.application_number
839
840    @property
841    def label(self):
842        container_title = self.context.__parent__.title
843        return '%s Application Form %s' % (
844            container_title, self.context.application_number)
845
846    def getTransitions(self):
847        """Return a list of dicts of allowed transition ids and titles.
848
849        Each list entry provides keys ``name`` and ``title`` for
850        internal name and (human readable) title of a single
851        transition.
852        """
853        allowed_transitions = self.wf_info.getManualTransitions()
854        return [dict(name='', title='No transition')] +[
855            dict(name=x, title=y) for x, y in allowed_transitions]
856
857    @grok.action('Save')
858    def save(self, **data):
859        form = self.request.form
860        password = form.get('password', None)
861        password_ctl = form.get('control_password', None)
862        if password:
863            validator = getUtility(IPasswordValidator)
864            errors = validator.validate_password(password, password_ctl)
865            if errors:
866                self.flash( ' '.join(errors))
867                return
868        if self.passport_changed is False:  # False is not None!
869            return # error during image upload. Ignore other values
870        changed_fields = self.applyData(self.context, **data)
871        # Turn list of lists into single list
872        if changed_fields:
873            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
874        else:
875            changed_fields = []
876        if self.passport_changed:
877            changed_fields.append('passport')
878        if password:
879            # Now we know that the form has no errors and can set password ...
880            IUserAccount(self.context).setPassword(password)
881            changed_fields.append('password')
882        fields_string = ' + '.join(changed_fields)
883        trans_id = form.get('transition', None)
884        if trans_id:
885            self.wf_info.fireTransition(trans_id)
886        self.flash('Form has been saved.')
887        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
888        if fields_string:
889            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
890        return
891
892    def formatDatetime(self,datetimeobj):
893        if isinstance(datetimeobj, datetime):
894            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
895        else:
896            return None
897
898    def unremovable(self, ticket):
899        usertype = getattr(self.request.principal, 'user_type', None)
900        if not usertype:
901            return False
902        return self.request.principal.user_type == 'applicant' and ticket.r_code
903
904    # This method is also used by the ApplicantEditFormPage
905    def delPaymentTickets(self, **data):
906        form = self.request.form
907        if form.has_key('val_id'):
908            child_id = form['val_id']
909        else:
910            self.flash('No payment selected.')
911            self.redirect(self.url(self.context))
912            return
913        if not isinstance(child_id, list):
914            child_id = [child_id]
915        deleted = []
916        for id in child_id:
917            # Applicants are not allowed to remove used payment tickets
918            if not self.unremovable(self.context[id]):
919                try:
920                    del self.context[id]
921                    deleted.append(id)
922                except:
923                    self.flash('Could not delete %s: %s: %s' % (
924                            id, sys.exc_info()[0], sys.exc_info()[1]))
925        if len(deleted):
926            self.flash('Successfully removed: %s' % ', '.join(deleted))
927            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
928            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
929        return
930
931    @grok.action('Add online payment ticket')
932    def addPaymentTicket(self, **data):
933        self.redirect(self.url(self.context, '@@addafp'))
934
935    @grok.action('Remove selected tickets')
936    def removePaymentTickets(self, **data):
937        self.delPaymentTickets(**data)
938        self.redirect(self.url(self.context) + '/@@manage')
939        return
940
941class ApplicantEditFormPage(ApplicantManageFormPage):
942    """An applicant-centered edit view for applicant data.
943    """
944    grok.context(IApplicantEdit)
945    grok.name('edit')
946    grok.require('waeup.handleApplication')
947    form_fields = grok.AutoFields(IApplicantEdit).omit(
948        'locked', 'course_admitted', 'student_id',
949        'screening_score', 'applicant_id'
950        )
951    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
952    grok.template('applicanteditpage')
953    manage_applications = False
954    title = u'Your Application Form'
955
956    @property
957    def display_actions(self):
958        state = IWorkflowState(self.context).getState()
959        if state == INITIALIZED:
960            actions = [[],[]]
961        elif state == STARTED:
962            actions = [['Save'],
963                       ['Add online payment ticket','Remove selected tickets']]
964        elif state == PAID:
965            actions = [['Save', 'Final Submit'],
966                       ['Remove selected tickets']]
967        elif state == SUBMITTED:
968            actions = [[],[]]
969        return actions
970
971    def emit_lock_message(self):
972        self.flash('The requested form is locked (read-only).')
973        self.redirect(self.url(self.context))
974        return
975
976    def update(self):
977        if self.context.locked:
978            self.emit_lock_message()
979            return
980        datepicker.need() # Enable jQuery datepicker in date fields.
981        super(ApplicantEditFormPage, self).update()
982        return
983
984    def dataNotComplete(self):
985        if not self.request.form.get('confirm_passport', False):
986            return 'Passport confirmation box not ticked.'
987        return False
988
989    @grok.action('Add online payment ticket')
990    def addPaymentTicket(self, **data):
991        self.redirect(self.url(self.context, '@@addafp'))
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        if self.passport_changed is False:  # False is not None!
1002            return # error during image upload. Ignore other values
1003        self.applyData(self.context, **data)
1004        self.flash('Form has been saved.')
1005        return
1006
1007    @grok.action('Final Submit')
1008    def finalsubmit(self, **data):
1009        if self.passport_changed is False:  # False is not None!
1010            return # error during image upload. Ignore other values
1011        self.applyData(self.context, **data)
1012        self.context._p_changed = True
1013        if self.dataNotComplete():
1014            self.flash(self.dataNotComplete())
1015            return
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.