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

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

Define send_mail function and move sendCredentials code from browser.py to utils.py.

  • Property svn:keywords set to Id
File size: 38.0 KB
RevLine 
[5273]1## $Id: browser.py 7399 2011-12-20 07:39:33Z 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
[7392]25from zope.component import getUtility, createObject, getAdapter
[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 (
[7392]52    ISIRPObject, ILocalRolesAssignable, IExtFileStore, IPDF,
[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):
[7392]709        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
710            view=self)
[6358]711        self.response.setHeader(
712            'Content-Type', 'application/pdf')
[7392]713        return pdfstream
[6358]714
[6383]715class ApplicantManageActionButton(ManageActionButton):
[6198]716    grok.context(IApplicant)
[7200]717    grok.view(ApplicantDisplayFormPage)
[7136]718    grok.require('waeup.manageApplication')
[6383]719    text = 'Manage application record'
[7200]720    target = 'manage'
[6198]721
[7240]722class ApplicantEditActionButton(ManageActionButton):
723    grok.context(IApplicant)
724    grok.view(ApplicantDisplayFormPage)
725    grok.require('waeup.handleApplication')
726    text = 'Edit application record'
727    target ='edit'
[7081]728
[7240]729    @property
730    def target_url(self):
731        """Get a URL to the target...
732        """
733        if self.context.locked:
734            return
735        return self.view.url(self.view.context, self.target)
736
[7081]737def handle_img_upload(upload, context, view):
[7063]738    """Handle upload of applicant image.
[7081]739
740    Returns `True` in case of success or `False`.
741
742    Please note that file pointer passed in (`upload`) most probably
743    points to end of file when leaving this function.
[7063]744    """
[7081]745    size = file_size(upload)
746    if size > MAX_UPLOAD_SIZE:
747        view.flash('Uploaded image is too big!')
748        return False
[7247]749    dummy, ext = os.path.splitext(upload.filename)
750    ext.lower()
751    if ext != '.jpg':
752        view.flash('jpg file extension expected.')
753        return False
[7081]754    upload.seek(0) # file pointer moved when determining size
[7063]755    store = getUtility(IExtFileStore)
756    file_id = IFileStoreNameChooser(context).chooseName()
757    store.createFile(file_id, upload)
[7081]758    return True
[7063]759
[7321]760class ApplicantManageFormPage(SIRPEditFormPage):
[6196]761    """A full edit view for applicant data.
762    """
763    grok.context(IApplicant)
[7200]764    grok.name('manage')
[7136]765    grok.require('waeup.manageApplication')
[6476]766    form_fields = grok.AutoFields(IApplicant)
[6196]767    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7351]768    form_fields['student_id'].for_display = True
[7378]769    form_fields['applicant_id'].for_display = True
[7200]770    grok.template('applicanteditpage')
[6322]771    manage_applications = True
[6196]772    pnav = 3
[7250]773    display_actions = [['Save', 'Final Submit'],
774                       ['Add online payment ticket','Remove selected tickets']]
[6196]775
776    def update(self):
777        datepicker.need() # Enable jQuery datepicker in date fields.
[7330]778        warning.need()
[7200]779        super(ApplicantManageFormPage, self).update()
[6353]780        self.wf_info = IWorkflowInfo(self.context)
[7081]781        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
[7084]782        self.passport_changed = None
[6598]783        upload = self.request.form.get('form.passport', None)
784        if upload:
785            # We got a fresh upload
[7084]786            self.passport_changed = handle_img_upload(
787                upload, self.context, self)
[6196]788        return
789
790    @property
791    def title(self):
[7240]792        return 'Application Record %s' % self.context.application_number
[6196]793
794    @property
795    def label(self):
796        container_title = self.context.__parent__.title
[7240]797        return '%s Application Form %s' % (
798            container_title, self.context.application_number)
[6196]799
[6303]800    def getTransitions(self):
[6351]801        """Return a list of dicts of allowed transition ids and titles.
[6353]802
803        Each list entry provides keys ``name`` and ``title`` for
804        internal name and (human readable) title of a single
805        transition.
[6349]806        """
[6353]807        allowed_transitions = self.wf_info.getManualTransitions()
[6355]808        return [dict(name='', title='No transition')] +[
809            dict(name=x, title=y) for x, y in allowed_transitions]
[6303]810
[6196]811    @grok.action('Save')
812    def save(self, **data):
[7240]813        form = self.request.form
814        password = form.get('password', None)
815        password_ctl = form.get('control_password', None)
816        if password:
817            validator = getUtility(IPasswordValidator)
818            errors = validator.validate_password(password, password_ctl)
819            if errors:
820                self.flash( ' '.join(errors))
821                return
[7084]822        if self.passport_changed is False:  # False is not None!
823            return # error during image upload. Ignore other values
[6475]824        changed_fields = self.applyData(self.context, **data)
[7199]825        # Turn list of lists into single list
826        if changed_fields:
827            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7240]828        else:
829            changed_fields = []
830        if self.passport_changed:
831            changed_fields.append('passport')
832        if password:
833            # Now we know that the form has no errors and can set password ...
834            IUserAccount(self.context).setPassword(password)
835            changed_fields.append('password')
[7199]836        fields_string = ' + '.join(changed_fields)
[7085]837        trans_id = form.get('transition', None)
838        if trans_id:
839            self.wf_info.fireTransition(trans_id)
[6196]840        self.flash('Form has been saved.')
[6475]841        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[6644]842        if fields_string:
843            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
[6196]844        return
845
[7250]846    def unremovable(self, ticket):
[7330]847        return False
[7250]848
849    # This method is also used by the ApplicantEditFormPage
850    def delPaymentTickets(self, **data):
851        form = self.request.form
852        if form.has_key('val_id'):
853            child_id = form['val_id']
854        else:
855            self.flash('No payment selected.')
856            self.redirect(self.url(self.context))
857            return
858        if not isinstance(child_id, list):
859            child_id = [child_id]
860        deleted = []
861        for id in child_id:
862            # Applicants are not allowed to remove used payment tickets
863            if not self.unremovable(self.context[id]):
864                try:
865                    del self.context[id]
866                    deleted.append(id)
867                except:
868                    self.flash('Could not delete %s: %s: %s' % (
869                            id, sys.exc_info()[0], sys.exc_info()[1]))
870        if len(deleted):
871            self.flash('Successfully removed: %s' % ', '.join(deleted))
872            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
[7363]873            self.context.loggerInfo(
874                ob_class, 'removed: % s' % ', '.join(deleted))
[7250]875        return
876
[7252]877    # We explicitely want the forms to be validated before payment tickets
878    # can be created. If no validation is requested, use
879    # 'validator=NullValidator' in the grok.action directive
[7250]880    @grok.action('Add online payment ticket')
881    def addPaymentTicket(self, **data):
882        self.redirect(self.url(self.context, '@@addafp'))
[7252]883        return
[7250]884
[7330]885    @jsaction('Remove selected tickets')
[7250]886    def removePaymentTickets(self, **data):
887        self.delPaymentTickets(**data)
888        self.redirect(self.url(self.context) + '/@@manage')
889        return
890
[7200]891class ApplicantEditFormPage(ApplicantManageFormPage):
[5982]892    """An applicant-centered edit view for applicant data.
893    """
[6196]894    grok.context(IApplicantEdit)
[5273]895    grok.name('edit')
[6198]896    grok.require('waeup.handleApplication')
[6459]897    form_fields = grok.AutoFields(IApplicantEdit).omit(
[6476]898        'locked', 'course_admitted', 'student_id',
[7378]899        'screening_score', 'reg_number'
[6459]900        )
[6054]901    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[7363]902    #form_fields['phone'].custom_widget = PhoneWidget
[7200]903    grok.template('applicanteditpage')
[6322]904    manage_applications = False
[6465]905    title = u'Your Application Form'
[5484]906
[7250]907    @property
908    def display_actions(self):
909        state = IWorkflowState(self.context).getState()
910        if state == INITIALIZED:
911            actions = [[],[]]
912        elif state == STARTED:
913            actions = [['Save'],
914                       ['Add online payment ticket','Remove selected tickets']]
915        elif state == PAID:
916            actions = [['Save', 'Final Submit'],
917                       ['Remove selected tickets']]
[7351]918        else:
[7250]919            actions = [[],[]]
920        return actions
921
[7330]922    def unremovable(self, ticket):
923        state = IWorkflowState(self.context).getState()
924        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
925
[7145]926    def emit_lock_message(self):
[6105]927        self.flash('The requested form is locked (read-only).')
[5941]928        self.redirect(self.url(self.context))
929        return
[6078]930
[5686]931    def update(self):
[5941]932        if self.context.locked:
[7145]933            self.emit_lock_message()
[5941]934            return
[7200]935        super(ApplicantEditFormPage, self).update()
[5686]936        return
[5952]937
[6196]938    def dataNotComplete(self):
[7252]939        store = getUtility(IExtFileStore)
940        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
941            return 'No passport picture uploaded.'
[6322]942        if not self.request.form.get('confirm_passport', False):
[7252]943            return 'Passport picture confirmation box not ticked.'
[6196]944        return False
[5952]945
[7252]946    # We explicitely want the forms to be validated before payment tickets
947    # can be created. If no validation is requested, use
948    # 'validator=NullValidator' in the grok.action directive
[7250]949    @grok.action('Add online payment ticket')
950    def addPaymentTicket(self, **data):
951        self.redirect(self.url(self.context, '@@addafp'))
[7252]952        return
[7250]953
[7330]954    @jsaction('Remove selected tickets')
[7250]955    def removePaymentTickets(self, **data):
956        self.delPaymentTickets(**data)
957        self.redirect(self.url(self.context) + '/@@edit')
958        return
959
[5273]960    @grok.action('Save')
961    def save(self, **data):
[7084]962        if self.passport_changed is False:  # False is not None!
963            return # error during image upload. Ignore other values
[5273]964        self.applyData(self.context, **data)
[6196]965        self.flash('Form has been saved.')
[5273]966        return
967
[5484]968    @grok.action('Final Submit')
969    def finalsubmit(self, **data):
[7084]970        if self.passport_changed is False:  # False is not None!
971            return # error during image upload. Ignore other values
[6196]972        if self.dataNotComplete():
973            self.flash(self.dataNotComplete())
[5941]974            return
[7252]975        self.applyData(self.context, **data)
[6303]976        state = IWorkflowState(self.context).getState()
[6322]977        # This shouldn't happen, but the application officer
978        # might have forgotten to lock the form after changing the state
[7250]979        if state != PAID:
[6322]980            self.flash('This form cannot be submitted. Wrong state!')
[6303]981            return
982        IWorkflowInfo(self.context).fireTransition('submit')
[6476]983        self.context.application_date = datetime.now()
[5941]984        self.context.locked = True
[6196]985        self.flash('Form has been submitted.')
986        self.redirect(self.url(self.context))
[5273]987        return
[5941]988
[6367]989class ApplicantViewActionButton(ManageActionButton):
990    grok.context(IApplicant)
[7200]991    grok.view(ApplicantManageFormPage)
[7240]992    grok.require('waeup.viewApplication')
[6383]993    icon = 'actionicon_view.png'
[6367]994    text = 'View application record'
[6598]995    target = 'index'
[7063]996
997class PassportImage(grok.View):
998    """Renders the passport image for applicants.
999    """
1000    grok.name('passport.jpg')
1001    grok.context(IApplicant)
[7113]1002    grok.require('waeup.viewApplication')
[7063]1003
1004    def render(self):
1005        # A filename chooser turns a context into a filename suitable
1006        # for file storage.
1007        image = getUtility(IExtFileStore).getFileByContext(self.context)
1008        self.response.setHeader(
1009            'Content-Type', 'image/jpeg')
1010        if image is None:
1011            # show placeholder image
[7089]1012            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
[7063]1013        return image
[7363]1014
1015class ApplicantRegistrationPage(SIRPAddFormPage):
1016    """Captcha'd registration page for applicants.
1017    """
1018    grok.context(IApplicantsContainer)
1019    grok.name('register')
[7373]1020    grok.require('waeup.Anonymous')
[7363]1021    grok.template('applicantregister')
1022    form_fields = grok.AutoFields(IApplicantEdit).select(
1023        'firstname', 'middlename', 'lastname', 'email', 'phone')
1024    form_fields['phone'].custom_widget = PhoneWidget
1025
[7368]1026    @property
1027    def title(self):
1028        return "Applicants Container: %s" % self.context.title
1029
1030    @property
1031    def label(self):
1032        return "Register for %s Application" % self.context.title
1033
[7363]1034    def update(self):
[7368]1035        # Check if application has started ...
1036        if not self.context.startdate or self.context.startdate > date.today():
1037            self.flash('Application has not yet started.')
1038            self.redirect(self.url(self.context))
1039            return
1040        # ... or ended
1041        if not self.context.enddate or self.context.enddate < date.today():
1042            self.flash('Application has ended.')
1043            self.redirect(self.url(self.context))
1044            return
1045        # Handle captcha
[7363]1046        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1047        self.captcha_result = self.captcha.verify(self.request)
1048        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1049        return
1050
[7380]1051    @grok.action('Get login credentials')
[7363]1052    def register(self, **data):
1053        if not self.captcha_result.is_valid:
1054            # captcha will display error messages automatically.
1055            # No need to flash something.
1056            return
[7365]1057        # Add applicant and create password
[7363]1058        applicant = createObject('waeup.Applicant')
1059        self.applyData(applicant, **data)
1060        self.context.addApplicant(applicant)
[7399]1061        sirp_utils = getUtility(ISIRPUtils)
1062        password = sirp_utils.genPassword()
[7380]1063        IUserAccount(applicant).setPassword(password)
[7365]1064        # Send email with credentials
[7399]1065        login_url = self.url(grok.getSite(), 'login')
1066        if sirp_utils.sendCredentials(applicant, password, login_url):
[7380]1067            self.redirect(self.url(self.context, 'registration_complete',
1068                                   data = dict(email=applicant.email)))
1069            return
1070        else:
1071            self.flash('Email could not been sent. Please retry later.')
1072        return
1073
1074class ApplicantRegistrationEmailSent(SIRPPage):
1075    """Landing page after successful registration.
1076    """
1077    grok.name('registration_complete')
1078    grok.require('waeup.Public')
1079    grok.template('applicantregemailsent')
1080    title = 'Registration Completed'
1081    label = 'Your registration was successful'
1082
1083    def update(self, email=None):
1084        self.email = email
1085        return
Note: See TracBrowser for help on using the repository browser.