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

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

Only anonymous users should be able to use the registration page.

  • Property svn:keywords set to Id
File size: 41.3 KB
Line 
1## $Id: browser.py 7373 2011-12-18 11:05:55Z 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
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    grok.template('applicanteditpage')
855    manage_applications = True
856    pnav = 3
857    display_actions = [['Save', 'Final Submit'],
858                       ['Add online payment ticket','Remove selected tickets']]
859
860    def update(self):
861        datepicker.need() # Enable jQuery datepicker in date fields.
862        warning.need()
863        super(ApplicantManageFormPage, self).update()
864        self.wf_info = IWorkflowInfo(self.context)
865        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
866        self.passport_changed = None
867        upload = self.request.form.get('form.passport', None)
868        if upload:
869            # We got a fresh upload
870            self.passport_changed = handle_img_upload(
871                upload, self.context, self)
872        return
873
874    @property
875    def title(self):
876        return 'Application Record %s' % self.context.application_number
877
878    @property
879    def label(self):
880        container_title = self.context.__parent__.title
881        return '%s Application Form %s' % (
882            container_title, self.context.application_number)
883
884    def getTransitions(self):
885        """Return a list of dicts of allowed transition ids and titles.
886
887        Each list entry provides keys ``name`` and ``title`` for
888        internal name and (human readable) title of a single
889        transition.
890        """
891        allowed_transitions = self.wf_info.getManualTransitions()
892        return [dict(name='', title='No transition')] +[
893            dict(name=x, title=y) for x, y in allowed_transitions]
894
895    @grok.action('Save')
896    def save(self, **data):
897        form = self.request.form
898        password = form.get('password', None)
899        password_ctl = form.get('control_password', None)
900        if password:
901            validator = getUtility(IPasswordValidator)
902            errors = validator.validate_password(password, password_ctl)
903            if errors:
904                self.flash( ' '.join(errors))
905                return
906        if self.passport_changed is False:  # False is not None!
907            return # error during image upload. Ignore other values
908        changed_fields = self.applyData(self.context, **data)
909        # Turn list of lists into single list
910        if changed_fields:
911            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
912        else:
913            changed_fields = []
914        if self.passport_changed:
915            changed_fields.append('passport')
916        if password:
917            # Now we know that the form has no errors and can set password ...
918            IUserAccount(self.context).setPassword(password)
919            changed_fields.append('password')
920        fields_string = ' + '.join(changed_fields)
921        trans_id = form.get('transition', None)
922        if trans_id:
923            self.wf_info.fireTransition(trans_id)
924        self.flash('Form has been saved.')
925        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
926        if fields_string:
927            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
928        return
929
930    def unremovable(self, ticket):
931        return False
932
933    # This method is also used by the ApplicantEditFormPage
934    def delPaymentTickets(self, **data):
935        form = self.request.form
936        if form.has_key('val_id'):
937            child_id = form['val_id']
938        else:
939            self.flash('No payment selected.')
940            self.redirect(self.url(self.context))
941            return
942        if not isinstance(child_id, list):
943            child_id = [child_id]
944        deleted = []
945        for id in child_id:
946            # Applicants are not allowed to remove used payment tickets
947            if not self.unremovable(self.context[id]):
948                try:
949                    del self.context[id]
950                    deleted.append(id)
951                except:
952                    self.flash('Could not delete %s: %s: %s' % (
953                            id, sys.exc_info()[0], sys.exc_info()[1]))
954        if len(deleted):
955            self.flash('Successfully removed: %s' % ', '.join(deleted))
956            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
957            self.context.loggerInfo(
958                ob_class, 'removed: % s' % ', '.join(deleted))
959        return
960
961    # We explicitely want the forms to be validated before payment tickets
962    # can be created. If no validation is requested, use
963    # 'validator=NullValidator' in the grok.action directive
964    @grok.action('Add online payment ticket')
965    def addPaymentTicket(self, **data):
966        self.redirect(self.url(self.context, '@@addafp'))
967        return
968
969    @jsaction('Remove selected tickets')
970    def removePaymentTickets(self, **data):
971        self.delPaymentTickets(**data)
972        self.redirect(self.url(self.context) + '/@@manage')
973        return
974
975class ApplicantEditFormPage(ApplicantManageFormPage):
976    """An applicant-centered edit view for applicant data.
977    """
978    grok.context(IApplicantEdit)
979    grok.name('edit')
980    grok.require('waeup.handleApplication')
981    form_fields = grok.AutoFields(IApplicantEdit).omit(
982        'locked', 'course_admitted', 'student_id',
983        'screening_score', 'applicant_id', 'reg_number'
984        )
985    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
986    #form_fields['phone'].custom_widget = PhoneWidget
987    grok.template('applicanteditpage')
988    manage_applications = False
989    title = u'Your Application Form'
990
991    @property
992    def display_actions(self):
993        state = IWorkflowState(self.context).getState()
994        if state == INITIALIZED:
995            actions = [[],[]]
996        elif state == STARTED:
997            actions = [['Save'],
998                       ['Add online payment ticket','Remove selected tickets']]
999        elif state == PAID:
1000            actions = [['Save', 'Final Submit'],
1001                       ['Remove selected tickets']]
1002        else:
1003            actions = [[],[]]
1004        return actions
1005
1006    def unremovable(self, ticket):
1007        state = IWorkflowState(self.context).getState()
1008        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
1009
1010    def emit_lock_message(self):
1011        self.flash('The requested form is locked (read-only).')
1012        self.redirect(self.url(self.context))
1013        return
1014
1015    def update(self):
1016        if self.context.locked:
1017            self.emit_lock_message()
1018            return
1019        super(ApplicantEditFormPage, self).update()
1020        return
1021
1022    def dataNotComplete(self):
1023        store = getUtility(IExtFileStore)
1024        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1025            return 'No passport picture uploaded.'
1026        if not self.request.form.get('confirm_passport', False):
1027            return 'Passport picture confirmation box not ticked.'
1028        return False
1029
1030    # We explicitely want the forms to be validated before payment tickets
1031    # can be created. If no validation is requested, use
1032    # 'validator=NullValidator' in the grok.action directive
1033    @grok.action('Add online payment ticket')
1034    def addPaymentTicket(self, **data):
1035        self.redirect(self.url(self.context, '@@addafp'))
1036        return
1037
1038    @jsaction('Remove selected tickets')
1039    def removePaymentTickets(self, **data):
1040        self.delPaymentTickets(**data)
1041        self.redirect(self.url(self.context) + '/@@edit')
1042        return
1043
1044    @grok.action('Save')
1045    def save(self, **data):
1046        if self.passport_changed is False:  # False is not None!
1047            return # error during image upload. Ignore other values
1048        self.applyData(self.context, **data)
1049        self.flash('Form has been saved.')
1050        return
1051
1052    @grok.action('Final Submit')
1053    def finalsubmit(self, **data):
1054        if self.passport_changed is False:  # False is not None!
1055            return # error during image upload. Ignore other values
1056        if self.dataNotComplete():
1057            self.flash(self.dataNotComplete())
1058            return
1059        self.applyData(self.context, **data)
1060        state = IWorkflowState(self.context).getState()
1061        # This shouldn't happen, but the application officer
1062        # might have forgotten to lock the form after changing the state
1063        if state != PAID:
1064            self.flash('This form cannot be submitted. Wrong state!')
1065            return
1066        IWorkflowInfo(self.context).fireTransition('submit')
1067        self.context.application_date = datetime.now()
1068        self.context.locked = True
1069        self.flash('Form has been submitted.')
1070        self.redirect(self.url(self.context))
1071        return
1072
1073class ApplicantViewActionButton(ManageActionButton):
1074    grok.context(IApplicant)
1075    grok.view(ApplicantManageFormPage)
1076    grok.require('waeup.viewApplication')
1077    icon = 'actionicon_view.png'
1078    text = 'View application record'
1079    target = 'index'
1080
1081class PassportImage(grok.View):
1082    """Renders the passport image for applicants.
1083    """
1084    grok.name('passport.jpg')
1085    grok.context(IApplicant)
1086    grok.require('waeup.viewApplication')
1087
1088    def render(self):
1089        # A filename chooser turns a context into a filename suitable
1090        # for file storage.
1091        image = getUtility(IExtFileStore).getFileByContext(self.context)
1092        self.response.setHeader(
1093            'Content-Type', 'image/jpeg')
1094        if image is None:
1095            # show placeholder image
1096            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1097        return image
1098
1099class ApplicantRegistrationPage(SIRPAddFormPage):
1100    """Captcha'd registration page for applicants.
1101    """
1102    grok.context(IApplicantsContainer)
1103    grok.name('register')
1104    grok.require('waeup.Anonymous')
1105    grok.template('applicantregister')
1106    form_fields = grok.AutoFields(IApplicantEdit).select(
1107        'firstname', 'middlename', 'lastname', 'email', 'phone')
1108    form_fields['phone'].custom_widget = PhoneWidget
1109
1110    @property
1111    def title(self):
1112        return "Applicants Container: %s" % self.context.title
1113
1114    @property
1115    def label(self):
1116        return "Register for %s Application" % self.context.title
1117
1118    def update(self):
1119        # Check if application has started ...
1120        if not self.context.startdate or self.context.startdate > date.today():
1121            self.flash('Application has not yet started.')
1122            self.redirect(self.url(self.context))
1123            return
1124        # ... or ended
1125        if not self.context.enddate or self.context.enddate < date.today():
1126            self.flash('Application has ended.')
1127            self.redirect(self.url(self.context))
1128            return
1129        # Handle captcha
1130        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1131        self.captcha_result = self.captcha.verify(self.request)
1132        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1133        return
1134
1135    @grok.action('Send login credentials')
1136    def register(self, **data):
1137        if not self.captcha_result.is_valid:
1138            # captcha will display error messages automatically.
1139            # No need to flash something.
1140            return
1141        sirp_utils = getUtility(ISIRPUtils)
1142        # Add applicant and create password
1143        applicant = createObject('waeup.Applicant')
1144        self.applyData(applicant, **data)
1145        self.context.addApplicant(applicant)
1146        pwd = sirp_utils.genPassword()
1147        IUserAccount(applicant).setPassword(pwd)
1148        # Send email with credentials
1149        username = applicant.applicant_id
1150        fullname = applicant.display_fullname
1151        subject = 'Your SIRP credentials'
1152        msg = 'You have successfully registered for the'
1153        email_to = applicant.email
1154        login_url = self.url(grok.getSite(), 'login')
1155        success = sirp_utils.sendPassword(fullname,msg,username,
1156            pwd,login_url,email_to,subject)
1157        if success:
1158            self.flash('An email with your user name and password ' +
1159                'has been sent to %s.' % email_to)
1160        else:
1161            self.flash('An smtp server error occurred.')
1162        return
Note: See TracBrowser for help on using the repository browser.