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

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

For imostate we have to customize the workflow. Applicants do not pay.

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