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

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

Do not store a reference to a certificate but a dictionary with the code, title, department title and faculty title of certificates. In this way we can preserve the information even if the certificate has been removed.

The assignment of dynamic roles is not necessary in the application section. We can assign local roles in applicants containers. That's sufficient.

  • Property svn:keywords set to Id
File size: 38.2 KB
Line 
1## $Id: browser.py 7341 2011-12-14 13:38:59Z 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',
441        'password')
442    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
443    label = 'Applicant'
444    pnav = 3
445
446    def update(self):
447        self.passport_url = self.url(self.context, 'passport.jpg')
448        # Mark application as started if applicant logs in for the first time
449        usertype = getattr(self.request.principal, 'user_type', None)
450        if usertype == 'applicant' and \
451            IWorkflowState(self.context).getState() == INITIALIZED:
452            IWorkflowInfo(self.context).fireTransition('start')
453        return
454
455    @property
456    def hasPassword(self):
457        if self.context.password:
458            return 'set'
459        return 'unset'
460
461    @property
462    def title(self):
463        return 'Application Record %s' % self.context.application_number
464
465    @property
466    def label(self):
467        container_title = self.context.__parent__.title
468        return '%s Application Record %s' % (
469            container_title, self.context.application_number)
470
471    def getDepartmentAdmitted(self):
472        course_admitted = getattr(self.context,'course_admitted',None)
473        if course_admitted:
474            return course_admitted['dep']
475
476    def getFacultyAdmitted(self):
477        course_admitted = getattr(self.context,'course_admitted',None)
478        if course_admitted:
479            return course_admitted['fac']
480
481class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
482    grok.context(IApplicant)
483    grok.name('base')
484    form_fields = grok.AutoFields(IApplicant).select(
485        'applicant_id', 'firstname', 'lastname','email', 'course1')
486
487class CreateStudentPage(grok.View):
488    """Create a student object from applicatnt data
489    and copy applicant object.
490    """
491    grok.context(IApplicant)
492    grok.name('createstudent')
493    grok.require('waeup.manageStudent')
494
495    def update(self):
496        msg = self.context.createStudent()[1]
497        self.flash(msg)
498        self.redirect(self.url(self.context))
499        return
500
501    def render(self):
502        return
503
504class AcceptanceFeePaymentAddPage(grok.View):
505    """ Page to add an online payment ticket
506    """
507    grok.context(IApplicant)
508    grok.name('addafp')
509    grok.require('waeup.payApplicant')
510
511    def update(self):
512        p_category = 'acceptance'
513        d = {}
514        session = str(self.context.__parent__.year)
515        try:
516            academic_session = grok.getSite()['configuration'][session]
517        except KeyError:
518            self.flash('Session configuration object is not available.')
519            return
520        timestamp = "%d" % int(time()*1000)
521        #order_id = "%s%s" % (student_id[1:],timestamp)
522        for key in self.context.keys():
523            ticket = self.context[key]
524            if ticket.p_state == 'paid':
525                  self.flash(
526                      'This type of payment has already been made.')
527                  self.redirect(self.url(self.context))
528                  return
529        payment = createObject(u'waeup.ApplicantOnlinePayment')
530        payment.p_id = "p%s" % timestamp
531        payment.p_item = self.context.__parent__.title
532        payment.p_year = self.context.__parent__.year
533        payment.p_category = p_category
534        payment.amount_auth = academic_session.acceptance_fee
535        payment.surcharge_1 = academic_session.surcharge_1
536        payment.surcharge_2 = academic_session.surcharge_2
537        payment.surcharge_3 = academic_session.surcharge_3
538        self.context[payment.p_id] = payment
539        self.flash('Payment ticket created.')
540        return
541
542    def render(self):
543        usertype = getattr(self.request.principal, 'user_type', None)
544        if usertype == 'applicant':
545            self.redirect(self.url(self.context, '@@edit'))
546            return
547        self.redirect(self.url(self.context, '@@manage'))
548        return
549
550
551class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
552    """ Page to view an online payment ticket
553    """
554    grok.context(IApplicantOnlinePayment)
555    grok.name('index')
556    grok.require('waeup.viewApplication')
557    form_fields = grok.AutoFields(IApplicantOnlinePayment)
558    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
559    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
560    pnav = 3
561
562    @property
563    def title(self):
564        return 'Online Payment Ticket %s' % self.context.p_id
565
566    @property
567    def label(self):
568        return '%s: Online Payment Ticket %s' % (
569            self.context.__parent__.fullname,self.context.p_id)
570
571class PaymentReceiptActionButton(ManageActionButton):
572    grok.order(1)
573    grok.context(IApplicantOnlinePayment)
574    grok.view(OnlinePaymentDisplayFormPage)
575    grok.require('waeup.viewApplication')
576    icon = 'actionicon_pdf.png'
577    text = 'Download payment receipt'
578    target = 'payment_receipt.pdf'
579
580    @property
581    def target_url(self):
582        if self.context.p_state != 'paid':
583            return ''
584        return self.view.url(self.view.context, self.target)
585
586class RequestCallbackActionButton(ManageActionButton):
587    grok.order(2)
588    grok.context(IApplicantOnlinePayment)
589    grok.view(OnlinePaymentDisplayFormPage)
590    grok.require('waeup.payApplicant')
591    icon = 'actionicon_call.png'
592    text = 'Request callback'
593    target = 'callback'
594
595    @property
596    def target_url(self):
597        if self.context.p_state != 'unpaid':
598            return ''
599        return self.view.url(self.view.context, self.target)
600
601class OnlinePaymentCallbackPage(grok.View):
602    """ Callback view
603    """
604    grok.context(IApplicantOnlinePayment)
605    grok.name('callback')
606    grok.require('waeup.payApplicant')
607
608    # This update method simulates a valid callback und must be
609    # specified in the customization package. The parameters must be taken
610    # from the incoming request.
611    def update(self):
612        self.wf_info = IWorkflowInfo(self.context.__parent__)
613        try:
614            self.wf_info.fireTransition('pay')
615        except InvalidTransitionError:
616            self.flash('Error: %s' % sys.exc_info()[1])
617            return
618        self.context.r_amount_approved = self.context.amount_auth
619        self.context.r_card_num = u'0000'
620        self.context.r_code = u'00'
621        self.context.p_state = 'paid'
622        self.context.payment_date = datetime.now()
623        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
624        self.context.__parent__.loggerInfo(
625            ob_class, 'valid callback: %s' % self.context.p_id)
626        self.flash('Valid callback received.')
627        return
628
629    def render(self):
630        self.redirect(self.url(self.context, '@@index'))
631        return
632
633class ExportPDFPaymentSlipPage(grok.View):
634    """Deliver a PDF slip of the context.
635    """
636    grok.context(IApplicantOnlinePayment)
637    grok.name('payment_receipt.pdf')
638    grok.require('waeup.viewApplication')
639    form_fields = grok.AutoFields(IApplicantOnlinePayment)
640    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
641    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
642    prefix = 'form'
643    title = 'Payment Data'
644
645    @property
646    def label(self):
647        return 'Online Payment Receipt %s' % self.context.p_id
648
649    def render(self):
650        if self.context.p_state != 'paid':
651            self.flash('Ticket not yet paid.')
652            self.redirect(self.url(self.context))
653            return
654        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
655            self.request)
656        students_utils = getUtility(IStudentsUtils)
657        return students_utils.renderPDF(self,'payment_receipt.pdf',
658            self.context.__parent__, applicantview)
659
660class PDFActionButton(ManageActionButton):
661    grok.context(IApplicant)
662    grok.require('waeup.viewApplication')
663    icon = 'actionicon_pdf.png'
664    text = 'Download application slip'
665    target = 'application_slip.pdf'
666
667class ExportPDFPage(grok.View):
668    """Deliver a PDF slip of the context.
669    """
670    grok.context(IApplicant)
671    grok.name('application_slip.pdf')
672    grok.require('waeup.viewApplication')
673    form_fields = grok.AutoFields(IApplicant).omit(
674        'locked',
675        )
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 getDepartmentAdmitted(self):
686        course_admitted = getattr(self.context,'course_admitted',None)
687        if course_admitted:
688            return course_admitted['dep']
689
690    def getFacultyAdmitted(self):
691        course_admitted = getattr(self.context,'course_admitted',None)
692        if course_admitted:
693            return course_admitted['fac']
694
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        #f_label = '<font size=12>Admitted Course of Study:</font>'
758        #f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
759        #f_label = Paragraph(f_label, style["Normal"])
760        #f_text = Paragraph(f_text, style["Normal"])
761        #data.append([f_label,f_text])
762
763        f_label = '<font size=12>Department:</font>'
764        f_text = '<font size=12>%s</font>' % self.getDepartmentAdmitted()
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>' % self.getFacultyAdmitted()
771        f_label = Paragraph(f_label, style["Normal"])
772        f_text = Paragraph(f_text, style["Normal"])
773        data.append([f_label,f_text])
774
775        # Create table
776        table = Table(data,style=SLIP_STYLE)
777        story.append(table)
778        frame_body.addFromList(story,pdf)
779
780        story = []
781        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
782        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
783        f_text = '<font size=10>%s</font>' % timestamp
784        story.append(Paragraph(f_text, style["Normal"]))
785        frame_footer.addFromList(story,pdf)
786
787        self.response.setHeader(
788            'Content-Type', 'application/pdf')
789        return pdf.getpdfdata()
790
791class ApplicantManageActionButton(ManageActionButton):
792    grok.context(IApplicant)
793    grok.view(ApplicantDisplayFormPage)
794    grok.require('waeup.manageApplication')
795    text = 'Manage application record'
796    target = 'manage'
797
798class ApplicantEditActionButton(ManageActionButton):
799    grok.context(IApplicant)
800    grok.view(ApplicantDisplayFormPage)
801    grok.require('waeup.handleApplication')
802    text = 'Edit application record'
803    target ='edit'
804
805    @property
806    def target_url(self):
807        """Get a URL to the target...
808        """
809        if self.context.locked:
810            return
811        return self.view.url(self.view.context, self.target)
812
813def handle_img_upload(upload, context, view):
814    """Handle upload of applicant image.
815
816    Returns `True` in case of success or `False`.
817
818    Please note that file pointer passed in (`upload`) most probably
819    points to end of file when leaving this function.
820    """
821    size = file_size(upload)
822    if size > MAX_UPLOAD_SIZE:
823        view.flash('Uploaded image is too big!')
824        return False
825    dummy, ext = os.path.splitext(upload.filename)
826    ext.lower()
827    if ext != '.jpg':
828        view.flash('jpg file extension expected.')
829        return False
830    upload.seek(0) # file pointer moved when determining size
831    store = getUtility(IExtFileStore)
832    file_id = IFileStoreNameChooser(context).chooseName()
833    store.createFile(file_id, upload)
834    return True
835
836class ApplicantManageFormPage(SIRPEditFormPage):
837    """A full edit view for applicant data.
838    """
839    grok.context(IApplicant)
840    grok.name('manage')
841    grok.require('waeup.manageApplication')
842    form_fields = grok.AutoFields(IApplicant)
843    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
844    grok.template('applicanteditpage')
845    manage_applications = True
846    pnav = 3
847    display_actions = [['Save', 'Final Submit'],
848                       ['Add online payment ticket','Remove selected tickets']]
849
850    def update(self):
851        datepicker.need() # Enable jQuery datepicker in date fields.
852        warning.need()
853        super(ApplicantManageFormPage, self).update()
854        self.wf_info = IWorkflowInfo(self.context)
855        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
856        self.passport_changed = None
857        upload = self.request.form.get('form.passport', None)
858        if upload:
859            # We got a fresh upload
860            self.passport_changed = handle_img_upload(
861                upload, self.context, self)
862        return
863
864    @property
865    def title(self):
866        return 'Application Record %s' % self.context.application_number
867
868    @property
869    def label(self):
870        container_title = self.context.__parent__.title
871        return '%s Application Form %s' % (
872            container_title, self.context.application_number)
873
874    def getTransitions(self):
875        """Return a list of dicts of allowed transition ids and titles.
876
877        Each list entry provides keys ``name`` and ``title`` for
878        internal name and (human readable) title of a single
879        transition.
880        """
881        allowed_transitions = self.wf_info.getManualTransitions()
882        return [dict(name='', title='No transition')] +[
883            dict(name=x, title=y) for x, y in allowed_transitions]
884
885    @grok.action('Save')
886    def save(self, **data):
887        form = self.request.form
888        password = form.get('password', None)
889        password_ctl = form.get('control_password', None)
890        if password:
891            validator = getUtility(IPasswordValidator)
892            errors = validator.validate_password(password, password_ctl)
893            if errors:
894                self.flash( ' '.join(errors))
895                return
896        if self.passport_changed is False:  # False is not None!
897            return # error during image upload. Ignore other values
898        changed_fields = self.applyData(self.context, **data)
899        # Turn list of lists into single list
900        if changed_fields:
901            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
902        else:
903            changed_fields = []
904        if self.passport_changed:
905            changed_fields.append('passport')
906        if password:
907            # Now we know that the form has no errors and can set password ...
908            IUserAccount(self.context).setPassword(password)
909            changed_fields.append('password')
910        fields_string = ' + '.join(changed_fields)
911        trans_id = form.get('transition', None)
912        if trans_id:
913            self.wf_info.fireTransition(trans_id)
914        self.flash('Form has been saved.')
915        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
916        if fields_string:
917            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
918        return
919
920    def unremovable(self, ticket):
921        return False
922
923    # This method is also used by the ApplicantEditFormPage
924    def delPaymentTickets(self, **data):
925        form = self.request.form
926        if form.has_key('val_id'):
927            child_id = form['val_id']
928        else:
929            self.flash('No payment selected.')
930            self.redirect(self.url(self.context))
931            return
932        if not isinstance(child_id, list):
933            child_id = [child_id]
934        deleted = []
935        for id in child_id:
936            # Applicants are not allowed to remove used payment tickets
937            if not self.unremovable(self.context[id]):
938                try:
939                    del self.context[id]
940                    deleted.append(id)
941                except:
942                    self.flash('Could not delete %s: %s: %s' % (
943                            id, sys.exc_info()[0], sys.exc_info()[1]))
944        if len(deleted):
945            self.flash('Successfully removed: %s' % ', '.join(deleted))
946            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
947            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
948        return
949
950    # We explicitely want the forms to be validated before payment tickets
951    # can be created. If no validation is requested, use
952    # 'validator=NullValidator' in the grok.action directive
953    @grok.action('Add online payment ticket')
954    def addPaymentTicket(self, **data):
955        self.redirect(self.url(self.context, '@@addafp'))
956        return
957
958    @jsaction('Remove selected tickets')
959    def removePaymentTickets(self, **data):
960        self.delPaymentTickets(**data)
961        self.redirect(self.url(self.context) + '/@@manage')
962        return
963
964class ApplicantEditFormPage(ApplicantManageFormPage):
965    """An applicant-centered edit view for applicant data.
966    """
967    grok.context(IApplicantEdit)
968    grok.name('edit')
969    grok.require('waeup.handleApplication')
970    form_fields = grok.AutoFields(IApplicantEdit).omit(
971        'locked', 'course_admitted', 'student_id',
972        'screening_score', 'applicant_id', 'reg_number'
973        )
974    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
975    grok.template('applicanteditpage')
976    manage_applications = False
977    title = u'Your Application Form'
978
979    @property
980    def display_actions(self):
981        state = IWorkflowState(self.context).getState()
982        if state == INITIALIZED:
983            actions = [[],[]]
984        elif state == STARTED:
985            actions = [['Save'],
986                       ['Add online payment ticket','Remove selected tickets']]
987        elif state == PAID:
988            actions = [['Save', 'Final Submit'],
989                       ['Remove selected tickets']]
990        elif state == SUBMITTED:
991            actions = [[],[]]
992        return actions
993
994    def unremovable(self, ticket):
995        state = IWorkflowState(self.context).getState()
996        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
997
998    def emit_lock_message(self):
999        self.flash('The requested form is locked (read-only).')
1000        self.redirect(self.url(self.context))
1001        return
1002
1003    def update(self):
1004        if self.context.locked:
1005            self.emit_lock_message()
1006            return
1007        super(ApplicantEditFormPage, self).update()
1008        return
1009
1010    def dataNotComplete(self):
1011        store = getUtility(IExtFileStore)
1012        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1013            return 'No passport picture uploaded.'
1014        if not self.request.form.get('confirm_passport', False):
1015            return 'Passport picture confirmation box not ticked.'
1016        return False
1017
1018    # We explicitely want the forms to be validated before payment tickets
1019    # can be created. If no validation is requested, use
1020    # 'validator=NullValidator' in the grok.action directive
1021    @grok.action('Add online payment ticket')
1022    def addPaymentTicket(self, **data):
1023        self.redirect(self.url(self.context, '@@addafp'))
1024        return
1025
1026    @jsaction('Remove selected tickets')
1027    def removePaymentTickets(self, **data):
1028        self.delPaymentTickets(**data)
1029        self.redirect(self.url(self.context) + '/@@edit')
1030        return
1031
1032    @grok.action('Save')
1033    def save(self, **data):
1034        if self.passport_changed is False:  # False is not None!
1035            return # error during image upload. Ignore other values
1036        self.applyData(self.context, **data)
1037        self.flash('Form has been saved.')
1038        return
1039
1040    @grok.action('Final Submit')
1041    def finalsubmit(self, **data):
1042        if self.passport_changed is False:  # False is not None!
1043            return # error during image upload. Ignore other values
1044        if self.dataNotComplete():
1045            self.flash(self.dataNotComplete())
1046            return
1047        self.applyData(self.context, **data)
1048        state = IWorkflowState(self.context).getState()
1049        # This shouldn't happen, but the application officer
1050        # might have forgotten to lock the form after changing the state
1051        if state != PAID:
1052            self.flash('This form cannot be submitted. Wrong state!')
1053            return
1054        IWorkflowInfo(self.context).fireTransition('submit')
1055        self.context.application_date = datetime.now()
1056        self.context.locked = True
1057        self.flash('Form has been submitted.')
1058        self.redirect(self.url(self.context))
1059        return
1060
1061class ApplicantViewActionButton(ManageActionButton):
1062    grok.context(IApplicant)
1063    grok.view(ApplicantManageFormPage)
1064    grok.require('waeup.viewApplication')
1065    icon = 'actionicon_view.png'
1066    text = 'View application record'
1067    target = 'index'
1068
1069class PassportImage(grok.View):
1070    """Renders the passport image for applicants.
1071    """
1072    grok.name('passport.jpg')
1073    grok.context(IApplicant)
1074    grok.require('waeup.viewApplication')
1075
1076    def render(self):
1077        # A filename chooser turns a context into a filename suitable
1078        # for file storage.
1079        image = getUtility(IExtFileStore).getFileByContext(self.context)
1080        self.response.setHeader(
1081            'Content-Type', 'image/jpeg')
1082        if image is None:
1083            # show placeholder image
1084            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1085        return image
Note: See TracBrowser for help on using the repository browser.