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

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

Reverse sorting.

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