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

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

Remove all ApplicantsContainerProvider? components. Experience has shown that we only need one type of ApplicantsContainers? and one type of Applicants but with different interfaces for form generation.

  • Property svn:keywords set to Id
File size: 34.7 KB
Line 
1## $Id: browser.py 8008 2012-03-30 10:23:32Z 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.component import getUtility, createObject, getAdapter
26from zope.i18n import translate
27from hurry.workflow.interfaces import (
28    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
29from waeup.kofa.applicants.container import ApplicantsContainer
30from waeup.kofa.applicants.interfaces import (
31    IApplicant, IApplicantEdit, IApplicantsRoot,
32    IApplicantsContainer, IApplicantsContainerAdd,
33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils
34    )
35from waeup.kofa.applicants.workflow import INITIALIZED, STARTED, PAID, SUBMITTED
36from waeup.kofa.browser import (
37    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
38    DEFAULT_PASSPORT_IMAGE_PATH)
39from waeup.kofa.browser.interfaces import ICaptchaManager
40from waeup.kofa.browser.breadcrumbs import Breadcrumb
41from waeup.kofa.browser.layout import (
42    NullValidator, jsaction, action, UtilityView)
43from waeup.kofa.browser.pages import add_local_role, del_local_roles
44from waeup.kofa.browser.resources import datepicker, tabs, datatable, warning
45from waeup.kofa.interfaces import (
46    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
47    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
48from waeup.kofa.interfaces import MessageFactory as _
49from waeup.kofa.permissions import get_users_with_local_roles
50from waeup.kofa.students.interfaces import IStudentsUtils
51from waeup.kofa.utils.helpers import string_from_bytes, file_size
52from waeup.kofa.widgets.datewidget import (
53    FriendlyDateWidget, FriendlyDateDisplayWidget,
54    FriendlyDatetimeDisplayWidget)
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    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
154    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
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 = 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['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
227    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
228    form_fields['description'].custom_widget = ReSTDisplayWidget
229
230    @property
231    def introduction(self):
232        # Here we know that the cookie has been set
233        lang = self.request.cookies.get('kofa.language')
234        html = self.context.description_dict.get(lang,'')
235        if html =='':
236            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
237            html = self.context.description_dict.get(portal_language,'')
238        if html =='':
239            return ''
240        else:
241            return html
242
243    @property
244    def label(self):
245        return "%s" % self.context.title
246
247class ApplicantsContainerManageFormPage(KofaEditFormPage):
248    grok.context(IApplicantsContainer)
249    grok.name('manage')
250    grok.template('applicantscontainermanagepage')
251    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
252    taboneactions = [_('Save'),_('Cancel')]
253    tabtwoactions = [_('Add applicant'), _('Remove selected'),_('Cancel')]
254    tabthreeactions1 = [_('Remove selected local roles')]
255    tabthreeactions2 = [_('Add local role')]
256    # Use friendlier date widget...
257    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
258    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
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    form_fields['phone'].custom_widget = PhoneWidget
360    label = _('Add applicant')
361    pnav = 3
362
363    @action(_('Create application record'))
364    def addApplicant(self, **data):
365        applicant = createObject(u'waeup.Applicant')
366        self.applyData(applicant, **data)
367        self.context.addApplicant(applicant)
368        self.flash(_('Applicant record created.'))
369        self.redirect(
370            self.url(self.context[applicant.application_number], 'index'))
371        return
372
373class ApplicantDisplayFormPage(KofaDisplayFormPage):
374    grok.context(IApplicant)
375    grok.name('index')
376    grok.require('waeup.viewApplication')
377    grok.template('applicantdisplaypage')
378    form_fields = grok.AutoFields(IApplicant).omit(
379        'locked', 'course_admitted', 'password')
380    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
381    label = _('Applicant')
382    pnav = 3
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} 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, 'b':self.context.p_id})
502
503class OnlinePaymentCallbackPage(UtilityView, grok.View):
504    """ Callback view
505    """
506    grok.context(IApplicantOnlinePayment)
507    grok.name('simulate_callback')
508    grok.require('waeup.payApplicant')
509
510    # This update method simulates a valid callback und must be
511    # neutralized in the customization package.
512    def update(self):
513        self.wf_info = IWorkflowInfo(self.context.__parent__)
514        try:
515            self.wf_info.fireTransition('pay')
516        except InvalidTransitionError:
517            self.flash('Error: %s' % sys.exc_info()[1])
518            return
519        self.context.r_amount_approved = self.context.amount_auth
520        self.context.r_card_num = u'0000'
521        self.context.r_code = u'00'
522        self.context.p_state = 'paid'
523        self.context.payment_date = datetime.now()
524        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
525        self.context.__parent__.loggerInfo(
526            ob_class, 'valid callback: %s' % self.context.p_id)
527        self.flash(_('Valid callback received.'))
528        return
529
530    def render(self):
531        self.redirect(self.url(self.context, '@@index'))
532        return
533
534class ExportPDFPaymentSlipPage(UtilityView, grok.View):
535    """Deliver a PDF slip of the context.
536    """
537    grok.context(IApplicantOnlinePayment)
538    grok.name('payment_receipt.pdf')
539    grok.require('waeup.viewApplication')
540    form_fields = grok.AutoFields(IApplicantOnlinePayment)
541    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
542    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
543    prefix = 'form'
544
545    @property
546    def title(self):
547        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
548        return translate(_('Payment Data'), 'waeup.kofa',
549            target_language=portal_language)
550
551    @property
552    def label(self):
553        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
554        return translate(_('Online Payment Receipt'),
555            'waeup.kofa', target_language=portal_language) \
556            + ' %s' % self.context.p_id
557
558    def render(self):
559        if self.context.p_state != 'paid':
560            self.flash(_('Ticket not yet paid.'))
561            self.redirect(self.url(self.context))
562            return
563        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
564            self.request)
565        students_utils = getUtility(IStudentsUtils)
566        return students_utils.renderPDF(self,'payment_receipt.pdf',
567            self.context.__parent__, applicantview)
568
569class ExportPDFPage(UtilityView, grok.View):
570    """Deliver a PDF slip of the context.
571    """
572    grok.context(IApplicant)
573    grok.name('application_slip.pdf')
574    grok.require('waeup.viewApplication')
575    form_fields = grok.AutoFields(IApplicant).omit(
576        'locked', 'course_admitted')
577    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
578    prefix = 'form'
579
580    #@property
581    #def label(self):
582    #    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
583    #    container_title = self.context.__parent__.title
584    #    label = translate('Application Record',
585    #        'waeup.kofa', target_language=portal_language)
586    #    return container_title +  label + self.context.application_number
587
588    #def getCourseAdmitted(self):
589    #    """Return title and code in html format to the certificate
590    #       admitted.
591    #    """
592    #    course_admitted = self.context.course_admitted
593    #    #if ICertificate.providedBy(course_admitted):
594    #    if getattr(course_admitted, '__parent__',None):
595    #        title = course_admitted.title
596    #        code = course_admitted.code
597    #        return '%s - %s' %(code,title)
598    #    return ''
599
600    #def setUpWidgets(self, ignore_request=False):
601    #    self.adapters = {}
602    #    self.widgets = setUpEditWidgets(
603    #        self.form_fields, self.prefix, self.context, self.request,
604    #        adapters=self.adapters, for_display=True,
605    #        ignore_request=ignore_request
606    #        )
607
608    def render(self):
609        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
610            view=self)
611        self.response.setHeader(
612            'Content-Type', 'application/pdf')
613        return pdfstream
614
615def handle_img_upload(upload, context, view):
616    """Handle upload of applicant image.
617
618    Returns `True` in case of success or `False`.
619
620    Please note that file pointer passed in (`upload`) most probably
621    points to end of file when leaving this function.
622    """
623    size = file_size(upload)
624    if size > MAX_UPLOAD_SIZE:
625        view.flash(_('Uploaded image is too big!'))
626        return False
627    dummy, ext = os.path.splitext(upload.filename)
628    ext.lower()
629    if ext != '.jpg':
630        view.flash(_('jpg file extension expected.'))
631        return False
632    upload.seek(0) # file pointer moved when determining size
633    store = getUtility(IExtFileStore)
634    file_id = IFileStoreNameChooser(context).chooseName()
635    store.createFile(file_id, upload)
636    return True
637
638class ApplicantManageFormPage(KofaEditFormPage):
639    """A full edit view for applicant data.
640    """
641    grok.context(IApplicant)
642    grok.name('manage')
643    grok.require('waeup.manageApplication')
644    form_fields = grok.AutoFields(IApplicant)
645    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
646    form_fields['student_id'].for_display = True
647    form_fields['applicant_id'].for_display = True
648    form_fields['phone'].custom_widget = PhoneWidget
649    grok.template('applicanteditpage')
650    manage_applications = True
651    pnav = 3
652    display_actions = [[_('Save'), _('Final Submit')],
653        [_('Add online payment ticket'),_('Remove selected tickets')]]
654
655    def update(self):
656        datepicker.need() # Enable jQuery datepicker in date fields.
657        warning.need()
658        super(ApplicantManageFormPage, self).update()
659        self.wf_info = IWorkflowInfo(self.context)
660        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
661        self.passport_changed = None
662        upload = self.request.form.get('form.passport', None)
663        if upload:
664            # We got a fresh upload
665            self.passport_changed = handle_img_upload(
666                upload, self.context, self)
667        return
668
669    @property
670    def label(self):
671        container_title = self.context.__parent__.title
672        return _('${a} Application Form ${b}', mapping = {
673            'a':container_title, 'b':self.context.application_number})
674
675    def getTransitions(self):
676        """Return a list of dicts of allowed transition ids and titles.
677
678        Each list entry provides keys ``name`` and ``title`` for
679        internal name and (human readable) title of a single
680        transition.
681        """
682        allowed_transitions = self.wf_info.getManualTransitions()
683        return [dict(name='', title=_('No transition'))] +[
684            dict(name=x, title=y) for x, y in allowed_transitions]
685
686    @action(_('Save'), style='primary')
687    def save(self, **data):
688        form = self.request.form
689        password = form.get('password', None)
690        password_ctl = form.get('control_password', None)
691        if password:
692            validator = getUtility(IPasswordValidator)
693            errors = validator.validate_password(password, password_ctl)
694            if errors:
695                self.flash( ' '.join(errors))
696                return
697        if self.passport_changed is False:  # False is not None!
698            return # error during image upload. Ignore other values
699        changed_fields = self.applyData(self.context, **data)
700        # Turn list of lists into single list
701        if changed_fields:
702            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
703        else:
704            changed_fields = []
705        if self.passport_changed:
706            changed_fields.append('passport')
707        if password:
708            # Now we know that the form has no errors and can set password ...
709            IUserAccount(self.context).setPassword(password)
710            changed_fields.append('password')
711        fields_string = ' + '.join(changed_fields)
712        trans_id = form.get('transition', None)
713        if trans_id:
714            self.wf_info.fireTransition(trans_id)
715        self.flash(_('Form has been saved.'))
716        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
717        if fields_string:
718            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
719        return
720
721    def unremovable(self, ticket):
722        return False
723
724    # This method is also used by the ApplicantEditFormPage
725    def delPaymentTickets(self, **data):
726        form = self.request.form
727        if form.has_key('val_id'):
728            child_id = form['val_id']
729        else:
730            self.flash(_('No payment selected.'))
731            self.redirect(self.url(self.context))
732            return
733        if not isinstance(child_id, list):
734            child_id = [child_id]
735        deleted = []
736        for id in child_id:
737            # Applicants are not allowed to remove used payment tickets
738            if not self.unremovable(self.context[id]):
739                try:
740                    del self.context[id]
741                    deleted.append(id)
742                except:
743                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
744                            id, sys.exc_info()[0], sys.exc_info()[1]))
745        if len(deleted):
746            self.flash(_('Successfully removed: ${a}',
747                mapping = {'a':', '.join(deleted)}))
748            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
749            self.context.loggerInfo(
750                ob_class, 'removed: % s' % ', '.join(deleted))
751        return
752
753    # We explicitely want the forms to be validated before payment tickets
754    # can be created. If no validation is requested, use
755    # 'validator=NullValidator' in the action directive
756    @action(_('Add online payment ticket'))
757    def addPaymentTicket(self, **data):
758        self.redirect(self.url(self.context, '@@addafp'))
759        return
760
761    @jsaction(_('Remove selected tickets'))
762    def removePaymentTickets(self, **data):
763        self.delPaymentTickets(**data)
764        self.redirect(self.url(self.context) + '/@@manage')
765        return
766
767class ApplicantEditFormPage(ApplicantManageFormPage):
768    """An applicant-centered edit view for applicant data.
769    """
770    grok.context(IApplicantEdit)
771    grok.name('edit')
772    grok.require('waeup.handleApplication')
773    form_fields = grok.AutoFields(IApplicantEdit).omit(
774        'locked', 'course_admitted', 'student_id',
775        'screening_score', 'reg_number'
776        )
777    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
778    form_fields['phone'].custom_widget = PhoneWidget
779    form_fields['applicant_id'].for_display = True
780    grok.template('applicanteditpage')
781    manage_applications = False
782
783    @property
784    def display_actions(self):
785        state = IWorkflowState(self.context).getState()
786        if state == INITIALIZED:
787            actions = [[],[]]
788        elif state == STARTED:
789            actions = [[_('Save')],
790                [_('Add online payment ticket'),_('Remove selected tickets')]]
791        elif state == PAID:
792            actions = [[_('Save'), _('Final Submit')],
793                [_('Remove selected tickets')]]
794        else:
795            actions = [[],[]]
796        return actions
797
798    def unremovable(self, ticket):
799        state = IWorkflowState(self.context).getState()
800        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
801
802    def emit_lock_message(self):
803        self.flash(_('The requested form is locked (read-only).'))
804        self.redirect(self.url(self.context))
805        return
806
807    def update(self):
808        if self.context.locked:
809            self.emit_lock_message()
810            return
811        super(ApplicantEditFormPage, self).update()
812        return
813
814    def dataNotComplete(self):
815        store = getUtility(IExtFileStore)
816        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
817            return _('No passport picture uploaded.')
818        if not self.request.form.get('confirm_passport', False):
819            return _('Passport picture confirmation box not ticked.')
820        return False
821
822    # We explicitely want the forms to be validated before payment tickets
823    # can be created. If no validation is requested, use
824    # 'validator=NullValidator' in the action directive
825    @action(_('Add online payment ticket'))
826    def addPaymentTicket(self, **data):
827        self.redirect(self.url(self.context, '@@addafp'))
828        return
829
830    @jsaction(_('Remove selected tickets'))
831    def removePaymentTickets(self, **data):
832        self.delPaymentTickets(**data)
833        self.redirect(self.url(self.context) + '/@@edit')
834        return
835
836    @action(_('Save'), style='primary')
837    def save(self, **data):
838        if self.passport_changed is False:  # False is not None!
839            return # error during image upload. Ignore other values
840        self.applyData(self.context, **data)
841        self.flash('Form has been saved.')
842        return
843
844    @action(_('Final Submit'))
845    def finalsubmit(self, **data):
846        if self.passport_changed is False:  # False is not None!
847            return # error during image upload. Ignore other values
848        if self.dataNotComplete():
849            self.flash(self.dataNotComplete())
850            return
851        self.applyData(self.context, **data)
852        state = IWorkflowState(self.context).getState()
853        # This shouldn't happen, but the application officer
854        # might have forgotten to lock the form after changing the state
855        if state != PAID:
856            self.flash(_('This form cannot be submitted. Wrong state!'))
857            return
858        IWorkflowInfo(self.context).fireTransition('submit')
859        self.context.application_date = datetime.now()
860        self.context.locked = True
861        self.flash(_('Form has been submitted.'))
862        self.redirect(self.url(self.context))
863        return
864
865class PassportImage(grok.View):
866    """Renders the passport image for applicants.
867    """
868    grok.name('passport.jpg')
869    grok.context(IApplicant)
870    grok.require('waeup.viewApplication')
871
872    def render(self):
873        # A filename chooser turns a context into a filename suitable
874        # for file storage.
875        image = getUtility(IExtFileStore).getFileByContext(self.context)
876        self.response.setHeader(
877            'Content-Type', 'image/jpeg')
878        if image is None:
879            # show placeholder image
880            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
881        return image
882
883class ApplicantRegistrationPage(KofaAddFormPage):
884    """Captcha'd registration page for applicants.
885    """
886    grok.context(IApplicantsContainer)
887    grok.name('register')
888    grok.require('waeup.Anonymous')
889    grok.template('applicantregister')
890    form_fields = grok.AutoFields(IApplicantEdit).select(
891        'firstname', 'middlename', 'lastname', 'email', 'phone')
892    form_fields['phone'].custom_widget = PhoneWidget
893
894    @property
895    def label(self):
896        return _('Register for ${a} Application',
897            mapping = {'a':self.context.title})
898
899    def update(self):
900        # Check if application has started ...
901        if not self.context.startdate or self.context.startdate > date.today():
902            self.flash(_('Application has not yet started.'))
903            self.redirect(self.url(self.context))
904            return
905        # ... or ended
906        if not self.context.enddate or self.context.enddate < date.today():
907            self.flash(_('Application has ended.'))
908            self.redirect(self.url(self.context))
909            return
910        # Handle captcha
911        self.captcha = getUtility(ICaptchaManager).getCaptcha()
912        self.captcha_result = self.captcha.verify(self.request)
913        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
914        return
915
916    @action(_('Get login credentials'), style='primary')
917    def register(self, **data):
918        if not self.captcha_result.is_valid:
919            # captcha will display error messages automatically.
920            # No need to flash something.
921            return
922        # Add applicant and create password
923        applicant = createObject(u'waeup.Applicant')
924        self.applyData(applicant, **data)
925        self.context.addApplicant(applicant)
926        kofa_utils = getUtility(IKofaUtils)
927        password = kofa_utils.genPassword()
928        IUserAccount(applicant).setPassword(password)
929        # Send email with credentials
930        login_url = self.url(grok.getSite(), 'login')
931        msg = _('You have successfully been registered for the')
932        if kofa_utils.sendCredentials(IUserAccount(applicant),
933            password, login_url, msg):
934            self.redirect(self.url(self.context, 'registration_complete',
935                                   data = dict(email=applicant.email)))
936            return
937        else:
938            self.flash(_('Email could not been sent. Please retry later.'))
939        return
940
941class ApplicantRegistrationEmailSent(KofaPage):
942    """Landing page after successful registration.
943    """
944    grok.name('registration_complete')
945    grok.require('waeup.Public')
946    grok.template('applicantregemailsent')
947    label = _('Your registration was successful.')
948
949    def update(self, email=None):
950        self.email = email
951        return
Note: See TracBrowser for help on using the repository browser.