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

Last change on this file since 8160 was 8160, checked in by Henrik Bettermann, 12 years ago

Use new date and datetime widgets everywhere in base package.

Use standard datetime widgets for imports. I don't know if this is really necessary.

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