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

Last change on this file since 11847 was 11826, checked in by Henrik Bettermann, 10 years ago

Hide export download button 24 hours after file generation.

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