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

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

Page titles are deprecated. We only have labels.

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