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

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

Prepare browser components for file uploads in custom packages.

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