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

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

More docs.

  • Property svn:keywords set to Id
File size: 47.0 KB
Line 
1## $Id: browser.py 13101 2015-06-25 17:55: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, 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        # Always refresh title. So we can change titles
381        # if APP_TYPES_DICT has been edited.
382        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
383        title = apptypes_dict[self.context.prefix][0]
384        #self.context.title = u'%s %s/%s' % (
385        #    title, self.context.year, self.context.year + 1)
386        self.flash(_('Form has been saved.'))
387        fields_string = ' + '.join(changed_fields)
388        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
389        return
390
391    @jsaction(_('Remove selected'))
392    def delApplicant(self, **data):
393        form = self.request.form
394        if 'val_id' in form:
395            child_id = form['val_id']
396        else:
397            self.flash(_('No applicant selected!'), type='warning')
398            self.redirect(self.url(self.context, '@@manage')+'#tab2')
399            return
400        if not isinstance(child_id, list):
401            child_id = [child_id]
402        deleted = []
403        for id in child_id:
404            try:
405                del self.context[id]
406                deleted.append(id)
407            except:
408                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
409                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
410        if len(deleted):
411            self.flash(_('Successfully removed: ${a}',
412                mapping = {'a':', '.join(deleted)}))
413        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
414        return
415
416    @action(_('Create students from selected'))
417    def createStudents(self, **data):
418        form = self.request.form
419        if 'val_id' in form:
420            child_id = form['val_id']
421        else:
422            self.flash(_('No applicant selected!'), type='warning')
423            self.redirect(self.url(self.context, '@@manage')+'#tab2')
424            return
425        if not isinstance(child_id, list):
426            child_id = [child_id]
427        created = []
428        for id in child_id:
429            success, msg = self.context[id].createStudent(view=self)
430            if success:
431                created.append(id)
432        if len(created):
433            self.flash(_('${a} students successfully created.',
434                mapping = {'a': len(created)}))
435        else:
436            self.flash(_('No student could be created.'), type='warning')
437        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
438        return
439
440    @action(_('Cancel'), validator=NullValidator)
441    def cancel(self, **data):
442        self.redirect(self.url(self.context))
443        return
444
445    @action(_('Add local role'), validator=NullValidator)
446    def addLocalRole(self, **data):
447        return add_local_role(self,3, **data)
448
449    @action(_('Remove selected local roles'))
450    def delLocalRoles(self, **data):
451        return del_local_roles(self,3,**data)
452
453class ApplicantAddFormPage(KofaAddFormPage):
454    """Add-form to add an applicant.
455    """
456    grok.context(IApplicantsContainer)
457    grok.require('waeup.manageApplication')
458    grok.name('addapplicant')
459    #grok.template('applicantaddpage')
460    form_fields = grok.AutoFields(IApplicant).select(
461        'firstname', 'middlename', 'lastname',
462        'email', 'phone')
463    label = _('Add applicant')
464    pnav = 3
465
466    @action(_('Create application record'))
467    def addApplicant(self, **data):
468        applicant = createObject(u'waeup.Applicant')
469        self.applyData(applicant, **data)
470        self.context.addApplicant(applicant)
471        self.flash(_('Application record created.'))
472        self.redirect(
473            self.url(self.context[applicant.application_number], 'index'))
474        return
475
476class ApplicantDisplayFormPage(KofaDisplayFormPage):
477    """A display view for applicant data.
478    """
479    grok.context(IApplicant)
480    grok.name('index')
481    grok.require('waeup.viewApplication')
482    grok.template('applicantdisplaypage')
483    label = _('Applicant')
484    pnav = 3
485    hide_hint = False
486
487    @property
488    def form_fields(self):
489        if self.context.special:
490            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
491        else:
492            form_fields = grok.AutoFields(IApplicant).omit(
493                'locked', 'course_admitted', 'password', 'suspended')
494        return form_fields
495
496    @property
497    def target(self):
498        return getattr(self.context.__parent__, 'prefix', None)
499
500    @property
501    def separators(self):
502        return getUtility(IApplicantsUtils).SEPARATORS_DICT
503
504    def update(self):
505        self.passport_url = self.url(self.context, 'passport.jpg')
506        # Mark application as started if applicant logs in for the first time
507        usertype = getattr(self.request.principal, 'user_type', None)
508        if usertype == 'applicant' and \
509            IWorkflowState(self.context).getState() == INITIALIZED:
510            IWorkflowInfo(self.context).fireTransition('start')
511        if usertype == 'applicant' and self.context.state == 'created':
512            session = '%s/%s' % (self.context.__parent__.year,
513                                 self.context.__parent__.year+1)
514            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
515            msg = _(
516                '\n <strong>Congratulations!</strong>' +
517                ' You have been offered provisional admission into the' +
518                ' ${c} Academic Session of ${d}.'
519                ' Your student record has been created for you.' +
520                ' Please, logout again and proceed to the' +
521                ' login page of the portal.'
522                ' Then enter your new student credentials:' +
523                ' user name= ${a}, password = ${b}.' +
524                ' Change your password when you have logged in.',
525                mapping = {
526                    'a':self.context.student_id,
527                    'b':self.context.application_number,
528                    'c':session,
529                    'd':title}
530                )
531            self.flash(msg)
532        return
533
534    @property
535    def hasPassword(self):
536        if self.context.password:
537            return _('set')
538        return _('unset')
539
540    @property
541    def label(self):
542        container_title = self.context.__parent__.title
543        return _('${a} <br /> Application Record ${b}', mapping = {
544            'a':container_title, 'b':self.context.application_number})
545
546    def getCourseAdmitted(self):
547        """Return link, title and code in html format to the certificate
548           admitted.
549        """
550        course_admitted = self.context.course_admitted
551        if getattr(course_admitted, '__parent__',None):
552            url = self.url(course_admitted)
553            title = course_admitted.title
554            code = course_admitted.code
555            return '<a href="%s">%s - %s</a>' %(url,code,title)
556        return ''
557
558class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
559    grok.context(IApplicant)
560    grok.name('base')
561    form_fields = grok.AutoFields(IApplicant).select(
562        'applicant_id','email', 'course1')
563
564class CreateStudentPage(UtilityView, grok.View):
565    """Create a student object from applicant data.
566    """
567    grok.context(IApplicant)
568    grok.name('createstudent')
569    grok.require('waeup.manageStudent')
570
571    def update(self):
572        msg = self.context.createStudent(view=self)[1]
573        self.flash(msg, type='warning')
574        self.redirect(self.url(self.context))
575        return
576
577    def render(self):
578        return
579
580class CreateAllStudentsPage(UtilityView, grok.View):
581    """Create all student objects from applicant data
582    in the root container or in a specific  applicants container only.
583    Only PortalManagers can do this.
584    """
585    #grok.context(IApplicantsContainer)
586    grok.name('createallstudents')
587    grok.require('waeup.managePortal')
588
589    def update(self):
590        cat = getUtility(ICatalog, name='applicants_catalog')
591        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
592        created = []
593        container_only = False
594        applicants_root = grok.getSite()['applicants']
595        if isinstance(self.context, ApplicantsContainer):
596            container_only = True
597        for result in results:
598            if container_only and result.__parent__ is not self.context:
599                continue
600            success, msg = result.createStudent(view=self)
601            if success:
602                created.append(result.applicant_id)
603            else:
604                ob_class = self.__implemented__.__name__.replace(
605                    'waeup.kofa.','')
606                applicants_root.logger.info(
607                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
608        if len(created):
609            self.flash(_('${a} students successfully created.',
610                mapping = {'a': len(created)}))
611        else:
612            self.flash(_('No student could be created.'), type='warning')
613        self.redirect(self.url(self.context))
614        return
615
616    def render(self):
617        return
618
619class ApplicationFeePaymentAddPage(UtilityView, grok.View):
620    """ Page to add an online payment ticket
621    """
622    grok.context(IApplicant)
623    grok.name('addafp')
624    grok.require('waeup.payApplicant')
625    factory = u'waeup.ApplicantOnlinePayment'
626
627    @property
628    def custom_requirements(self):
629        return ''
630
631    def update(self):
632        # Additional requirements in custom packages.
633        if self.custom_requirements:
634            self.flash(
635                self.custom_requirements,
636                type='danger')
637            self.redirect(self.url(self.context))
638            return
639        if not self.context.special:
640            for key in self.context.keys():
641                ticket = self.context[key]
642                if ticket.p_state == 'paid':
643                      self.flash(
644                          _('This type of payment has already been made.'),
645                          type='warning')
646                      self.redirect(self.url(self.context))
647                      return
648        applicants_utils = getUtility(IApplicantsUtils)
649        container = self.context.__parent__
650        payment = createObject(self.factory)
651        failure = applicants_utils.setPaymentDetails(
652            container, payment, self.context)
653        if failure is not None:
654            self.flash(failure[0], type='danger')
655            self.redirect(self.url(self.context))
656            return
657        self.context[payment.p_id] = payment
658        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
659        self.flash(_('Payment ticket created.'))
660        self.redirect(self.url(payment))
661        return
662
663    def render(self):
664        return
665
666
667class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
668    """ Page to view an online payment ticket
669    """
670    grok.context(IApplicantOnlinePayment)
671    grok.name('index')
672    grok.require('waeup.viewApplication')
673    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
674    form_fields[
675        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
676    form_fields[
677        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
678    pnav = 3
679
680    @property
681    def label(self):
682        return _('${a}: Online Payment Ticket ${b}', mapping = {
683            'a':self.context.__parent__.display_fullname,
684            'b':self.context.p_id})
685
686class OnlinePaymentApprovePage(UtilityView, grok.View):
687    """ Approval view
688    """
689    grok.context(IApplicantOnlinePayment)
690    grok.name('approve')
691    grok.require('waeup.managePortal')
692
693    def update(self):
694        flashtype, msg, log = self.context.approveApplicantPayment()
695        if log is not None:
696            applicant = self.context.__parent__
697            # Add log message to applicants.log
698            applicant.writeLogMessage(self, log)
699            # Add log message to payments.log
700            self.context.logger.info(
701                '%s,%s,%s,%s,%s,,,,,,' % (
702                applicant.applicant_id,
703                self.context.p_id, self.context.p_category,
704                self.context.amount_auth, self.context.r_code))
705        self.flash(msg, type=flashtype)
706        return
707
708    def render(self):
709        self.redirect(self.url(self.context, '@@index'))
710        return
711
712class ExportPDFPaymentSlipPage(UtilityView, grok.View):
713    """Deliver a PDF slip of the context.
714    """
715    grok.context(IApplicantOnlinePayment)
716    grok.name('payment_slip.pdf')
717    grok.require('waeup.viewApplication')
718    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
719    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
720    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
721    prefix = 'form'
722    note = None
723
724    @property
725    def title(self):
726        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
727        return translate(_('Payment Data'), 'waeup.kofa',
728            target_language=portal_language)
729
730    @property
731    def label(self):
732        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
733        return translate(_('Online Payment Slip'),
734            'waeup.kofa', target_language=portal_language) \
735            + ' %s' % self.context.p_id
736
737    @property
738    def payment_slip_download_warning(self):
739        if self.context.__parent__.state != SUBMITTED:
740            return _('Please submit the application form before '
741                     'trying to download payment slips.')
742        return ''
743
744    def render(self):
745        if self.payment_slip_download_warning:
746            self.flash(self.payment_slip_download_warning, type='danger')
747            self.redirect(self.url(self.context))
748            return
749        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
750            self.request)
751        students_utils = getUtility(IStudentsUtils)
752        return students_utils.renderPDF(self,'payment_slip.pdf',
753            self.context.__parent__, applicantview, note=self.note)
754
755class ExportPDFPageApplicationSlip(UtilityView, grok.View):
756    """Deliver a PDF slip of the context.
757    """
758    grok.context(IApplicant)
759    grok.name('application_slip.pdf')
760    grok.require('waeup.viewApplication')
761    prefix = 'form'
762
763    def update(self):
764        if self.context.state in ('initialized', 'started', 'paid'):
765            self.flash(
766                _('Please pay and submit before trying to download '
767                  'the application slip.'), type='warning')
768            return self.redirect(self.url(self.context))
769        return
770
771    def render(self):
772        try:
773            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
774                view=self)
775        except IOError:
776            self.flash(
777                _('Your image file is corrupted. '
778                  'Please replace.'), type='danger')
779            return self.redirect(self.url(self.context))
780        self.response.setHeader(
781            'Content-Type', 'application/pdf')
782        return pdfstream
783
784def handle_img_upload(upload, context, view):
785    """Handle upload of applicant image.
786
787    Returns `True` in case of success or `False`.
788
789    Please note that file pointer passed in (`upload`) most probably
790    points to end of file when leaving this function.
791    """
792    size = file_size(upload)
793    if size > MAX_UPLOAD_SIZE:
794        view.flash(_('Uploaded image is too big!'), type='danger')
795        return False
796    dummy, ext = os.path.splitext(upload.filename)
797    ext.lower()
798    if ext != '.jpg':
799        view.flash(_('jpg file extension expected.'), type='danger')
800        return False
801    upload.seek(0) # file pointer moved when determining size
802    store = getUtility(IExtFileStore)
803    file_id = IFileStoreNameChooser(context).chooseName()
804    store.createFile(file_id, upload)
805    return True
806
807class ApplicantManageFormPage(KofaEditFormPage):
808    """A full edit view for applicant data.
809    """
810    grok.context(IApplicant)
811    grok.name('manage')
812    grok.require('waeup.manageApplication')
813    grok.template('applicanteditpage')
814    manage_applications = True
815    pnav = 3
816    display_actions = [[_('Save'), _('Finally Submit')],
817        [_('Add online payment ticket'),_('Remove selected tickets')]]
818
819    @property
820    def form_fields(self):
821        if self.context.special:
822            form_fields = grok.AutoFields(ISpecialApplicant)
823            form_fields['applicant_id'].for_display = True
824        else:
825            form_fields = grok.AutoFields(IApplicant)
826            form_fields['student_id'].for_display = True
827            form_fields['applicant_id'].for_display = True
828        return form_fields
829
830    @property
831    def target(self):
832        return getattr(self.context.__parent__, 'prefix', None)
833
834    @property
835    def separators(self):
836        return getUtility(IApplicantsUtils).SEPARATORS_DICT
837
838    @property
839    def custom_upload_requirements(self):
840        return ''
841
842    def update(self):
843        super(ApplicantManageFormPage, self).update()
844        self.wf_info = IWorkflowInfo(self.context)
845        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
846        self.upload_success = None
847        upload = self.request.form.get('form.passport', None)
848        if upload:
849            if self.custom_upload_requirements:
850                self.flash(
851                    self.custom_upload_requirements,
852                    type='danger')
853                self.redirect(self.url(self.context))
854                return
855            # We got a fresh upload, upload_success is
856            # either True or False
857            self.upload_success = handle_img_upload(
858                upload, self.context, self)
859            if self.upload_success:
860                self.context.writeLogMessage(self, 'saved: passport')
861        return
862
863    @property
864    def label(self):
865        container_title = self.context.__parent__.title
866        return _('${a} <br /> Application Form ${b}', mapping = {
867            'a':container_title, 'b':self.context.application_number})
868
869    def getTransitions(self):
870        """Return a list of dicts of allowed transition ids and titles.
871
872        Each list entry provides keys ``name`` and ``title`` for
873        internal name and (human readable) title of a single
874        transition.
875        """
876        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
877            if not t[0] in ('pay', 'create')]
878        return [dict(name='', title=_('No transition'))] +[
879            dict(name=x, title=y) for x, y in allowed_transitions]
880
881    @action(_('Save'), style='primary')
882    def save(self, **data):
883        form = self.request.form
884        password = form.get('password', None)
885        password_ctl = form.get('control_password', None)
886        if password:
887            validator = getUtility(IPasswordValidator)
888            errors = validator.validate_password(password, password_ctl)
889            if errors:
890                self.flash( ' '.join(errors), type='danger')
891                return
892        if self.upload_success is False:  # False is not None!
893            # Error during image upload. Ignore other values.
894            return
895        changed_fields = self.applyData(self.context, **data)
896        # Turn list of lists into single list
897        if changed_fields:
898            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
899        else:
900            changed_fields = []
901        if password:
902            # Now we know that the form has no errors and can set password ...
903            IUserAccount(self.context).setPassword(password)
904            changed_fields.append('password')
905        fields_string = ' + '.join(changed_fields)
906        trans_id = form.get('transition', None)
907        if trans_id:
908            self.wf_info.fireTransition(trans_id)
909        self.flash(_('Form has been saved.'))
910        if fields_string:
911            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
912        return
913
914    def unremovable(self, ticket):
915        return False
916
917    # This method is also used by the ApplicantEditFormPage
918    def delPaymentTickets(self, **data):
919        form = self.request.form
920        if 'val_id' in form:
921            child_id = form['val_id']
922        else:
923            self.flash(_('No payment selected.'), type='warning')
924            self.redirect(self.url(self.context))
925            return
926        if not isinstance(child_id, list):
927            child_id = [child_id]
928        deleted = []
929        for id in child_id:
930            # Applicants are not allowed to remove used payment tickets
931            if not self.unremovable(self.context[id]):
932                try:
933                    del self.context[id]
934                    deleted.append(id)
935                except:
936                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
937                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
938        if len(deleted):
939            self.flash(_('Successfully removed: ${a}',
940                mapping = {'a':', '.join(deleted)}))
941            self.context.writeLogMessage(
942                self, 'removed: % s' % ', '.join(deleted))
943        return
944
945    # We explicitely want the forms to be validated before payment tickets
946    # can be created. If no validation is requested, use
947    # 'validator=NullValidator' in the action directive
948    @action(_('Add online payment ticket'), style='primary')
949    def addPaymentTicket(self, **data):
950        self.redirect(self.url(self.context, '@@addafp'))
951        return
952
953    @jsaction(_('Remove selected tickets'))
954    def removePaymentTickets(self, **data):
955        self.delPaymentTickets(**data)
956        self.redirect(self.url(self.context) + '/@@manage')
957        return
958
959    # Not used in base package
960    def file_exists(self, attr):
961        file = getUtility(IExtFileStore).getFileByContext(
962            self.context, attr=attr)
963        if file:
964            return True
965        else:
966            return False
967
968class ApplicantEditFormPage(ApplicantManageFormPage):
969    """An applicant-centered edit view for applicant data.
970    """
971    grok.context(IApplicantEdit)
972    grok.name('edit')
973    grok.require('waeup.handleApplication')
974    grok.template('applicanteditpage')
975    manage_applications = False
976    submit_state = PAID
977
978    @property
979    def form_fields(self):
980        if self.context.special:
981            form_fields = grok.AutoFields(ISpecialApplicant).omit(
982                'locked', 'suspended')
983            form_fields['applicant_id'].for_display = True
984        else:
985            form_fields = grok.AutoFields(IApplicantEdit).omit(
986                'locked', 'course_admitted', 'student_id',
987                'suspended'
988                )
989            form_fields['applicant_id'].for_display = True
990            form_fields['reg_number'].for_display = True
991        return form_fields
992
993    @property
994    def display_actions(self):
995        state = IWorkflowState(self.context).getState()
996        # If the form is unlocked, applicants are allowed to save the form
997        # and remove unused tickets.
998        actions = [[_('Save')], [_('Remove selected tickets')]]
999        # Only in state started they can also add tickets.
1000        if state == STARTED:
1001            actions = [[_('Save')],
1002                [_('Add online payment ticket'),_('Remove selected tickets')]]
1003        # In state paid, they can submit the data and further add tickets
1004        # if the application is special.
1005        elif self.context.special and state == PAID:
1006            actions = [[_('Save'), _('Finally Submit')],
1007                [_('Add online payment ticket'),_('Remove selected tickets')]]
1008        elif state == PAID:
1009            actions = [[_('Save'), _('Finally Submit')],
1010                [_('Remove selected tickets')]]
1011        return actions
1012
1013    def unremovable(self, ticket):
1014        return ticket.r_code
1015
1016    def emit_lock_message(self):
1017        self.flash(_('The requested form is locked (read-only).'),
1018                   type='warning')
1019        self.redirect(self.url(self.context))
1020        return
1021
1022    def update(self):
1023        if self.context.locked or (
1024            self.context.__parent__.expired and
1025            self.context.__parent__.strict_deadline):
1026            self.emit_lock_message()
1027            return
1028        super(ApplicantEditFormPage, self).update()
1029        return
1030
1031    def dataNotComplete(self):
1032        store = getUtility(IExtFileStore)
1033        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1034            return _('No passport picture uploaded.')
1035        if not self.request.form.get('confirm_passport', False):
1036            return _('Passport picture confirmation box not ticked.')
1037        return False
1038
1039    # We explicitely want the forms to be validated before payment tickets
1040    # can be created. If no validation is requested, use
1041    # 'validator=NullValidator' in the action directive
1042    @action(_('Add online payment ticket'), style='primary')
1043    def addPaymentTicket(self, **data):
1044        self.redirect(self.url(self.context, '@@addafp'))
1045        return
1046
1047    @jsaction(_('Remove selected tickets'))
1048    def removePaymentTickets(self, **data):
1049        self.delPaymentTickets(**data)
1050        self.redirect(self.url(self.context) + '/@@edit')
1051        return
1052
1053    @action(_('Save'), style='primary')
1054    def save(self, **data):
1055        if self.upload_success is False:  # False is not None!
1056            # Error during image upload. Ignore other values.
1057            return
1058        if data.get('course1', 1) == data.get('course2', 2):
1059            self.flash(_('1st and 2nd choice must be different.'),
1060                       type='warning')
1061            return
1062        self.applyData(self.context, **data)
1063        self.flash(_('Form has been saved.'))
1064        return
1065
1066    @action(_('Finally Submit'), warning=WARNING)
1067    def finalsubmit(self, **data):
1068        if self.upload_success is False:  # False is not None!
1069            return # error during image upload. Ignore other values
1070        if self.dataNotComplete():
1071            self.flash(self.dataNotComplete(), type='danger')
1072            return
1073        self.applyData(self.context, **data)
1074        state = IWorkflowState(self.context).getState()
1075        # This shouldn't happen, but the application officer
1076        # might have forgotten to lock the form after changing the state
1077        if state != self.submit_state:
1078            self.flash(_('The form cannot be submitted. Wrong state!'),
1079                       type='danger')
1080            return
1081        IWorkflowInfo(self.context).fireTransition('submit')
1082        # application_date is used in export files for sorting.
1083        # We can thus store utc.
1084        self.context.application_date = datetime.utcnow()
1085        self.flash(_('Form has been submitted.'))
1086        self.redirect(self.url(self.context))
1087        return
1088
1089class PassportImage(grok.View):
1090    """Renders the passport image for applicants.
1091    """
1092    grok.name('passport.jpg')
1093    grok.context(IApplicant)
1094    grok.require('waeup.viewApplication')
1095
1096    def render(self):
1097        # A filename chooser turns a context into a filename suitable
1098        # for file storage.
1099        image = getUtility(IExtFileStore).getFileByContext(self.context)
1100        self.response.setHeader(
1101            'Content-Type', 'image/jpeg')
1102        if image is None:
1103            # show placeholder image
1104            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1105        return image
1106
1107class ApplicantRegistrationPage(KofaAddFormPage):
1108    """Captcha'd registration page for applicants.
1109    """
1110    grok.context(IApplicantsContainer)
1111    grok.name('register')
1112    grok.require('waeup.Anonymous')
1113    grok.template('applicantregister')
1114
1115    @property
1116    def form_fields(self):
1117        form_fields = None
1118        if self.context.mode == 'update':
1119            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1120                'lastname','reg_number','email')
1121        else: #if self.context.mode == 'create':
1122            form_fields = grok.AutoFields(IApplicantEdit).select(
1123                'firstname', 'middlename', 'lastname', 'email', 'phone')
1124        return form_fields
1125
1126    @property
1127    def label(self):
1128        return _('Apply for ${a}',
1129            mapping = {'a':self.context.title})
1130
1131    def update(self):
1132        if self.context.expired:
1133            self.flash(_('Outside application period.'), type='warning')
1134            self.redirect(self.url(self.context))
1135            return
1136        # Handle captcha
1137        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1138        self.captcha_result = self.captcha.verify(self.request)
1139        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1140        return
1141
1142    def _redirect(self, email, password, applicant_id):
1143        # Forward only email to landing page in base package.
1144        self.redirect(self.url(self.context, 'registration_complete',
1145            data = dict(email=email)))
1146        return
1147
1148    @action(_('Send login credentials to email address'), style='primary')
1149    def register(self, **data):
1150        if not self.captcha_result.is_valid:
1151            # Captcha will display error messages automatically.
1152            # No need to flash something.
1153            return
1154        if self.context.mode == 'create':
1155            # Add applicant
1156            applicant = createObject(u'waeup.Applicant')
1157            self.applyData(applicant, **data)
1158            self.context.addApplicant(applicant)
1159            applicant.reg_number = applicant.applicant_id
1160            notify(grok.ObjectModifiedEvent(applicant))
1161        elif self.context.mode == 'update':
1162            # Update applicant
1163            reg_number = data.get('reg_number','')
1164            lastname = data.get('lastname','')
1165            cat = getUtility(ICatalog, name='applicants_catalog')
1166            results = list(
1167                cat.searchResults(reg_number=(reg_number, reg_number)))
1168            if results:
1169                applicant = results[0]
1170                if getattr(applicant,'lastname',None) is None:
1171                    self.flash(_('An error occurred.'), type='danger')
1172                    return
1173                elif applicant.lastname.lower() != lastname.lower():
1174                    # Don't tell the truth here. Anonymous must not
1175                    # know that a record was found and only the lastname
1176                    # verification failed.
1177                    self.flash(
1178                        _('No application record found.'), type='warning')
1179                    return
1180                elif applicant.password is not None and \
1181                    applicant.state != INITIALIZED:
1182                    self.flash(_('Your password has already been set and used. '
1183                                 'Please proceed to the login page.'),
1184                               type='warning')
1185                    return
1186                # Store email address but nothing else.
1187                applicant.email = data['email']
1188                notify(grok.ObjectModifiedEvent(applicant))
1189            else:
1190                # No record found, this is the truth.
1191                self.flash(_('No application record found.'), type='warning')
1192                return
1193        else:
1194            # Does not happen but anyway ...
1195            return
1196        kofa_utils = getUtility(IKofaUtils)
1197        password = kofa_utils.genPassword()
1198        IUserAccount(applicant).setPassword(password)
1199        # Send email with credentials
1200        login_url = self.url(grok.getSite(), 'login')
1201        url_info = u'Login: %s' % login_url
1202        msg = _('You have successfully been registered for the')
1203        if kofa_utils.sendCredentials(IUserAccount(applicant),
1204            password, url_info, msg):
1205            email_sent = applicant.email
1206        else:
1207            email_sent = None
1208        self._redirect(email=email_sent, password=password,
1209            applicant_id=applicant.applicant_id)
1210        return
1211
1212class ApplicantRegistrationEmailSent(KofaPage):
1213    """Landing page after successful registration.
1214
1215    """
1216    grok.name('registration_complete')
1217    grok.require('waeup.Public')
1218    grok.template('applicantregemailsent')
1219    label = _('Your registration was successful.')
1220
1221    def update(self, email=None, applicant_id=None, password=None):
1222        self.email = email
1223        self.password = password
1224        self.applicant_id = applicant_id
1225        return
1226
1227class ExportJobContainerOverview(KofaPage):
1228    """Page that lists active applicant data export jobs and provides links
1229    to discard or download CSV files.
1230
1231    """
1232    grok.context(VirtualApplicantsExportJobContainer)
1233    grok.require('waeup.manageApplication')
1234    grok.name('index.html')
1235    grok.template('exportjobsindex')
1236    label = _('Data Exports')
1237    pnav = 3
1238
1239    def update(self, CREATE=None, DISCARD=None, job_id=None):
1240        if CREATE:
1241            self.redirect(self.url('@@start_export'))
1242            return
1243        if DISCARD and job_id:
1244            entry = self.context.entry_from_job_id(job_id)
1245            self.context.delete_export_entry(entry)
1246            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1247            self.context.logger.info(
1248                '%s - discarded: job_id=%s' % (ob_class, job_id))
1249            self.flash(_('Discarded export') + ' %s' % job_id)
1250        self.entries = doll_up(self, user=self.request.principal.id)
1251        return
1252
1253class ExportJobContainerJobStart(KofaPage):
1254    """Page that starts an applicants export job.
1255
1256    """
1257    grok.context(VirtualApplicantsExportJobContainer)
1258    grok.require('waeup.manageApplication')
1259    grok.name('start_export')
1260
1261    def update(self):
1262        exporter = 'applicants'
1263        container_code = self.context.__parent__.code
1264        job_id = self.context.start_export_job(exporter,
1265                                      self.request.principal.id,
1266                                      container=container_code)
1267
1268        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1269        self.context.logger.info(
1270            '%s - exported: %s (%s), job_id=%s'
1271            % (ob_class, exporter, container_code, job_id))
1272        self.flash(_('Export started.'))
1273        self.redirect(self.url(self.context))
1274        return
1275
1276    def render(self):
1277        return
1278
1279class ExportJobContainerDownload(ExportCSVView):
1280    """Page that downloads a students export csv file.
1281
1282    """
1283    grok.context(VirtualApplicantsExportJobContainer)
1284    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.