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

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

Officers must be aware of what they are doing when puring containers.

  • Property svn:keywords set to Id
File size: 50.2 KB
Line 
1## $Id: browser.py 13232 2015-08-27 06:09:58Z 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, reverse=True)
96            return values
97        values = sorted([container for container in self.context.values()
98                         if not container.hidden],
99                        key=lambda value: value.enddate, reverse=True)
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    """Form to pre-fill applicants containers.
480    """
481    grok.context(IApplicantsContainer)
482    grok.require('waeup.manageApplication')
483    grok.name('prefill')
484    grok.template('prefillcontainer')
485    label = _('Pre-fill container')
486    pnav = 3
487    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
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):
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 ApplicantsContainerPurgeFormPage(KofaEditFormPage):
517    """Form to pre-fill applicants containers.
518    """
519    grok.context(IApplicantsContainer)
520    grok.require('waeup.manageApplication')
521    grok.name('purge')
522    grok.template('purgecontainer')
523    label = _('Purge container')
524    pnav = 3
525    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
526
527    @action(_('Remove initialized records'),
528              tooltip=_('Don\'t use if application is in progress!'),
529              warning=_('Are you really sure?'),
530              style='primary')
531    def purgeInitialized(self):
532        form = self.request.form
533        purged = 0
534        keys = [key for key in self.context.keys()]
535        for key in keys:
536            if self.context[key].state == 'initialized':
537                del self.context[key]
538                purged += 1
539        self.flash(_('%s application records purged.' % purged))
540        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
541        self.redirect(self.url(self.context, 'index'))
542        return
543
544    @action(_('Cancel'), validator=NullValidator)
545    def cancel(self, **data):
546        self.redirect(self.url(self.context))
547        return
548
549class ApplicantDisplayFormPage(KofaDisplayFormPage):
550    """A display view for applicant data.
551    """
552    grok.context(IApplicant)
553    grok.name('index')
554    grok.require('waeup.viewApplication')
555    grok.template('applicantdisplaypage')
556    label = _('Applicant')
557    pnav = 3
558    hide_hint = False
559
560    @property
561    def form_fields(self):
562        if self.context.special:
563            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
564        else:
565            form_fields = grok.AutoFields(IApplicant).omit(
566                'locked', 'course_admitted', 'password', 'suspended')
567        return form_fields
568
569    @property
570    def target(self):
571        return getattr(self.context.__parent__, 'prefix', None)
572
573    @property
574    def separators(self):
575        return getUtility(IApplicantsUtils).SEPARATORS_DICT
576
577    def update(self):
578        self.passport_url = self.url(self.context, 'passport.jpg')
579        # Mark application as started if applicant logs in for the first time
580        usertype = getattr(self.request.principal, 'user_type', None)
581        if usertype == 'applicant' and \
582            IWorkflowState(self.context).getState() == INITIALIZED:
583            IWorkflowInfo(self.context).fireTransition('start')
584        if usertype == 'applicant' and self.context.state == 'created':
585            session = '%s/%s' % (self.context.__parent__.year,
586                                 self.context.__parent__.year+1)
587            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
588            msg = _(
589                '\n <strong>Congratulations!</strong>' +
590                ' You have been offered provisional admission into the' +
591                ' ${c} Academic Session of ${d}.'
592                ' Your student record has been created for you.' +
593                ' Please, logout again and proceed to the' +
594                ' login page of the portal.'
595                ' Then enter your new student credentials:' +
596                ' user name= ${a}, password = ${b}.' +
597                ' Change your password when you have logged in.',
598                mapping = {
599                    'a':self.context.student_id,
600                    'b':self.context.application_number,
601                    'c':session,
602                    'd':title}
603                )
604            self.flash(msg)
605        return
606
607    @property
608    def hasPassword(self):
609        if self.context.password:
610            return _('set')
611        return _('unset')
612
613    @property
614    def label(self):
615        container_title = self.context.__parent__.title
616        return _('${a} <br /> Application Record ${b}', mapping = {
617            'a':container_title, 'b':self.context.application_number})
618
619    def getCourseAdmitted(self):
620        """Return link, title and code in html format to the certificate
621           admitted.
622        """
623        course_admitted = self.context.course_admitted
624        if getattr(course_admitted, '__parent__',None):
625            url = self.url(course_admitted)
626            title = course_admitted.title
627            code = course_admitted.code
628            return '<a href="%s">%s - %s</a>' %(url,code,title)
629        return ''
630
631class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
632    grok.context(IApplicant)
633    grok.name('base')
634    form_fields = grok.AutoFields(IApplicant).select(
635        'applicant_id','email', 'course1')
636
637class CreateStudentPage(UtilityView, grok.View):
638    """Create a student object from applicant data.
639    """
640    grok.context(IApplicant)
641    grok.name('createstudent')
642    grok.require('waeup.manageStudent')
643
644    def update(self):
645        msg = self.context.createStudent(view=self)[1]
646        self.flash(msg, type='warning')
647        self.redirect(self.url(self.context))
648        return
649
650    def render(self):
651        return
652
653class CreateAllStudentsPage(UtilityView, grok.View):
654    """Create all student objects from applicant data
655    in the root container or in a specific  applicants container only.
656    Only PortalManagers can do this.
657    """
658    #grok.context(IApplicantsContainer)
659    grok.name('createallstudents')
660    grok.require('waeup.managePortal')
661
662    def update(self):
663        cat = getUtility(ICatalog, name='applicants_catalog')
664        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
665        created = []
666        container_only = False
667        applicants_root = grok.getSite()['applicants']
668        if isinstance(self.context, ApplicantsContainer):
669            container_only = True
670        for result in results:
671            if container_only and result.__parent__ is not self.context:
672                continue
673            success, msg = result.createStudent(view=self)
674            if success:
675                created.append(result.applicant_id)
676            else:
677                ob_class = self.__implemented__.__name__.replace(
678                    'waeup.kofa.','')
679                applicants_root.logger.info(
680                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
681        if len(created):
682            self.flash(_('${a} students successfully created.',
683                mapping = {'a': len(created)}))
684        else:
685            self.flash(_('No student could be created.'), type='warning')
686        self.redirect(self.url(self.context))
687        return
688
689    def render(self):
690        return
691
692class ApplicationFeePaymentAddPage(UtilityView, grok.View):
693    """ Page to add an online payment ticket
694    """
695    grok.context(IApplicant)
696    grok.name('addafp')
697    grok.require('waeup.payApplicant')
698    factory = u'waeup.ApplicantOnlinePayment'
699
700    @property
701    def custom_requirements(self):
702        return ''
703
704    def update(self):
705        # Additional requirements in custom packages.
706        if self.custom_requirements:
707            self.flash(
708                self.custom_requirements,
709                type='danger')
710            self.redirect(self.url(self.context))
711            return
712        if not self.context.special:
713            for key in self.context.keys():
714                ticket = self.context[key]
715                if ticket.p_state == 'paid':
716                      self.flash(
717                          _('This type of payment has already been made.'),
718                          type='warning')
719                      self.redirect(self.url(self.context))
720                      return
721        applicants_utils = getUtility(IApplicantsUtils)
722        container = self.context.__parent__
723        payment = createObject(self.factory)
724        failure = applicants_utils.setPaymentDetails(
725            container, payment, self.context)
726        if failure is not None:
727            self.flash(failure, type='danger')
728            self.redirect(self.url(self.context))
729            return
730        self.context[payment.p_id] = payment
731        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
732        self.flash(_('Payment ticket created.'))
733        self.redirect(self.url(payment))
734        return
735
736    def render(self):
737        return
738
739
740class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
741    """ Page to view an online payment ticket
742    """
743    grok.context(IApplicantOnlinePayment)
744    grok.name('index')
745    grok.require('waeup.viewApplication')
746    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
747    form_fields[
748        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
749    form_fields[
750        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
751    pnav = 3
752
753    @property
754    def label(self):
755        return _('${a}: Online Payment Ticket ${b}', mapping = {
756            'a':self.context.__parent__.display_fullname,
757            'b':self.context.p_id})
758
759class OnlinePaymentApprovePage(UtilityView, grok.View):
760    """ Approval view
761    """
762    grok.context(IApplicantOnlinePayment)
763    grok.name('approve')
764    grok.require('waeup.managePortal')
765
766    def update(self):
767        flashtype, msg, log = self.context.approveApplicantPayment()
768        if log is not None:
769            applicant = self.context.__parent__
770            # Add log message to applicants.log
771            applicant.writeLogMessage(self, log)
772            # Add log message to payments.log
773            self.context.logger.info(
774                '%s,%s,%s,%s,%s,,,,,,' % (
775                applicant.applicant_id,
776                self.context.p_id, self.context.p_category,
777                self.context.amount_auth, self.context.r_code))
778        self.flash(msg, type=flashtype)
779        return
780
781    def render(self):
782        self.redirect(self.url(self.context, '@@index'))
783        return
784
785class ExportPDFPaymentSlipPage(UtilityView, grok.View):
786    """Deliver a PDF slip of the context.
787    """
788    grok.context(IApplicantOnlinePayment)
789    grok.name('payment_slip.pdf')
790    grok.require('waeup.viewApplication')
791    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
792    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
793    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
794    prefix = 'form'
795    note = None
796
797    @property
798    def title(self):
799        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
800        return translate(_('Payment Data'), 'waeup.kofa',
801            target_language=portal_language)
802
803    @property
804    def label(self):
805        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
806        return translate(_('Online Payment Slip'),
807            'waeup.kofa', target_language=portal_language) \
808            + ' %s' % self.context.p_id
809
810    @property
811    def payment_slip_download_warning(self):
812        if self.context.__parent__.state != SUBMITTED:
813            return _('Please submit the application form before '
814                     'trying to download payment slips.')
815        return ''
816
817    def render(self):
818        if self.payment_slip_download_warning:
819            self.flash(self.payment_slip_download_warning, type='danger')
820            self.redirect(self.url(self.context))
821            return
822        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
823            self.request)
824        students_utils = getUtility(IStudentsUtils)
825        return students_utils.renderPDF(self,'payment_slip.pdf',
826            self.context.__parent__, applicantview, note=self.note)
827
828class ExportPDFPageApplicationSlip(UtilityView, grok.View):
829    """Deliver a PDF slip of the context.
830    """
831    grok.context(IApplicant)
832    grok.name('application_slip.pdf')
833    grok.require('waeup.viewApplication')
834    prefix = 'form'
835
836    def update(self):
837        if self.context.state in ('initialized', 'started', 'paid'):
838            self.flash(
839                _('Please pay and submit before trying to download '
840                  'the application slip.'), type='warning')
841            return self.redirect(self.url(self.context))
842        return
843
844    def render(self):
845        try:
846            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
847                view=self)
848        except IOError:
849            self.flash(
850                _('Your image file is corrupted. '
851                  'Please replace.'), type='danger')
852            return self.redirect(self.url(self.context))
853        self.response.setHeader(
854            'Content-Type', 'application/pdf')
855        return pdfstream
856
857def handle_img_upload(upload, context, view):
858    """Handle upload of applicant image.
859
860    Returns `True` in case of success or `False`.
861
862    Please note that file pointer passed in (`upload`) most probably
863    points to end of file when leaving this function.
864    """
865    size = file_size(upload)
866    if size > MAX_UPLOAD_SIZE:
867        view.flash(_('Uploaded image is too big!'), type='danger')
868        return False
869    dummy, ext = os.path.splitext(upload.filename)
870    ext.lower()
871    if ext != '.jpg':
872        view.flash(_('jpg file extension expected.'), type='danger')
873        return False
874    upload.seek(0) # file pointer moved when determining size
875    store = getUtility(IExtFileStore)
876    file_id = IFileStoreNameChooser(context).chooseName()
877    store.createFile(file_id, upload)
878    return True
879
880class ApplicantManageFormPage(KofaEditFormPage):
881    """A full edit view for applicant data.
882    """
883    grok.context(IApplicant)
884    grok.name('manage')
885    grok.require('waeup.manageApplication')
886    grok.template('applicanteditpage')
887    manage_applications = True
888    pnav = 3
889    display_actions = [[_('Save'), _('Finally Submit')],
890        [_('Add online payment ticket'),_('Remove selected tickets')]]
891
892    @property
893    def form_fields(self):
894        if self.context.special:
895            form_fields = grok.AutoFields(ISpecialApplicant)
896            form_fields['applicant_id'].for_display = True
897        else:
898            form_fields = grok.AutoFields(IApplicant)
899            form_fields['student_id'].for_display = True
900            form_fields['applicant_id'].for_display = True
901        return form_fields
902
903    @property
904    def target(self):
905        return getattr(self.context.__parent__, 'prefix', None)
906
907    @property
908    def separators(self):
909        return getUtility(IApplicantsUtils).SEPARATORS_DICT
910
911    @property
912    def custom_upload_requirements(self):
913        return ''
914
915    def update(self):
916        super(ApplicantManageFormPage, self).update()
917        self.wf_info = IWorkflowInfo(self.context)
918        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
919        self.upload_success = None
920        upload = self.request.form.get('form.passport', None)
921        if upload:
922            if self.custom_upload_requirements:
923                self.flash(
924                    self.custom_upload_requirements,
925                    type='danger')
926                self.redirect(self.url(self.context))
927                return
928            # We got a fresh upload, upload_success is
929            # either True or False
930            self.upload_success = handle_img_upload(
931                upload, self.context, self)
932            if self.upload_success:
933                self.context.writeLogMessage(self, 'saved: passport')
934        return
935
936    @property
937    def label(self):
938        container_title = self.context.__parent__.title
939        return _('${a} <br /> Application Form ${b}', mapping = {
940            'a':container_title, 'b':self.context.application_number})
941
942    def getTransitions(self):
943        """Return a list of dicts of allowed transition ids and titles.
944
945        Each list entry provides keys ``name`` and ``title`` for
946        internal name and (human readable) title of a single
947        transition.
948        """
949        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
950            if not t[0] in ('pay', 'create')]
951        return [dict(name='', title=_('No transition'))] +[
952            dict(name=x, title=y) for x, y in allowed_transitions]
953
954    @action(_('Save'), style='primary')
955    def save(self, **data):
956        form = self.request.form
957        password = form.get('password', None)
958        password_ctl = form.get('control_password', None)
959        if password:
960            validator = getUtility(IPasswordValidator)
961            errors = validator.validate_password(password, password_ctl)
962            if errors:
963                self.flash( ' '.join(errors), type='danger')
964                return
965        if self.upload_success is False:  # False is not None!
966            # Error during image upload. Ignore other values.
967            return
968        changed_fields = self.applyData(self.context, **data)
969        # Turn list of lists into single list
970        if changed_fields:
971            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
972        else:
973            changed_fields = []
974        if password:
975            # Now we know that the form has no errors and can set password ...
976            IUserAccount(self.context).setPassword(password)
977            changed_fields.append('password')
978        fields_string = ' + '.join(changed_fields)
979        trans_id = form.get('transition', None)
980        if trans_id:
981            self.wf_info.fireTransition(trans_id)
982        self.flash(_('Form has been saved.'))
983        if fields_string:
984            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
985        return
986
987    def unremovable(self, ticket):
988        return False
989
990    # This method is also used by the ApplicantEditFormPage
991    def delPaymentTickets(self, **data):
992        form = self.request.form
993        if 'val_id' in form:
994            child_id = form['val_id']
995        else:
996            self.flash(_('No payment selected.'), type='warning')
997            self.redirect(self.url(self.context))
998            return
999        if not isinstance(child_id, list):
1000            child_id = [child_id]
1001        deleted = []
1002        for id in child_id:
1003            # Applicants are not allowed to remove used payment tickets
1004            if not self.unremovable(self.context[id]):
1005                try:
1006                    del self.context[id]
1007                    deleted.append(id)
1008                except:
1009                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1010                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1011        if len(deleted):
1012            self.flash(_('Successfully removed: ${a}',
1013                mapping = {'a':', '.join(deleted)}))
1014            self.context.writeLogMessage(
1015                self, 'removed: % s' % ', '.join(deleted))
1016        return
1017
1018    # We explicitely want the forms to be validated before payment tickets
1019    # can be created. If no validation is requested, use
1020    # 'validator=NullValidator' in the action directive
1021    @action(_('Add online payment ticket'), style='primary')
1022    def addPaymentTicket(self, **data):
1023        self.redirect(self.url(self.context, '@@addafp'))
1024        return
1025
1026    @jsaction(_('Remove selected tickets'))
1027    def removePaymentTickets(self, **data):
1028        self.delPaymentTickets(**data)
1029        self.redirect(self.url(self.context) + '/@@manage')
1030        return
1031
1032    # Not used in base package
1033    def file_exists(self, attr):
1034        file = getUtility(IExtFileStore).getFileByContext(
1035            self.context, attr=attr)
1036        if file:
1037            return True
1038        else:
1039            return False
1040
1041class ApplicantEditFormPage(ApplicantManageFormPage):
1042    """An applicant-centered edit view for applicant data.
1043    """
1044    grok.context(IApplicantEdit)
1045    grok.name('edit')
1046    grok.require('waeup.handleApplication')
1047    grok.template('applicanteditpage')
1048    manage_applications = False
1049    submit_state = PAID
1050
1051    @property
1052    def form_fields(self):
1053        if self.context.special:
1054            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1055                'locked', 'suspended')
1056            form_fields['applicant_id'].for_display = True
1057        else:
1058            form_fields = grok.AutoFields(IApplicantEdit).omit(
1059                'locked', 'course_admitted', 'student_id',
1060                'suspended'
1061                )
1062            form_fields['applicant_id'].for_display = True
1063            form_fields['reg_number'].for_display = True
1064        return form_fields
1065
1066    @property
1067    def display_actions(self):
1068        state = IWorkflowState(self.context).getState()
1069        # If the form is unlocked, applicants are allowed to save the form
1070        # and remove unused tickets.
1071        actions = [[_('Save')], [_('Remove selected tickets')]]
1072        # Only in state started they can also add tickets.
1073        if state == STARTED:
1074            actions = [[_('Save')],
1075                [_('Add online payment ticket'),_('Remove selected tickets')]]
1076        # In state paid, they can submit the data and further add tickets
1077        # if the application is special.
1078        elif self.context.special and state == PAID:
1079            actions = [[_('Save'), _('Finally Submit')],
1080                [_('Add online payment ticket'),_('Remove selected tickets')]]
1081        elif state == PAID:
1082            actions = [[_('Save'), _('Finally Submit')],
1083                [_('Remove selected tickets')]]
1084        return actions
1085
1086    def unremovable(self, ticket):
1087        return ticket.r_code
1088
1089    def emit_lock_message(self):
1090        self.flash(_('The requested form is locked (read-only).'),
1091                   type='warning')
1092        self.redirect(self.url(self.context))
1093        return
1094
1095    def update(self):
1096        if self.context.locked or (
1097            self.context.__parent__.expired and
1098            self.context.__parent__.strict_deadline):
1099            self.emit_lock_message()
1100            return
1101        super(ApplicantEditFormPage, self).update()
1102        return
1103
1104    def dataNotComplete(self):
1105        store = getUtility(IExtFileStore)
1106        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1107            return _('No passport picture uploaded.')
1108        if not self.request.form.get('confirm_passport', False):
1109            return _('Passport picture confirmation box not ticked.')
1110        return False
1111
1112    # We explicitely want the forms to be validated before payment tickets
1113    # can be created. If no validation is requested, use
1114    # 'validator=NullValidator' in the action directive
1115    @action(_('Add online payment ticket'), style='primary')
1116    def addPaymentTicket(self, **data):
1117        self.redirect(self.url(self.context, '@@addafp'))
1118        return
1119
1120    @jsaction(_('Remove selected tickets'))
1121    def removePaymentTickets(self, **data):
1122        self.delPaymentTickets(**data)
1123        self.redirect(self.url(self.context) + '/@@edit')
1124        return
1125
1126    @action(_('Save'), style='primary')
1127    def save(self, **data):
1128        if self.upload_success is False:  # False is not None!
1129            # Error during image upload. Ignore other values.
1130            return
1131        if data.get('course1', 1) == data.get('course2', 2):
1132            self.flash(_('1st and 2nd choice must be different.'),
1133                       type='warning')
1134            return
1135        self.applyData(self.context, **data)
1136        self.flash(_('Form has been saved.'))
1137        return
1138
1139    @action(_('Finally Submit'), warning=WARNING)
1140    def finalsubmit(self, **data):
1141        if self.upload_success is False:  # False is not None!
1142            return # error during image upload. Ignore other values
1143        if self.dataNotComplete():
1144            self.flash(self.dataNotComplete(), type='danger')
1145            return
1146        self.applyData(self.context, **data)
1147        state = IWorkflowState(self.context).getState()
1148        # This shouldn't happen, but the application officer
1149        # might have forgotten to lock the form after changing the state
1150        if state != self.submit_state:
1151            self.flash(_('The form cannot be submitted. Wrong state!'),
1152                       type='danger')
1153            return
1154        IWorkflowInfo(self.context).fireTransition('submit')
1155        # application_date is used in export files for sorting.
1156        # We can thus store utc.
1157        self.context.application_date = datetime.utcnow()
1158        self.flash(_('Form has been submitted.'))
1159        self.redirect(self.url(self.context))
1160        return
1161
1162class PassportImage(grok.View):
1163    """Renders the passport image for applicants.
1164    """
1165    grok.name('passport.jpg')
1166    grok.context(IApplicant)
1167    grok.require('waeup.viewApplication')
1168
1169    def render(self):
1170        # A filename chooser turns a context into a filename suitable
1171        # for file storage.
1172        image = getUtility(IExtFileStore).getFileByContext(self.context)
1173        self.response.setHeader(
1174            'Content-Type', 'image/jpeg')
1175        if image is None:
1176            # show placeholder image
1177            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1178        return image
1179
1180class ApplicantRegistrationPage(KofaAddFormPage):
1181    """Captcha'd registration page for applicants.
1182    """
1183    grok.context(IApplicantsContainer)
1184    grok.name('register')
1185    grok.require('waeup.Anonymous')
1186    grok.template('applicantregister')
1187
1188    @property
1189    def form_fields(self):
1190        form_fields = None
1191        if self.context.mode == 'update':
1192            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1193                'lastname','reg_number','email')
1194        else: #if self.context.mode == 'create':
1195            form_fields = grok.AutoFields(IApplicantEdit).select(
1196                'firstname', 'middlename', 'lastname', 'email', 'phone')
1197        return form_fields
1198
1199    @property
1200    def label(self):
1201        return _('Apply for ${a}',
1202            mapping = {'a':self.context.title})
1203
1204    def update(self):
1205        if self.context.expired:
1206            self.flash(_('Outside application period.'), type='warning')
1207            self.redirect(self.url(self.context))
1208            return
1209        # Handle captcha
1210        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1211        self.captcha_result = self.captcha.verify(self.request)
1212        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1213        return
1214
1215    def _redirect(self, email, password, applicant_id):
1216        # Forward only email to landing page in base package.
1217        self.redirect(self.url(self.context, 'registration_complete',
1218            data = dict(email=email)))
1219        return
1220
1221    @action(_('Send login credentials to email address'), style='primary')
1222    def register(self, **data):
1223        if not self.captcha_result.is_valid:
1224            # Captcha will display error messages automatically.
1225            # No need to flash something.
1226            return
1227        if self.context.mode == 'create':
1228            # Check if there are unused records in this container which
1229            # can be taken
1230            applicant = self.context.first_unused
1231            if applicant is None:
1232                # Add applicant
1233                applicant = createObject(u'waeup.Applicant')
1234                self.context.addApplicant(applicant)
1235            self.applyData(applicant, **data)
1236            applicant.reg_number = applicant.applicant_id
1237            notify(grok.ObjectModifiedEvent(applicant))
1238        elif self.context.mode == 'update':
1239            # Update applicant
1240            reg_number = data.get('reg_number','')
1241            lastname = data.get('lastname','')
1242            cat = getUtility(ICatalog, name='applicants_catalog')
1243            results = list(
1244                cat.searchResults(reg_number=(reg_number, reg_number)))
1245            if results:
1246                applicant = results[0]
1247                if getattr(applicant,'lastname',None) is None:
1248                    self.flash(_('An error occurred.'), type='danger')
1249                    return
1250                elif applicant.lastname.lower() != lastname.lower():
1251                    # Don't tell the truth here. Anonymous must not
1252                    # know that a record was found and only the lastname
1253                    # verification failed.
1254                    self.flash(
1255                        _('No application record found.'), type='warning')
1256                    return
1257                elif applicant.password is not None and \
1258                    applicant.state != INITIALIZED:
1259                    self.flash(_('Your password has already been set and used. '
1260                                 'Please proceed to the login page.'),
1261                               type='warning')
1262                    return
1263                # Store email address but nothing else.
1264                applicant.email = data['email']
1265                notify(grok.ObjectModifiedEvent(applicant))
1266            else:
1267                # No record found, this is the truth.
1268                self.flash(_('No application record found.'), type='warning')
1269                return
1270        else:
1271            # Does not happen but anyway ...
1272            return
1273        kofa_utils = getUtility(IKofaUtils)
1274        password = kofa_utils.genPassword()
1275        IUserAccount(applicant).setPassword(password)
1276        # Send email with credentials
1277        login_url = self.url(grok.getSite(), 'login')
1278        url_info = u'Login: %s' % login_url
1279        msg = _('You have successfully been registered for the')
1280        if kofa_utils.sendCredentials(IUserAccount(applicant),
1281            password, url_info, msg):
1282            email_sent = applicant.email
1283        else:
1284            email_sent = None
1285        self._redirect(email=email_sent, password=password,
1286            applicant_id=applicant.applicant_id)
1287        return
1288
1289class ApplicantRegistrationEmailSent(KofaPage):
1290    """Landing page after successful registration.
1291
1292    """
1293    grok.name('registration_complete')
1294    grok.require('waeup.Public')
1295    grok.template('applicantregemailsent')
1296    label = _('Your registration was successful.')
1297
1298    def update(self, email=None, applicant_id=None, password=None):
1299        self.email = email
1300        self.password = password
1301        self.applicant_id = applicant_id
1302        return
1303
1304class ExportJobContainerOverview(KofaPage):
1305    """Page that lists active applicant data export jobs and provides links
1306    to discard or download CSV files.
1307
1308    """
1309    grok.context(VirtualApplicantsExportJobContainer)
1310    grok.require('waeup.manageApplication')
1311    grok.name('index.html')
1312    grok.template('exportjobsindex')
1313    label = _('Data Exports')
1314    pnav = 3
1315
1316    def update(self, CREATE=None, DISCARD=None, job_id=None):
1317        if CREATE:
1318            self.redirect(self.url('@@start_export'))
1319            return
1320        if DISCARD and job_id:
1321            entry = self.context.entry_from_job_id(job_id)
1322            self.context.delete_export_entry(entry)
1323            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1324            self.context.logger.info(
1325                '%s - discarded: job_id=%s' % (ob_class, job_id))
1326            self.flash(_('Discarded export') + ' %s' % job_id)
1327        self.entries = doll_up(self, user=self.request.principal.id)
1328        return
1329
1330class ExportJobContainerJobStart(KofaPage):
1331    """Page that starts an applicants export job.
1332
1333    """
1334    grok.context(VirtualApplicantsExportJobContainer)
1335    grok.require('waeup.manageApplication')
1336    grok.name('start_export')
1337
1338    def update(self):
1339        utils = queryUtility(IKofaUtils)
1340        if not utils.expensive_actions_allowed():
1341            self.flash(_(
1342                "Currently, exporters cannot be started due to high "
1343                "system load. Please try again later."), type='danger')
1344            self.entries = doll_up(self, user=None)
1345            return
1346        exporter = 'applicants'
1347        container_code = self.context.__parent__.code
1348        job_id = self.context.start_export_job(exporter,
1349                                      self.request.principal.id,
1350                                      container=container_code)
1351
1352        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1353        self.context.logger.info(
1354            '%s - exported: %s (%s), job_id=%s'
1355            % (ob_class, exporter, container_code, job_id))
1356        self.flash(_('Export started.'))
1357        self.redirect(self.url(self.context))
1358        return
1359
1360    def render(self):
1361        return
1362
1363class ExportJobContainerDownload(ExportCSVView):
1364    """Page that downloads a students export csv file.
1365
1366    """
1367    grok.context(VirtualApplicantsExportJobContainer)
1368    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.