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

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

Method for (customizable) application passport upload requirements added.

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