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

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

Add methods for approving payments and implement pages for approving payments (work in progress).

  • Property svn:keywords set to Id
File size: 37.8 KB
Line 
1## $Id: browser.py 8420 2012-05-11 14:18:47Z 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 OnlinePaymentApprovePage(UtilityView, grok.View):
594    """ Approval view
595    """
596    grok.context(IApplicantOnlinePayment)
597    grok.name('approve')
598    grok.require('waeup.managePortal')
599
600    def update(self):
601        self.wf_info = IWorkflowInfo(self.context.__parent__)
602        try:
603            self.wf_info.fireTransition('pay')
604        except InvalidTransitionError:
605            self.flash('Error: %s' % sys.exc_info()[1])
606            return
607        self.context.approve()
608        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
609        self.context.__parent__.loggerInfo(
610            ob_class, 'valid callback: %s' % self.context.p_id)
611        self.flash(_('Valid callback received.'))
612        return
613
614    def render(self):
615        self.redirect(self.url(self.context, '@@index'))
616        return
617
618class ExportPDFPaymentSlipPage(UtilityView, grok.View):
619    """Deliver a PDF slip of the context.
620    """
621    grok.context(IApplicantOnlinePayment)
622    grok.name('payment_slip.pdf')
623    grok.require('waeup.viewApplication')
624    form_fields = grok.AutoFields(IApplicantOnlinePayment)
625    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
626    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
627    prefix = 'form'
628    note = None
629
630    @property
631    def title(self):
632        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
633        return translate(_('Payment Data'), 'waeup.kofa',
634            target_language=portal_language)
635
636    @property
637    def label(self):
638        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
639        return translate(_('Online Payment Slip'),
640            'waeup.kofa', target_language=portal_language) \
641            + ' %s' % self.context.p_id
642
643    def render(self):
644        #if self.context.p_state != 'paid':
645        #    self.flash(_('Ticket not yet paid.'))
646        #    self.redirect(self.url(self.context))
647        #    return
648        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
649            self.request)
650        students_utils = getUtility(IStudentsUtils)
651        return students_utils.renderPDF(self,'payment_slip.pdf',
652            self.context.__parent__, applicantview, note=self.note)
653
654class ExportPDFPage(UtilityView, grok.View):
655    """Deliver a PDF slip of the context.
656    """
657    grok.context(IApplicant)
658    grok.name('application_slip.pdf')
659    grok.require('waeup.viewApplication')
660    prefix = 'form'
661
662    def render(self):
663        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
664            view=self)
665        self.response.setHeader(
666            'Content-Type', 'application/pdf')
667        return pdfstream
668
669def handle_img_upload(upload, context, view):
670    """Handle upload of applicant image.
671
672    Returns `True` in case of success or `False`.
673
674    Please note that file pointer passed in (`upload`) most probably
675    points to end of file when leaving this function.
676    """
677    size = file_size(upload)
678    if size > MAX_UPLOAD_SIZE:
679        view.flash(_('Uploaded image is too big!'))
680        return False
681    dummy, ext = os.path.splitext(upload.filename)
682    ext.lower()
683    if ext != '.jpg':
684        view.flash(_('jpg file extension expected.'))
685        return False
686    upload.seek(0) # file pointer moved when determining size
687    store = getUtility(IExtFileStore)
688    file_id = IFileStoreNameChooser(context).chooseName()
689    store.createFile(file_id, upload)
690    return True
691
692class ApplicantManageFormPage(KofaEditFormPage):
693    """A full edit view for applicant data.
694    """
695    grok.context(IApplicant)
696    grok.name('manage')
697    grok.require('waeup.manageApplication')
698    form_fields = grok.AutoFields(IApplicant)
699    form_fields['student_id'].for_display = True
700    form_fields['applicant_id'].for_display = True
701    grok.template('applicanteditpage')
702    manage_applications = True
703    pnav = 3
704    display_actions = [[_('Save'), _('Final Submit')],
705        [_('Add online payment ticket'),_('Remove selected tickets')]]
706
707    @property
708    def separators(self):
709        return getUtility(IApplicantsUtils).SEPARATORS_DICT
710
711    def update(self):
712        datepicker.need() # Enable jQuery datepicker in date fields.
713        warning.need()
714        super(ApplicantManageFormPage, self).update()
715        self.wf_info = IWorkflowInfo(self.context)
716        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
717        self.passport_changed = None
718        upload = self.request.form.get('form.passport', None)
719        if upload:
720            # We got a fresh upload
721            self.passport_changed = handle_img_upload(
722                upload, self.context, self)
723        return
724
725    @property
726    def label(self):
727        container_title = self.context.__parent__.title
728        return _('${a} <br /> Application Form ${b}', mapping = {
729            'a':container_title, 'b':self.context.application_number})
730
731    def getTransitions(self):
732        """Return a list of dicts of allowed transition ids and titles.
733
734        Each list entry provides keys ``name`` and ``title`` for
735        internal name and (human readable) title of a single
736        transition.
737        """
738        allowed_transitions = self.wf_info.getManualTransitions()
739        return [dict(name='', title=_('No transition'))] +[
740            dict(name=x, title=y) for x, y in allowed_transitions]
741
742    @action(_('Save'), style='primary')
743    def save(self, **data):
744        form = self.request.form
745        password = form.get('password', None)
746        password_ctl = form.get('control_password', None)
747        if password:
748            validator = getUtility(IPasswordValidator)
749            errors = validator.validate_password(password, password_ctl)
750            if errors:
751                self.flash( ' '.join(errors))
752                return
753        if self.passport_changed is False:  # False is not None!
754            return # error during image upload. Ignore other values
755        changed_fields = self.applyData(self.context, **data)
756        # Turn list of lists into single list
757        if changed_fields:
758            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
759        else:
760            changed_fields = []
761        if self.passport_changed:
762            changed_fields.append('passport')
763        if password:
764            # Now we know that the form has no errors and can set password ...
765            IUserAccount(self.context).setPassword(password)
766            changed_fields.append('password')
767        fields_string = ' + '.join(changed_fields)
768        trans_id = form.get('transition', None)
769        if trans_id:
770            self.wf_info.fireTransition(trans_id)
771        self.flash(_('Form has been saved.'))
772        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
773        if fields_string:
774            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
775        return
776
777    def unremovable(self, ticket):
778        return False
779
780    # This method is also used by the ApplicantEditFormPage
781    def delPaymentTickets(self, **data):
782        form = self.request.form
783        if form.has_key('val_id'):
784            child_id = form['val_id']
785        else:
786            self.flash(_('No payment selected.'))
787            self.redirect(self.url(self.context))
788            return
789        if not isinstance(child_id, list):
790            child_id = [child_id]
791        deleted = []
792        for id in child_id:
793            # Applicants are not allowed to remove used payment tickets
794            if not self.unremovable(self.context[id]):
795                try:
796                    del self.context[id]
797                    deleted.append(id)
798                except:
799                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
800                            id, sys.exc_info()[0], sys.exc_info()[1]))
801        if len(deleted):
802            self.flash(_('Successfully removed: ${a}',
803                mapping = {'a':', '.join(deleted)}))
804            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
805            self.context.loggerInfo(
806                ob_class, 'removed: % s' % ', '.join(deleted))
807        return
808
809    # We explicitely want the forms to be validated before payment tickets
810    # can be created. If no validation is requested, use
811    # 'validator=NullValidator' in the action directive
812    @action(_('Add online payment ticket'))
813    def addPaymentTicket(self, **data):
814        self.redirect(self.url(self.context, '@@addafp'))
815        return
816
817    @jsaction(_('Remove selected tickets'))
818    def removePaymentTickets(self, **data):
819        self.delPaymentTickets(**data)
820        self.redirect(self.url(self.context) + '/@@manage')
821        return
822
823class ApplicantEditFormPage(ApplicantManageFormPage):
824    """An applicant-centered edit view for applicant data.
825    """
826    grok.context(IApplicantEdit)
827    grok.name('edit')
828    grok.require('waeup.handleApplication')
829    form_fields = grok.AutoFields(IApplicantEdit).omit(
830        'locked', 'course_admitted', 'student_id',
831        'screening_score',
832        )
833    form_fields['applicant_id'].for_display = True
834    form_fields['reg_number'].for_display = True
835    grok.template('applicanteditpage')
836    manage_applications = False
837
838    @property
839    def display_actions(self):
840        state = IWorkflowState(self.context).getState()
841        if state == INITIALIZED:
842            actions = [[],[]]
843        elif state == STARTED:
844            actions = [[_('Save')],
845                [_('Add online payment ticket'),_('Remove selected tickets')]]
846        elif state == PAID:
847            actions = [[_('Save'), _('Final Submit')],
848                [_('Remove selected tickets')]]
849        else:
850            actions = [[],[]]
851        return actions
852
853    def unremovable(self, ticket):
854        state = IWorkflowState(self.context).getState()
855        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
856
857    def emit_lock_message(self):
858        self.flash(_('The requested form is locked (read-only).'))
859        self.redirect(self.url(self.context))
860        return
861
862    def update(self):
863        if self.context.locked:
864            self.emit_lock_message()
865            return
866        super(ApplicantEditFormPage, self).update()
867        return
868
869    def dataNotComplete(self):
870        store = getUtility(IExtFileStore)
871        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
872            return _('No passport picture uploaded.')
873        if not self.request.form.get('confirm_passport', False):
874            return _('Passport picture confirmation box not ticked.')
875        return False
876
877    # We explicitely want the forms to be validated before payment tickets
878    # can be created. If no validation is requested, use
879    # 'validator=NullValidator' in the action directive
880    @action(_('Add online payment ticket'))
881    def addPaymentTicket(self, **data):
882        self.redirect(self.url(self.context, '@@addafp'))
883        return
884
885    @jsaction(_('Remove selected tickets'))
886    def removePaymentTickets(self, **data):
887        self.delPaymentTickets(**data)
888        self.redirect(self.url(self.context) + '/@@edit')
889        return
890
891    @action(_('Save'), style='primary')
892    def save(self, **data):
893        if self.passport_changed is False:  # False is not None!
894            return # error during image upload. Ignore other values
895        self.applyData(self.context, **data)
896        self.flash('Form has been saved.')
897        return
898
899    @action(_('Final Submit'))
900    def finalsubmit(self, **data):
901        if self.passport_changed is False:  # False is not None!
902            return # error during image upload. Ignore other values
903        if self.dataNotComplete():
904            self.flash(self.dataNotComplete())
905            return
906        self.applyData(self.context, **data)
907        state = IWorkflowState(self.context).getState()
908        # This shouldn't happen, but the application officer
909        # might have forgotten to lock the form after changing the state
910        if state != PAID:
911            self.flash(_('This form cannot be submitted. Wrong state!'))
912            return
913        IWorkflowInfo(self.context).fireTransition('submit')
914        self.context.application_date = datetime.utcnow()
915        self.context.locked = True
916        self.flash(_('Form has been submitted.'))
917        self.redirect(self.url(self.context))
918        return
919
920class PassportImage(grok.View):
921    """Renders the passport image for applicants.
922    """
923    grok.name('passport.jpg')
924    grok.context(IApplicant)
925    grok.require('waeup.viewApplication')
926
927    def render(self):
928        # A filename chooser turns a context into a filename suitable
929        # for file storage.
930        image = getUtility(IExtFileStore).getFileByContext(self.context)
931        self.response.setHeader(
932            'Content-Type', 'image/jpeg')
933        if image is None:
934            # show placeholder image
935            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
936        return image
937
938class ApplicantRegistrationPage(KofaAddFormPage):
939    """Captcha'd registration page for applicants.
940    """
941    grok.context(IApplicantsContainer)
942    grok.name('register')
943    grok.require('waeup.Anonymous')
944    grok.template('applicantregister')
945
946    @property
947    def form_fields(self):
948        form_fields = None
949        if self.context.mode == 'update':
950            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
951                'firstname','reg_number','email')
952        else: #if self.context.mode == 'create':
953            form_fields = grok.AutoFields(IApplicantEdit).select(
954                'firstname', 'middlename', 'lastname', 'email', 'phone')
955        return form_fields
956
957    @property
958    def label(self):
959        return _('Apply for ${a}',
960            mapping = {'a':self.context.title})
961
962    def update(self):
963        # Check if application has started ...
964        if not self.context.startdate or (
965            self.context.startdate > datetime.now(pytz.utc)):
966            self.flash(_('Application has not yet started.'))
967            self.redirect(self.url(self.context))
968            return
969        # ... or ended
970        if not self.context.enddate or (
971            self.context.enddate < datetime.now(pytz.utc)):
972            self.flash(_('Application has ended.'))
973            self.redirect(self.url(self.context))
974            return
975        # Handle captcha
976        self.captcha = getUtility(ICaptchaManager).getCaptcha()
977        self.captcha_result = self.captcha.verify(self.request)
978        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
979        return
980
981    @action(_('Get login credentials'), style='primary')
982    def register(self, **data):
983        if not self.captcha_result.is_valid:
984            # Captcha will display error messages automatically.
985            # No need to flash something.
986            return
987        if self.context.mode == 'create':
988            # Add applicant
989            applicant = createObject(u'waeup.Applicant')
990            self.applyData(applicant, **data)
991            self.context.addApplicant(applicant)
992            applicant.reg_number = applicant.applicant_id
993            notify(grok.ObjectModifiedEvent(applicant))
994        elif self.context.mode == 'update':
995            # Update applicant
996            reg_number = data.get('reg_number','')
997            firstname = data.get('firstname','')
998            cat = getUtility(ICatalog, name='applicants_catalog')
999            results = list(
1000                cat.searchResults(reg_number=(reg_number, reg_number)))
1001            if results:
1002                applicant = results[0]
1003                if getattr(applicant,'firstname',None) is None:
1004                    self.flash(_('An error occurred.'))
1005                    return
1006                elif applicant.firstname.lower() != firstname.lower():
1007                    # Don't tell the truth here. Anonymous must not
1008                    # know that a record was found and only the firstname
1009                    # verification failed.
1010                    self.flash(_('No application record found.'))
1011                    return
1012                elif applicant.password is not None:
1013                    self.flash(_('Your password has already been set. '
1014                                 'Please proceed to the login page.'))
1015                    return
1016                # Store email address but nothing else.
1017                applicant.email = data['email']
1018                notify(grok.ObjectModifiedEvent(applicant))
1019            else:
1020                # No record found, this is the truth.
1021                self.flash(_('No application record found.'))
1022                return
1023        else:
1024            # Does not happen but anyway ...
1025            return
1026        kofa_utils = getUtility(IKofaUtils)
1027        password = kofa_utils.genPassword()
1028        IUserAccount(applicant).setPassword(password)
1029        # Send email with credentials
1030        login_url = self.url(grok.getSite(), 'login')
1031        msg = _('You have successfully been registered for the')
1032        if kofa_utils.sendCredentials(IUserAccount(applicant),
1033            password, login_url, msg):
1034            self.redirect(self.url(self.context, 'registration_complete',
1035                                   data = dict(email=applicant.email)))
1036            return
1037        else:
1038            self.flash(_('Email could not been sent. Please retry later.'))
1039        return
1040
1041class ApplicantRegistrationEmailSent(KofaPage):
1042    """Landing page after successful registration.
1043    """
1044    grok.name('registration_complete')
1045    grok.require('waeup.Public')
1046    grok.template('applicantregemailsent')
1047    label = _('Your registration was successful.')
1048
1049    def update(self, email=None):
1050        self.email = email
1051        return
Note: See TracBrowser for help on using the repository browser.