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

Last change on this file since 13217 was 13217, checked in by Henrik Bettermann, 9 years ago

Add pre-fill UI components.

Take unused records first during self-registration.

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