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
RevLine 
[5273]1## $Id: browser.py 7240 2011-11-30 23:13:26Z henrik $
[6078]2##
[7192]3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
[5273]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.
[6078]8##
[5273]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.
[6078]13##
[5273]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##
[5824]18"""UI components for basic applicants and related components.
[5273]19"""
[7063]20import os
[6082]21import sys
[5273]22import grok
23
[6816]24from datetime import datetime, date
25from zope.authentication.interfaces import ILogout, IAuthentication
[7240]26from zope.component import getUtility, createObject
[6081]27from zope.formlib.widget import CustomWidgetFactory
[6358]28from zope.formlib.form import setUpEditWidgets
[5937]29from zope.securitypolicy.interfaces import IPrincipalRoleManager
[6153]30from zope.traversing.browser import absoluteURL
[6081]31
[6303]32from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[6357]33from reportlab.pdfgen import canvas
[6364]34from reportlab.lib.units import cm
[6390]35from reportlab.lib.pagesizes import A4
[6364]36from reportlab.lib.styles import getSampleStyleSheet
37from reportlab.platypus import (Frame, Paragraph, Image,
38    Table, Spacer)
39from reportlab.platypus.tables import TableStyle
[6357]40
[6469]41from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
42from waeup.sirp.accesscodes.workflow import USED
[5273]43from waeup.sirp.browser import (
[6321]44    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
[6081]45from waeup.sirp.browser.breadcrumbs import Breadcrumb
[6103]46from waeup.sirp.browser.layout import NullValidator
[6321]47from waeup.sirp.browser.pages import add_local_role, del_local_roles
[6013]48from waeup.sirp.browser.resources import datepicker, tabs, datatable
[6153]49from waeup.sirp.browser.viewlets import (
50    ManageActionButton, PrimaryNavTab, LeftSidebarLink
51    )
[7063]52from waeup.sirp.interfaces import (
[7240]53    IWAeUPObject, ILocalRolesAssignable, IExtFileStore,
54    IFileStoreNameChooser, IPasswordValidator, IUserAccount)
[6321]55from waeup.sirp.permissions import get_users_with_local_roles
[7094]56from waeup.sirp.browser import DEFAULT_PASSPORT_IMAGE_PATH
[6254]57from waeup.sirp.university.interfaces import ICertificate
[7081]58from waeup.sirp.utils.helpers import string_from_bytes, file_size
[6054]59from waeup.sirp.widgets.datewidget import (
60    FriendlyDateWidget, FriendlyDateDisplayWidget)
[6084]61from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
[5303]62from waeup.sirp.widgets.objectwidget import (
[5301]63    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
[7240]64from waeup.sirp.applicants import Applicant, get_applicant_data
[6081]65from waeup.sirp.applicants.interfaces import (
[7240]66    IApplicant, IApplicantEdit, IApplicantsRoot,
[7063]67    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
[7094]68    MAX_UPLOAD_SIZE,
[5686]69    )
[6353]70from waeup.sirp.applicants.workflow import INITIALIZED, STARTED
[7240]71from waeup.sirp.students.viewlets import PrimaryStudentNavTab
[5320]72
[7240]73grok.context(IWAeUPObject) # Make IWAeUPObject the default context
[5273]74
[6067]75class ApplicantsRootPage(WAeUPPage):
[5822]76    grok.context(IApplicantsRoot)
77    grok.name('index')
[6153]78    grok.require('waeup.Public')
[5822]79    title = 'Applicants'
[6069]80    label = 'Application Section'
[5843]81    pnav = 3
[6012]82
83    def update(self):
[6067]84        super(ApplicantsRootPage, self).update()
[6012]85        datatable.need()
86        return
87
[5828]88class ManageApplicantsRootActionButton(ManageActionButton):
89    grok.context(IApplicantsRoot)
[6067]90    grok.view(ApplicantsRootPage)
[7136]91    grok.require('waeup.manageApplication')
[6069]92    text = 'Manage application section'
[5828]93
[6069]94class ApplicantsRootManageFormPage(WAeUPEditFormPage):
[5828]95    grok.context(IApplicantsRoot)
96    grok.name('manage')
[6107]97    grok.template('applicantsrootmanagepage')
[6069]98    title = 'Applicants'
99    label = 'Manage application section'
[5843]100    pnav = 3
[7136]101    grok.require('waeup.manageApplication')
[6069]102    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
[6184]103    tabtwoactions1 = ['Remove selected local roles']
104    tabtwoactions2 = ['Add local role']
[6069]105    subunits = 'Applicants Containers'
[6078]106
[6069]107    def update(self):
108        tabs.need()
[6108]109        datatable.need()
[6069]110        return super(ApplicantsRootManageFormPage, self).update()
[5828]111
[6184]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
[6069]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))
[6078]143        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
144        return
[5828]145
[6069]146    @grok.action('Add applicants container', validator=NullValidator)
147    def addApplicantsContainer(self, **data):
148        self.redirect(self.url(self.context, '@@add'))
[6078]149        return
150
[6069]151    @grok.action('Cancel', validator=NullValidator)
152    def cancel(self, **data):
153        self.redirect(self.url(self.context))
[6078]154        return
155
[6184]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
[6069]164class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
[5822]165    grok.context(IApplicantsRoot)
[7136]166    grok.require('waeup.manageApplication')
[5822]167    grok.name('add')
[6107]168    grok.template('applicantscontaineraddpage')
[6069]169    title = 'Applicants'
170    label = 'Add applicants container'
[5843]171    pnav = 3
[6078]172
[6103]173    form_fields = grok.AutoFields(
174        IApplicantsContainerAdd).omit('code').omit('title')
[6083]175    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
176    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[6078]177
[6083]178    def update(self):
179        datepicker.need() # Enable jQuery datepicker in date fields.
180        return super(ApplicantsContainerAddFormPage, self).update()
181
[6069]182    @grok.action('Add applicants container')
183    def addApplicantsContainer(self, **data):
[6103]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)
[6087]188        if code in self.context.keys():
[6105]189            self.flash(
190                'An applicants container for the same application '
191                'type and entrance year exists already in the database.')
[5822]192            return
193        # Add new applicants container...
[6083]194        provider = data['provider'][1]
[5822]195        container = provider.factory()
[6069]196        self.applyData(container, **data)
[6087]197        container.code = code
198        container.title = title
199        self.context[code] = container
[6105]200        self.flash('Added: "%s".' % code)
[6069]201        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
[5822]202        return
[6078]203
[6103]204    @grok.action('Cancel', validator=NullValidator)
[6069]205    def cancel(self, **data):
[6103]206        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
[6078]207
[5845]208class ApplicantsRootBreadcrumb(Breadcrumb):
209    """A breadcrumb for applicantsroot.
210    """
211    grok.context(IApplicantsRoot)
[6654]212    title = u'Applicants'
[6078]213
[5845]214class ApplicantsContainerBreadcrumb(Breadcrumb):
215    """A breadcrumb for applicantscontainers.
216    """
217    grok.context(IApplicantsContainer)
[6319]218
[6153]219class ApplicantBreadcrumb(Breadcrumb):
220    """A breadcrumb for applicants.
221    """
222    grok.context(IApplicant)
[6319]223
[6153]224    @property
225    def title(self):
226        """Get a title for a context.
227        """
[7240]228        return self.context.application_number
[5828]229
[7184]230class ApplicantsAuthTab(PrimaryNavTab):
[6153]231    """Applicants tab in primary navigation.
[5828]232    """
233    grok.context(IWAeUPObject)
234    grok.order(3)
[7240]235    grok.require('waeup.viewApplicationsTab')
[5828]236    grok.template('primarynavtab')
[5843]237    pnav = 3
[5828]238    tab_title = u'Applicants'
239
240    @property
241    def link_target(self):
242        return self.view.application_url('applicants')
243
[7184]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
[7240]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
[6029]281class ApplicantsContainerPage(WAeUPDisplayFormPage):
[5830]282    """The standard view for regular applicant containers.
283    """
284    grok.context(IApplicantsContainer)
285    grok.name('index')
[6153]286    grok.require('waeup.Public')
[6029]287    grok.template('applicantscontainerpage')
[5850]288    pnav = 3
[6053]289
[6105]290    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
[6054]291    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
292    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
[6084]293    form_fields['description'].custom_widget = ReSTDisplayWidget
[6053]294
[5837]295    @property
296    def title(self):
[6087]297        return "Applicants Container: %s" % self.context.title
[5837]298
299    @property
300    def label(self):
[6087]301        return self.context.title
[5830]302
[6107]303class ApplicantsContainerManageActionButton(ManageActionButton):
[6336]304    grok.order(1)
[5832]305    grok.context(IApplicantsContainer)
306    grok.view(ApplicantsContainerPage)
[7136]307    grok.require('waeup.manageApplication')
[6070]308    text = 'Manage applicants container'
[5832]309
[7240]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'
[5832]318
[6107]319class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
[5837]320    grok.context(IApplicantsContainer)
[5850]321    grok.name('manage')
[6107]322    grok.template('applicantscontainermanagepage')
[6105]323    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
324    taboneactions = ['Save','Cancel']
325    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
[6184]326    tabthreeactions1 = ['Remove selected local roles']
327    tabthreeactions2 = ['Add local role']
[5844]328    # Use friendlier date widget...
[6054]329    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
330    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[7136]331    grok.require('waeup.manageApplication')
[5850]332
333    @property
334    def title(self):
[6087]335        return "Applicants Container: %s" % self.context.title
[6078]336
[5850]337    @property
338    def label(self):
[6087]339        return 'Manage applicants container'
[5850]340
[5845]341    pnav = 3
[5837]342
343    def update(self):
[5850]344        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]345        tabs.need()
[6015]346        datatable.need()  # Enable jQurey datatables for contents listing
[6107]347        return super(ApplicantsContainerManageFormPage, self).update()
[5837]348
[6184]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
[5850]363    @grok.action('Save')
[5837]364    def apply(self, **data):
365        self.applyData(self.context, **data)
366        self.flash('Data saved.')
367        return
[6078]368
[6105]369    # ToDo: Show warning message before deletion
370    @grok.action('Remove selected')
371    def delApplicant(self, **data):
[6189]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
[6105]393
394    @grok.action('Add applicant', validator=NullValidator)
395    def addApplicant(self, **data):
[6327]396        self.redirect(self.url(self.context, 'addapplicant'))
397        return
[6105]398
399    @grok.action('Cancel', validator=NullValidator)
[5837]400    def cancel(self, **data):
401        self.redirect(self.url(self.context))
402        return
[5886]403
[6184]404    @grok.action('Add local role', validator=NullValidator)
405    def addLocalRole(self, **data):
406        return add_local_role(self,3, **data)
[6105]407
[6184]408    @grok.action('Remove selected local roles')
409    def delLocalRoles(self, **data):
410        return del_local_roles(self,3,**data)
411
[7240]412# Not used anymore
[7200]413class ApplicantLoginPage(WAeUPPage):
[5886]414    grok.context(IApplicantsContainer)
415    grok.name('login')
[6153]416    grok.require('waeup.Public')
[5886]417
[6110]418    @property
419    def title(self):
420        return u"Applicant Login: %s" % self.context.title
[6078]421
[5886]422    @property
423    def label(self):
[6724]424        return u"Login for '%s' applicants only" % self.context.title
[5886]425
426    pnav = 3
[6319]427
[6110]428    @property
429    def ac_prefix(self):
430        return self.context.ac_prefix
[6078]431
[5896]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)
[5886]435        if SUBMIT is None:
436            return
[5894]437        if self.request.principal.id == 'zope.anybody':
[6105]438            self.flash('Entered credentials are invalid.')
[5886]439            return
[7240]440#        if not IApplicantPrincipal.providedBy(self.request.principal):
441#            # Don't care if user is already authenticated as non-applicant
[5894]442            return
[6377]443
444        # From here we handle an applicant (not an officer browsing)
[5905]445        pin = self.request.principal.access_code
[6375]446
[6816]447        # If application has not yet started,
448        # logout without marking AC as used
[7224]449        if not self.context.startdate or self.context.startdate > date.today():
[6816]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
[7237]458        if not pin in self.context.keys() and (not self.context.enddate or
459                                        self.context.enddate < date.today()):
[6816]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)
[7063]467        if get_access_code(pin).state != USED:
[6471]468            comment = u"AC invalidated"
[6469]469            # Here we know that the ac is in state initialized so we do not
470            # expect an exception
[6471]471            invalidate_accesscode(pin,comment)
[6375]472
[6377]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
[6359]478
[6377]479        role_manager = IPrincipalRoleManager(self.context[pin])
[6394]480        role_manager.assignRoleToPrincipal(
481            'waeup.local.ApplicationOwner', self.request.principal.id)
[6359]482
[7166]483        # Assign current principal the global Applicant role
[6397]484        role_manager = IPrincipalRoleManager(grok.getSite())
[6394]485        role_manager.assignRoleToPrincipal(
[7166]486            'waeup.Applicant', self.request.principal.id)
[6377]487
488        # Mark application as started
[7240]489        #if IWorkflowState(self.context[pin]).getState() is INITIALIZED:
490        #    IWorkflowInfo(self.context[pin]).fireTransition('start')
[6377]491
[5937]492        self.redirect(self.url(self.context[pin], 'edit'))
[5886]493        return
[6319]494
[7240]495    def render(self):
496        return
497
[6327]498class ApplicantAddFormPage(WAeUPAddFormPage):
[6622]499    """Add-form to add an applicant.
[6327]500    """
501    grok.context(IApplicantsContainer)
[7136]502    grok.require('waeup.manageApplication')
[6327]503    grok.name('addapplicant')
[7240]504    #grok.template('applicantaddpage')
505    form_fields = grok.AutoFields(IApplicant).select(
506        'firstname', 'middlenames', 'lastname',
507        'email', 'phone')
[6327]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):
[7240]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'))
[6327]523        return
524
[7200]525class ApplicantDisplayFormPage(WAeUPDisplayFormPage):
[5273]526    grok.context(IApplicant)
527    grok.name('index')
[7113]528    grok.require('waeup.viewApplication')
[7200]529    grok.template('applicantdisplaypage')
[6320]530    form_fields = grok.AutoFields(IApplicant).omit(
[7240]531        'locked', 'course_admitted', 'password')
[6054]532    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[5273]533    label = 'Applicant'
[5843]534    pnav = 3
[5273]535
[7063]536    def update(self):
537        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]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')
[7063]541        return
542
[6196]543    @property
[7240]544    def hasPassword(self):
545        if self.context.password:
546            return 'set'
547        return 'unset'
548
549    @property
[6196]550    def title(self):
[7240]551        return 'Application Record %s' % self.context.application_number
[6196]552
553    @property
554    def label(self):
555        container_title = self.context.__parent__.title
[7240]556        return '%s Application Record %s' % (
557            container_title, self.context.application_number)
[6196]558
[6254]559    def getCourseAdmitted(self):
[6355]560        """Return link, title and code in html format to the certificate
561           admitted.
[6351]562        """
[6254]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
[6366]568            return '<a href="%s">%s - %s</a>' %(url,code,title)
[6457]569        return ''
[6254]570
[6358]571class PDFActionButton(ManageActionButton):
572    grok.context(IApplicant)
[7136]573    grok.require('waeup.viewApplication')
[6358]574    icon = 'actionicon_pdf.png'
[6367]575    text = 'Download application slip'
[6358]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')
[7136]583    grok.require('waeup.viewApplication')
[6358]584    form_fields = grok.AutoFields(IApplicant).omit(
[7240]585        'locked', 'course_admitted')
[6358]586    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
587    prefix = 'form'
588
[6363]589    @property
590    def label(self):
591        container_title = self.context.__parent__.title
[7240]592        return '%s Application Record %s' % (
593            container_title, self.context.application_number)
[6363]594
[6358]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
[6365]603            return '%s - %s' %(code,title)
[6462]604        return ''
[6358]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):
[6364]615        SLIP_STYLE = TableStyle(
616            [('VALIGN',(0,0),(-1,-1),'TOP')]
617            )
[6358]618
619        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
[6364]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')
[6358]625        width, height = A4
626        style = getSampleStyleSheet()
[6365]627        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
[6363]628
[6358]629        story = []
[6365]630        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
[6363]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 = []
[6365]636        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
[6364]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"]))
[6363]642        story.append(Spacer(1, 24))
[7063]643
[7068]644
[7063]645        # insert passport photograph
646        img = getUtility(IExtFileStore).getFileByContext(self.context)
647        if img is None:
[7089]648            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7063]649        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
[7068]650        story.append(doc_img)
651        story.append(Spacer(1, 18))
[7063]652
653        # render widget fields
[7068]654        data = []
[6358]655        self.setUpWidgets()
656        for widget in self.widgets:
[6462]657            f_label = '<font size=12>%s</font>:' % widget.label.strip()
[6363]658            f_label = Paragraph(f_label, style["Normal"])
[7063]659            f_text = '<font size=12>%s</font>' % widget()
660            f_text = Paragraph(f_text, style["Normal"])
661            data.append([f_label,f_text])
[6462]662        f_label = '<font size=12>Admitted Course of Study:</font>'
[6363]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])
[6364]667        table = Table(data,style=SLIP_STYLE)
[6363]668        story.append(table)
669        frame_body.addFromList(story,pdf)
670
[6364]671        story = []
[6365]672        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
[6364]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
[6358]678        self.response.setHeader(
679            'Content-Type', 'application/pdf')
680        return pdf.getpdfdata()
681
[6383]682class ApplicantManageActionButton(ManageActionButton):
[6198]683    grok.context(IApplicant)
[7200]684    grok.view(ApplicantDisplayFormPage)
[7136]685    grok.require('waeup.manageApplication')
[6383]686    text = 'Manage application record'
[7200]687    target = 'manage'
[6198]688
[7240]689class ApplicantEditActionButton(ManageActionButton):
690    grok.context(IApplicant)
691    grok.view(ApplicantDisplayFormPage)
692    grok.require('waeup.handleApplication')
693    text = 'Edit application record'
694    target ='edit'
[7081]695
[7240]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
[7081]704def handle_img_upload(upload, context, view):
[7063]705    """Handle upload of applicant image.
[7081]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.
[7063]711    """
[7081]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
[7063]717    store = getUtility(IExtFileStore)
718    file_id = IFileStoreNameChooser(context).chooseName()
719    store.createFile(file_id, upload)
[7081]720    return True
[7063]721
[7200]722class ApplicantManageFormPage(WAeUPEditFormPage):
[6196]723    """A full edit view for applicant data.
724    """
725    grok.context(IApplicant)
[7200]726    grok.name('manage')
[7136]727    grok.require('waeup.manageApplication')
[6476]728    form_fields = grok.AutoFields(IApplicant)
[6196]729    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]730    grok.template('applicanteditpage')
[6322]731    manage_applications = True
[6196]732    pnav = 3
733
734    def update(self):
735        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]736        super(ApplicantManageFormPage, self).update()
[6353]737        self.wf_info = IWorkflowInfo(self.context)
[7081]738        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]739        self.passport_changed = None
[6598]740        upload = self.request.form.get('form.passport', None)
741        if upload:
742            # We got a fresh upload
[7084]743            self.passport_changed = handle_img_upload(
744                upload, self.context, self)
[6196]745        return
746
747    @property
748    def title(self):
[7240]749        return 'Application Record %s' % self.context.application_number
[6196]750
751    @property
752    def label(self):
753        container_title = self.context.__parent__.title
[7240]754        return '%s Application Form %s' % (
755            container_title, self.context.application_number)
[6196]756
[6303]757    def getTransitions(self):
[6351]758        """Return a list of dicts of allowed transition ids and titles.
[6353]759
760        Each list entry provides keys ``name`` and ``title`` for
761        internal name and (human readable) title of a single
762        transition.
[6349]763        """
[6353]764        allowed_transitions = self.wf_info.getManualTransitions()
[6355]765        return [dict(name='', title='No transition')] +[
766            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]767
[6196]768    @grok.action('Save')
769    def save(self, **data):
[7240]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
[7084]779        if self.passport_changed is False:  # False is not None!
780            return # error during image upload. Ignore other values
[6475]781        changed_fields = self.applyData(self.context, **data)
[7199]782        # Turn list of lists into single list
783        if changed_fields:
784            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]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')
[7199]793        fields_string = ' + '.join(changed_fields)
[7085]794        trans_id = form.get('transition', None)
795        if trans_id:
796            self.wf_info.fireTransition(trans_id)
[6196]797        self.flash('Form has been saved.')
[6475]798        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[6644]799        if fields_string:
800            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6196]801        return
802
[7200]803class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]804    """An applicant-centered edit view for applicant data.
805    """
[6196]806    grok.context(IApplicantEdit)
[5273]807    grok.name('edit')
[6198]808    grok.require('waeup.handleApplication')
[6459]809    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]810        'locked', 'course_admitted', 'student_id',
[7240]811        'screening_score', 'applicant_id'
[6459]812        )
[6054]813    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]814    grok.template('applicanteditpage')
[6322]815    manage_applications = False
[6465]816    title = u'Your Application Form'
[5484]817
[7145]818    def emit_lock_message(self):
[6105]819        self.flash('The requested form is locked (read-only).')
[5941]820        self.redirect(self.url(self.context))
821        return
[6078]822
[5686]823    def update(self):
[5941]824        if self.context.locked:
[7145]825            self.emit_lock_message()
[5941]826            return
[6040]827        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]828        super(ApplicantEditFormPage, self).update()
[5686]829        return
[5952]830
[6196]831    def dataNotComplete(self):
[6322]832        if not self.request.form.get('confirm_passport', False):
[6196]833            return 'Passport confirmation box not ticked.'
834        return False
[5952]835
[5273]836    @grok.action('Save')
837    def save(self, **data):
[7084]838        if self.passport_changed is False:  # False is not None!
839            return # error during image upload. Ignore other values
[5273]840        self.applyData(self.context, **data)
[7094]841        #self.context._p_changed = True
[6196]842        self.flash('Form has been saved.')
[5273]843        return
844
[5484]845    @grok.action('Final Submit')
846    def finalsubmit(self, **data):
[7084]847        if self.passport_changed is False:  # False is not None!
848            return # error during image upload. Ignore other values
[5273]849        self.applyData(self.context, **data)
[5484]850        self.context._p_changed = True
[6196]851        if self.dataNotComplete():
852            self.flash(self.dataNotComplete())
[5941]853            return
[6303]854        state = IWorkflowState(self.context).getState()
[6322]855        # This shouldn't happen, but the application officer
856        # might have forgotten to lock the form after changing the state
[6303]857        if state != STARTED:
[6322]858            self.flash('This form cannot be submitted. Wrong state!')
[6303]859            return
860        IWorkflowInfo(self.context).fireTransition('submit')
[6476]861        self.context.application_date = datetime.now()
[5941]862        self.context.locked = True
[6196]863        self.flash('Form has been submitted.')
864        self.redirect(self.url(self.context))
[5273]865        return
[5941]866
[6367]867class ApplicantViewActionButton(ManageActionButton):
868    grok.context(IApplicant)
[7200]869    grok.view(ApplicantManageFormPage)
[7240]870    grok.require('waeup.viewApplication')
[6383]871    icon = 'actionicon_view.png'
[6367]872    text = 'View application record'
[6598]873    target = 'index'
[7063]874
875class PassportImage(grok.View):
876    """Renders the passport image for applicants.
877    """
878    grok.name('passport.jpg')
879    grok.context(IApplicant)
[7113]880    grok.require('waeup.viewApplication')
[7063]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
[7089]890            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]891        return image
Note: See TracBrowser for help on using the repository browser.