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

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

Change order of ManageActionButtons?.

Change label of ApplicantRegistrationPage?.

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