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

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

Rollback of r7341 as discussed on the phone. But now we get other problems (see email). A regression test will follow.

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