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

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

Fix title assignment and add test for checking that the title looks as expected.

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