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

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

If start date doesn't exist, application is treated as not yet started.

  • Property svn:keywords set to Id
File size: 31.0 KB
Line 
1## $Id: browser.py 7224 2011-11-27 18:52:20Z 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
23
24from datetime import datetime, date
25from zope.authentication.interfaces import ILogout, IAuthentication
26from zope.component import getUtility
27from zope.formlib.widget import CustomWidgetFactory
28from zope.formlib.form import setUpEditWidgets
29from zope.securitypolicy.interfaces import IPrincipalRoleManager
30from zope.traversing.browser import absoluteURL
31
32from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
33from reportlab.pdfgen import canvas
34from reportlab.lib.units import cm
35from reportlab.lib.pagesizes import A4
36from reportlab.lib.styles import getSampleStyleSheet
37from reportlab.platypus import (Frame, Paragraph, Image,
38    Table, Spacer)
39from reportlab.platypus.tables import TableStyle
40
41from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
42from waeup.sirp.accesscodes.workflow import USED
43from waeup.sirp.browser import (
44    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
45from waeup.sirp.browser.breadcrumbs import Breadcrumb
46from waeup.sirp.browser.layout import NullValidator
47from waeup.sirp.browser.pages import add_local_role, del_local_roles
48from waeup.sirp.browser.resources import datepicker, tabs, datatable
49from waeup.sirp.browser.viewlets import (
50    ManageActionButton, PrimaryNavTab, LeftSidebarLink
51    )
52from waeup.sirp.interfaces import (
53    IWAeUPObject, ILocalRolesAssignable, IExtFileStore, IFileStoreNameChooser)
54from waeup.sirp.permissions import get_users_with_local_roles
55from waeup.sirp.browser import DEFAULT_PASSPORT_IMAGE_PATH
56from waeup.sirp.university.interfaces import ICertificate
57from waeup.sirp.utils.helpers import string_from_bytes, file_size
58from waeup.sirp.widgets.datewidget import (
59    FriendlyDateWidget, FriendlyDateDisplayWidget)
60from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
61from waeup.sirp.widgets.objectwidget import (
62    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
63from waeup.sirp.applicants import ResultEntry, Applicant, get_applicant_data
64from waeup.sirp.applicants.interfaces import (
65    IApplicant, IApplicantPrincipal,IApplicantEdit, IApplicantsRoot,
66    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
67    MAX_UPLOAD_SIZE,
68    )
69from waeup.sirp.applicants.workflow import INITIALIZED, STARTED
70
71results_widget = CustomWidgetFactory(
72    WAeUPObjectWidget, ResultEntry)
73
74results_display_widget = CustomWidgetFactory(
75    WAeUPObjectDisplayWidget, ResultEntry)
76
77class ApplicantsRootPage(WAeUPPage):
78    grok.context(IApplicantsRoot)
79    grok.name('index')
80    grok.require('waeup.Public')
81    title = 'Applicants'
82    label = 'Application Section'
83    pnav = 3
84
85    def update(self):
86        super(ApplicantsRootPage, self).update()
87        datatable.need()
88        return
89
90class ManageApplicantsRootActionButton(ManageActionButton):
91    grok.context(IApplicantsRoot)
92    grok.view(ApplicantsRootPage)
93    grok.require('waeup.manageApplication')
94    text = 'Manage application section'
95
96class ApplicantsRootManageFormPage(WAeUPEditFormPage):
97    grok.context(IApplicantsRoot)
98    grok.name('manage')
99    grok.template('applicantsrootmanagepage')
100    title = 'Applicants'
101    label = 'Manage application section'
102    pnav = 3
103    grok.require('waeup.manageApplication')
104    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
105    tabtwoactions1 = ['Remove selected local roles']
106    tabtwoactions2 = ['Add local role']
107    subunits = 'Applicants Containers'
108
109    def update(self):
110        tabs.need()
111        datatable.need()
112        return super(ApplicantsRootManageFormPage, self).update()
113
114    def getLocalRoles(self):
115        roles = ILocalRolesAssignable(self.context)
116        return roles()
117
118    def getUsers(self):
119        """Get a list of all users.
120        """
121        for key, val in grok.getSite()['users'].items():
122            url = self.url(val)
123            yield(dict(url=url, name=key, val=val))
124
125    def getUsersWithLocalRoles(self):
126        return get_users_with_local_roles(self.context)
127
128    # ToDo: Show warning message before deletion
129    @grok.action('Remove selected')
130    def delApplicantsContainers(self, **data):
131        form = self.request.form
132        child_id = form['val_id']
133        if not isinstance(child_id, list):
134            child_id = [child_id]
135        deleted = []
136        for id in child_id:
137            try:
138                del self.context[id]
139                deleted.append(id)
140            except:
141                self.flash('Could not delete %s: %s: %s' % (
142                        id, sys.exc_info()[0], sys.exc_info()[1]))
143        if len(deleted):
144            self.flash('Successfully removed: %s' % ', '.join(deleted))
145        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
146        return
147
148    @grok.action('Add applicants container', validator=NullValidator)
149    def addApplicantsContainer(self, **data):
150        self.redirect(self.url(self.context, '@@add'))
151        return
152
153    @grok.action('Cancel', validator=NullValidator)
154    def cancel(self, **data):
155        self.redirect(self.url(self.context))
156        return
157
158    @grok.action('Add local role', validator=NullValidator)
159    def addLocalRole(self, **data):
160        return add_local_role(self,2, **data)
161
162    @grok.action('Remove selected local roles')
163    def delLocalRoles(self, **data):
164        return del_local_roles(self,2,**data)
165
166class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
167    grok.context(IApplicantsRoot)
168    grok.require('waeup.manageApplication')
169    grok.name('add')
170    grok.template('applicantscontaineraddpage')
171    title = 'Applicants'
172    label = 'Add applicants container'
173    pnav = 3
174
175    form_fields = grok.AutoFields(
176        IApplicantsContainerAdd).omit('code').omit('title')
177    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
178    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
179
180    def update(self):
181        datepicker.need() # Enable jQuery datepicker in date fields.
182        return super(ApplicantsContainerAddFormPage, self).update()
183
184    @grok.action('Add applicants container')
185    def addApplicantsContainer(self, **data):
186        year = data['year']
187        code = u'%s%s' % (data['prefix'], year)
188        prefix = application_types_vocab.getTerm(data['prefix'])
189        title = u'%s %s/%s' % (prefix.title, year, year + 1)
190        if code in self.context.keys():
191            self.flash(
192                'An applicants container for the same application '
193                'type and entrance year exists already in the database.')
194            return
195        # Add new applicants container...
196        provider = data['provider'][1]
197        container = provider.factory()
198        self.applyData(container, **data)
199        container.code = code
200        container.title = title
201        self.context[code] = container
202        self.flash('Added: "%s".' % code)
203        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
204        return
205
206    @grok.action('Cancel', validator=NullValidator)
207    def cancel(self, **data):
208        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
209
210class ApplicantsRootBreadcrumb(Breadcrumb):
211    """A breadcrumb for applicantsroot.
212    """
213    grok.context(IApplicantsRoot)
214    title = u'Applicants'
215
216class ApplicantsContainerBreadcrumb(Breadcrumb):
217    """A breadcrumb for applicantscontainers.
218    """
219    grok.context(IApplicantsContainer)
220
221class ApplicantBreadcrumb(Breadcrumb):
222    """A breadcrumb for applicants.
223    """
224    grok.context(IApplicant)
225
226    @property
227    def title(self):
228        """Get a title for a context.
229        """
230        return self.context.access_code
231
232class ApplicantsAuthTab(PrimaryNavTab):
233    """Applicants tab in primary navigation.
234    """
235    grok.context(IWAeUPObject)
236    grok.order(3)
237    grok.require('waeup.viewApplication')
238    grok.template('primarynavtab')
239    pnav = 3
240    tab_title = u'Applicants'
241
242    @property
243    def link_target(self):
244        return self.view.application_url('applicants')
245
246class ApplicantsAnonTab(ApplicantsAuthTab):
247    """Applicants tab in primary navigation.
248
249    Display tab only for anonymous. Authenticated users can call the
250    form from the user navigation bar.
251    """
252    grok.require('waeup.Anonymous')
253    tab_title = u'Application'
254
255    # Also zope.manager has role Anonymous.
256    # To avoid displaying this tab, uncomment the following.
257    #def tab_title(self):
258    #    userid = self.request.principal.id
259    #    if userid != 'zope.anybody':
260    #        tt = u''
261    #    else:
262    #        tt = u'Application'
263    #    return tt
264
265class ApplicantsContainerPage(WAeUPDisplayFormPage):
266    """The standard view for regular applicant containers.
267    """
268    grok.context(IApplicantsContainer)
269    grok.name('index')
270    grok.require('waeup.Public')
271    grok.template('applicantscontainerpage')
272    pnav = 3
273
274    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
275    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
276    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
277    form_fields['description'].custom_widget = ReSTDisplayWidget
278
279    @property
280    def title(self):
281        return "Applicants Container: %s" % self.context.title
282
283    @property
284    def label(self):
285        return self.context.title
286
287class ApplicantsContainerManageActionButton(ManageActionButton):
288    grok.order(1)
289    grok.context(IApplicantsContainer)
290    grok.view(ApplicantsContainerPage)
291    grok.require('waeup.manageApplication')
292    text = 'Manage applicants container'
293
294class ApplicantLoginActionButton(ManageActionButton):
295    grok.order(2)
296    grok.context(IApplicantsContainer)
297    grok.view(ApplicantsContainerPage)
298    grok.require('waeup.Anonymous')
299    icon = 'login.png'
300    text = 'Login for applicants'
301    target = 'login'
302
303class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
304    grok.context(IApplicantsContainer)
305    grok.name('manage')
306    grok.template('applicantscontainermanagepage')
307    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
308    taboneactions = ['Save','Cancel']
309    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
310    tabthreeactions1 = ['Remove selected local roles']
311    tabthreeactions2 = ['Add local role']
312    # Use friendlier date widget...
313    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
314    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
315    grok.require('waeup.manageApplication')
316
317    @property
318    def title(self):
319        return "Applicants Container: %s" % self.context.title
320
321    @property
322    def label(self):
323        return 'Manage applicants container'
324
325    pnav = 3
326
327    def update(self):
328        datepicker.need() # Enable jQuery datepicker in date fields.
329        tabs.need()
330        datatable.need()  # Enable jQurey datatables for contents listing
331        return super(ApplicantsContainerManageFormPage, self).update()
332
333    def getLocalRoles(self):
334        roles = ILocalRolesAssignable(self.context)
335        return roles()
336
337    def getUsers(self):
338        """Get a list of all users.
339        """
340        for key, val in grok.getSite()['users'].items():
341            url = self.url(val)
342            yield(dict(url=url, name=key, val=val))
343
344    def getUsersWithLocalRoles(self):
345        return get_users_with_local_roles(self.context)
346
347    @grok.action('Save')
348    def apply(self, **data):
349        self.applyData(self.context, **data)
350        self.flash('Data saved.')
351        return
352
353    # ToDo: Show warning message before deletion
354    @grok.action('Remove selected')
355    def delApplicant(self, **data):
356        form = self.request.form
357        if form.has_key('val_id'):
358            child_id = form['val_id']
359        else:
360            self.flash('No applicant selected!')
361            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
362            return
363        if not isinstance(child_id, list):
364            child_id = [child_id]
365        deleted = []
366        for id in child_id:
367            try:
368                del self.context[id]
369                deleted.append(id)
370            except:
371                self.flash('Could not delete %s: %s: %s' % (
372                        id, sys.exc_info()[0], sys.exc_info()[1]))
373        if len(deleted):
374            self.flash('Successfully removed: %s' % ', '.join(deleted))
375        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
376        return
377
378    @grok.action('Add applicant', validator=NullValidator)
379    def addApplicant(self, **data):
380        self.redirect(self.url(self.context, 'addapplicant'))
381        return
382
383    @grok.action('Cancel', validator=NullValidator)
384    def cancel(self, **data):
385        self.redirect(self.url(self.context))
386        return
387
388    @grok.action('Add local role', validator=NullValidator)
389    def addLocalRole(self, **data):
390        return add_local_role(self,3, **data)
391
392    @grok.action('Remove selected local roles')
393    def delLocalRoles(self, **data):
394        return del_local_roles(self,3,**data)
395
396class ApplicantLoginPage(WAeUPPage):
397    grok.context(IApplicantsContainer)
398    grok.name('login')
399    grok.require('waeup.Public')
400
401    @property
402    def title(self):
403        return u"Applicant Login: %s" % self.context.title
404
405    @property
406    def label(self):
407        return u"Login for '%s' applicants only" % self.context.title
408
409    pnav = 3
410
411    @property
412    def ac_prefix(self):
413        return self.context.ac_prefix
414
415    def update(self, SUBMIT=None):
416        self.ac_series = self.request.form.get('form.ac_series', None)
417        self.ac_number = self.request.form.get('form.ac_number', None)
418        if SUBMIT is None:
419            return
420        if self.request.principal.id == 'zope.anybody':
421            self.flash('Entered credentials are invalid.')
422            return
423        if not IApplicantPrincipal.providedBy(self.request.principal):
424            # Don't care if user is already authenticated as non-applicant
425            return
426
427        # From here we handle an applicant (not an officer browsing)
428        pin = self.request.principal.access_code
429
430        # If application has not yet started,
431        # logout without marking AC as used
432        if not self.context.startdate or self.context.startdate > date.today():
433            self.flash('Application has not yet started.')
434            auth = getUtility(IAuthentication)
435            ILogout(auth).logout(self.request)
436            self.redirect(self.url(self.context, 'login'))
437            return
438
439        # If application has ended and applicant record doesn't exist,
440        # logout without marking AC as used
441        if not pin in self.context.keys() and (
442            self.context.enddate < date.today()):
443            self.flash('Application has ended.')
444            auth = getUtility(IAuthentication)
445            ILogout(auth).logout(self.request)
446            self.redirect(self.url(self.context, 'login'))
447            return
448
449        # Mark AC as used (this also fires a pin related transition)
450        if get_access_code(pin).state != USED:
451            comment = u"AC invalidated"
452            # Here we know that the ac is in state initialized so we do not
453            # expect an exception
454            invalidate_accesscode(pin,comment)
455
456        if not pin in self.context.keys():
457            # Create applicant record
458            applicant = Applicant()
459            applicant.access_code = pin
460            self.context[pin] = applicant
461
462        role_manager = IPrincipalRoleManager(self.context[pin])
463        role_manager.assignRoleToPrincipal(
464            'waeup.local.ApplicationOwner', self.request.principal.id)
465
466        # Assign current principal the global Applicant role
467        role_manager = IPrincipalRoleManager(grok.getSite())
468        role_manager.assignRoleToPrincipal(
469            'waeup.Applicant', self.request.principal.id)
470
471        # Mark application as started
472        if IWorkflowState(self.context[pin]).getState() is INITIALIZED:
473            IWorkflowInfo(self.context[pin]).fireTransition('start')
474
475        self.redirect(self.url(self.context[pin], 'edit'))
476        return
477
478class ApplicantAddFormPage(WAeUPAddFormPage):
479    """Add-form to add an applicant.
480    """
481    grok.context(IApplicantsContainer)
482    grok.require('waeup.manageApplication')
483    grok.name('addapplicant')
484    grok.template('applicantaddpage')
485    title = 'Applicants'
486    label = 'Add applicant'
487    pnav = 3
488
489    @property
490    def title(self):
491        return "Applicants Container: %s" % self.context.title
492
493    @property
494    def ac_prefix(self):
495        return self.context.ac_prefix
496
497    @grok.action('Create application record')
498    def addApplicant(self, **data):
499        ac_series = self.request.form.get('form.ac_series', None)
500        ac_number = self.request.form.get('form.ac_number', None)
501        pin = '%s-%s-%s' % (self.ac_prefix,ac_series,ac_number)
502        if not invalidate_accesscode(pin, comment=u"Invalidated by system"):
503            self.flash('%s is not a valid access code.' % pin)
504            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
505            return
506        else:
507            # Create applicant record
508            applicant = Applicant()
509            applicant.access_code = pin
510            self.context[pin] = applicant
511        self.redirect(self.url(self.context[pin], 'manage'))
512        return
513
514class AccessCodeViewLink(LeftSidebarLink):
515    grok.order(1)
516    grok.require('waeup.Public')
517    icon = 'actionicon_view.png'
518    title = 'View Record'
519    target = '/@@index'
520
521    @property
522    def url(self):
523        if not IApplicantPrincipal.providedBy(self.request.principal):
524            return ''
525        access_code = getattr(self.request.principal,'access_code',None)
526        if access_code:
527            applicant_object = get_applicant_data(access_code)
528            return absoluteURL(applicant_object, self.request) + self.target
529        return ''
530
531class AccessCodeEditLink(AccessCodeViewLink):
532    grok.order(2)
533    grok.require('waeup.Public')
534    icon = 'actionicon_modify.png'
535    title = 'Edit Record'
536    target = '/@@edit'
537
538    @property
539    def url(self):
540        if not IApplicantPrincipal.providedBy(self.request.principal):
541            return ''
542        access_code = getattr(self.request.principal,'access_code',None)
543        if access_code:
544            applicant_object = get_applicant_data(access_code)
545            if applicant_object.locked:
546                return ''
547            return absoluteURL(applicant_object, self.request) + self.target
548        return ''
549
550class AccessCodeSlipLink(AccessCodeViewLink):
551    grok.order(3)
552    grok.require('waeup.Public')
553    icon = 'actionicon_pdf.png'
554    title = 'Download Slip'
555    target = '/application_slip.pdf'
556
557class ApplicantDisplayFormPage(WAeUPDisplayFormPage):
558    grok.context(IApplicant)
559    grok.name('index')
560    grok.require('waeup.viewApplication')
561    grok.template('applicantdisplaypage')
562    form_fields = grok.AutoFields(IApplicant).omit(
563        'locked').omit('course_admitted')
564    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
565    label = 'Applicant'
566    pnav = 3
567
568    def update(self):
569        self.passport_url = self.url(self.context, 'passport.jpg')
570        return
571
572    @property
573    def title(self):
574        if self.request.principal.title == 'Applicant':
575            return u'Your Application Record'
576        return '%s' % self.context.access_code
577
578    @property
579    def label(self):
580        container_title = self.context.__parent__.title
581        return '%s Application Record' % container_title
582
583    def getCourseAdmitted(self):
584        """Return link, title and code in html format to the certificate
585           admitted.
586        """
587        course_admitted = self.context.course_admitted
588        if ICertificate.providedBy(course_admitted):
589            url = self.url(course_admitted)
590            title = course_admitted.title
591            code = course_admitted.code
592            return '<a href="%s">%s - %s</a>' %(url,code,title)
593        return ''
594
595class PDFActionButton(ManageActionButton):
596    grok.context(IApplicant)
597    grok.require('waeup.viewApplication')
598    icon = 'actionicon_pdf.png'
599    text = 'Download application slip'
600    target = 'application_slip.pdf'
601
602class ExportPDFPage(grok.View):
603    """Deliver a PDF slip of the context.
604    """
605    grok.context(IApplicant)
606    grok.name('application_slip.pdf')
607    grok.require('waeup.viewApplication')
608    form_fields = grok.AutoFields(IApplicant).omit(
609        'locked').omit('course_admitted')
610    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
611    prefix = 'form'
612
613    @property
614    def label(self):
615        container_title = self.context.__parent__.title
616        return '%s Application Record' % container_title
617
618    def getCourseAdmitted(self):
619        """Return title and code in html format to the certificate
620           admitted.
621        """
622        course_admitted = self.context.course_admitted
623        if ICertificate.providedBy(course_admitted):
624            title = course_admitted.title
625            code = course_admitted.code
626            return '%s - %s' %(code,title)
627        return ''
628
629    def setUpWidgets(self, ignore_request=False):
630        self.adapters = {}
631        self.widgets = setUpEditWidgets(
632            self.form_fields, self.prefix, self.context, self.request,
633            adapters=self.adapters, for_display=True,
634            ignore_request=ignore_request
635            )
636
637    def render(self):
638        SLIP_STYLE = TableStyle(
639            [('VALIGN',(0,0),(-1,-1),'TOP')]
640            )
641
642        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
643        pdf.setTitle(self.label)
644        pdf.setSubject('Application')
645        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
646            self.request.principal.id))
647        pdf.setCreator('WAeUP SIRP')
648        width, height = A4
649        style = getSampleStyleSheet()
650        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
651
652        story = []
653        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
654        header_title = getattr(grok.getSite(), 'name', u'Sample University')
655        story.append(Paragraph(header_title, style["Heading1"]))
656        frame_header.addFromList(story,pdf)
657
658        story = []
659        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
660        story.append(Paragraph(self.label, style["Heading2"]))
661        story.append(Spacer(1, 18))
662        for msg in self.context.history.messages:
663            f_msg = '<font face="Courier" size=10>%s</font>' % msg
664            story.append(Paragraph(f_msg, style["Normal"]))
665        story.append(Spacer(1, 24))
666
667
668        # insert passport photograph
669        img = getUtility(IExtFileStore).getFileByContext(self.context)
670        if img is None:
671            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
672        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
673        story.append(doc_img)
674        story.append(Spacer(1, 18))
675
676        # render widget fields
677        data = []
678        self.setUpWidgets()
679        for widget in self.widgets:
680            f_label = '<font size=12>%s</font>:' % widget.label.strip()
681            f_label = Paragraph(f_label, style["Normal"])
682            f_text = '<font size=12>%s</font>' % widget()
683            f_text = Paragraph(f_text, style["Normal"])
684            data.append([f_label,f_text])
685        f_label = '<font size=12>Admitted Course of Study:</font>'
686        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
687        f_label = Paragraph(f_label, style["Normal"])
688        f_text = Paragraph(f_text, style["Normal"])
689        data.append([f_label,f_text])
690        table = Table(data,style=SLIP_STYLE)
691        story.append(table)
692        frame_body.addFromList(story,pdf)
693
694        story = []
695        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
696        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
697        f_text = '<font size=10>%s</font>' % timestamp
698        story.append(Paragraph(f_text, style["Normal"]))
699        frame_footer.addFromList(story,pdf)
700
701        self.response.setHeader(
702            'Content-Type', 'application/pdf')
703        return pdf.getpdfdata()
704
705class ApplicantManageActionButton(ManageActionButton):
706    grok.context(IApplicant)
707    grok.view(ApplicantDisplayFormPage)
708    grok.require('waeup.manageApplication')
709    text = 'Manage application record'
710    target = 'manage'
711
712
713def handle_img_upload(upload, context, view):
714    """Handle upload of applicant image.
715
716    Returns `True` in case of success or `False`.
717
718    Please note that file pointer passed in (`upload`) most probably
719    points to end of file when leaving this function.
720    """
721    size = file_size(upload)
722    if size > MAX_UPLOAD_SIZE:
723        view.flash('Uploaded image is too big!')
724        return False
725    upload.seek(0) # file pointer moved when determining size
726    store = getUtility(IExtFileStore)
727    file_id = IFileStoreNameChooser(context).chooseName()
728    store.createFile(file_id, upload)
729    return True
730
731class ApplicantManageFormPage(WAeUPEditFormPage):
732    """A full edit view for applicant data.
733    """
734    grok.context(IApplicant)
735    grok.name('manage')
736    grok.require('waeup.manageApplication')
737    form_fields = grok.AutoFields(IApplicant)
738    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
739    grok.template('applicanteditpage')
740    manage_applications = True
741    pnav = 3
742
743    def update(self):
744        datepicker.need() # Enable jQuery datepicker in date fields.
745        super(ApplicantManageFormPage, self).update()
746        self.wf_info = IWorkflowInfo(self.context)
747        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
748        self.passport_changed = None
749        upload = self.request.form.get('form.passport', None)
750        if upload:
751            # We got a fresh upload
752            self.passport_changed = handle_img_upload(
753                upload, self.context, self)
754        return
755
756    @property
757    def title(self):
758        return self.context.access_code
759
760    @property
761    def label(self):
762        container_title = self.context.__parent__.title
763        return '%s Application Form' % container_title
764
765    def getTransitions(self):
766        """Return a list of dicts of allowed transition ids and titles.
767
768        Each list entry provides keys ``name`` and ``title`` for
769        internal name and (human readable) title of a single
770        transition.
771        """
772        allowed_transitions = self.wf_info.getManualTransitions()
773        return [dict(name='', title='No transition')] +[
774            dict(name=x, title=y) for x, y in allowed_transitions]
775
776    @grok.action('Save')
777    def save(self, **data):
778        if self.passport_changed is False:  # False is not None!
779            return # error during image upload. Ignore other values
780        changed_fields = self.applyData(self.context, **data)
781        # Turn list of lists into single list
782        if changed_fields:
783            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
784        fields_string = ' + '.join(changed_fields)
785        if self.passport_changed:
786            fields_string += ' + passport'
787        #self.context._p_changed = True
788        form = self.request.form
789        trans_id = form.get('transition', None)
790        if trans_id:
791            self.wf_info.fireTransition(trans_id)
792        self.flash('Form has been saved.')
793        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
794        if fields_string:
795            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
796        return
797
798class ApplicantEditFormPage(ApplicantManageFormPage):
799    """An applicant-centered edit view for applicant data.
800    """
801    grok.context(IApplicantEdit)
802    grok.name('edit')
803    grok.require('waeup.handleApplication')
804    form_fields = grok.AutoFields(IApplicantEdit).omit(
805        'locked', 'course_admitted', 'student_id',
806        'screening_score',
807        )
808    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
809    grok.template('applicanteditpage')
810    manage_applications = False
811    title = u'Your Application Form'
812
813    def emit_lock_message(self):
814        self.flash('The requested form is locked (read-only).')
815        self.redirect(self.url(self.context))
816        return
817
818    def update(self):
819        if self.context.locked:
820            self.emit_lock_message()
821            return
822        datepicker.need() # Enable jQuery datepicker in date fields.
823        super(ApplicantEditFormPage, self).update()
824        return
825
826    def dataNotComplete(self):
827        if not self.request.form.get('confirm_passport', False):
828            return 'Passport confirmation box not ticked.'
829        return False
830
831    @grok.action('Save')
832    def save(self, **data):
833        if self.passport_changed is False:  # False is not None!
834            return # error during image upload. Ignore other values
835        self.applyData(self.context, **data)
836        #self.context._p_changed = True
837        self.flash('Form has been saved.')
838        return
839
840    @grok.action('Final Submit')
841    def finalsubmit(self, **data):
842        if self.passport_changed is False:  # False is not None!
843            return # error during image upload. Ignore other values
844        self.applyData(self.context, **data)
845        self.context._p_changed = True
846        if self.dataNotComplete():
847            self.flash(self.dataNotComplete())
848            return
849        state = IWorkflowState(self.context).getState()
850        # This shouldn't happen, but the application officer
851        # might have forgotten to lock the form after changing the state
852        if state != STARTED:
853            self.flash('This form cannot be submitted. Wrong state!')
854            return
855        IWorkflowInfo(self.context).fireTransition('submit')
856        self.context.application_date = datetime.now()
857        self.context.locked = True
858        self.flash('Form has been submitted.')
859        self.redirect(self.url(self.context))
860        return
861
862class ApplicantViewActionButton(ManageActionButton):
863    grok.context(IApplicant)
864    grok.view(ApplicantManageFormPage)
865    grok.require('waeup.manageApplication')
866    icon = 'actionicon_view.png'
867    text = 'View application record'
868    target = 'index'
869
870class PassportImage(grok.View):
871    """Renders the passport image for applicants.
872    """
873    grok.name('passport.jpg')
874    grok.context(IApplicant)
875    grok.require('waeup.viewApplication')
876
877    def render(self):
878        # A filename chooser turns a context into a filename suitable
879        # for file storage.
880        image = getUtility(IExtFileStore).getFileByContext(self.context)
881        self.response.setHeader(
882            'Content-Type', 'image/jpeg')
883        if image is None:
884            # show placeholder image
885            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
886        return image
Note: See TracBrowser for help on using the repository browser.