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

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

Define formatDatetime (for displaying datetime objects in data tables) centrally in layout.py.

  • Property svn:keywords set to Id
File size: 37.4 KB
RevLine 
[5273]1## $Id: browser.py 7254 2011-12-03 05:09:21Z 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
[7250]23from time import time
[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 (
[7250]60    FriendlyDateWidget, FriendlyDateDisplayWidget,
61    FriendlyDatetimeDisplayWidget)
[6084]62from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
[5303]63from waeup.sirp.widgets.objectwidget import (
[5301]64    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
[7240]65from waeup.sirp.applicants import Applicant, get_applicant_data
[6081]66from waeup.sirp.applicants.interfaces import (
[7240]67    IApplicant, IApplicantEdit, IApplicantsRoot,
[7063]68    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
[7250]69    MAX_UPLOAD_SIZE, IApplicantOnlinePayment,
[5686]70    )
[7250]71from waeup.sirp.applicants.workflow import INITIALIZED, STARTED, PAID
[7240]72from waeup.sirp.students.viewlets import PrimaryStudentNavTab
[7250]73from waeup.sirp.students.interfaces import IStudentsUtils
[5320]74
[7240]75grok.context(IWAeUPObject) # Make IWAeUPObject the default context
[5273]76
[6067]77class ApplicantsRootPage(WAeUPPage):
[5822]78    grok.context(IApplicantsRoot)
79    grok.name('index')
[6153]80    grok.require('waeup.Public')
[5822]81    title = 'Applicants'
[6069]82    label = 'Application Section'
[5843]83    pnav = 3
[6012]84
85    def update(self):
[6067]86        super(ApplicantsRootPage, self).update()
[6012]87        datatable.need()
88        return
89
[5828]90class ManageApplicantsRootActionButton(ManageActionButton):
91    grok.context(IApplicantsRoot)
[6067]92    grok.view(ApplicantsRootPage)
[7136]93    grok.require('waeup.manageApplication')
[6069]94    text = 'Manage application section'
[5828]95
[6069]96class ApplicantsRootManageFormPage(WAeUPEditFormPage):
[5828]97    grok.context(IApplicantsRoot)
98    grok.name('manage')
[6107]99    grok.template('applicantsrootmanagepage')
[6069]100    title = 'Applicants'
101    label = 'Manage application section'
[5843]102    pnav = 3
[7136]103    grok.require('waeup.manageApplication')
[6069]104    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
[6184]105    tabtwoactions1 = ['Remove selected local roles']
106    tabtwoactions2 = ['Add local role']
[6069]107    subunits = 'Applicants Containers'
[6078]108
[6069]109    def update(self):
110        tabs.need()
[6108]111        datatable.need()
[6069]112        return super(ApplicantsRootManageFormPage, self).update()
[5828]113
[6184]114    def getLocalRoles(self):
115        roles = ILocalRolesAssignable(self.context)
116        return roles()
117
118    def getUsers(self):
119        """Get a list of all users.
120        """
121        for key, val in grok.getSite()['users'].items():
122            url = self.url(val)
123            yield(dict(url=url, name=key, val=val))
124
125    def getUsersWithLocalRoles(self):
126        return get_users_with_local_roles(self.context)
127
[6069]128    # ToDo: Show warning message before deletion
129    @grok.action('Remove selected')
130    def delApplicantsContainers(self, **data):
131        form = self.request.form
132        child_id = form['val_id']
133        if not isinstance(child_id, list):
134            child_id = [child_id]
135        deleted = []
136        for id in child_id:
137            try:
138                del self.context[id]
139                deleted.append(id)
140            except:
141                self.flash('Could not delete %s: %s: %s' % (
142                        id, sys.exc_info()[0], sys.exc_info()[1]))
143        if len(deleted):
144            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6078]145        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
146        return
[5828]147
[6069]148    @grok.action('Add applicants container', validator=NullValidator)
149    def addApplicantsContainer(self, **data):
150        self.redirect(self.url(self.context, '@@add'))
[6078]151        return
152
[6069]153    @grok.action('Cancel', validator=NullValidator)
154    def cancel(self, **data):
155        self.redirect(self.url(self.context))
[6078]156        return
157
[6184]158    @grok.action('Add local role', validator=NullValidator)
159    def addLocalRole(self, **data):
160        return add_local_role(self,2, **data)
161
162    @grok.action('Remove selected local roles')
163    def delLocalRoles(self, **data):
164        return del_local_roles(self,2,**data)
165
[6069]166class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
[5822]167    grok.context(IApplicantsRoot)
[7136]168    grok.require('waeup.manageApplication')
[5822]169    grok.name('add')
[6107]170    grok.template('applicantscontaineraddpage')
[6069]171    title = 'Applicants'
172    label = 'Add applicants container'
[5843]173    pnav = 3
[6078]174
[6103]175    form_fields = grok.AutoFields(
176        IApplicantsContainerAdd).omit('code').omit('title')
[6083]177    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
178    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[6078]179
[6083]180    def update(self):
181        datepicker.need() # Enable jQuery datepicker in date fields.
182        return super(ApplicantsContainerAddFormPage, self).update()
183
[6069]184    @grok.action('Add applicants container')
185    def addApplicantsContainer(self, **data):
[6103]186        year = data['year']
187        code = u'%s%s' % (data['prefix'], year)
188        prefix = application_types_vocab.getTerm(data['prefix'])
189        title = u'%s %s/%s' % (prefix.title, year, year + 1)
[6087]190        if code in self.context.keys():
[6105]191            self.flash(
192                'An applicants container for the same application '
193                'type and entrance year exists already in the database.')
[5822]194            return
195        # Add new applicants container...
[6083]196        provider = data['provider'][1]
[5822]197        container = provider.factory()
[6069]198        self.applyData(container, **data)
[6087]199        container.code = code
200        container.title = title
201        self.context[code] = container
[6105]202        self.flash('Added: "%s".' % code)
[6069]203        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
[5822]204        return
[6078]205
[6103]206    @grok.action('Cancel', validator=NullValidator)
[6069]207    def cancel(self, **data):
[6103]208        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
[6078]209
[5845]210class ApplicantsRootBreadcrumb(Breadcrumb):
211    """A breadcrumb for applicantsroot.
212    """
213    grok.context(IApplicantsRoot)
[6654]214    title = u'Applicants'
[6078]215
[5845]216class ApplicantsContainerBreadcrumb(Breadcrumb):
217    """A breadcrumb for applicantscontainers.
218    """
219    grok.context(IApplicantsContainer)
[6319]220
[6153]221class ApplicantBreadcrumb(Breadcrumb):
222    """A breadcrumb for applicants.
223    """
224    grok.context(IApplicant)
[6319]225
[6153]226    @property
227    def title(self):
228        """Get a title for a context.
229        """
[7240]230        return self.context.application_number
[5828]231
[7250]232class OnlinePaymentBreadcrumb(Breadcrumb):
233    """A breadcrumb for payments.
234    """
235    grok.context(IApplicantOnlinePayment)
236
237    @property
238    def title(self):
239        return self.context.p_id
240
[7184]241class ApplicantsAuthTab(PrimaryNavTab):
[6153]242    """Applicants tab in primary navigation.
[5828]243    """
244    grok.context(IWAeUPObject)
245    grok.order(3)
[7250]246    grok.require('waeup.viewApplicantsTab')
[5843]247    pnav = 3
[5828]248    tab_title = u'Applicants'
249
250    @property
251    def link_target(self):
252        return self.view.application_url('applicants')
253
[7184]254class ApplicantsAnonTab(ApplicantsAuthTab):
255    """Applicants tab in primary navigation.
256
257    Display tab only for anonymous. Authenticated users can call the
258    form from the user navigation bar.
259    """
260    grok.require('waeup.Anonymous')
261    tab_title = u'Application'
262
263    # Also zope.manager has role Anonymous.
[7243]264    # To avoid displaying this tab, we have to check the principal id too.
265    @property
266    def link_target(self):
267        if self.request.principal.id == 'zope.anybody':
268            return self.view.application_url('applicants')
269        return
[7184]270
[7240]271class MyApplicationDataTab(PrimaryStudentNavTab):
272    """MyData-tab in primary navigation.
273    """
274    grok.order(3)
275    grok.require('waeup.viewMyApplicationDataTab')
276    pnav = 3
277    tab_title = u'My Data'
278
279    @property
280    def link_target(self):
281        try:
282            container, application_number = self.request.principal.id.split('_')
283        except ValueError:
284            return
285        rel_link = '/applicants/%s/%s' % (container, application_number)
286        return self.view.application_url() + rel_link
287
[6029]288class ApplicantsContainerPage(WAeUPDisplayFormPage):
[5830]289    """The standard view for regular applicant containers.
290    """
291    grok.context(IApplicantsContainer)
292    grok.name('index')
[6153]293    grok.require('waeup.Public')
[6029]294    grok.template('applicantscontainerpage')
[5850]295    pnav = 3
[6053]296
[6105]297    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
[6054]298    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
299    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
[6084]300    form_fields['description'].custom_widget = ReSTDisplayWidget
[6053]301
[5837]302    @property
303    def title(self):
[6087]304        return "Applicants Container: %s" % self.context.title
[5837]305
306    @property
307    def label(self):
[6087]308        return self.context.title
[5830]309
[6107]310class ApplicantsContainerManageActionButton(ManageActionButton):
[6336]311    grok.order(1)
[5832]312    grok.context(IApplicantsContainer)
313    grok.view(ApplicantsContainerPage)
[7136]314    grok.require('waeup.manageApplication')
[6070]315    text = 'Manage applicants container'
[5832]316
[7240]317#class ApplicantLoginActionButton(ManageActionButton):
318#    grok.order(2)
319#    grok.context(IApplicantsContainer)
320#    grok.view(ApplicantsContainerPage)
321#    grok.require('waeup.Anonymous')
322#    icon = 'login.png'
323#    text = 'Login for applicants'
324#    target = 'login'
[5832]325
[6107]326class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
[5837]327    grok.context(IApplicantsContainer)
[5850]328    grok.name('manage')
[6107]329    grok.template('applicantscontainermanagepage')
[6105]330    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
331    taboneactions = ['Save','Cancel']
332    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
[6184]333    tabthreeactions1 = ['Remove selected local roles']
334    tabthreeactions2 = ['Add local role']
[5844]335    # Use friendlier date widget...
[6054]336    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
337    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[7136]338    grok.require('waeup.manageApplication')
[5850]339
340    @property
341    def title(self):
[6087]342        return "Applicants Container: %s" % self.context.title
[6078]343
[5850]344    @property
345    def label(self):
[6087]346        return 'Manage applicants container'
[5850]347
[5845]348    pnav = 3
[5837]349
350    def update(self):
[5850]351        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]352        tabs.need()
[6015]353        datatable.need()  # Enable jQurey datatables for contents listing
[6107]354        return super(ApplicantsContainerManageFormPage, self).update()
[5837]355
[6184]356    def getLocalRoles(self):
357        roles = ILocalRolesAssignable(self.context)
358        return roles()
359
360    def getUsers(self):
361        """Get a list of all users.
362        """
363        for key, val in grok.getSite()['users'].items():
364            url = self.url(val)
365            yield(dict(url=url, name=key, val=val))
366
367    def getUsersWithLocalRoles(self):
368        return get_users_with_local_roles(self.context)
369
[5850]370    @grok.action('Save')
[5837]371    def apply(self, **data):
372        self.applyData(self.context, **data)
373        self.flash('Data saved.')
374        return
[6078]375
[6105]376    # ToDo: Show warning message before deletion
377    @grok.action('Remove selected')
378    def delApplicant(self, **data):
[6189]379        form = self.request.form
380        if form.has_key('val_id'):
381            child_id = form['val_id']
382        else:
383            self.flash('No applicant selected!')
384            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
385            return
386        if not isinstance(child_id, list):
387            child_id = [child_id]
388        deleted = []
389        for id in child_id:
390            try:
391                del self.context[id]
392                deleted.append(id)
393            except:
394                self.flash('Could not delete %s: %s: %s' % (
395                        id, sys.exc_info()[0], sys.exc_info()[1]))
396        if len(deleted):
397            self.flash('Successfully removed: %s' % ', '.join(deleted))
398        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
399        return
[6105]400
401    @grok.action('Add applicant', validator=NullValidator)
402    def addApplicant(self, **data):
[6327]403        self.redirect(self.url(self.context, 'addapplicant'))
404        return
[6105]405
406    @grok.action('Cancel', validator=NullValidator)
[5837]407    def cancel(self, **data):
408        self.redirect(self.url(self.context))
409        return
[5886]410
[6184]411    @grok.action('Add local role', validator=NullValidator)
412    def addLocalRole(self, **data):
413        return add_local_role(self,3, **data)
[6105]414
[6184]415    @grok.action('Remove selected local roles')
416    def delLocalRoles(self, **data):
417        return del_local_roles(self,3,**data)
418
[6327]419class ApplicantAddFormPage(WAeUPAddFormPage):
[6622]420    """Add-form to add an applicant.
[6327]421    """
422    grok.context(IApplicantsContainer)
[7136]423    grok.require('waeup.manageApplication')
[6327]424    grok.name('addapplicant')
[7240]425    #grok.template('applicantaddpage')
426    form_fields = grok.AutoFields(IApplicant).select(
427        'firstname', 'middlenames', 'lastname',
428        'email', 'phone')
[6327]429    title = 'Applicants'
430    label = 'Add applicant'
431    pnav = 3
432
433    @property
434    def title(self):
435        return "Applicants Container: %s" % self.context.title
436
437    @grok.action('Create application record')
438    def addApplicant(self, **data):
[7240]439        applicant = createObject(u'waeup.Applicant', container = self.context)
440        self.applyData(applicant, **data)
441        self.context.addApplicant(applicant)
442        self.flash('Applicant record created.')
443        self.redirect(self.url(self.context[applicant.application_number], 'index'))
[6327]444        return
445
[7200]446class ApplicantDisplayFormPage(WAeUPDisplayFormPage):
[5273]447    grok.context(IApplicant)
448    grok.name('index')
[7113]449    grok.require('waeup.viewApplication')
[7200]450    grok.template('applicantdisplaypage')
[6320]451    form_fields = grok.AutoFields(IApplicant).omit(
[7240]452        'locked', 'course_admitted', 'password')
[6054]453    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[5273]454    label = 'Applicant'
[5843]455    pnav = 3
[5273]456
[7063]457    def update(self):
458        self.passport_url = self.url(self.context, 'passport.jpg')
[7240]459        # Mark application as started if applicant logs in for the first time
460        if IWorkflowState(self.context).getState() == INITIALIZED:
461            IWorkflowInfo(self.context).fireTransition('start')
[7063]462        return
463
[6196]464    @property
[7240]465    def hasPassword(self):
466        if self.context.password:
467            return 'set'
468        return 'unset'
469
470    @property
[6196]471    def title(self):
[7240]472        return 'Application Record %s' % self.context.application_number
[6196]473
474    @property
475    def label(self):
476        container_title = self.context.__parent__.title
[7240]477        return '%s Application Record %s' % (
478            container_title, self.context.application_number)
[6196]479
[6254]480    def getCourseAdmitted(self):
[6355]481        """Return link, title and code in html format to the certificate
482           admitted.
[6351]483        """
[6254]484        course_admitted = self.context.course_admitted
485        if ICertificate.providedBy(course_admitted):
486            url = self.url(course_admitted)
487            title = course_admitted.title
488            code = course_admitted.code
[6366]489            return '<a href="%s">%s - %s</a>' %(url,code,title)
[6457]490        return ''
[6254]491
[7250]492class AcceptanceFeePaymentAddPage(grok.View):
493    """ Page to add an online payment ticket
494    """
495    grok.context(IApplicant)
496    grok.name('addafp')
497    grok.require('waeup.payApplicant')
498
499    def update(self):
500        p_category = 'acceptance'
501        d = {}
502        session = str(self.context.__parent__.year)
503        try:
504            academic_session = grok.getSite()['configuration'][session]
505        except KeyError:
506            self.flash('Session configuration object is not available.')
507            return
508        timestamp = "%d" % int(time()*1000)
509        #order_id = "%s%s" % (student_id[1:],timestamp)
510        for key in self.context.keys():
511            ticket = self.context[key]
512            if ticket.p_state == 'paid':
513                  self.flash(
514                      'This type of payment has already been made.')
515                  self.redirect(self.url(self.context))
516                  return
517        payment = createObject(u'waeup.ApplicantOnlinePayment')
518        payment.p_id = "p%s" % timestamp
519        payment.p_item = self.context.__parent__.title
520        payment.p_year = self.context.__parent__.year
521        payment.p_category = p_category
522        payment.amount_auth = academic_session.acceptance_fee
523        payment.surcharge_1 = academic_session.surcharge_1
524        payment.surcharge_2 = academic_session.surcharge_2
525        payment.surcharge_3 = academic_session.surcharge_3
526        self.context[payment.p_id] = payment
527        self.flash('Payment ticket created.')
528        return
529
530    def render(self):
531        usertype = getattr(self.request.principal, 'user_type', None)
532        if usertype == 'applicant':
533            self.redirect(self.url(self.context, '@@edit'))
534            return
535        self.redirect(self.url(self.context, '@@manage'))
536        return
537
538
539class OnlinePaymentDisplayFormPage(WAeUPDisplayFormPage):
540    """ Page to view an online payment ticket
541    """
542    grok.context(IApplicantOnlinePayment)
543    grok.name('index')
544    grok.require('waeup.viewApplication')
545    form_fields = grok.AutoFields(IApplicantOnlinePayment)
546    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
547    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
548    pnav = 3
549
550    @property
551    def title(self):
552        return 'Online Payment Ticket %s' % self.context.p_id
553
554    @property
555    def label(self):
556        return '%s: Online Payment Ticket %s' % (
557            self.context.__parent__.fullname,self.context.p_id)
558
559class PaymentReceiptActionButton(ManageActionButton):
560    grok.order(1)
561    grok.context(IApplicantOnlinePayment)
562    grok.view(OnlinePaymentDisplayFormPage)
563    grok.require('waeup.viewApplication')
564    icon = 'actionicon_pdf.png'
565    text = 'Download payment receipt'
566    target = 'payment_receipt.pdf'
567
568    @property
569    def target_url(self):
570        if self.context.p_state != 'paid':
571            return ''
572        return self.view.url(self.view.context, self.target)
573
574class RequestCallbackActionButton(ManageActionButton):
575    grok.order(2)
576    grok.context(IApplicantOnlinePayment)
577    grok.view(OnlinePaymentDisplayFormPage)
578    grok.require('waeup.payApplicant')
579    icon = 'actionicon_call.png'
580    text = 'Request callback'
581    target = 'callback'
582
583    @property
584    def target_url(self):
585        if self.context.p_state != 'unpaid':
586            return ''
587        return self.view.url(self.view.context, self.target)
588
589class OnlinePaymentCallbackPage(grok.View):
590    """ Callback view
591    """
592    grok.context(IApplicantOnlinePayment)
593    grok.name('callback')
594    grok.require('waeup.payApplicant')
595
596    # This update method simulates a valid callback und must be
597    # specified in the customization package. The parameters must be taken
598    # from the incoming request.
599    def update(self):
600        if self.context.p_state == 'paid':
601            self.flash('This ticket has already been paid.')
602            return
603        self.context.r_amount_approved = self.context.amount_auth
604        self.context.r_card_num = u'0000'
605        self.context.r_code = u'00'
606        self.context.p_state = 'paid'
607        self.context.payment_date = datetime.now()
608        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
609        self.context.__parent__.loggerInfo(
610            ob_class, 'valid callback: %s' % self.context.p_id)
611        self.wf_info = IWorkflowInfo(self.context.__parent__)
612        self.wf_info.fireTransition('pay')
613        self.flash('Valid callback received.')
614        return
615
616    def render(self):
617        self.redirect(self.url(self.context, '@@index'))
618        return
619
620class ExportPDFPaymentSlipPage(grok.View):
621    """Deliver a PDF slip of the context.
622    """
623    grok.context(IApplicantOnlinePayment)
624    grok.name('payment_receipt.pdf')
625    grok.require('waeup.viewApplication')
626    form_fields = grok.AutoFields(IApplicantOnlinePayment)
627    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
628    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
629    prefix = 'form'
630
631    @property
632    def label(self):
633        return 'Online Payment Receipt %s' % self.context.p_id
634
635    def render(self):
636        if self.context.p_state != 'paid':
637            self.flash('Ticket not yet paid.')
638            self.redirect(self.url(self.context))
639            return
640        applicantview = ApplicantDisplayFormPage(self.context.__parent__,
641            self.request)
642        students_utils = getUtility(IStudentsUtils)
643        return students_utils.renderPDF(self,'Payment', 'payment_receipt.pdf',
644            self.context.__parent__, applicantview)
645
[6358]646class PDFActionButton(ManageActionButton):
647    grok.context(IApplicant)
[7136]648    grok.require('waeup.viewApplication')
[6358]649    icon = 'actionicon_pdf.png'
[6367]650    text = 'Download application slip'
[6358]651    target = 'application_slip.pdf'
652
653class ExportPDFPage(grok.View):
654    """Deliver a PDF slip of the context.
655    """
656    grok.context(IApplicant)
657    grok.name('application_slip.pdf')
[7136]658    grok.require('waeup.viewApplication')
[6358]659    form_fields = grok.AutoFields(IApplicant).omit(
[7240]660        'locked', 'course_admitted')
[6358]661    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
662    prefix = 'form'
663
[6363]664    @property
665    def label(self):
666        container_title = self.context.__parent__.title
[7240]667        return '%s Application Record %s' % (
668            container_title, self.context.application_number)
[6363]669
[6358]670    def getCourseAdmitted(self):
671        """Return title and code in html format to the certificate
672           admitted.
673        """
674        course_admitted = self.context.course_admitted
675        if ICertificate.providedBy(course_admitted):
676            title = course_admitted.title
677            code = course_admitted.code
[6365]678            return '%s - %s' %(code,title)
[6462]679        return ''
[6358]680
681    def setUpWidgets(self, ignore_request=False):
682        self.adapters = {}
683        self.widgets = setUpEditWidgets(
684            self.form_fields, self.prefix, self.context, self.request,
685            adapters=self.adapters, for_display=True,
686            ignore_request=ignore_request
687            )
688
689    def render(self):
[6364]690        SLIP_STYLE = TableStyle(
691            [('VALIGN',(0,0),(-1,-1),'TOP')]
692            )
[6358]693
694        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
[6364]695        pdf.setTitle(self.label)
696        pdf.setSubject('Application')
697        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
698            self.request.principal.id))
699        pdf.setCreator('WAeUP SIRP')
[6358]700        width, height = A4
701        style = getSampleStyleSheet()
[6365]702        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
[6363]703
[6358]704        story = []
[6365]705        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
[6363]706        header_title = getattr(grok.getSite(), 'name', u'Sample University')
707        story.append(Paragraph(header_title, style["Heading1"]))
708        frame_header.addFromList(story,pdf)
709
710        story = []
[6365]711        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
[6364]712        story.append(Paragraph(self.label, style["Heading2"]))
713        story.append(Spacer(1, 18))
714        for msg in self.context.history.messages:
715            f_msg = '<font face="Courier" size=10>%s</font>' % msg
716            story.append(Paragraph(f_msg, style["Normal"]))
[6363]717        story.append(Spacer(1, 24))
[7063]718
[7068]719
[7063]720        # insert passport photograph
721        img = getUtility(IExtFileStore).getFileByContext(self.context)
722        if img is None:
[7089]723            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7063]724        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
[7068]725        story.append(doc_img)
726        story.append(Spacer(1, 18))
[7063]727
728        # render widget fields
[7068]729        data = []
[6358]730        self.setUpWidgets()
731        for widget in self.widgets:
[6462]732            f_label = '<font size=12>%s</font>:' % widget.label.strip()
[6363]733            f_label = Paragraph(f_label, style["Normal"])
[7063]734            f_text = '<font size=12>%s</font>' % widget()
735            f_text = Paragraph(f_text, style["Normal"])
736            data.append([f_label,f_text])
[6462]737        f_label = '<font size=12>Admitted Course of Study:</font>'
[6363]738        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
739        f_label = Paragraph(f_label, style["Normal"])
740        f_text = Paragraph(f_text, style["Normal"])
741        data.append([f_label,f_text])
[6364]742        table = Table(data,style=SLIP_STYLE)
[6363]743        story.append(table)
744        frame_body.addFromList(story,pdf)
745
[6364]746        story = []
[6365]747        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
[6364]748        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
749        f_text = '<font size=10>%s</font>' % timestamp
750        story.append(Paragraph(f_text, style["Normal"]))
751        frame_footer.addFromList(story,pdf)
752
[6358]753        self.response.setHeader(
754            'Content-Type', 'application/pdf')
755        return pdf.getpdfdata()
756
[6383]757class ApplicantManageActionButton(ManageActionButton):
[6198]758    grok.context(IApplicant)
[7200]759    grok.view(ApplicantDisplayFormPage)
[7136]760    grok.require('waeup.manageApplication')
[6383]761    text = 'Manage application record'
[7200]762    target = 'manage'
[6198]763
[7240]764class ApplicantEditActionButton(ManageActionButton):
765    grok.context(IApplicant)
766    grok.view(ApplicantDisplayFormPage)
767    grok.require('waeup.handleApplication')
768    text = 'Edit application record'
769    target ='edit'
[7081]770
[7240]771    @property
772    def target_url(self):
773        """Get a URL to the target...
774        """
775        if self.context.locked:
776            return
777        return self.view.url(self.view.context, self.target)
778
[7081]779def handle_img_upload(upload, context, view):
[7063]780    """Handle upload of applicant image.
[7081]781
782    Returns `True` in case of success or `False`.
783
784    Please note that file pointer passed in (`upload`) most probably
785    points to end of file when leaving this function.
[7063]786    """
[7081]787    size = file_size(upload)
788    if size > MAX_UPLOAD_SIZE:
789        view.flash('Uploaded image is too big!')
790        return False
[7247]791    dummy, ext = os.path.splitext(upload.filename)
792    ext.lower()
793    if ext != '.jpg':
794        view.flash('jpg file extension expected.')
795        return False
[7081]796    upload.seek(0) # file pointer moved when determining size
[7063]797    store = getUtility(IExtFileStore)
798    file_id = IFileStoreNameChooser(context).chooseName()
799    store.createFile(file_id, upload)
[7081]800    return True
[7063]801
[7200]802class ApplicantManageFormPage(WAeUPEditFormPage):
[6196]803    """A full edit view for applicant data.
804    """
805    grok.context(IApplicant)
[7200]806    grok.name('manage')
[7136]807    grok.require('waeup.manageApplication')
[6476]808    form_fields = grok.AutoFields(IApplicant)
[6196]809    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]810    grok.template('applicanteditpage')
[6322]811    manage_applications = True
[6196]812    pnav = 3
[7250]813    display_actions = [['Save', 'Final Submit'],
814                       ['Add online payment ticket','Remove selected tickets']]
[6196]815
816    def update(self):
817        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]818        super(ApplicantManageFormPage, self).update()
[6353]819        self.wf_info = IWorkflowInfo(self.context)
[7081]820        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]821        self.passport_changed = None
[6598]822        upload = self.request.form.get('form.passport', None)
823        if upload:
824            # We got a fresh upload
[7084]825            self.passport_changed = handle_img_upload(
826                upload, self.context, self)
[6196]827        return
828
829    @property
830    def title(self):
[7240]831        return 'Application Record %s' % self.context.application_number
[6196]832
833    @property
834    def label(self):
835        container_title = self.context.__parent__.title
[7240]836        return '%s Application Form %s' % (
837            container_title, self.context.application_number)
[6196]838
[6303]839    def getTransitions(self):
[6351]840        """Return a list of dicts of allowed transition ids and titles.
[6353]841
842        Each list entry provides keys ``name`` and ``title`` for
843        internal name and (human readable) title of a single
844        transition.
[6349]845        """
[6353]846        allowed_transitions = self.wf_info.getManualTransitions()
[6355]847        return [dict(name='', title='No transition')] +[
848            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]849
[6196]850    @grok.action('Save')
851    def save(self, **data):
[7240]852        form = self.request.form
853        password = form.get('password', None)
854        password_ctl = form.get('control_password', None)
855        if password:
856            validator = getUtility(IPasswordValidator)
857            errors = validator.validate_password(password, password_ctl)
858            if errors:
859                self.flash( ' '.join(errors))
860                return
[7084]861        if self.passport_changed is False:  # False is not None!
862            return # error during image upload. Ignore other values
[6475]863        changed_fields = self.applyData(self.context, **data)
[7199]864        # Turn list of lists into single list
865        if changed_fields:
866            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]867        else:
868            changed_fields = []
869        if self.passport_changed:
870            changed_fields.append('passport')
871        if password:
872            # Now we know that the form has no errors and can set password ...
873            IUserAccount(self.context).setPassword(password)
874            changed_fields.append('password')
[7199]875        fields_string = ' + '.join(changed_fields)
[7085]876        trans_id = form.get('transition', None)
877        if trans_id:
878            self.wf_info.fireTransition(trans_id)
[6196]879        self.flash('Form has been saved.')
[6475]880        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[6644]881        if fields_string:
882            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6196]883        return
884
[7250]885    def unremovable(self, ticket):
886        usertype = getattr(self.request.principal, 'user_type', None)
887        if not usertype:
888            return False
889        return self.request.principal.user_type == 'applicant' and ticket.r_code
890
891    # This method is also used by the ApplicantEditFormPage
892    def delPaymentTickets(self, **data):
893        form = self.request.form
894        if form.has_key('val_id'):
895            child_id = form['val_id']
896        else:
897            self.flash('No payment selected.')
898            self.redirect(self.url(self.context))
899            return
900        if not isinstance(child_id, list):
901            child_id = [child_id]
902        deleted = []
903        for id in child_id:
904            # Applicants are not allowed to remove used payment tickets
905            if not self.unremovable(self.context[id]):
906                try:
907                    del self.context[id]
908                    deleted.append(id)
909                except:
910                    self.flash('Could not delete %s: %s: %s' % (
911                            id, sys.exc_info()[0], sys.exc_info()[1]))
912        if len(deleted):
913            self.flash('Successfully removed: %s' % ', '.join(deleted))
914            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
915            self.context.loggerInfo(ob_class, 'removed: % s' % ', '.join(deleted))
916        return
917
[7252]918    # We explicitely want the forms to be validated before payment tickets
919    # can be created. If no validation is requested, use
920    # 'validator=NullValidator' in the grok.action directive
[7250]921    @grok.action('Add online payment ticket')
922    def addPaymentTicket(self, **data):
923        self.redirect(self.url(self.context, '@@addafp'))
[7252]924        return
[7250]925
926    @grok.action('Remove selected tickets')
927    def removePaymentTickets(self, **data):
928        self.delPaymentTickets(**data)
929        self.redirect(self.url(self.context) + '/@@manage')
930        return
931
[7200]932class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]933    """An applicant-centered edit view for applicant data.
934    """
[6196]935    grok.context(IApplicantEdit)
[5273]936    grok.name('edit')
[6198]937    grok.require('waeup.handleApplication')
[6459]938    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]939        'locked', 'course_admitted', 'student_id',
[7240]940        'screening_score', 'applicant_id'
[6459]941        )
[6054]942    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7200]943    grok.template('applicanteditpage')
[6322]944    manage_applications = False
[6465]945    title = u'Your Application Form'
[5484]946
[7250]947    @property
948    def display_actions(self):
949        state = IWorkflowState(self.context).getState()
950        if state == INITIALIZED:
951            actions = [[],[]]
952        elif state == STARTED:
953            actions = [['Save'],
954                       ['Add online payment ticket','Remove selected tickets']]
955        elif state == PAID:
956            actions = [['Save', 'Final Submit'],
957                       ['Remove selected tickets']]
958        elif state == SUBMITTED:
959            actions = [[],[]]
960        return actions
961
[7145]962    def emit_lock_message(self):
[6105]963        self.flash('The requested form is locked (read-only).')
[5941]964        self.redirect(self.url(self.context))
965        return
[6078]966
[5686]967    def update(self):
[5941]968        if self.context.locked:
[7145]969            self.emit_lock_message()
[5941]970            return
[6040]971        datepicker.need() # Enable jQuery datepicker in date fields.
[7200]972        super(ApplicantEditFormPage, self).update()
[5686]973        return
[5952]974
[6196]975    def dataNotComplete(self):
[7252]976        store = getUtility(IExtFileStore)
977        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
978            return 'No passport picture uploaded.'
[6322]979        if not self.request.form.get('confirm_passport', False):
[7252]980            return 'Passport picture confirmation box not ticked.'
[6196]981        return False
[5952]982
[7252]983    # We explicitely want the forms to be validated before payment tickets
984    # can be created. If no validation is requested, use
985    # 'validator=NullValidator' in the grok.action directive
[7250]986    @grok.action('Add online payment ticket')
987    def addPaymentTicket(self, **data):
988        self.redirect(self.url(self.context, '@@addafp'))
[7252]989        return
[7250]990
991    @grok.action('Remove selected tickets')
992    def removePaymentTickets(self, **data):
993        self.delPaymentTickets(**data)
994        self.redirect(self.url(self.context) + '/@@edit')
995        return
996
[5273]997    @grok.action('Save')
998    def save(self, **data):
[7252]999        #import pdb; pdb.set_trace()
[7084]1000        if self.passport_changed is False:  # False is not None!
1001            return # error during image upload. Ignore other values
[5273]1002        self.applyData(self.context, **data)
[6196]1003        self.flash('Form has been saved.')
[5273]1004        return
1005
[5484]1006    @grok.action('Final Submit')
1007    def finalsubmit(self, **data):
[7084]1008        if self.passport_changed is False:  # False is not None!
1009            return # error during image upload. Ignore other values
[6196]1010        if self.dataNotComplete():
1011            self.flash(self.dataNotComplete())
[5941]1012            return
[7252]1013        self.applyData(self.context, **data)
[6303]1014        state = IWorkflowState(self.context).getState()
[6322]1015        # This shouldn't happen, but the application officer
1016        # might have forgotten to lock the form after changing the state
[7250]1017        if state != PAID:
[6322]1018            self.flash('This form cannot be submitted. Wrong state!')
[6303]1019            return
1020        IWorkflowInfo(self.context).fireTransition('submit')
[6476]1021        self.context.application_date = datetime.now()
[5941]1022        self.context.locked = True
[6196]1023        self.flash('Form has been submitted.')
1024        self.redirect(self.url(self.context))
[5273]1025        return
[5941]1026
[6367]1027class ApplicantViewActionButton(ManageActionButton):
1028    grok.context(IApplicant)
[7200]1029    grok.view(ApplicantManageFormPage)
[7240]1030    grok.require('waeup.viewApplication')
[6383]1031    icon = 'actionicon_view.png'
[6367]1032    text = 'View application record'
[6598]1033    target = 'index'
[7063]1034
1035class PassportImage(grok.View):
1036    """Renders the passport image for applicants.
1037    """
1038    grok.name('passport.jpg')
1039    grok.context(IApplicant)
[7113]1040    grok.require('waeup.viewApplication')
[7063]1041
1042    def render(self):
1043        # A filename chooser turns a context into a filename suitable
1044        # for file storage.
1045        image = getUtility(IExtFileStore).getFileByContext(self.context)
1046        self.response.setHeader(
1047            'Content-Type', 'image/jpeg')
1048        if image is None:
1049            # show placeholder image
[7089]1050            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1051        return image
Note: See TracBrowser for help on using the repository browser.