source: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py @ 11102

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

Some repairs.

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