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

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

Backup of local changes. The branch can now serve as basis for discussion today.

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