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

Last change on this file since 8201 was 8200, checked in by uli, 13 years ago
  • Use datetime for applicants containers and
  • Register new datetime widget as default for entering datetimes.

Fixed all tests to work with the new stuff.

As this is just a quick shot I put all changes into one commit
to ease any later rollback.

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