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

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

Store utc without tzinfo in persistent datetime objects. Localisation will be done in views only.

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