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

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

Add method and helpers to mass-create students from applicants.

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