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

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

Reorganize application record update order and notify the catalog after modifying attributes.

Everything is now perfectly tested.

  • Property svn:keywords set to Id
File size: 35.5 KB
Line 
1## $Id: browser.py 8042 2012-04-04 21:28:16Z 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.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    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
385    label = _('Applicant')
386    pnav = 3
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} 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 AcceptanceFeePaymentAddPage(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
451    def update(self):
452        p_category = 'acceptance'
453        session = str(self.context.__parent__.year)
454        try:
455            academic_session = grok.getSite()['configuration'][session]
456        except KeyError:
457            self.flash(_('Session configuration object is not available.'))
458            return
459        timestamp = "%d" % int(time()*1000)
460        for key in self.context.keys():
461            ticket = self.context[key]
462            if ticket.p_state == 'paid':
463                  self.flash(
464                      _('This type of payment has already been made.'))
465                  self.redirect(self.url(self.context))
466                  return
467        payment = createObject(u'waeup.ApplicantOnlinePayment')
468        payment.p_id = "p%s" % timestamp
469        payment.p_item = self.context.__parent__.title
470        payment.p_year = self.context.__parent__.year
471        payment.p_category = p_category
472        payment.amount_auth = academic_session.acceptance_fee
473        payment.surcharge_1 = academic_session.surcharge_1
474        payment.surcharge_2 = academic_session.surcharge_2
475        payment.surcharge_3 = academic_session.surcharge_3
476        self.context[payment.p_id] = payment
477        self.flash(_('Payment ticket created.'))
478        return
479
480    def render(self):
481        usertype = getattr(self.request.principal, 'user_type', None)
482        if usertype == 'applicant':
483            self.redirect(self.url(self.context, '@@edit'))
484            return
485        self.redirect(self.url(self.context, '@@manage'))
486        return
487
488
489class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
490    """ Page to view an online payment ticket
491    """
492    grok.context(IApplicantOnlinePayment)
493    grok.name('index')
494    grok.require('waeup.viewApplication')
495    form_fields = grok.AutoFields(IApplicantOnlinePayment)
496    form_fields[
497        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
498    form_fields[
499        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
500    pnav = 3
501
502    @property
503    def label(self):
504        return _('${a}: Online Payment Ticket ${b}', mapping = {
505            'a':self.context.__parent__.display_fullname, 'b':self.context.p_id})
506
507class OnlinePaymentCallbackPage(UtilityView, grok.View):
508    """ Callback view
509    """
510    grok.context(IApplicantOnlinePayment)
511    grok.name('simulate_callback')
512    grok.require('waeup.payApplicant')
513
514    # This update method simulates a valid callback und must be
515    # neutralized in the customization package.
516    def update(self):
517        self.wf_info = IWorkflowInfo(self.context.__parent__)
518        try:
519            self.wf_info.fireTransition('pay')
520        except InvalidTransitionError:
521            self.flash('Error: %s' % sys.exc_info()[1])
522            return
523        self.context.r_amount_approved = self.context.amount_auth
524        self.context.r_card_num = u'0000'
525        self.context.r_code = u'00'
526        self.context.p_state = 'paid'
527        self.context.payment_date = datetime.now()
528        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
529        self.context.__parent__.loggerInfo(
530            ob_class, 'valid callback: %s' % self.context.p_id)
531        self.flash(_('Valid callback received.'))
532        return
533
534    def render(self):
535        self.redirect(self.url(self.context, '@@index'))
536        return
537
538class ExportPDFPaymentSlipPage(UtilityView, grok.View):
539    """Deliver a PDF slip of the context.
540    """
541    grok.context(IApplicantOnlinePayment)
542    grok.name('payment_receipt.pdf')
543    grok.require('waeup.viewApplication')
544    form_fields = grok.AutoFields(IApplicantOnlinePayment)
545    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
546    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
547    prefix = 'form'
548
549    @property
550    def title(self):
551        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
552        return translate(_('Payment Data'), 'waeup.kofa',
553            target_language=portal_language)
554
555    @property
556    def label(self):
557        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
558        return translate(_('Online Payment Receipt'),
559            'waeup.kofa', target_language=portal_language) \
560            + ' %s' % self.context.p_id
561
562    def render(self):
563        if self.context.p_state != 'paid':
564            self.flash(_('Ticket not yet paid.'))
565            self.redirect(self.url(self.context))
566            return
567        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
568            self.request)
569        students_utils = getUtility(IStudentsUtils)
570        return students_utils.renderPDF(self,'payment_receipt.pdf',
571            self.context.__parent__, applicantview)
572
573class ExportPDFPage(UtilityView, grok.View):
574    """Deliver a PDF slip of the context.
575    """
576    grok.context(IApplicant)
577    grok.name('application_slip.pdf')
578    grok.require('waeup.viewApplication')
579    prefix = 'form'
580
581    def render(self):
582        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
583            view=self)
584        self.response.setHeader(
585            'Content-Type', 'application/pdf')
586        return pdfstream
587
588def handle_img_upload(upload, context, view):
589    """Handle upload of applicant image.
590
591    Returns `True` in case of success or `False`.
592
593    Please note that file pointer passed in (`upload`) most probably
594    points to end of file when leaving this function.
595    """
596    size = file_size(upload)
597    if size > MAX_UPLOAD_SIZE:
598        view.flash(_('Uploaded image is too big!'))
599        return False
600    dummy, ext = os.path.splitext(upload.filename)
601    ext.lower()
602    if ext != '.jpg':
603        view.flash(_('jpg file extension expected.'))
604        return False
605    upload.seek(0) # file pointer moved when determining size
606    store = getUtility(IExtFileStore)
607    file_id = IFileStoreNameChooser(context).chooseName()
608    store.createFile(file_id, upload)
609    return True
610
611class ApplicantManageFormPage(KofaEditFormPage):
612    """A full edit view for applicant data.
613    """
614    grok.context(IApplicant)
615    grok.name('manage')
616    grok.require('waeup.manageApplication')
617    form_fields = grok.AutoFields(IApplicant)
618    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
619    form_fields['student_id'].for_display = True
620    form_fields['applicant_id'].for_display = True
621    form_fields['phone'].custom_widget = PhoneWidget
622    grok.template('applicanteditpage')
623    manage_applications = True
624    pnav = 3
625    display_actions = [[_('Save'), _('Final Submit')],
626        [_('Add online payment ticket'),_('Remove selected tickets')]]
627
628    def update(self):
629        datepicker.need() # Enable jQuery datepicker in date fields.
630        warning.need()
631        super(ApplicantManageFormPage, self).update()
632        self.wf_info = IWorkflowInfo(self.context)
633        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
634        self.passport_changed = None
635        upload = self.request.form.get('form.passport', None)
636        if upload:
637            # We got a fresh upload
638            self.passport_changed = handle_img_upload(
639                upload, self.context, self)
640        return
641
642    @property
643    def label(self):
644        container_title = self.context.__parent__.title
645        return _('${a} Application Form ${b}', mapping = {
646            'a':container_title, 'b':self.context.application_number})
647
648    def getTransitions(self):
649        """Return a list of dicts of allowed transition ids and titles.
650
651        Each list entry provides keys ``name`` and ``title`` for
652        internal name and (human readable) title of a single
653        transition.
654        """
655        allowed_transitions = self.wf_info.getManualTransitions()
656        return [dict(name='', title=_('No transition'))] +[
657            dict(name=x, title=y) for x, y in allowed_transitions]
658
659    @action(_('Save'), style='primary')
660    def save(self, **data):
661        form = self.request.form
662        password = form.get('password', None)
663        password_ctl = form.get('control_password', None)
664        if password:
665            validator = getUtility(IPasswordValidator)
666            errors = validator.validate_password(password, password_ctl)
667            if errors:
668                self.flash( ' '.join(errors))
669                return
670        if self.passport_changed is False:  # False is not None!
671            return # error during image upload. Ignore other values
672        changed_fields = self.applyData(self.context, **data)
673        # Turn list of lists into single list
674        if changed_fields:
675            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
676        else:
677            changed_fields = []
678        if self.passport_changed:
679            changed_fields.append('passport')
680        if password:
681            # Now we know that the form has no errors and can set password ...
682            IUserAccount(self.context).setPassword(password)
683            changed_fields.append('password')
684        fields_string = ' + '.join(changed_fields)
685        trans_id = form.get('transition', None)
686        if trans_id:
687            self.wf_info.fireTransition(trans_id)
688        self.flash(_('Form has been saved.'))
689        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
690        if fields_string:
691            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
692        return
693
694    def unremovable(self, ticket):
695        return False
696
697    # This method is also used by the ApplicantEditFormPage
698    def delPaymentTickets(self, **data):
699        form = self.request.form
700        if form.has_key('val_id'):
701            child_id = form['val_id']
702        else:
703            self.flash(_('No payment selected.'))
704            self.redirect(self.url(self.context))
705            return
706        if not isinstance(child_id, list):
707            child_id = [child_id]
708        deleted = []
709        for id in child_id:
710            # Applicants are not allowed to remove used payment tickets
711            if not self.unremovable(self.context[id]):
712                try:
713                    del self.context[id]
714                    deleted.append(id)
715                except:
716                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
717                            id, sys.exc_info()[0], sys.exc_info()[1]))
718        if len(deleted):
719            self.flash(_('Successfully removed: ${a}',
720                mapping = {'a':', '.join(deleted)}))
721            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
722            self.context.loggerInfo(
723                ob_class, 'removed: % s' % ', '.join(deleted))
724        return
725
726    # We explicitely want the forms to be validated before payment tickets
727    # can be created. If no validation is requested, use
728    # 'validator=NullValidator' in the action directive
729    @action(_('Add online payment ticket'))
730    def addPaymentTicket(self, **data):
731        self.redirect(self.url(self.context, '@@addafp'))
732        return
733
734    @jsaction(_('Remove selected tickets'))
735    def removePaymentTickets(self, **data):
736        self.delPaymentTickets(**data)
737        self.redirect(self.url(self.context) + '/@@manage')
738        return
739
740class ApplicantEditFormPage(ApplicantManageFormPage):
741    """An applicant-centered edit view for applicant data.
742    """
743    grok.context(IApplicantEdit)
744    grok.name('edit')
745    grok.require('waeup.handleApplication')
746    form_fields = grok.AutoFields(IApplicantEdit).omit(
747        'locked', 'course_admitted', 'student_id',
748        'screening_score',
749        )
750    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
751    form_fields['phone'].custom_widget = PhoneWidget
752    form_fields['applicant_id'].for_display = True
753    form_fields['reg_number'].for_display = True
754    grok.template('applicanteditpage')
755    manage_applications = False
756
757    @property
758    def display_actions(self):
759        state = IWorkflowState(self.context).getState()
760        if state == INITIALIZED:
761            actions = [[],[]]
762        elif state == STARTED:
763            actions = [[_('Save')],
764                [_('Add online payment ticket'),_('Remove selected tickets')]]
765        elif state == PAID:
766            actions = [[_('Save'), _('Final Submit')],
767                [_('Remove selected tickets')]]
768        else:
769            actions = [[],[]]
770        return actions
771
772    def unremovable(self, ticket):
773        state = IWorkflowState(self.context).getState()
774        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
775
776    def emit_lock_message(self):
777        self.flash(_('The requested form is locked (read-only).'))
778        self.redirect(self.url(self.context))
779        return
780
781    def update(self):
782        if self.context.locked:
783            self.emit_lock_message()
784            return
785        super(ApplicantEditFormPage, self).update()
786        return
787
788    def dataNotComplete(self):
789        store = getUtility(IExtFileStore)
790        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
791            return _('No passport picture uploaded.')
792        if not self.request.form.get('confirm_passport', False):
793            return _('Passport picture confirmation box not ticked.')
794        return False
795
796    # We explicitely want the forms to be validated before payment tickets
797    # can be created. If no validation is requested, use
798    # 'validator=NullValidator' in the action directive
799    @action(_('Add online payment ticket'))
800    def addPaymentTicket(self, **data):
801        self.redirect(self.url(self.context, '@@addafp'))
802        return
803
804    @jsaction(_('Remove selected tickets'))
805    def removePaymentTickets(self, **data):
806        self.delPaymentTickets(**data)
807        self.redirect(self.url(self.context) + '/@@edit')
808        return
809
810    @action(_('Save'), style='primary')
811    def save(self, **data):
812        if self.passport_changed is False:  # False is not None!
813            return # error during image upload. Ignore other values
814        self.applyData(self.context, **data)
815        self.flash('Form has been saved.')
816        return
817
818    @action(_('Final Submit'))
819    def finalsubmit(self, **data):
820        if self.passport_changed is False:  # False is not None!
821            return # error during image upload. Ignore other values
822        if self.dataNotComplete():
823            self.flash(self.dataNotComplete())
824            return
825        self.applyData(self.context, **data)
826        state = IWorkflowState(self.context).getState()
827        # This shouldn't happen, but the application officer
828        # might have forgotten to lock the form after changing the state
829        if state != PAID:
830            self.flash(_('This form cannot be submitted. Wrong state!'))
831            return
832        IWorkflowInfo(self.context).fireTransition('submit')
833        self.context.application_date = datetime.now()
834        self.context.locked = True
835        self.flash(_('Form has been submitted.'))
836        self.redirect(self.url(self.context))
837        return
838
839class PassportImage(grok.View):
840    """Renders the passport image for applicants.
841    """
842    grok.name('passport.jpg')
843    grok.context(IApplicant)
844    grok.require('waeup.viewApplication')
845
846    def render(self):
847        # A filename chooser turns a context into a filename suitable
848        # for file storage.
849        image = getUtility(IExtFileStore).getFileByContext(self.context)
850        self.response.setHeader(
851            'Content-Type', 'image/jpeg')
852        if image is None:
853            # show placeholder image
854            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
855        return image
856
857class ApplicantRegistrationPage(KofaAddFormPage):
858    """Captcha'd registration page for applicants.
859    """
860    grok.context(IApplicantsContainer)
861    grok.name('register')
862    grok.require('waeup.Anonymous')
863    grok.template('applicantregister')
864
865    @property
866    def form_fields(self):
867        form_fields = None
868        if self.context.mode == 'create':
869            form_fields = grok.AutoFields(IApplicantEdit).select(
870                'firstname', 'middlename', 'lastname', 'email', 'phone')
871            form_fields['phone'].custom_widget = PhoneWidget
872        elif self.context.mode == 'update':
873            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
874                'firstname','reg_number','email')
875        return form_fields
876
877    @property
878    def label(self):
879        return _('Register for ${a} Application',
880            mapping = {'a':self.context.title})
881
882    def update(self):
883        # Check if application has started ...
884        if not self.context.startdate or self.context.startdate > date.today():
885            self.flash(_('Application has not yet started.'))
886            self.redirect(self.url(self.context))
887            return
888        # ... or ended
889        if not self.context.enddate or self.context.enddate < date.today():
890            self.flash(_('Application has ended.'))
891            self.redirect(self.url(self.context))
892            return
893        # Handle captcha
894        self.captcha = getUtility(ICaptchaManager).getCaptcha()
895        self.captcha_result = self.captcha.verify(self.request)
896        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
897        return
898
899    @action(_('Get login credentials'), style='primary')
900    def register(self, **data):
901        if not self.captcha_result.is_valid:
902            # Captcha will display error messages automatically.
903            # No need to flash something.
904            return
905        if self.context.mode == 'create':
906            # Add applicant
907            applicant = createObject(u'waeup.Applicant')
908            self.applyData(applicant, **data)
909            self.context.addApplicant(applicant)
910            applicant.reg_number = applicant.applicant_id
911            notify(grok.ObjectModifiedEvent(applicant))
912        elif self.context.mode == 'update':
913            # Update applicant
914            reg_number = data.get('reg_number','')
915            firstname = data.get('firstname','')
916            cat = getUtility(ICatalog, name='applicants_catalog')
917            results = list(
918                cat.searchResults(reg_number=(reg_number, reg_number)))
919            if results:
920                applicant = results[0]
921                if getattr(applicant,'firstname',None) is None:
922                    self.flash(_('An error occurred.'))
923                    return
924                elif applicant.firstname.lower() != firstname.lower():
925                    # Don't tell the truth here. Anonymous must not
926                    # know that a record was found and only the firstname
927                    # verification failed.
928                    self.flash(_('No application record found.'))
929                    return
930                elif applicant.password is not None:
931                    self.flash(_('Your password has already been set. '
932                                 'Please proceed to the login page.'))
933                    return
934                # Store email address but nothing else.
935                applicant.email = data['email']
936                notify(grok.ObjectModifiedEvent(applicant))
937            else:
938                # No record found, this is the truth.
939                self.flash(_('No application record found.'))
940                return
941        else:
942            # Does not happen but anyway ...
943            return
944        kofa_utils = getUtility(IKofaUtils)
945        password = kofa_utils.genPassword()
946        IUserAccount(applicant).setPassword(password)
947        # Send email with credentials
948        login_url = self.url(grok.getSite(), 'login')
949        msg = _('You have successfully been registered for the')
950        if kofa_utils.sendCredentials(IUserAccount(applicant),
951            password, login_url, msg):
952            self.redirect(self.url(self.context, 'registration_complete',
953                                   data = dict(email=applicant.email)))
954            return
955        else:
956            self.flash(_('Email could not been sent. Please retry later.'))
957        return
958
959class ApplicantRegistrationEmailSent(KofaPage):
960    """Landing page after successful registration.
961    """
962    grok.name('registration_complete')
963    grok.require('waeup.Public')
964    grok.template('applicantregemailsent')
965    label = _('Your registration was successful.')
966
967    def update(self, email=None):
968        self.email = email
969        return
Note: See TracBrowser for help on using the repository browser.