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

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

Adjust size of phone widget. The default country code is still not being set.

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