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

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

Take unused records instead of creating new records during self-registration.

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