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

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

Rebuild applicants package (1st part). Applicants now have an applicant_id and a password and can use the regular login page to enter the portal.

Add user_type attribute to SIRPPrincipal objects.

Add some permissions in students package.

Some tests are still missing and will be re-added soon.

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