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

Last change on this file since 8157 was 8150, checked in by uli, 13 years ago

Register FormattedDateDisplayWidget? for FormattedDate? schema type.

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