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

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

Managers do not 'pay' fees for applicants and students, they approve payments made.

Add respective transitions.

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