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

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

Insert description and use FriendlyDatetimeDisplayWidget? in display view.

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