source: main/waeup.sirp/branches/henrik-bootstrap/src/waeup/sirp/applicants/browser.py @ 7441

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

Put viewlets into their own module.

Remove unneeded imports.

  • Property svn:keywords set to Id
File size: 33.5 KB
Line 
1## $Id: browser.py 7438 2011-12-22 18:24:35Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from time import time
24from datetime import datetime, date
25from zope.component import getUtility, createObject, getAdapter
26from zope.formlib.form import setUpEditWidgets
27from hurry.workflow.interfaces import (
28    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
29from waeup.sirp.applicants.interfaces import (
30    IApplicant, IApplicantEdit, IApplicantsRoot,
31    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab,
32    MAX_UPLOAD_SIZE, IApplicantOnlinePayment,
33    )
34from waeup.sirp.applicants.workflow import INITIALIZED, STARTED, PAID, SUBMITTED
35from waeup.sirp.browser import (
36    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
37    DEFAULT_PASSPORT_IMAGE_PATH)
38from waeup.sirp.browser.interfaces import ICaptchaManager
39from waeup.sirp.browser.breadcrumbs import Breadcrumb
40from waeup.sirp.browser.layout import NullValidator, jsaction
41from waeup.sirp.browser.pages import add_local_role, del_local_roles
42from waeup.sirp.browser.resources import datepicker, tabs, datatable, warning
43from waeup.sirp.interfaces import (
44    ISIRPObject, ILocalRolesAssignable, IExtFileStore, IPDF,
45    IFileStoreNameChooser, IPasswordValidator, IUserAccount, ISIRPUtils)
46from waeup.sirp.permissions import get_users_with_local_roles
47from waeup.sirp.students.interfaces import IStudentsUtils
48from waeup.sirp.utils.helpers import string_from_bytes, file_size
49from waeup.sirp.widgets.datewidget import (
50    FriendlyDateWidget, FriendlyDateDisplayWidget,
51    FriendlyDatetimeDisplayWidget)
52from waeup.sirp.widgets.phonewidget import PhoneWidget
53from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
54
55grok.context(ISIRPObject) # Make ISIRPObject the default context
56
57class ApplicantsRootPage(SIRPPage):
58    grok.context(IApplicantsRoot)
59    grok.name('index')
60    grok.require('waeup.Public')
61    title = 'Applicants'
62    label = 'Application Section'
63    pnav = 3
64
65    def update(self):
66        super(ApplicantsRootPage, self).update()
67        datatable.need()
68        return
69
70class ApplicantsRootManageFormPage(SIRPEditFormPage):
71    grok.context(IApplicantsRoot)
72    grok.name('manage')
73    grok.template('applicantsrootmanagepage')
74    title = 'Applicants'
75    label = 'Manage application section'
76    pnav = 3
77    grok.require('waeup.manageApplication')
78    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
79    tabtwoactions1 = ['Remove selected local roles']
80    tabtwoactions2 = ['Add local role']
81    subunits = 'Applicants Containers'
82
83    def update(self):
84        tabs.need()
85        datatable.need()
86        warning.need()
87        return super(ApplicantsRootManageFormPage, self).update()
88
89    def getLocalRoles(self):
90        roles = ILocalRolesAssignable(self.context)
91        return roles()
92
93    def getUsers(self):
94        """Get a list of all users.
95        """
96        for key, val in grok.getSite()['users'].items():
97            url = self.url(val)
98            yield(dict(url=url, name=key, val=val))
99
100    def getUsersWithLocalRoles(self):
101        return get_users_with_local_roles(self.context)
102
103    @jsaction('Remove selected')
104    def delApplicantsContainers(self, **data):
105        form = self.request.form
106        child_id = form['val_id']
107        if not isinstance(child_id, list):
108            child_id = [child_id]
109        deleted = []
110        for id in child_id:
111            try:
112                del self.context[id]
113                deleted.append(id)
114            except:
115                self.flash('Could not delete %s: %s: %s' % (
116                        id, sys.exc_info()[0], sys.exc_info()[1]))
117        if len(deleted):
118            self.flash('Successfully removed: %s' % ', '.join(deleted))
119        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
120        return
121
122    @grok.action('Add applicants container', validator=NullValidator)
123    def addApplicantsContainer(self, **data):
124        self.redirect(self.url(self.context, '@@add'))
125        return
126
127    @grok.action('Cancel', validator=NullValidator)
128    def cancel(self, **data):
129        self.redirect(self.url(self.context))
130        return
131
132    @grok.action('Add local role', validator=NullValidator)
133    def addLocalRole(self, **data):
134        return add_local_role(self,2, **data)
135
136    @grok.action('Remove selected local roles')
137    def delLocalRoles(self, **data):
138        return del_local_roles(self,2,**data)
139
140class ApplicantsContainerAddFormPage(SIRPAddFormPage):
141    grok.context(IApplicantsRoot)
142    grok.require('waeup.manageApplication')
143    grok.name('add')
144    grok.template('applicantscontaineraddpage')
145    title = 'Applicants'
146    label = 'Add applicants container'
147    pnav = 3
148
149    form_fields = grok.AutoFields(
150        IApplicantsContainerAdd).omit('code').omit('title')
151    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
152    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
153
154    def update(self):
155        datepicker.need() # Enable jQuery datepicker in date fields.
156        return super(ApplicantsContainerAddFormPage, self).update()
157
158    @grok.action('Add applicants container')
159    def addApplicantsContainer(self, **data):
160        year = data['year']
161        code = u'%s%s' % (data['prefix'], year)
162        prefix = application_types_vocab.getTerm(data['prefix'])
163        title = u'%s %s/%s' % (prefix.title, year, year + 1)
164        if code in self.context.keys():
165            self.flash(
166                'An applicants container for the same application '
167                'type and entrance year exists already in the database.')
168            return
169        # Add new applicants container...
170        provider = data['provider'][1]
171        container = provider.factory()
172        self.applyData(container, **data)
173        container.code = code
174        container.title = title
175        self.context[code] = container
176        self.flash('Added: "%s".' % code)
177        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
178        return
179
180    @grok.action('Cancel', validator=NullValidator)
181    def cancel(self, **data):
182        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
183
184class ApplicantsRootBreadcrumb(Breadcrumb):
185    """A breadcrumb for applicantsroot.
186    """
187    grok.context(IApplicantsRoot)
188    title = u'Applicants'
189
190class ApplicantsContainerBreadcrumb(Breadcrumb):
191    """A breadcrumb for applicantscontainers.
192    """
193    grok.context(IApplicantsContainer)
194
195class ApplicantBreadcrumb(Breadcrumb):
196    """A breadcrumb for applicants.
197    """
198    grok.context(IApplicant)
199
200    @property
201    def title(self):
202        """Get a title for a context.
203        """
204        return self.context.application_number
205
206class OnlinePaymentBreadcrumb(Breadcrumb):
207    """A breadcrumb for payments.
208    """
209    grok.context(IApplicantOnlinePayment)
210
211    @property
212    def title(self):
213        return self.context.p_id
214
215class ApplicantsContainerPage(SIRPDisplayFormPage):
216    """The standard view for regular applicant containers.
217    """
218    grok.context(IApplicantsContainer)
219    grok.name('index')
220    grok.require('waeup.Public')
221    grok.template('applicantscontainerpage')
222    pnav = 3
223
224    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
225    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
226    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
227    form_fields['description'].custom_widget = ReSTDisplayWidget
228
229    @property
230    def title(self):
231        return "Applicants Container: %s" % self.context.title
232
233    @property
234    def label(self):
235        return self.context.title
236
237class ApplicantsContainerManageFormPage(SIRPEditFormPage):
238    grok.context(IApplicantsContainer)
239    grok.name('manage')
240    grok.template('applicantscontainermanagepage')
241    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
242    taboneactions = ['Save','Cancel']
243    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
244    tabthreeactions1 = ['Remove selected local roles']
245    tabthreeactions2 = ['Add local role']
246    # Use friendlier date widget...
247    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
248    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
249    grok.require('waeup.manageApplication')
250
251    @property
252    def title(self):
253        return "Applicants Container: %s" % self.context.title
254
255    @property
256    def label(self):
257        return 'Manage applicants container'
258
259    pnav = 3
260
261    def update(self):
262        datepicker.need() # Enable jQuery datepicker in date fields.
263        tabs.need()
264        warning.need()
265        datatable.need()  # Enable jQurey datatables for contents listing
266        return super(ApplicantsContainerManageFormPage, self).update()
267
268    def getLocalRoles(self):
269        roles = ILocalRolesAssignable(self.context)
270        return roles()
271
272    def getUsers(self):
273        """Get a list of all users.
274        """
275        for key, val in grok.getSite()['users'].items():
276            url = self.url(val)
277            yield(dict(url=url, name=key, val=val))
278
279    def getUsersWithLocalRoles(self):
280        return get_users_with_local_roles(self.context)
281
282    @grok.action('Save')
283    def apply(self, **data):
284        self.applyData(self.context, **data)
285        self.flash('Data saved.')
286        return
287
288    @jsaction('Remove selected')
289    def delApplicant(self, **data):
290        form = self.request.form
291        if form.has_key('val_id'):
292            child_id = form['val_id']
293        else:
294            self.flash('No applicant selected!')
295            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
296            return
297        if not isinstance(child_id, list):
298            child_id = [child_id]
299        deleted = []
300        for id in child_id:
301            try:
302                del self.context[id]
303                deleted.append(id)
304            except:
305                self.flash('Could not delete %s: %s: %s' % (
306                        id, sys.exc_info()[0], sys.exc_info()[1]))
307        if len(deleted):
308            self.flash('Successfully removed: %s' % ', '.join(deleted))
309        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
310        return
311
312    @grok.action('Add applicant', validator=NullValidator)
313    def addApplicant(self, **data):
314        self.redirect(self.url(self.context, 'addapplicant'))
315        return
316
317    @grok.action('Cancel', validator=NullValidator)
318    def cancel(self, **data):
319        self.redirect(self.url(self.context))
320        return
321
322    @grok.action('Add local role', validator=NullValidator)
323    def addLocalRole(self, **data):
324        return add_local_role(self,3, **data)
325
326    @grok.action('Remove selected local roles')
327    def delLocalRoles(self, **data):
328        return del_local_roles(self,3,**data)
329
330class ApplicantAddFormPage(SIRPAddFormPage):
331    """Add-form to add an applicant.
332    """
333    grok.context(IApplicantsContainer)
334    grok.require('waeup.manageApplication')
335    grok.name('addapplicant')
336    #grok.template('applicantaddpage')
337    form_fields = grok.AutoFields(IApplicant).select(
338        'firstname', 'middlename', 'lastname',
339        'email', 'phone')
340    label = 'Add applicant'
341    pnav = 3
342
343    @property
344    def title(self):
345        return "Applicants Container: %s" % self.context.title
346
347    @grok.action('Create application record')
348    def addApplicant(self, **data):
349        applicant = createObject(u'waeup.Applicant')
350        self.applyData(applicant, **data)
351        self.context.addApplicant(applicant)
352        self.flash('Applicant record created.')
353        self.redirect(
354            self.url(self.context[applicant.application_number], 'index'))
355        return
356
357class ApplicantDisplayFormPage(SIRPDisplayFormPage):
358    grok.context(IApplicant)
359    grok.name('index')
360    grok.require('waeup.viewApplication')
361    grok.template('applicantdisplaypage')
362    form_fields = grok.AutoFields(IApplicant).omit(
363        'locked', 'course_admitted', 'password')
364    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
365    label = 'Applicant'
366    pnav = 3
367
368    def update(self):
369        self.passport_url = self.url(self.context, 'passport.jpg')
370        # Mark application as started if applicant logs in for the first time
371        usertype = getattr(self.request.principal, 'user_type', None)
372        if usertype == 'applicant' and \
373            IWorkflowState(self.context).getState() == INITIALIZED:
374            IWorkflowInfo(self.context).fireTransition('start')
375        return
376
377    @property
378    def hasPassword(self):
379        if self.context.password:
380            return 'set'
381        return 'unset'
382
383    @property
384    def title(self):
385        return 'Application Record %s' % self.context.application_number
386
387    @property
388    def label(self):
389        container_title = self.context.__parent__.title
390        return '%s Application Record %s' % (
391            container_title, self.context.application_number)
392
393    def getCourseAdmitted(self):
394        """Return link, title and code in html format to the certificate
395           admitted.
396        """
397        course_admitted = self.context.course_admitted
398        if getattr(course_admitted, '__parent__',None):
399            url = self.url(course_admitted)
400            title = course_admitted.title
401            code = course_admitted.code
402            return '<a href="%s">%s - %s</a>' %(url,code,title)
403        return ''
404
405class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
406    grok.context(IApplicant)
407    grok.name('base')
408    form_fields = grok.AutoFields(IApplicant).select(
409        'applicant_id', 'firstname', 'lastname','email', 'course1')
410
411class CreateStudentPage(grok.View):
412    """Create a student object from applicatnt data
413    and copy applicant object.
414    """
415    grok.context(IApplicant)
416    grok.name('createstudent')
417    grok.require('waeup.manageStudent')
418
419    def update(self):
420        msg = self.context.createStudent()[1]
421        self.flash(msg)
422        self.redirect(self.url(self.context))
423        return
424
425    def render(self):
426        return
427
428class AcceptanceFeePaymentAddPage(grok.View):
429    """ Page to add an online payment ticket
430    """
431    grok.context(IApplicant)
432    grok.name('addafp')
433    grok.require('waeup.payApplicant')
434
435    def update(self):
436        p_category = 'acceptance'
437        session = str(self.context.__parent__.year)
438        try:
439            academic_session = grok.getSite()['configuration'][session]
440        except KeyError:
441            self.flash('Session configuration object is not available.')
442            return
443        timestamp = "%d" % int(time()*1000)
444        for key in self.context.keys():
445            ticket = self.context[key]
446            if ticket.p_state == 'paid':
447                  self.flash(
448                      'This type of payment has already been made.')
449                  self.redirect(self.url(self.context))
450                  return
451        payment = createObject(u'waeup.ApplicantOnlinePayment')
452        payment.p_id = "p%s" % timestamp
453        payment.p_item = self.context.__parent__.title
454        payment.p_year = self.context.__parent__.year
455        payment.p_category = p_category
456        payment.amount_auth = academic_session.acceptance_fee
457        payment.surcharge_1 = academic_session.surcharge_1
458        payment.surcharge_2 = academic_session.surcharge_2
459        payment.surcharge_3 = academic_session.surcharge_3
460        self.context[payment.p_id] = payment
461        self.flash('Payment ticket created.')
462        return
463
464    def render(self):
465        usertype = getattr(self.request.principal, 'user_type', None)
466        if usertype == 'applicant':
467            self.redirect(self.url(self.context, '@@edit'))
468            return
469        self.redirect(self.url(self.context, '@@manage'))
470        return
471
472
473class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
474    """ Page to view an online payment ticket
475    """
476    grok.context(IApplicantOnlinePayment)
477    grok.name('index')
478    grok.require('waeup.viewApplication')
479    form_fields = grok.AutoFields(IApplicantOnlinePayment)
480    form_fields[
481        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
482    form_fields[
483        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
484    pnav = 3
485
486    @property
487    def title(self):
488        return 'Online Payment Ticket %s' % self.context.p_id
489
490    @property
491    def label(self):
492        return '%s: Online Payment Ticket %s' % (
493            self.context.__parent__.display_fullname,self.context.p_id)
494
495class OnlinePaymentCallbackPage(grok.View):
496    """ Callback view
497    """
498    grok.context(IApplicantOnlinePayment)
499    grok.name('callback')
500    grok.require('waeup.payApplicant')
501
502    # This update method simulates a valid callback und must be
503    # specified in the customization package. The parameters must be taken
504    # from the incoming request.
505    def update(self):
506        self.wf_info = IWorkflowInfo(self.context.__parent__)
507        try:
508            self.wf_info.fireTransition('pay')
509        except InvalidTransitionError:
510            self.flash('Error: %s' % sys.exc_info()[1])
511            return
512        self.context.r_amount_approved = self.context.amount_auth
513        self.context.r_card_num = u'0000'
514        self.context.r_code = u'00'
515        self.context.p_state = 'paid'
516        self.context.payment_date = datetime.now()
517        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
518        self.context.__parent__.loggerInfo(
519            ob_class, 'valid callback: %s' % self.context.p_id)
520        self.flash('Valid callback received.')
521        return
522
523    def render(self):
524        self.redirect(self.url(self.context, '@@index'))
525        return
526
527class ExportPDFPaymentSlipPage(grok.View):
528    """Deliver a PDF slip of the context.
529    """
530    grok.context(IApplicantOnlinePayment)
531    grok.name('payment_receipt.pdf')
532    grok.require('waeup.viewApplication')
533    form_fields = grok.AutoFields(IApplicantOnlinePayment)
534    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
535    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
536    prefix = 'form'
537    title = 'Payment Data'
538
539    @property
540    def label(self):
541        return 'Online Payment Receipt %s' % self.context.p_id
542
543    def render(self):
544        if self.context.p_state != 'paid':
545            self.flash('Ticket not yet paid.')
546            self.redirect(self.url(self.context))
547            return
548        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
549            self.request)
550        students_utils = getUtility(IStudentsUtils)
551        return students_utils.renderPDF(self,'payment_receipt.pdf',
552            self.context.__parent__, applicantview)
553
554class ExportPDFPage(grok.View):
555    """Deliver a PDF slip of the context.
556    """
557    grok.context(IApplicant)
558    grok.name('application_slip.pdf')
559    grok.require('waeup.viewApplication')
560    form_fields = grok.AutoFields(IApplicant).omit(
561        'locked', 'course_admitted')
562    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
563    prefix = 'form'
564
565    @property
566    def label(self):
567        container_title = self.context.__parent__.title
568        return '%s Application Record %s' % (
569            container_title, self.context.application_number)
570
571    def getCourseAdmitted(self):
572        """Return title and code in html format to the certificate
573           admitted.
574        """
575        course_admitted = self.context.course_admitted
576        #if ICertificate.providedBy(course_admitted):
577        if getattr(course_admitted, '__parent__',None):
578            title = course_admitted.title
579            code = course_admitted.code
580            return '%s - %s' %(code,title)
581        return ''
582
583    def setUpWidgets(self, ignore_request=False):
584        self.adapters = {}
585        self.widgets = setUpEditWidgets(
586            self.form_fields, self.prefix, self.context, self.request,
587            adapters=self.adapters, for_display=True,
588            ignore_request=ignore_request
589            )
590
591    def render(self):
592        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
593            view=self)
594        self.response.setHeader(
595            'Content-Type', 'application/pdf')
596        return pdfstream
597
598def handle_img_upload(upload, context, view):
599    """Handle upload of applicant image.
600
601    Returns `True` in case of success or `False`.
602
603    Please note that file pointer passed in (`upload`) most probably
604    points to end of file when leaving this function.
605    """
606    size = file_size(upload)
607    if size > MAX_UPLOAD_SIZE:
608        view.flash('Uploaded image is too big!')
609        return False
610    dummy, ext = os.path.splitext(upload.filename)
611    ext.lower()
612    if ext != '.jpg':
613        view.flash('jpg file extension expected.')
614        return False
615    upload.seek(0) # file pointer moved when determining size
616    store = getUtility(IExtFileStore)
617    file_id = IFileStoreNameChooser(context).chooseName()
618    store.createFile(file_id, upload)
619    return True
620
621class ApplicantManageFormPage(SIRPEditFormPage):
622    """A full edit view for applicant data.
623    """
624    grok.context(IApplicant)
625    grok.name('manage')
626    grok.require('waeup.manageApplication')
627    form_fields = grok.AutoFields(IApplicant)
628    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
629    form_fields['student_id'].for_display = True
630    form_fields['applicant_id'].for_display = True
631    form_fields['phone'].custom_widget = PhoneWidget
632    grok.template('applicanteditpage')
633    manage_applications = True
634    pnav = 3
635    display_actions = [['Save', 'Final Submit'],
636                       ['Add online payment ticket','Remove selected tickets']]
637
638    def update(self):
639        datepicker.need() # Enable jQuery datepicker in date fields.
640        warning.need()
641        super(ApplicantManageFormPage, self).update()
642        self.wf_info = IWorkflowInfo(self.context)
643        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
644        self.passport_changed = None
645        upload = self.request.form.get('form.passport', None)
646        if upload:
647            # We got a fresh upload
648            self.passport_changed = handle_img_upload(
649                upload, self.context, self)
650        return
651
652    @property
653    def title(self):
654        return 'Application Record %s' % self.context.application_number
655
656    @property
657    def label(self):
658        container_title = self.context.__parent__.title
659        return '%s Application Form %s' % (
660            container_title, self.context.application_number)
661
662    def getTransitions(self):
663        """Return a list of dicts of allowed transition ids and titles.
664
665        Each list entry provides keys ``name`` and ``title`` for
666        internal name and (human readable) title of a single
667        transition.
668        """
669        allowed_transitions = self.wf_info.getManualTransitions()
670        return [dict(name='', title='No transition')] +[
671            dict(name=x, title=y) for x, y in allowed_transitions]
672
673    @grok.action('Save')
674    def save(self, **data):
675        form = self.request.form
676        password = form.get('password', None)
677        password_ctl = form.get('control_password', None)
678        if password:
679            validator = getUtility(IPasswordValidator)
680            errors = validator.validate_password(password, password_ctl)
681            if errors:
682                self.flash( ' '.join(errors))
683                return
684        if self.passport_changed is False:  # False is not None!
685            return # error during image upload. Ignore other values
686        changed_fields = self.applyData(self.context, **data)
687        # Turn list of lists into single list
688        if changed_fields:
689            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
690        else:
691            changed_fields = []
692        if self.passport_changed:
693            changed_fields.append('passport')
694        if password:
695            # Now we know that the form has no errors and can set password ...
696            IUserAccount(self.context).setPassword(password)
697            changed_fields.append('password')
698        fields_string = ' + '.join(changed_fields)
699        trans_id = form.get('transition', None)
700        if trans_id:
701            self.wf_info.fireTransition(trans_id)
702        self.flash('Form has been saved.')
703        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
704        if fields_string:
705            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
706        return
707
708    def unremovable(self, ticket):
709        return False
710
711    # This method is also used by the ApplicantEditFormPage
712    def delPaymentTickets(self, **data):
713        form = self.request.form
714        if form.has_key('val_id'):
715            child_id = form['val_id']
716        else:
717            self.flash('No payment selected.')
718            self.redirect(self.url(self.context))
719            return
720        if not isinstance(child_id, list):
721            child_id = [child_id]
722        deleted = []
723        for id in child_id:
724            # Applicants are not allowed to remove used payment tickets
725            if not self.unremovable(self.context[id]):
726                try:
727                    del self.context[id]
728                    deleted.append(id)
729                except:
730                    self.flash('Could not delete %s: %s: %s' % (
731                            id, sys.exc_info()[0], sys.exc_info()[1]))
732        if len(deleted):
733            self.flash('Successfully removed: %s' % ', '.join(deleted))
734            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
735            self.context.loggerInfo(
736                ob_class, 'removed: % s' % ', '.join(deleted))
737        return
738
739    # We explicitely want the forms to be validated before payment tickets
740    # can be created. If no validation is requested, use
741    # 'validator=NullValidator' in the grok.action directive
742    @grok.action('Add online payment ticket')
743    def addPaymentTicket(self, **data):
744        self.redirect(self.url(self.context, '@@addafp'))
745        return
746
747    @jsaction('Remove selected tickets')
748    def removePaymentTickets(self, **data):
749        self.delPaymentTickets(**data)
750        self.redirect(self.url(self.context) + '/@@manage')
751        return
752
753class ApplicantEditFormPage(ApplicantManageFormPage):
754    """An applicant-centered edit view for applicant data.
755    """
756    grok.context(IApplicantEdit)
757    grok.name('edit')
758    grok.require('waeup.handleApplication')
759    form_fields = grok.AutoFields(IApplicantEdit).omit(
760        'locked', 'course_admitted', 'student_id',
761        'screening_score', 'reg_number'
762        )
763    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
764    form_fields['phone'].custom_widget = PhoneWidget
765    grok.template('applicanteditpage')
766    manage_applications = False
767    title = u'Your Application Form'
768
769    @property
770    def display_actions(self):
771        state = IWorkflowState(self.context).getState()
772        if state == INITIALIZED:
773            actions = [[],[]]
774        elif state == STARTED:
775            actions = [['Save'],
776                       ['Add online payment ticket','Remove selected tickets']]
777        elif state == PAID:
778            actions = [['Save', 'Final Submit'],
779                       ['Remove selected tickets']]
780        else:
781            actions = [[],[]]
782        return actions
783
784    def unremovable(self, ticket):
785        state = IWorkflowState(self.context).getState()
786        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
787
788    def emit_lock_message(self):
789        self.flash('The requested form is locked (read-only).')
790        self.redirect(self.url(self.context))
791        return
792
793    def update(self):
794        if self.context.locked:
795            self.emit_lock_message()
796            return
797        super(ApplicantEditFormPage, self).update()
798        return
799
800    def dataNotComplete(self):
801        store = getUtility(IExtFileStore)
802        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
803            return 'No passport picture uploaded.'
804        if not self.request.form.get('confirm_passport', False):
805            return 'Passport picture confirmation box not ticked.'
806        return False
807
808    # We explicitely want the forms to be validated before payment tickets
809    # can be created. If no validation is requested, use
810    # 'validator=NullValidator' in the grok.action directive
811    @grok.action('Add online payment ticket')
812    def addPaymentTicket(self, **data):
813        self.redirect(self.url(self.context, '@@addafp'))
814        return
815
816    @jsaction('Remove selected tickets')
817    def removePaymentTickets(self, **data):
818        self.delPaymentTickets(**data)
819        self.redirect(self.url(self.context) + '/@@edit')
820        return
821
822    @grok.action('Save')
823    def save(self, **data):
824        if self.passport_changed is False:  # False is not None!
825            return # error during image upload. Ignore other values
826        self.applyData(self.context, **data)
827        self.flash('Form has been saved.')
828        return
829
830    @grok.action('Final Submit')
831    def finalsubmit(self, **data):
832        if self.passport_changed is False:  # False is not None!
833            return # error during image upload. Ignore other values
834        if self.dataNotComplete():
835            self.flash(self.dataNotComplete())
836            return
837        self.applyData(self.context, **data)
838        state = IWorkflowState(self.context).getState()
839        # This shouldn't happen, but the application officer
840        # might have forgotten to lock the form after changing the state
841        if state != PAID:
842            self.flash('This form cannot be submitted. Wrong state!')
843            return
844        IWorkflowInfo(self.context).fireTransition('submit')
845        self.context.application_date = datetime.now()
846        self.context.locked = True
847        self.flash('Form has been submitted.')
848        self.redirect(self.url(self.context))
849        return
850
851class PassportImage(grok.View):
852    """Renders the passport image for applicants.
853    """
854    grok.name('passport.jpg')
855    grok.context(IApplicant)
856    grok.require('waeup.viewApplication')
857
858    def render(self):
859        # A filename chooser turns a context into a filename suitable
860        # for file storage.
861        image = getUtility(IExtFileStore).getFileByContext(self.context)
862        self.response.setHeader(
863            'Content-Type', 'image/jpeg')
864        if image is None:
865            # show placeholder image
866            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
867        return image
868
869class ApplicantRegistrationPage(SIRPAddFormPage):
870    """Captcha'd registration page for applicants.
871    """
872    grok.context(IApplicantsContainer)
873    grok.name('register')
874    grok.require('waeup.Anonymous')
875    grok.template('applicantregister')
876    form_fields = grok.AutoFields(IApplicantEdit).select(
877        'firstname', 'middlename', 'lastname', 'email', 'phone')
878    form_fields['phone'].custom_widget = PhoneWidget
879
880    @property
881    def title(self):
882        return "Applicants Container: %s" % self.context.title
883
884    @property
885    def label(self):
886        return "Register for %s Application" % self.context.title
887
888    def update(self):
889        # Check if application has started ...
890        if not self.context.startdate or self.context.startdate > date.today():
891            self.flash('Application has not yet started.')
892            self.redirect(self.url(self.context))
893            return
894        # ... or ended
895        if not self.context.enddate or self.context.enddate < date.today():
896            self.flash('Application has ended.')
897            self.redirect(self.url(self.context))
898            return
899        # Handle captcha
900        self.captcha = getUtility(ICaptchaManager).getCaptcha()
901        self.captcha_result = self.captcha.verify(self.request)
902        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
903        return
904
905    @grok.action('Get login credentials')
906    def register(self, **data):
907        if not self.captcha_result.is_valid:
908            # captcha will display error messages automatically.
909            # No need to flash something.
910            return
911        # Add applicant and create password
912        applicant = createObject('waeup.Applicant')
913        self.applyData(applicant, **data)
914        self.context.addApplicant(applicant)
915        sirp_utils = getUtility(ISIRPUtils)
916        password = sirp_utils.genPassword()
917        IUserAccount(applicant).setPassword(password)
918        # Send email with credentials
919        login_url = self.url(grok.getSite(), 'login')
920        msg = 'You have successfully been registered for the'
921        if sirp_utils.sendCredentials(IUserAccount(applicant),
922            password, login_url, msg):
923            self.redirect(self.url(self.context, 'registration_complete',
924                                   data = dict(email=applicant.email)))
925            return
926        else:
927            self.flash('Email could not been sent. Please retry later.')
928        return
929
930class ApplicantRegistrationEmailSent(SIRPPage):
931    """Landing page after successful registration.
932    """
933    grok.name('registration_complete')
934    grok.require('waeup.Public')
935    grok.template('applicantregemailsent')
936    title = 'Registration Completed'
937    label = 'Your registration was successful'
938
939    def update(self, email=None):
940        self.email = email
941        return
Note: See TracBrowser for help on using the repository browser.