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

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

Tune handle_img_upload: we check file size now and emit a flash
message when there is a problem with it. More tests could happen here,
for instance for file type, etc.

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