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

Last change on this file since 7363 was 7363, checked in by uli, 13 years ago

Add pages for captcha-driven applicant registration.

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