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

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

Translate transitions and history.

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