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

Last change on this file since 7388 was 7380, checked in by uli, 13 years ago

Split overlong register method: put sendmail stuff into separate
method.

  • Property svn:keywords set to Id
File size: 42.1 KB
Line 
1## $Id: browser.py 7380 2011-12-18 15:26:41Z uli $
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
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,
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        # 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
716        SLIP_STYLE = TableStyle(
717            [('VALIGN',(0,0),(-1,-1),'TOP')]
718            )
719
720        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
721        pdf.setTitle(self.label)
722        pdf.setSubject('Application')
723        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
724            self.request.principal.id))
725        pdf.setCreator('SIRP SIRP')
726        width, height = A4
727        style = getSampleStyleSheet()
728        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
729
730        story = []
731        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
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 = []
737        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
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"]))
743        story.append(Spacer(1, 24))
744        # Setup table data
745        data = []
746        # Insert passport photograph
747        img = getUtility(IExtFileStore).getFileByContext(self.context)
748        if img is None:
749            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
750        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
751        data.append([doc_img])
752        data.append([Spacer(1, 18)])
753        # Render widget fields
754        self.setUpWidgets()
755        for widget in self.widgets:
756            f_label = '<font size=12>%s</font>:' % widget.label.strip()
757            f_label = Paragraph(f_label, style["Normal"])
758            f_text = '<font size=12>%s</font>' % widget()
759            f_text = Paragraph(f_text, style["Normal"])
760            data.append([f_label,f_text])
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
764        f_label = Paragraph(f_label, style["Normal"])
765        f_text = Paragraph(f_text, style["Normal"])
766        data.append([f_label,f_text])
767
768        course_admitted = self.context.course_admitted
769        if getattr(course_admitted, '__parent__',None):
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])
776
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
784        # Create table
785        table = Table(data,style=SLIP_STYLE)
786        story.append(table)
787        frame_body.addFromList(story,pdf)
788
789        story = []
790        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
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
796        self.response.setHeader(
797            'Content-Type', 'application/pdf')
798        return pdf.getpdfdata()
799
800class ApplicantManageActionButton(ManageActionButton):
801    grok.context(IApplicant)
802    grok.view(ApplicantDisplayFormPage)
803    grok.require('waeup.manageApplication')
804    text = 'Manage application record'
805    target = 'manage'
806
807class ApplicantEditActionButton(ManageActionButton):
808    grok.context(IApplicant)
809    grok.view(ApplicantDisplayFormPage)
810    grok.require('waeup.handleApplication')
811    text = 'Edit application record'
812    target ='edit'
813
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
822def handle_img_upload(upload, context, view):
823    """Handle upload of applicant image.
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.
829    """
830    size = file_size(upload)
831    if size > MAX_UPLOAD_SIZE:
832        view.flash('Uploaded image is too big!')
833        return False
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
839    upload.seek(0) # file pointer moved when determining size
840    store = getUtility(IExtFileStore)
841    file_id = IFileStoreNameChooser(context).chooseName()
842    store.createFile(file_id, upload)
843    return True
844
845class ApplicantManageFormPage(SIRPEditFormPage):
846    """A full edit view for applicant data.
847    """
848    grok.context(IApplicant)
849    grok.name('manage')
850    grok.require('waeup.manageApplication')
851    form_fields = grok.AutoFields(IApplicant)
852    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
853    form_fields['student_id'].for_display = True
854    form_fields['applicant_id'].for_display = True
855    grok.template('applicanteditpage')
856    manage_applications = True
857    pnav = 3
858    display_actions = [['Save', 'Final Submit'],
859                       ['Add online payment ticket','Remove selected tickets']]
860
861    def update(self):
862        datepicker.need() # Enable jQuery datepicker in date fields.
863        warning.need()
864        super(ApplicantManageFormPage, self).update()
865        self.wf_info = IWorkflowInfo(self.context)
866        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
867        self.passport_changed = None
868        upload = self.request.form.get('form.passport', None)
869        if upload:
870            # We got a fresh upload
871            self.passport_changed = handle_img_upload(
872                upload, self.context, self)
873        return
874
875    @property
876    def title(self):
877        return 'Application Record %s' % self.context.application_number
878
879    @property
880    def label(self):
881        container_title = self.context.__parent__.title
882        return '%s Application Form %s' % (
883            container_title, self.context.application_number)
884
885    def getTransitions(self):
886        """Return a list of dicts of allowed transition ids and titles.
887
888        Each list entry provides keys ``name`` and ``title`` for
889        internal name and (human readable) title of a single
890        transition.
891        """
892        allowed_transitions = self.wf_info.getManualTransitions()
893        return [dict(name='', title='No transition')] +[
894            dict(name=x, title=y) for x, y in allowed_transitions]
895
896    @grok.action('Save')
897    def save(self, **data):
898        form = self.request.form
899        password = form.get('password', None)
900        password_ctl = form.get('control_password', None)
901        if password:
902            validator = getUtility(IPasswordValidator)
903            errors = validator.validate_password(password, password_ctl)
904            if errors:
905                self.flash( ' '.join(errors))
906                return
907        if self.passport_changed is False:  # False is not None!
908            return # error during image upload. Ignore other values
909        changed_fields = self.applyData(self.context, **data)
910        # Turn list of lists into single list
911        if changed_fields:
912            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
913        else:
914            changed_fields = []
915        if self.passport_changed:
916            changed_fields.append('passport')
917        if password:
918            # Now we know that the form has no errors and can set password ...
919            IUserAccount(self.context).setPassword(password)
920            changed_fields.append('password')
921        fields_string = ' + '.join(changed_fields)
922        trans_id = form.get('transition', None)
923        if trans_id:
924            self.wf_info.fireTransition(trans_id)
925        self.flash('Form has been saved.')
926        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
927        if fields_string:
928            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
929        return
930
931    def unremovable(self, ticket):
932        return False
933
934    # This method is also used by the ApplicantEditFormPage
935    def delPaymentTickets(self, **data):
936        form = self.request.form
937        if form.has_key('val_id'):
938            child_id = form['val_id']
939        else:
940            self.flash('No payment selected.')
941            self.redirect(self.url(self.context))
942            return
943        if not isinstance(child_id, list):
944            child_id = [child_id]
945        deleted = []
946        for id in child_id:
947            # Applicants are not allowed to remove used payment tickets
948            if not self.unremovable(self.context[id]):
949                try:
950                    del self.context[id]
951                    deleted.append(id)
952                except:
953                    self.flash('Could not delete %s: %s: %s' % (
954                            id, sys.exc_info()[0], sys.exc_info()[1]))
955        if len(deleted):
956            self.flash('Successfully removed: %s' % ', '.join(deleted))
957            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
958            self.context.loggerInfo(
959                ob_class, 'removed: % s' % ', '.join(deleted))
960        return
961
962    # We explicitely want the forms to be validated before payment tickets
963    # can be created. If no validation is requested, use
964    # 'validator=NullValidator' in the grok.action directive
965    @grok.action('Add online payment ticket')
966    def addPaymentTicket(self, **data):
967        self.redirect(self.url(self.context, '@@addafp'))
968        return
969
970    @jsaction('Remove selected tickets')
971    def removePaymentTickets(self, **data):
972        self.delPaymentTickets(**data)
973        self.redirect(self.url(self.context) + '/@@manage')
974        return
975
976class ApplicantEditFormPage(ApplicantManageFormPage):
977    """An applicant-centered edit view for applicant data.
978    """
979    grok.context(IApplicantEdit)
980    grok.name('edit')
981    grok.require('waeup.handleApplication')
982    form_fields = grok.AutoFields(IApplicantEdit).omit(
983        'locked', 'course_admitted', 'student_id',
984        'screening_score', 'reg_number'
985        )
986    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
987    #form_fields['phone'].custom_widget = PhoneWidget
988    grok.template('applicanteditpage')
989    manage_applications = False
990    title = u'Your Application Form'
991
992    @property
993    def display_actions(self):
994        state = IWorkflowState(self.context).getState()
995        if state == INITIALIZED:
996            actions = [[],[]]
997        elif state == STARTED:
998            actions = [['Save'],
999                       ['Add online payment ticket','Remove selected tickets']]
1000        elif state == PAID:
1001            actions = [['Save', 'Final Submit'],
1002                       ['Remove selected tickets']]
1003        else:
1004            actions = [[],[]]
1005        return actions
1006
1007    def unremovable(self, ticket):
1008        state = IWorkflowState(self.context).getState()
1009        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
1010
1011    def emit_lock_message(self):
1012        self.flash('The requested form is locked (read-only).')
1013        self.redirect(self.url(self.context))
1014        return
1015
1016    def update(self):
1017        if self.context.locked:
1018            self.emit_lock_message()
1019            return
1020        super(ApplicantEditFormPage, self).update()
1021        return
1022
1023    def dataNotComplete(self):
1024        store = getUtility(IExtFileStore)
1025        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1026            return 'No passport picture uploaded.'
1027        if not self.request.form.get('confirm_passport', False):
1028            return 'Passport picture confirmation box not ticked.'
1029        return False
1030
1031    # We explicitely want the forms to be validated before payment tickets
1032    # can be created. If no validation is requested, use
1033    # 'validator=NullValidator' in the grok.action directive
1034    @grok.action('Add online payment ticket')
1035    def addPaymentTicket(self, **data):
1036        self.redirect(self.url(self.context, '@@addafp'))
1037        return
1038
1039    @jsaction('Remove selected tickets')
1040    def removePaymentTickets(self, **data):
1041        self.delPaymentTickets(**data)
1042        self.redirect(self.url(self.context) + '/@@edit')
1043        return
1044
1045    @grok.action('Save')
1046    def save(self, **data):
1047        if self.passport_changed is False:  # False is not None!
1048            return # error during image upload. Ignore other values
1049        self.applyData(self.context, **data)
1050        self.flash('Form has been saved.')
1051        return
1052
1053    @grok.action('Final Submit')
1054    def finalsubmit(self, **data):
1055        if self.passport_changed is False:  # False is not None!
1056            return # error during image upload. Ignore other values
1057        if self.dataNotComplete():
1058            self.flash(self.dataNotComplete())
1059            return
1060        self.applyData(self.context, **data)
1061        state = IWorkflowState(self.context).getState()
1062        # This shouldn't happen, but the application officer
1063        # might have forgotten to lock the form after changing the state
1064        if state != PAID:
1065            self.flash('This form cannot be submitted. Wrong state!')
1066            return
1067        IWorkflowInfo(self.context).fireTransition('submit')
1068        self.context.application_date = datetime.now()
1069        self.context.locked = True
1070        self.flash('Form has been submitted.')
1071        self.redirect(self.url(self.context))
1072        return
1073
1074class ApplicantViewActionButton(ManageActionButton):
1075    grok.context(IApplicant)
1076    grok.view(ApplicantManageFormPage)
1077    grok.require('waeup.viewApplication')
1078    icon = 'actionicon_view.png'
1079    text = 'View application record'
1080    target = 'index'
1081
1082class PassportImage(grok.View):
1083    """Renders the passport image for applicants.
1084    """
1085    grok.name('passport.jpg')
1086    grok.context(IApplicant)
1087    grok.require('waeup.viewApplication')
1088
1089    def render(self):
1090        # A filename chooser turns a context into a filename suitable
1091        # for file storage.
1092        image = getUtility(IExtFileStore).getFileByContext(self.context)
1093        self.response.setHeader(
1094            'Content-Type', 'image/jpeg')
1095        if image is None:
1096            # show placeholder image
1097            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1098        return image
1099
1100class ApplicantRegistrationPage(SIRPAddFormPage):
1101    """Captcha'd registration page for applicants.
1102    """
1103    grok.context(IApplicantsContainer)
1104    grok.name('register')
1105    grok.require('waeup.Anonymous')
1106    grok.template('applicantregister')
1107    form_fields = grok.AutoFields(IApplicantEdit).select(
1108        'firstname', 'middlename', 'lastname', 'email', 'phone')
1109    form_fields['phone'].custom_widget = PhoneWidget
1110
1111    @property
1112    def title(self):
1113        return "Applicants Container: %s" % self.context.title
1114
1115    @property
1116    def label(self):
1117        return "Register for %s Application" % self.context.title
1118
1119    def update(self):
1120        # Check if application has started ...
1121        if not self.context.startdate or self.context.startdate > date.today():
1122            self.flash('Application has not yet started.')
1123            self.redirect(self.url(self.context))
1124            return
1125        # ... or ended
1126        if not self.context.enddate or self.context.enddate < date.today():
1127            self.flash('Application has ended.')
1128            self.redirect(self.url(self.context))
1129            return
1130        # Handle captcha
1131        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1132        self.captcha_result = self.captcha.verify(self.request)
1133        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1134        return
1135
1136    @grok.action('Get login credentials')
1137    def register(self, **data):
1138        if not self.captcha_result.is_valid:
1139            # captcha will display error messages automatically.
1140            # No need to flash something.
1141            return
1142        # Add applicant and create password
1143        applicant = createObject('waeup.Applicant')
1144        self.applyData(applicant, **data)
1145        self.context.addApplicant(applicant)
1146        password = getUtility(ISIRPUtils).genPassword()
1147        IUserAccount(applicant).setPassword(password)
1148        # Send email with credentials
1149        if self.sendCredentials(applicant, password):
1150            self.redirect(self.url(self.context, 'registration_complete',
1151                                   data = dict(email=applicant.email)))
1152            return
1153        else:
1154            self.flash('Email could not been sent. Please retry later.')
1155        return
1156
1157    def sendCredentials(self, applicant, password):
1158        """Send credentials as email.
1159
1160        Input is the applicant for which credentials are sent and the
1161        password.
1162
1163        Returns True or False to indicate successful operation.
1164        """
1165        sirp_utils = getUtility(ISIRPUtils)
1166        username = applicant.applicant_id
1167        fullname = applicant.display_fullname
1168        subject = 'Your SIRP credentials'
1169        msg = 'You have successfully been registered for the'
1170        email_to = applicant.email
1171        login_url = self.url(grok.getSite(), 'login')
1172        success = sirp_utils.sendPassword(fullname,msg,username,
1173            password,login_url,email_to,subject)
1174        return success
1175
1176class ApplicantRegistrationEmailSent(SIRPPage):
1177    """Landing page after successful registration.
1178    """
1179    grok.name('registration_complete')
1180    grok.require('waeup.Public')
1181    grok.template('applicantregemailsent')
1182    title = 'Registration Completed'
1183    label = 'Your registration was successful'
1184
1185    def update(self, email=None):
1186        self.email = email
1187        return
Note: See TracBrowser for help on using the repository browser.