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

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

Replace middlenames by middlename.

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