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

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

Implement search page for applicants. Add fullname to applicants_catalog.
Plugins must be updated and /reindex?ctlg=applicants must be performed.

Tests will follow.

Rename ApplicantCatalog? to ApplicantsCatalog?. This does not affect persistent data.

Rename StudentIndexes? to StudentsCatalog?.

Add more localization.

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