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

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

Rename state to app_state according to reg_state in students.

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