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

Last change on this file since 9771 was 9771, checked in by Henrik Bettermann, 12 years ago

Log payment data after application payment approval.

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