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

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

Continue internat. of applicants package including pdf exports.

  • Property svn:keywords set to Id
File size: 34.6 KB
Line 
1## $Id: browser.py 7714 2012-02-28 10:34:16Z 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:') + ' %s' % ', '.join(deleted))
121        self.redirect(self.url(self.context, '@@manage'))
122        return
123
124    @action(_('Add applicants container'), validator=NullValidator)
125    def addApplicantsContainer(self, **data):
126        self.redirect(self.url(self.context, '@@add'))
127        return
128
129    @action(_('Cancel'), validator=NullValidator)
130    def cancel(self, **data):
131        self.redirect(self.url(self.context))
132        return
133
134    @action(_('Add local role'), validator=NullValidator)
135    def addLocalRole(self, **data):
136        return add_local_role(self,3, **data)
137
138    @action(_('Remove selected local roles'))
139    def delLocalRoles(self, **data):
140        return del_local_roles(self,3,**data)
141
142class ApplicantsContainerAddFormPage(SIRPAddFormPage):
143    grok.context(IApplicantsRoot)
144    grok.require('waeup.manageApplication')
145    grok.name('add')
146    grok.template('applicantscontaineraddpage')
147    label = _('Add applicants container')
148    pnav = 3
149
150    form_fields = grok.AutoFields(
151        IApplicantsContainerAdd).omit('code').omit('title', 'description_dict')
152    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
153    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
154
155    def update(self):
156        datepicker.need() # Enable jQuery datepicker in date fields.
157        return super(ApplicantsContainerAddFormPage, self).update()
158
159    @action(_('Add applicants container'))
160    def addApplicantsContainer(self, **data):
161        year = data['year']
162        code = u'%s%s' % (data['prefix'], year)
163        appcats_dict = getUtility(IApplicantsUtils).getApplicationTypeDict()
164        title = appcats_dict[data['prefix']][0]
165        title = u'%s %s/%s' % (title, year, year + 1)
166        if code in self.context.keys():
167            self.flash(
168                _('An applicants container for the same application type and entrance year exists already in the database.'))
169            return
170        # Add new applicants container...
171        provider = data['provider'][1]
172        container = provider.factory()
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(SIRPDisplayFormPage):
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(
226        'title', 'description_dict')
227    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
228    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
229    form_fields['description'].custom_widget = ReSTDisplayWidget
230
231    @property
232    def introduction(self):
233        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
234        lang = self.request.cookies.get('sirp.language', portal_language)
235        html = self.context.description_dict.get(lang,'')
236        if html =='':
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(SIRPEditFormPage):
248    grok.context(IApplicantsContainer)
249    grok.name('manage')
250    grok.template('applicantscontainermanagepage')
251    form_fields = grok.AutoFields(IApplicantsContainer).omit(
252        'title', 'description_dict')
253    taboneactions = [_('Save'),_('Cancel')]
254    tabtwoactions = [_('Add applicant'), _('Remove selected'),_('Cancel')]
255    tabthreeactions1 = [_('Remove selected local roles')]
256    tabthreeactions2 = [_('Add local role')]
257    # Use friendlier date widget...
258    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
259    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
260    grok.require('waeup.manageApplication')
261
262    @property
263    def label(self):
264        return _('Manage applicants container')
265
266    pnav = 3
267
268    def update(self):
269        datepicker.need() # Enable jQuery datepicker in date fields.
270        tabs.need()
271        self.tab1 = self.tab2 = self.tab3 = ''
272        qs = self.request.get('QUERY_STRING', '')
273        if not qs:
274            qs = 'tab1'
275        setattr(self, qs, 'active')
276        warning.need()
277        datatable.need()  # Enable jQurey datatables for contents listing
278        return super(ApplicantsContainerManageFormPage, self).update()
279
280    def getLocalRoles(self):
281        roles = ILocalRolesAssignable(self.context)
282        return roles()
283
284    def getUsers(self):
285        """Get a list of all users.
286        """
287        for key, val in grok.getSite()['users'].items():
288            url = self.url(val)
289            yield(dict(url=url, name=key, val=val))
290
291    def getUsersWithLocalRoles(self):
292        return get_users_with_local_roles(self.context)
293
294    def _description(self):
295        view = ApplicantsContainerPage(
296            self.context,self.request)
297        view.setUpWidgets()
298        return view.widgets['description']()
299
300    @action(_('Save'), style='primary')
301    def save(self, **data):
302        self.applyData(self.context, **data)
303        self.context.description_dict = self._description()
304        self.flash(_('Form has been saved.'))
305        return
306
307    @jsaction(_('Remove selected'))
308    def delApplicant(self, **data):
309        form = self.request.form
310        if form.has_key('val_id'):
311            child_id = form['val_id']
312        else:
313            self.flash(_('No applicant selected!'))
314            self.redirect(self.url(self.context, '@@manage')+'?tab2')
315            return
316        if not isinstance(child_id, list):
317            child_id = [child_id]
318        deleted = []
319        for id in child_id:
320            try:
321                del self.context[id]
322                deleted.append(id)
323            except:
324                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
325                        id, sys.exc_info()[0], sys.exc_info()[1]))
326        if len(deleted):
327            self.flash(_('Successfully removed:') + ' %s' % ', '.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(SIRPAddFormPage):
350    """Add-form to add an applicant.
351    """
352    grok.context(IApplicantsContainer)
353    grok.require('waeup.manageApplication')
354    grok.name('addapplicant')
355    #grok.template('applicantaddpage')
356    form_fields = grok.AutoFields(IApplicant).select(
357        'firstname', 'middlename', 'lastname',
358        'email', 'phone')
359    label = _('Add applicant')
360    pnav = 3
361
362    @action(_('Create application record'))
363    def addApplicant(self, **data):
364        applicant = createObject(u'waeup.Applicant')
365        self.applyData(applicant, **data)
366        self.context.addApplicant(applicant)
367        self.flash(_('Applicant record created.'))
368        self.redirect(
369            self.url(self.context[applicant.application_number], 'index'))
370        return
371
372class ApplicantDisplayFormPage(SIRPDisplayFormPage):
373    grok.context(IApplicant)
374    grok.name('index')
375    grok.require('waeup.viewApplication')
376    grok.template('applicantdisplaypage')
377    form_fields = grok.AutoFields(IApplicant).omit(
378        'locked', 'course_admitted', 'password')
379    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
380    label = _('Applicant')
381    pnav = 3
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} 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(SIRPDisplayFormPage):
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    form_fields[
492        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
493    form_fields[
494        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
495    pnav = 3
496
497    @property
498    def label(self):
499        return _('${a}: Online Payment Ticket ${b}', mapping = {
500            'a':self.context.__parent__.display_fullname, 'b':self.context.p_id})
501
502class OnlinePaymentCallbackPage(UtilityView, grok.View):
503    """ Callback view
504    """
505    grok.context(IApplicantOnlinePayment)
506    grok.name('callback')
507    grok.require('waeup.payApplicant')
508
509    # This update method simulates a valid callback und must be
510    # specified in the customization package. The parameters must be taken
511    # from the incoming request.
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.sirp.','')
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(ISIRPUtils).PORTAL_LANGUAGE
548        return translate(_('Payment Data'), 'waeup.sirp',
549            target_language=portal_language)
550
551    @property
552    def label(self):
553        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
554        return translate(_('Online Payment Receipt'),
555            'waeup.sirp', 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(ISIRPUtils).PORTAL_LANGUAGE
583    #    container_title = self.context.__parent__.title
584    #    label = translate('Application Record',
585    #        'waeup.sirp', 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(SIRPEditFormPage):
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.sirp.','')
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:') + ' %s' % ', '.join(deleted))
747            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
748            self.context.loggerInfo(
749                ob_class, 'removed: % s' % ', '.join(deleted))
750        return
751
752    # We explicitely want the forms to be validated before payment tickets
753    # can be created. If no validation is requested, use
754    # 'validator=NullValidator' in the action directive
755    @action(_('Add online payment ticket'))
756    def addPaymentTicket(self, **data):
757        self.redirect(self.url(self.context, '@@addafp'))
758        return
759
760    @jsaction(_('Remove selected tickets'))
761    def removePaymentTickets(self, **data):
762        self.delPaymentTickets(**data)
763        self.redirect(self.url(self.context) + '/@@manage')
764        return
765
766class ApplicantEditFormPage(ApplicantManageFormPage):
767    """An applicant-centered edit view for applicant data.
768    """
769    grok.context(IApplicantEdit)
770    grok.name('edit')
771    grok.require('waeup.handleApplication')
772    form_fields = grok.AutoFields(IApplicantEdit).omit(
773        'locked', 'course_admitted', 'student_id',
774        'screening_score', 'reg_number'
775        )
776    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
777    form_fields['phone'].custom_widget = PhoneWidget
778    form_fields['applicant_id'].for_display = True
779    grok.template('applicanteditpage')
780    manage_applications = False
781
782    @property
783    def display_actions(self):
784        state = IWorkflowState(self.context).getState()
785        if state == INITIALIZED:
786            actions = [[],[]]
787        elif state == STARTED:
788            actions = [[_('Save')],
789                [_('Add online payment ticket'),_('Remove selected tickets')]]
790        elif state == PAID:
791            actions = [[_('Save'), _('Final Submit')],
792                [_('Remove selected tickets')]]
793        else:
794            actions = [[],[]]
795        return actions
796
797    def unremovable(self, ticket):
798        state = IWorkflowState(self.context).getState()
799        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
800
801    def emit_lock_message(self):
802        self.flash(_('The requested form is locked (read-only).'))
803        self.redirect(self.url(self.context))
804        return
805
806    def update(self):
807        if self.context.locked:
808            self.emit_lock_message()
809            return
810        super(ApplicantEditFormPage, self).update()
811        return
812
813    def dataNotComplete(self):
814        store = getUtility(IExtFileStore)
815        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
816            return _('No passport picture uploaded.')
817        if not self.request.form.get('confirm_passport', False):
818            return _('Passport picture confirmation box not ticked.')
819        return False
820
821    # We explicitely want the forms to be validated before payment tickets
822    # can be created. If no validation is requested, use
823    # 'validator=NullValidator' in the action directive
824    @action(_('Add online payment ticket'))
825    def addPaymentTicket(self, **data):
826        self.redirect(self.url(self.context, '@@addafp'))
827        return
828
829    @jsaction(_('Remove selected tickets'))
830    def removePaymentTickets(self, **data):
831        self.delPaymentTickets(**data)
832        self.redirect(self.url(self.context) + '/@@edit')
833        return
834
835    @action(_('Save'))
836    def save(self, **data):
837        if self.passport_changed is False:  # False is not None!
838            return # error during image upload. Ignore other values
839        self.applyData(self.context, **data)
840        self.flash('Form has been saved.')
841        return
842
843    @action(_('Final Submit'))
844    def finalsubmit(self, **data):
845        if self.passport_changed is False:  # False is not None!
846            return # error during image upload. Ignore other values
847        if self.dataNotComplete():
848            self.flash(self.dataNotComplete())
849            return
850        self.applyData(self.context, **data)
851        state = IWorkflowState(self.context).getState()
852        # This shouldn't happen, but the application officer
853        # might have forgotten to lock the form after changing the state
854        if state != PAID:
855            self.flash(_('This form cannot be submitted. Wrong state!'))
856            return
857        IWorkflowInfo(self.context).fireTransition('submit')
858        self.context.application_date = datetime.now()
859        self.context.locked = True
860        self.flash(_('Form has been submitted.'))
861        self.redirect(self.url(self.context))
862        return
863
864class PassportImage(grok.View):
865    """Renders the passport image for applicants.
866    """
867    grok.name('passport.jpg')
868    grok.context(IApplicant)
869    grok.require('waeup.viewApplication')
870
871    def render(self):
872        # A filename chooser turns a context into a filename suitable
873        # for file storage.
874        image = getUtility(IExtFileStore).getFileByContext(self.context)
875        self.response.setHeader(
876            'Content-Type', 'image/jpeg')
877        if image is None:
878            # show placeholder image
879            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
880        return image
881
882class ApplicantRegistrationPage(SIRPAddFormPage):
883    """Captcha'd registration page for applicants.
884    """
885    grok.context(IApplicantsContainer)
886    grok.name('register')
887    grok.require('waeup.Anonymous')
888    grok.template('applicantregister')
889    form_fields = grok.AutoFields(IApplicantEdit).select(
890        'firstname', 'middlename', 'lastname', 'email', 'phone')
891    form_fields['phone'].custom_widget = PhoneWidget
892
893    @property
894    def label(self):
895        return _('Register for ${a} Application',
896            mapping = {'a':self.context.title})
897
898    def update(self):
899        # Check if application has started ...
900        if not self.context.startdate or self.context.startdate > date.today():
901            self.flash(_('Application has not yet started.'))
902            self.redirect(self.url(self.context))
903            return
904        # ... or ended
905        if not self.context.enddate or self.context.enddate < date.today():
906            self.flash(_('Application has ended.'))
907            self.redirect(self.url(self.context))
908            return
909        # Handle captcha
910        self.captcha = getUtility(ICaptchaManager).getCaptcha()
911        self.captcha_result = self.captcha.verify(self.request)
912        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
913        return
914
915    @action(_('Get login credentials'), style='primary')
916    def register(self, **data):
917        if not self.captcha_result.is_valid:
918            # captcha will display error messages automatically.
919            # No need to flash something.
920            return
921        # Add applicant and create password
922        applicant = createObject('waeup.Applicant')
923        self.applyData(applicant, **data)
924        self.context.addApplicant(applicant)
925        sirp_utils = getUtility(ISIRPUtils)
926        password = sirp_utils.genPassword()
927        IUserAccount(applicant).setPassword(password)
928        # Send email with credentials
929        login_url = self.url(grok.getSite(), 'login')
930        msg = _('You have successfully been registered for the')
931        if sirp_utils.sendCredentials(IUserAccount(applicant),
932            password, login_url, msg):
933            self.redirect(self.url(self.context, 'registration_complete',
934                                   data = dict(email=applicant.email)))
935            return
936        else:
937            self.flash(_('Email could not been sent. Please retry later.'))
938        return
939
940class ApplicantRegistrationEmailSent(SIRPPage):
941    """Landing page after successful registration.
942    """
943    grok.name('registration_complete')
944    grok.require('waeup.Public')
945    grok.template('applicantregemailsent')
946    label = _('Your registration was successful.')
947
948    def update(self, email=None):
949        self.email = email
950        return
Note: See TracBrowser for help on using the repository browser.