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

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

Log applicant_id not access_code.

Histories should contain real names not user ids.

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