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

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

Using the phone widget lead to an error in tests but not in the UI. I don't understand why.

  • Property svn:keywords set to Id
File size: 38.1 KB
Line 
1## $Id: browser.py 7423 2011-12-21 11:38:00Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from time import time
24from datetime import datetime, date
25from zope.component import getUtility, createObject, getAdapter
26from zope.formlib.form import setUpEditWidgets
27from hurry.workflow.interfaces import (
28    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
29from reportlab.pdfgen import canvas
30from reportlab.lib.units import cm
31from reportlab.lib.pagesizes import A4
32from reportlab.lib.styles import getSampleStyleSheet
33from reportlab.platypus import (Frame, Paragraph, Image,
34    Table, Spacer)
35from reportlab.platypus.tables import TableStyle
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
42from waeup.sirp.browser import (
43    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
44    DEFAULT_PASSPORT_IMAGE_PATH)
45from waeup.sirp.browser.interfaces import ICaptchaManager
46from waeup.sirp.browser.breadcrumbs import Breadcrumb
47from waeup.sirp.browser.layout import NullValidator, jsaction, JSAction
48from waeup.sirp.browser.pages import add_local_role, del_local_roles
49from waeup.sirp.browser.resources import datepicker, tabs, datatable, warning
50from waeup.sirp.browser.viewlets import ManageActionButton, PrimaryNavTab
51from waeup.sirp.interfaces import (
52    ISIRPObject, ILocalRolesAssignable, IExtFileStore, IPDF,
53    IFileStoreNameChooser, IPasswordValidator, IUserAccount, ISIRPUtils)
54from waeup.sirp.permissions import get_users_with_local_roles
55from waeup.sirp.students.viewlets import PrimaryStudentNavTab
56from waeup.sirp.students.interfaces import IStudentsUtils
57from waeup.sirp.university.interfaces import ICertificate
58from waeup.sirp.utils.helpers import string_from_bytes, file_size
59from waeup.sirp.widgets.datewidget import (
60    FriendlyDateWidget, FriendlyDateDisplayWidget,
61    FriendlyDatetimeDisplayWidget)
62from waeup.sirp.widgets.phonewidget import PhoneWidget
63from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
64
65grok.context(ISIRPObject) # Make ISIRPObject the default context
66
67class ApplicantsRootPage(SIRPPage):
68    grok.context(IApplicantsRoot)
69    grok.name('index')
70    grok.require('waeup.Public')
71    title = 'Applicants'
72    label = 'Application Section'
73    pnav = 3
74
75    def update(self):
76        super(ApplicantsRootPage, self).update()
77        datatable.need()
78        return
79
80class ManageApplicantsRootActionButton(ManageActionButton):
81    grok.context(IApplicantsRoot)
82    grok.view(ApplicantsRootPage)
83    grok.require('waeup.manageApplication')
84    text = 'Manage application section'
85
86class ApplicantsRootManageFormPage(SIRPEditFormPage):
87    grok.context(IApplicantsRoot)
88    grok.name('manage')
89    grok.template('applicantsrootmanagepage')
90    title = 'Applicants'
91    label = 'Manage application section'
92    pnav = 3
93    grok.require('waeup.manageApplication')
94    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
95    tabtwoactions1 = ['Remove selected local roles']
96    tabtwoactions2 = ['Add local role']
97    subunits = 'Applicants Containers'
98
99    def update(self):
100        tabs.need()
101        datatable.need()
102        warning.need()
103        return super(ApplicantsRootManageFormPage, self).update()
104
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
119    @jsaction('Remove selected')
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))
135        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
136        return
137
138    @grok.action('Add applicants container', validator=NullValidator)
139    def addApplicantsContainer(self, **data):
140        self.redirect(self.url(self.context, '@@add'))
141        return
142
143    @grok.action('Cancel', validator=NullValidator)
144    def cancel(self, **data):
145        self.redirect(self.url(self.context))
146        return
147
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
156class ApplicantsContainerAddFormPage(SIRPAddFormPage):
157    grok.context(IApplicantsRoot)
158    grok.require('waeup.manageApplication')
159    grok.name('add')
160    grok.template('applicantscontaineraddpage')
161    title = 'Applicants'
162    label = 'Add applicants container'
163    pnav = 3
164
165    form_fields = grok.AutoFields(
166        IApplicantsContainerAdd).omit('code').omit('title')
167    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
168    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
169
170    def update(self):
171        datepicker.need() # Enable jQuery datepicker in date fields.
172        return super(ApplicantsContainerAddFormPage, self).update()
173
174    @grok.action('Add applicants container')
175    def addApplicantsContainer(self, **data):
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)
180        if code in self.context.keys():
181            self.flash(
182                'An applicants container for the same application '
183                'type and entrance year exists already in the database.')
184            return
185        # Add new applicants container...
186        provider = data['provider'][1]
187        container = provider.factory()
188        self.applyData(container, **data)
189        container.code = code
190        container.title = title
191        self.context[code] = container
192        self.flash('Added: "%s".' % code)
193        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
194        return
195
196    @grok.action('Cancel', validator=NullValidator)
197    def cancel(self, **data):
198        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
199
200class ApplicantsRootBreadcrumb(Breadcrumb):
201    """A breadcrumb for applicantsroot.
202    """
203    grok.context(IApplicantsRoot)
204    title = u'Applicants'
205
206class ApplicantsContainerBreadcrumb(Breadcrumb):
207    """A breadcrumb for applicantscontainers.
208    """
209    grok.context(IApplicantsContainer)
210
211class ApplicantBreadcrumb(Breadcrumb):
212    """A breadcrumb for applicants.
213    """
214    grok.context(IApplicant)
215
216    @property
217    def title(self):
218        """Get a title for a context.
219        """
220        return self.context.application_number
221
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
231class ApplicantsAuthTab(PrimaryNavTab):
232    """Applicants tab in primary navigation.
233    """
234    grok.context(ISIRPObject)
235    grok.order(3)
236    grok.require('waeup.viewApplicantsTab')
237    pnav = 3
238    tab_title = u'Applicants'
239
240    @property
241    def link_target(self):
242        return self.view.application_url('applicants')
243
244class ApplicantsAnonTab(ApplicantsAuthTab):
245    """Applicants tab in primary navigation.
246
247    Display tab only for anonymous. Authenticated users can call the
248    form from the user navigation bar.
249    """
250    grok.require('waeup.Anonymous')
251    tab_title = u'Application'
252
253    # Also zope.manager has role Anonymous.
254    # To avoid displaying this tab, 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
260
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
278class ApplicantsContainerPage(SIRPDisplayFormPage):
279    """The standard view for regular applicant containers.
280    """
281    grok.context(IApplicantsContainer)
282    grok.name('index')
283    grok.require('waeup.Public')
284    grok.template('applicantscontainerpage')
285    pnav = 3
286
287    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
288    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
289    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
290    form_fields['description'].custom_widget = ReSTDisplayWidget
291
292    @property
293    def title(self):
294        return "Applicants Container: %s" % self.context.title
295
296    @property
297    def label(self):
298        return self.context.title
299
300class ApplicantsContainerManageActionButton(ManageActionButton):
301    grok.order(1)
302    grok.context(IApplicantsContainer)
303    grok.view(ApplicantsContainerPage)
304    grok.require('waeup.manageApplication')
305    text = 'Manage applicants container'
306
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
316class ApplicantsContainerManageFormPage(SIRPEditFormPage):
317    grok.context(IApplicantsContainer)
318    grok.name('manage')
319    grok.template('applicantscontainermanagepage')
320    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
321    taboneactions = ['Save','Cancel']
322    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
323    tabthreeactions1 = ['Remove selected local roles']
324    tabthreeactions2 = ['Add local role']
325    # Use friendlier date widget...
326    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
327    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
328    grok.require('waeup.manageApplication')
329
330    @property
331    def title(self):
332        return "Applicants Container: %s" % self.context.title
333
334    @property
335    def label(self):
336        return 'Manage applicants container'
337
338    pnav = 3
339
340    def update(self):
341        datepicker.need() # Enable jQuery datepicker in date fields.
342        tabs.need()
343        warning.need()
344        datatable.need()  # Enable jQurey datatables for contents listing
345        return super(ApplicantsContainerManageFormPage, self).update()
346
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
361    @grok.action('Save')
362    def apply(self, **data):
363        self.applyData(self.context, **data)
364        self.flash('Data saved.')
365        return
366
367    @jsaction('Remove selected')
368    def delApplicant(self, **data):
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
390
391    @grok.action('Add applicant', validator=NullValidator)
392    def addApplicant(self, **data):
393        self.redirect(self.url(self.context, 'addapplicant'))
394        return
395
396    @grok.action('Cancel', validator=NullValidator)
397    def cancel(self, **data):
398        self.redirect(self.url(self.context))
399        return
400
401    @grok.action('Add local role', validator=NullValidator)
402    def addLocalRole(self, **data):
403        return add_local_role(self,3, **data)
404
405    @grok.action('Remove selected local roles')
406    def delLocalRoles(self, **data):
407        return del_local_roles(self,3,**data)
408
409class ApplicantAddFormPage(SIRPAddFormPage):
410    """Add-form to add an applicant.
411    """
412    grok.context(IApplicantsContainer)
413    grok.require('waeup.manageApplication')
414    grok.name('addapplicant')
415    #grok.template('applicantaddpage')
416    form_fields = grok.AutoFields(IApplicant).select(
417        'firstname', 'middlename', 'lastname',
418        'email', 'phone')
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):
428        applicant = createObject(u'waeup.Applicant')
429        self.applyData(applicant, **data)
430        self.context.addApplicant(applicant)
431        self.flash('Applicant record created.')
432        self.redirect(
433            self.url(self.context[applicant.application_number], 'index'))
434        return
435
436class ApplicantDisplayFormPage(SIRPDisplayFormPage):
437    grok.context(IApplicant)
438    grok.name('index')
439    grok.require('waeup.viewApplication')
440    grok.template('applicantdisplaypage')
441    form_fields = grok.AutoFields(IApplicant).omit(
442        'locked', 'course_admitted', 'password')
443    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
444    label = 'Applicant'
445    pnav = 3
446
447    def update(self):
448        self.passport_url = self.url(self.context, 'passport.jpg')
449        # Mark application as started if applicant logs in for the first time
450        usertype = getattr(self.request.principal, 'user_type', None)
451        if usertype == 'applicant' and \
452            IWorkflowState(self.context).getState() == INITIALIZED:
453            IWorkflowInfo(self.context).fireTransition('start')
454        return
455
456    @property
457    def hasPassword(self):
458        if self.context.password:
459            return 'set'
460        return 'unset'
461
462    @property
463    def title(self):
464        return 'Application Record %s' % self.context.application_number
465
466    @property
467    def label(self):
468        container_title = self.context.__parent__.title
469        return '%s Application Record %s' % (
470            container_title, self.context.application_number)
471
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
477        if getattr(course_admitted, '__parent__',None):
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 ''
483
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
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
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
553class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
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)
560    form_fields[
561        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
562    form_fields[
563        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
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' % (
573            self.context.__parent__.display_fullname,self.context.p_id)
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):
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])
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'
647    title = 'Payment Data'
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
658        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
659            self.request)
660        students_utils = getUtility(IStudentsUtils)
661        return students_utils.renderPDF(self,'payment_receipt.pdf',
662            self.context.__parent__, applicantview)
663
664class PDFActionButton(ManageActionButton):
665    grok.context(IApplicant)
666    grok.require('waeup.viewApplication')
667    icon = 'actionicon_pdf.png'
668    text = 'Download application slip'
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')
676    grok.require('waeup.viewApplication')
677    form_fields = grok.AutoFields(IApplicant).omit(
678        'locked', 'course_admitted')
679    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
680    prefix = 'form'
681
682    @property
683    def label(self):
684        container_title = self.context.__parent__.title
685        return '%s Application Record %s' % (
686            container_title, self.context.application_number)
687
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
693        #if ICertificate.providedBy(course_admitted):
694        if getattr(course_admitted, '__parent__',None):
695            title = course_admitted.title
696            code = course_admitted.code
697            return '%s - %s' %(code,title)
698        return ''
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):
709        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
710            view=self)
711        self.response.setHeader(
712            'Content-Type', 'application/pdf')
713        return pdfstream
714
715class ApplicantManageActionButton(ManageActionButton):
716    grok.context(IApplicant)
717    grok.view(ApplicantDisplayFormPage)
718    grok.require('waeup.manageApplication')
719    text = 'Manage application record'
720    target = 'manage'
721
722class ApplicantEditActionButton(ManageActionButton):
723    grok.context(IApplicant)
724    grok.view(ApplicantDisplayFormPage)
725    grok.require('waeup.handleApplication')
726    text = 'Edit application record'
727    target ='edit'
728
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
737def handle_img_upload(upload, context, view):
738    """Handle upload of applicant image.
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.
744    """
745    size = file_size(upload)
746    if size > MAX_UPLOAD_SIZE:
747        view.flash('Uploaded image is too big!')
748        return False
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
754    upload.seek(0) # file pointer moved when determining size
755    store = getUtility(IExtFileStore)
756    file_id = IFileStoreNameChooser(context).chooseName()
757    store.createFile(file_id, upload)
758    return True
759
760class ApplicantManageFormPage(SIRPEditFormPage):
761    """A full edit view for applicant data.
762    """
763    grok.context(IApplicant)
764    grok.name('manage')
765    grok.require('waeup.manageApplication')
766    form_fields = grok.AutoFields(IApplicant)
767    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
768    form_fields['student_id'].for_display = True
769    form_fields['applicant_id'].for_display = True
770    form_fields['phone'].custom_widget = PhoneWidget
771    grok.template('applicanteditpage')
772    manage_applications = True
773    pnav = 3
774    display_actions = [['Save', 'Final Submit'],
775                       ['Add online payment ticket','Remove selected tickets']]
776
777    def update(self):
778        datepicker.need() # Enable jQuery datepicker in date fields.
779        warning.need()
780        super(ApplicantManageFormPage, self).update()
781        self.wf_info = IWorkflowInfo(self.context)
782        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
783        self.passport_changed = None
784        upload = self.request.form.get('form.passport', None)
785        if upload:
786            # We got a fresh upload
787            self.passport_changed = handle_img_upload(
788                upload, self.context, self)
789        return
790
791    @property
792    def title(self):
793        return 'Application Record %s' % self.context.application_number
794
795    @property
796    def label(self):
797        container_title = self.context.__parent__.title
798        return '%s Application Form %s' % (
799            container_title, self.context.application_number)
800
801    def getTransitions(self):
802        """Return a list of dicts of allowed transition ids and titles.
803
804        Each list entry provides keys ``name`` and ``title`` for
805        internal name and (human readable) title of a single
806        transition.
807        """
808        allowed_transitions = self.wf_info.getManualTransitions()
809        return [dict(name='', title='No transition')] +[
810            dict(name=x, title=y) for x, y in allowed_transitions]
811
812    @grok.action('Save')
813    def save(self, **data):
814        form = self.request.form
815        password = form.get('password', None)
816        password_ctl = form.get('control_password', None)
817        if password:
818            validator = getUtility(IPasswordValidator)
819            errors = validator.validate_password(password, password_ctl)
820            if errors:
821                self.flash( ' '.join(errors))
822                return
823        if self.passport_changed is False:  # False is not None!
824            return # error during image upload. Ignore other values
825        changed_fields = self.applyData(self.context, **data)
826        # Turn list of lists into single list
827        if changed_fields:
828            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
829        else:
830            changed_fields = []
831        if self.passport_changed:
832            changed_fields.append('passport')
833        if password:
834            # Now we know that the form has no errors and can set password ...
835            IUserAccount(self.context).setPassword(password)
836            changed_fields.append('password')
837        fields_string = ' + '.join(changed_fields)
838        trans_id = form.get('transition', None)
839        if trans_id:
840            self.wf_info.fireTransition(trans_id)
841        self.flash('Form has been saved.')
842        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
843        if fields_string:
844            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
845        return
846
847    def unremovable(self, ticket):
848        return False
849
850    # This method is also used by the ApplicantEditFormPage
851    def delPaymentTickets(self, **data):
852        form = self.request.form
853        if form.has_key('val_id'):
854            child_id = form['val_id']
855        else:
856            self.flash('No payment selected.')
857            self.redirect(self.url(self.context))
858            return
859        if not isinstance(child_id, list):
860            child_id = [child_id]
861        deleted = []
862        for id in child_id:
863            # Applicants are not allowed to remove used payment tickets
864            if not self.unremovable(self.context[id]):
865                try:
866                    del self.context[id]
867                    deleted.append(id)
868                except:
869                    self.flash('Could not delete %s: %s: %s' % (
870                            id, sys.exc_info()[0], sys.exc_info()[1]))
871        if len(deleted):
872            self.flash('Successfully removed: %s' % ', '.join(deleted))
873            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
874            self.context.loggerInfo(
875                ob_class, 'removed: % s' % ', '.join(deleted))
876        return
877
878    # We explicitely want the forms to be validated before payment tickets
879    # can be created. If no validation is requested, use
880    # 'validator=NullValidator' in the grok.action directive
881    @grok.action('Add online payment ticket')
882    def addPaymentTicket(self, **data):
883        self.redirect(self.url(self.context, '@@addafp'))
884        return
885
886    @jsaction('Remove selected tickets')
887    def removePaymentTickets(self, **data):
888        self.delPaymentTickets(**data)
889        self.redirect(self.url(self.context) + '/@@manage')
890        return
891
892class ApplicantEditFormPage(ApplicantManageFormPage):
893    """An applicant-centered edit view for applicant data.
894    """
895    grok.context(IApplicantEdit)
896    grok.name('edit')
897    grok.require('waeup.handleApplication')
898    form_fields = grok.AutoFields(IApplicantEdit).omit(
899        'locked', 'course_admitted', 'student_id',
900        'screening_score', 'reg_number'
901        )
902    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
903    grok.template('applicanteditpage')
904    manage_applications = False
905    title = u'Your Application Form'
906
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']]
918        else:
919            actions = [[],[]]
920        return actions
921
922    def unremovable(self, ticket):
923        state = IWorkflowState(self.context).getState()
924        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
925
926    def emit_lock_message(self):
927        self.flash('The requested form is locked (read-only).')
928        self.redirect(self.url(self.context))
929        return
930
931    def update(self):
932        if self.context.locked:
933            self.emit_lock_message()
934            return
935        super(ApplicantEditFormPage, self).update()
936        return
937
938    def dataNotComplete(self):
939        store = getUtility(IExtFileStore)
940        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
941            return 'No passport picture uploaded.'
942        if not self.request.form.get('confirm_passport', False):
943            return 'Passport picture confirmation box not ticked.'
944        return False
945
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
949    @grok.action('Add online payment ticket')
950    def addPaymentTicket(self, **data):
951        self.redirect(self.url(self.context, '@@addafp'))
952        return
953
954    @jsaction('Remove selected tickets')
955    def removePaymentTickets(self, **data):
956        self.delPaymentTickets(**data)
957        self.redirect(self.url(self.context) + '/@@edit')
958        return
959
960    @grok.action('Save')
961    def save(self, **data):
962        if self.passport_changed is False:  # False is not None!
963            return # error during image upload. Ignore other values
964        self.applyData(self.context, **data)
965        self.flash('Form has been saved.')
966        return
967
968    @grok.action('Final Submit')
969    def finalsubmit(self, **data):
970        if self.passport_changed is False:  # False is not None!
971            return # error during image upload. Ignore other values
972        if self.dataNotComplete():
973            self.flash(self.dataNotComplete())
974            return
975        self.applyData(self.context, **data)
976        state = IWorkflowState(self.context).getState()
977        # This shouldn't happen, but the application officer
978        # might have forgotten to lock the form after changing the state
979        if state != PAID:
980            self.flash('This form cannot be submitted. Wrong state!')
981            return
982        IWorkflowInfo(self.context).fireTransition('submit')
983        self.context.application_date = datetime.now()
984        self.context.locked = True
985        self.flash('Form has been submitted.')
986        self.redirect(self.url(self.context))
987        return
988
989class ApplicantViewActionButton(ManageActionButton):
990    grok.context(IApplicant)
991    grok.view(ApplicantManageFormPage)
992    grok.require('waeup.viewApplication')
993    icon = 'actionicon_view.png'
994    text = 'View application record'
995    target = 'index'
996
997class PassportImage(grok.View):
998    """Renders the passport image for applicants.
999    """
1000    grok.name('passport.jpg')
1001    grok.context(IApplicant)
1002    grok.require('waeup.viewApplication')
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
1012            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1013        return image
1014
1015class ApplicantRegistrationPage(SIRPAddFormPage):
1016    """Captcha'd registration page for applicants.
1017    """
1018    grok.context(IApplicantsContainer)
1019    grok.name('register')
1020    grok.require('waeup.Anonymous')
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
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
1034    def update(self):
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
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
1051    @grok.action('Get login credentials')
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
1057        # Add applicant and create password
1058        applicant = createObject('waeup.Applicant')
1059        self.applyData(applicant, **data)
1060        self.context.addApplicant(applicant)
1061        sirp_utils = getUtility(ISIRPUtils)
1062        password = sirp_utils.genPassword()
1063        IUserAccount(applicant).setPassword(password)
1064        # Send email with credentials
1065        login_url = self.url(grok.getSite(), 'login')
1066        msg = 'You have successfully been registered for the'
1067        if sirp_utils.sendCredentials(IUserAccount(applicant),
1068            password, login_url, msg):
1069            self.redirect(self.url(self.context, 'registration_complete',
1070                                   data = dict(email=applicant.email)))
1071            return
1072        else:
1073            self.flash('Email could not been sent. Please retry later.')
1074        return
1075
1076class ApplicantRegistrationEmailSent(SIRPPage):
1077    """Landing page after successful registration.
1078    """
1079    grok.name('registration_complete')
1080    grok.require('waeup.Public')
1081    grok.template('applicantregemailsent')
1082    title = 'Registration Completed'
1083    label = 'Your registration was successful'
1084
1085    def update(self, email=None):
1086        self.email = email
1087        return
Note: See TracBrowser for help on using the repository browser.