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

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

More docs.

  • Property svn:keywords set to Id
File size: 46.7 KB
Line 
1## $Id: browser.py 13133 2015-07-02 10:06:52Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from datetime import datetime, date
24from zope.event import notify
25from zope.component import getUtility, createObject, getAdapter
26from zope.catalog.interfaces import ICatalog
27from zope.i18n import translate
28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
30from waeup.kofa.applicants.interfaces import (
31    IApplicant, IApplicantEdit, IApplicantsRoot,
32    IApplicantsContainer, IApplicantsContainerAdd,
33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
34    IApplicantRegisterUpdate, ISpecialApplicant
35    )
36from waeup.kofa.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 = _('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
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        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
175        self.context.logger.info(
176            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
177        self.redirect(self.url(self.context, '@@manage')+'#tab2')
178        return
179
180    @action(_('Add applicants container'), validator=NullValidator)
181    def addApplicantsContainer(self, **data):
182        self.redirect(self.url(self.context, '@@add'))
183        return
184
185    @action(_('Add local role'), validator=NullValidator)
186    def addLocalRole(self, **data):
187        return add_local_role(self,3, **data)
188
189    @action(_('Remove selected local roles'))
190    def delLocalRoles(self, **data):
191        return del_local_roles(self,3,**data)
192
193    @action(_('Save'), style='primary')
194    def save(self, **data):
195        self.applyData(self.context, **data)
196        description = getattr(self.context, 'description', None)
197        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
198        self.context.description_dict = html2dict(description, portal_language)
199        self.flash(_('Form has been saved.'))
200        return
201
202class ApplicantsContainerAddFormPage(KofaAddFormPage):
203    grok.context(IApplicantsRoot)
204    grok.require('waeup.manageApplication')
205    grok.name('add')
206    grok.template('applicantscontaineraddpage')
207    label = _('Add applicants container')
208    pnav = 3
209
210    form_fields = grok.AutoFields(
211        IApplicantsContainerAdd).omit('code').omit('title')
212
213    @action(_('Add applicants container'))
214    def addApplicantsContainer(self, **data):
215        year = data['year']
216        code = u'%s%s' % (data['prefix'], year)
217        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
218        title = apptypes_dict[data['prefix']][0]
219        title = u'%s %s/%s' % (title, year, year + 1)
220        if code in self.context.keys():
221            self.flash(
222              _('An applicants container for the same application '
223                'type and entrance year exists already in the database.'),
224                type='warning')
225            return
226        # Add new applicants container...
227        container = createObject(u'waeup.ApplicantsContainer')
228        self.applyData(container, **data)
229        container.code = code
230        container.title = title
231        self.context[code] = container
232        self.flash(_('Added:') + ' "%s".' % code)
233        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
234        self.context.logger.info('%s - added: %s' % (ob_class, code))
235        self.redirect(self.url(self.context, u'@@manage'))
236        return
237
238    @action(_('Cancel'), validator=NullValidator)
239    def cancel(self, **data):
240        self.redirect(self.url(self.context, '@@manage'))
241
242class ApplicantsRootBreadcrumb(Breadcrumb):
243    """A breadcrumb for applicantsroot.
244    """
245    grok.context(IApplicantsRoot)
246    title = _(u'Applicants')
247
248class ApplicantsContainerBreadcrumb(Breadcrumb):
249    """A breadcrumb for applicantscontainers.
250    """
251    grok.context(IApplicantsContainer)
252
253
254class ApplicantsExportsBreadcrumb(Breadcrumb):
255    """A breadcrumb for exports.
256    """
257    grok.context(VirtualApplicantsExportJobContainer)
258    title = _(u'Applicant Data Exports')
259    target = None
260
261class ApplicantBreadcrumb(Breadcrumb):
262    """A breadcrumb for applicants.
263    """
264    grok.context(IApplicant)
265
266    @property
267    def title(self):
268        """Get a title for a context.
269        """
270        return self.context.application_number
271
272class OnlinePaymentBreadcrumb(Breadcrumb):
273    """A breadcrumb for payments.
274    """
275    grok.context(IApplicantOnlinePayment)
276
277    @property
278    def title(self):
279        return self.context.p_id
280
281class ApplicantsStatisticsPage(KofaDisplayFormPage):
282    """Some statistics about applicants in a container.
283    """
284    grok.context(IApplicantsContainer)
285    grok.name('statistics')
286    grok.require('waeup.viewApplicationStatistics')
287    grok.template('applicantcontainerstatistics')
288
289    @property
290    def label(self):
291        return "%s" % self.context.title
292
293class ApplicantsContainerPage(KofaDisplayFormPage):
294    """The standard view for regular applicant containers.
295    """
296    grok.context(IApplicantsContainer)
297    grok.name('index')
298    grok.require('waeup.Public')
299    grok.template('applicantscontainerpage')
300    pnav = 3
301
302    @property
303    def form_fields(self):
304        form_fields = grok.AutoFields(IApplicantsContainer).omit(
305            'title', 'description')
306        form_fields[
307            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
308        form_fields[
309            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
310        if self.request.principal.id == 'zope.anybody':
311            form_fields = form_fields.omit(
312                'code', 'prefix', 'year', 'mode', 'hidden',
313                'strict_deadline', 'application_category',
314                'application_slip_notice')
315        return form_fields
316
317    @property
318    def introduction(self):
319        # Here we know that the cookie has been set
320        lang = self.request.cookies.get('kofa.language')
321        html = self.context.description_dict.get(lang,'')
322        if html == '':
323            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
324            html = self.context.description_dict.get(portal_language,'')
325        return html
326
327    @property
328    def label(self):
329        return "%s" % self.context.title
330
331class ApplicantsContainerManageFormPage(KofaEditFormPage):
332    grok.context(IApplicantsContainer)
333    grok.name('manage')
334    grok.template('applicantscontainermanagepage')
335    form_fields = grok.AutoFields(IApplicantsContainer)
336    taboneactions = [_('Save'),_('Cancel')]
337    tabtwoactions = [_('Remove selected'),_('Cancel'),
338        _('Create students from selected')]
339    tabthreeactions1 = [_('Remove selected local roles')]
340    tabthreeactions2 = [_('Add local role')]
341    # Use friendlier date widget...
342    grok.require('waeup.manageApplication')
343
344    @property
345    def label(self):
346        return _('Manage applicants container')
347
348    pnav = 3
349
350    @property
351    def showApplicants(self):
352        if len(self.context) < 5000:
353            return True
354        return False
355
356    def getLocalRoles(self):
357        roles = ILocalRolesAssignable(self.context)
358        return roles()
359
360    def getUsers(self):
361        """Get a list of all users.
362        """
363        for key, val in grok.getSite()['users'].items():
364            url = self.url(val)
365            yield(dict(url=url, name=key, val=val))
366
367    def getUsersWithLocalRoles(self):
368        return get_users_with_local_roles(self.context)
369
370    @action(_('Save'), style='primary')
371    def save(self, **data):
372        changed_fields = self.applyData(self.context, **data)
373        if changed_fields:
374            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
375        else:
376            changed_fields = []
377        description = getattr(self.context, 'description', None)
378        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
379        self.context.description_dict = html2dict(description, portal_language)
380        self.flash(_('Form has been saved.'))
381        fields_string = ' + '.join(changed_fields)
382        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
383        return
384
385    @jsaction(_('Remove selected'))
386    def delApplicant(self, **data):
387        form = self.request.form
388        if 'val_id' in form:
389            child_id = form['val_id']
390        else:
391            self.flash(_('No applicant selected!'), type='warning')
392            self.redirect(self.url(self.context, '@@manage')+'#tab2')
393            return
394        if not isinstance(child_id, list):
395            child_id = [child_id]
396        deleted = []
397        for id in child_id:
398            try:
399                del self.context[id]
400                deleted.append(id)
401            except:
402                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
403                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
404        if len(deleted):
405            self.flash(_('Successfully removed: ${a}',
406                mapping = {'a':', '.join(deleted)}))
407        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
408        return
409
410    @action(_('Create students from selected'))
411    def createStudents(self, **data):
412        form = self.request.form
413        if 'val_id' in form:
414            child_id = form['val_id']
415        else:
416            self.flash(_('No applicant selected!'), type='warning')
417            self.redirect(self.url(self.context, '@@manage')+'#tab2')
418            return
419        if not isinstance(child_id, list):
420            child_id = [child_id]
421        created = []
422        for id in child_id:
423            success, msg = self.context[id].createStudent(view=self)
424            if success:
425                created.append(id)
426        if len(created):
427            self.flash(_('${a} students successfully created.',
428                mapping = {'a': len(created)}))
429        else:
430            self.flash(_('No student could be created.'), type='warning')
431        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
432        return
433
434    @action(_('Cancel'), validator=NullValidator)
435    def cancel(self, **data):
436        self.redirect(self.url(self.context))
437        return
438
439    @action(_('Add local role'), validator=NullValidator)
440    def addLocalRole(self, **data):
441        return add_local_role(self,3, **data)
442
443    @action(_('Remove selected local roles'))
444    def delLocalRoles(self, **data):
445        return del_local_roles(self,3,**data)
446
447class ApplicantAddFormPage(KofaAddFormPage):
448    """Add-form to add an applicant.
449    """
450    grok.context(IApplicantsContainer)
451    grok.require('waeup.manageApplication')
452    grok.name('addapplicant')
453    #grok.template('applicantaddpage')
454    form_fields = grok.AutoFields(IApplicant).select(
455        'firstname', 'middlename', 'lastname',
456        'email', 'phone')
457    label = _('Add applicant')
458    pnav = 3
459
460    @action(_('Create application record'))
461    def addApplicant(self, **data):
462        applicant = createObject(u'waeup.Applicant')
463        self.applyData(applicant, **data)
464        self.context.addApplicant(applicant)
465        self.flash(_('Application record created.'))
466        self.redirect(
467            self.url(self.context[applicant.application_number], 'index'))
468        return
469
470class ApplicantDisplayFormPage(KofaDisplayFormPage):
471    """A display view for applicant data.
472    """
473    grok.context(IApplicant)
474    grok.name('index')
475    grok.require('waeup.viewApplication')
476    grok.template('applicantdisplaypage')
477    label = _('Applicant')
478    pnav = 3
479    hide_hint = False
480
481    @property
482    def form_fields(self):
483        if self.context.special:
484            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
485        else:
486            form_fields = grok.AutoFields(IApplicant).omit(
487                'locked', 'course_admitted', 'password', 'suspended')
488        return form_fields
489
490    @property
491    def target(self):
492        return getattr(self.context.__parent__, 'prefix', None)
493
494    @property
495    def separators(self):
496        return getUtility(IApplicantsUtils).SEPARATORS_DICT
497
498    def update(self):
499        self.passport_url = self.url(self.context, 'passport.jpg')
500        # Mark application as started if applicant logs in for the first time
501        usertype = getattr(self.request.principal, 'user_type', None)
502        if usertype == 'applicant' and \
503            IWorkflowState(self.context).getState() == INITIALIZED:
504            IWorkflowInfo(self.context).fireTransition('start')
505        if usertype == 'applicant' and self.context.state == 'created':
506            session = '%s/%s' % (self.context.__parent__.year,
507                                 self.context.__parent__.year+1)
508            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
509            msg = _(
510                '\n <strong>Congratulations!</strong>' +
511                ' You have been offered provisional admission into the' +
512                ' ${c} Academic Session of ${d}.'
513                ' Your student record has been created for you.' +
514                ' Please, logout again and proceed to the' +
515                ' login page of the portal.'
516                ' Then enter your new student credentials:' +
517                ' user name= ${a}, password = ${b}.' +
518                ' Change your password when you have logged in.',
519                mapping = {
520                    'a':self.context.student_id,
521                    'b':self.context.application_number,
522                    'c':session,
523                    'd':title}
524                )
525            self.flash(msg)
526        return
527
528    @property
529    def hasPassword(self):
530        if self.context.password:
531            return _('set')
532        return _('unset')
533
534    @property
535    def label(self):
536        container_title = self.context.__parent__.title
537        return _('${a} <br /> Application Record ${b}', mapping = {
538            'a':container_title, 'b':self.context.application_number})
539
540    def getCourseAdmitted(self):
541        """Return link, title and code in html format to the certificate
542           admitted.
543        """
544        course_admitted = self.context.course_admitted
545        if getattr(course_admitted, '__parent__',None):
546            url = self.url(course_admitted)
547            title = course_admitted.title
548            code = course_admitted.code
549            return '<a href="%s">%s - %s</a>' %(url,code,title)
550        return ''
551
552class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
553    grok.context(IApplicant)
554    grok.name('base')
555    form_fields = grok.AutoFields(IApplicant).select(
556        'applicant_id','email', 'course1')
557
558class CreateStudentPage(UtilityView, grok.View):
559    """Create a student object from applicant data.
560    """
561    grok.context(IApplicant)
562    grok.name('createstudent')
563    grok.require('waeup.manageStudent')
564
565    def update(self):
566        msg = self.context.createStudent(view=self)[1]
567        self.flash(msg, type='warning')
568        self.redirect(self.url(self.context))
569        return
570
571    def render(self):
572        return
573
574class CreateAllStudentsPage(UtilityView, grok.View):
575    """Create all student objects from applicant data
576    in the root container or in a specific  applicants container only.
577    Only PortalManagers can do this.
578    """
579    #grok.context(IApplicantsContainer)
580    grok.name('createallstudents')
581    grok.require('waeup.managePortal')
582
583    def update(self):
584        cat = getUtility(ICatalog, name='applicants_catalog')
585        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
586        created = []
587        container_only = False
588        applicants_root = grok.getSite()['applicants']
589        if isinstance(self.context, ApplicantsContainer):
590            container_only = True
591        for result in results:
592            if container_only and result.__parent__ is not self.context:
593                continue
594            success, msg = result.createStudent(view=self)
595            if success:
596                created.append(result.applicant_id)
597            else:
598                ob_class = self.__implemented__.__name__.replace(
599                    'waeup.kofa.','')
600                applicants_root.logger.info(
601                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
602        if len(created):
603            self.flash(_('${a} students successfully created.',
604                mapping = {'a': len(created)}))
605        else:
606            self.flash(_('No student could be created.'), type='warning')
607        self.redirect(self.url(self.context))
608        return
609
610    def render(self):
611        return
612
613class ApplicationFeePaymentAddPage(UtilityView, grok.View):
614    """ Page to add an online payment ticket
615    """
616    grok.context(IApplicant)
617    grok.name('addafp')
618    grok.require('waeup.payApplicant')
619    factory = u'waeup.ApplicantOnlinePayment'
620
621    @property
622    def custom_requirements(self):
623        return ''
624
625    def update(self):
626        # Additional requirements in custom packages.
627        if self.custom_requirements:
628            self.flash(
629                self.custom_requirements,
630                type='danger')
631            self.redirect(self.url(self.context))
632            return
633        if not self.context.special:
634            for key in self.context.keys():
635                ticket = self.context[key]
636                if ticket.p_state == 'paid':
637                      self.flash(
638                          _('This type of payment has already been made.'),
639                          type='warning')
640                      self.redirect(self.url(self.context))
641                      return
642        applicants_utils = getUtility(IApplicantsUtils)
643        container = self.context.__parent__
644        payment = createObject(self.factory)
645        failure = applicants_utils.setPaymentDetails(
646            container, payment, self.context)
647        if failure is not None:
648            self.flash(failure, type='danger')
649            self.redirect(self.url(self.context))
650            return
651        self.context[payment.p_id] = payment
652        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
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'), _('Finally 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        # If the form is unlocked, applicants are allowed to save the form
991        # and remove unused tickets.
992        actions = [[_('Save')], [_('Remove selected tickets')]]
993        # Only in state started they can also add tickets.
994        if state == STARTED:
995            actions = [[_('Save')],
996                [_('Add online payment ticket'),_('Remove selected tickets')]]
997        # In state paid, they can submit the data and further add tickets
998        # if the application is special.
999        elif self.context.special and state == PAID:
1000            actions = [[_('Save'), _('Finally Submit')],
1001                [_('Add online payment ticket'),_('Remove selected tickets')]]
1002        elif state == PAID:
1003            actions = [[_('Save'), _('Finally Submit')],
1004                [_('Remove selected tickets')]]
1005        return actions
1006
1007    def unremovable(self, ticket):
1008        return ticket.r_code
1009
1010    def emit_lock_message(self):
1011        self.flash(_('The requested form is locked (read-only).'),
1012                   type='warning')
1013        self.redirect(self.url(self.context))
1014        return
1015
1016    def update(self):
1017        if self.context.locked or (
1018            self.context.__parent__.expired and
1019            self.context.__parent__.strict_deadline):
1020            self.emit_lock_message()
1021            return
1022        super(ApplicantEditFormPage, self).update()
1023        return
1024
1025    def dataNotComplete(self):
1026        store = getUtility(IExtFileStore)
1027        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1028            return _('No passport picture uploaded.')
1029        if not self.request.form.get('confirm_passport', False):
1030            return _('Passport picture confirmation box not ticked.')
1031        return False
1032
1033    # We explicitely want the forms to be validated before payment tickets
1034    # can be created. If no validation is requested, use
1035    # 'validator=NullValidator' in the action directive
1036    @action(_('Add online payment ticket'), style='primary')
1037    def addPaymentTicket(self, **data):
1038        self.redirect(self.url(self.context, '@@addafp'))
1039        return
1040
1041    @jsaction(_('Remove selected tickets'))
1042    def removePaymentTickets(self, **data):
1043        self.delPaymentTickets(**data)
1044        self.redirect(self.url(self.context) + '/@@edit')
1045        return
1046
1047    @action(_('Save'), style='primary')
1048    def save(self, **data):
1049        if self.upload_success is False:  # False is not None!
1050            # Error during image upload. Ignore other values.
1051            return
1052        if data.get('course1', 1) == data.get('course2', 2):
1053            self.flash(_('1st and 2nd choice must be different.'),
1054                       type='warning')
1055            return
1056        self.applyData(self.context, **data)
1057        self.flash(_('Form has been saved.'))
1058        return
1059
1060    @action(_('Finally Submit'), warning=WARNING)
1061    def finalsubmit(self, **data):
1062        if self.upload_success is False:  # False is not None!
1063            return # error during image upload. Ignore other values
1064        if self.dataNotComplete():
1065            self.flash(self.dataNotComplete(), type='danger')
1066            return
1067        self.applyData(self.context, **data)
1068        state = IWorkflowState(self.context).getState()
1069        # This shouldn't happen, but the application officer
1070        # might have forgotten to lock the form after changing the state
1071        if state != self.submit_state:
1072            self.flash(_('The form cannot be submitted. Wrong state!'),
1073                       type='danger')
1074            return
1075        IWorkflowInfo(self.context).fireTransition('submit')
1076        # application_date is used in export files for sorting.
1077        # We can thus store utc.
1078        self.context.application_date = datetime.utcnow()
1079        self.flash(_('Form has been submitted.'))
1080        self.redirect(self.url(self.context))
1081        return
1082
1083class PassportImage(grok.View):
1084    """Renders the passport image for applicants.
1085    """
1086    grok.name('passport.jpg')
1087    grok.context(IApplicant)
1088    grok.require('waeup.viewApplication')
1089
1090    def render(self):
1091        # A filename chooser turns a context into a filename suitable
1092        # for file storage.
1093        image = getUtility(IExtFileStore).getFileByContext(self.context)
1094        self.response.setHeader(
1095            'Content-Type', 'image/jpeg')
1096        if image is None:
1097            # show placeholder image
1098            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1099        return image
1100
1101class ApplicantRegistrationPage(KofaAddFormPage):
1102    """Captcha'd registration page for applicants.
1103    """
1104    grok.context(IApplicantsContainer)
1105    grok.name('register')
1106    grok.require('waeup.Anonymous')
1107    grok.template('applicantregister')
1108
1109    @property
1110    def form_fields(self):
1111        form_fields = None
1112        if self.context.mode == 'update':
1113            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1114                'lastname','reg_number','email')
1115        else: #if self.context.mode == 'create':
1116            form_fields = grok.AutoFields(IApplicantEdit).select(
1117                'firstname', 'middlename', 'lastname', 'email', 'phone')
1118        return form_fields
1119
1120    @property
1121    def label(self):
1122        return _('Apply for ${a}',
1123            mapping = {'a':self.context.title})
1124
1125    def update(self):
1126        if self.context.expired:
1127            self.flash(_('Outside application period.'), type='warning')
1128            self.redirect(self.url(self.context))
1129            return
1130        # Handle captcha
1131        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1132        self.captcha_result = self.captcha.verify(self.request)
1133        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1134        return
1135
1136    def _redirect(self, email, password, applicant_id):
1137        # Forward only email to landing page in base package.
1138        self.redirect(self.url(self.context, 'registration_complete',
1139            data = dict(email=email)))
1140        return
1141
1142    @action(_('Send login credentials to email address'), style='primary')
1143    def register(self, **data):
1144        if not self.captcha_result.is_valid:
1145            # Captcha will display error messages automatically.
1146            # No need to flash something.
1147            return
1148        if self.context.mode == 'create':
1149            # Add applicant
1150            applicant = createObject(u'waeup.Applicant')
1151            self.applyData(applicant, **data)
1152            self.context.addApplicant(applicant)
1153            applicant.reg_number = applicant.applicant_id
1154            notify(grok.ObjectModifiedEvent(applicant))
1155        elif self.context.mode == 'update':
1156            # Update applicant
1157            reg_number = data.get('reg_number','')
1158            lastname = data.get('lastname','')
1159            cat = getUtility(ICatalog, name='applicants_catalog')
1160            results = list(
1161                cat.searchResults(reg_number=(reg_number, reg_number)))
1162            if results:
1163                applicant = results[0]
1164                if getattr(applicant,'lastname',None) is None:
1165                    self.flash(_('An error occurred.'), type='danger')
1166                    return
1167                elif applicant.lastname.lower() != lastname.lower():
1168                    # Don't tell the truth here. Anonymous must not
1169                    # know that a record was found and only the lastname
1170                    # verification failed.
1171                    self.flash(
1172                        _('No application record found.'), type='warning')
1173                    return
1174                elif applicant.password is not None and \
1175                    applicant.state != INITIALIZED:
1176                    self.flash(_('Your password has already been set and used. '
1177                                 'Please proceed to the login page.'),
1178                               type='warning')
1179                    return
1180                # Store email address but nothing else.
1181                applicant.email = data['email']
1182                notify(grok.ObjectModifiedEvent(applicant))
1183            else:
1184                # No record found, this is the truth.
1185                self.flash(_('No application record found.'), type='warning')
1186                return
1187        else:
1188            # Does not happen but anyway ...
1189            return
1190        kofa_utils = getUtility(IKofaUtils)
1191        password = kofa_utils.genPassword()
1192        IUserAccount(applicant).setPassword(password)
1193        # Send email with credentials
1194        login_url = self.url(grok.getSite(), 'login')
1195        url_info = u'Login: %s' % login_url
1196        msg = _('You have successfully been registered for the')
1197        if kofa_utils.sendCredentials(IUserAccount(applicant),
1198            password, url_info, msg):
1199            email_sent = applicant.email
1200        else:
1201            email_sent = None
1202        self._redirect(email=email_sent, password=password,
1203            applicant_id=applicant.applicant_id)
1204        return
1205
1206class ApplicantRegistrationEmailSent(KofaPage):
1207    """Landing page after successful registration.
1208
1209    """
1210    grok.name('registration_complete')
1211    grok.require('waeup.Public')
1212    grok.template('applicantregemailsent')
1213    label = _('Your registration was successful.')
1214
1215    def update(self, email=None, applicant_id=None, password=None):
1216        self.email = email
1217        self.password = password
1218        self.applicant_id = applicant_id
1219        return
1220
1221class ExportJobContainerOverview(KofaPage):
1222    """Page that lists active applicant data export jobs and provides links
1223    to discard or download CSV files.
1224
1225    """
1226    grok.context(VirtualApplicantsExportJobContainer)
1227    grok.require('waeup.manageApplication')
1228    grok.name('index.html')
1229    grok.template('exportjobsindex')
1230    label = _('Data Exports')
1231    pnav = 3
1232
1233    def update(self, CREATE=None, DISCARD=None, job_id=None):
1234        if CREATE:
1235            self.redirect(self.url('@@start_export'))
1236            return
1237        if DISCARD and job_id:
1238            entry = self.context.entry_from_job_id(job_id)
1239            self.context.delete_export_entry(entry)
1240            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1241            self.context.logger.info(
1242                '%s - discarded: job_id=%s' % (ob_class, job_id))
1243            self.flash(_('Discarded export') + ' %s' % job_id)
1244        self.entries = doll_up(self, user=self.request.principal.id)
1245        return
1246
1247class ExportJobContainerJobStart(KofaPage):
1248    """Page that starts an applicants export job.
1249
1250    """
1251    grok.context(VirtualApplicantsExportJobContainer)
1252    grok.require('waeup.manageApplication')
1253    grok.name('start_export')
1254
1255    def update(self):
1256        exporter = 'applicants'
1257        container_code = self.context.__parent__.code
1258        job_id = self.context.start_export_job(exporter,
1259                                      self.request.principal.id,
1260                                      container=container_code)
1261
1262        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1263        self.context.logger.info(
1264            '%s - exported: %s (%s), job_id=%s'
1265            % (ob_class, exporter, container_code, job_id))
1266        self.flash(_('Export started.'))
1267        self.redirect(self.url(self.context))
1268        return
1269
1270    def render(self):
1271        return
1272
1273class ExportJobContainerDownload(ExportCSVView):
1274    """Page that downloads a students export csv file.
1275
1276    """
1277    grok.context(VirtualApplicantsExportJobContainer)
1278    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.