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

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

Catch traceback when creating an application slip with a corrupted image file. This error caused a lot of trouble on KwaraPoly?.

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