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

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

The payment receipt is now called payment slip and can be downloaded even if not paid. This is necessary for eTranzact payments.

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