source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/browser.py @ 8033

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

Define two different application modes: create and update. The latter expects existing (imported) application records. The ApplicantRegistrationPage? renders different form fields and creates or updates application records depending on the selected mode.

The update registration mode is not yet secure enough. Further security features will be added.

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