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

Last change on this file since 12892 was 12892, 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 12892 2015-04-29 09:37:13Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from datetime import datetime, date
24from zope.event import notify
25from zope.component import getUtility, createObject, getAdapter
26from zope.catalog.interfaces import ICatalog
27from zope.i18n import translate
28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
30from waeup.kofa.applicants.interfaces import (
31    IApplicant, IApplicantEdit, IApplicantsRoot,
32    IApplicantsContainer, IApplicantsContainerAdd,
33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
34    IApplicantRegisterUpdate, ISpecialApplicant
35    )
36from waeup.kofa.utils.helpers import html2dict
37from waeup.kofa.applicants.container import (
38    ApplicantsContainer, VirtualApplicantsExportJobContainer)
39from waeup.kofa.applicants.applicant import search
40from waeup.kofa.applicants.workflow import (
41    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
42from waeup.kofa.browser import (
43#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
44    DEFAULT_PASSPORT_IMAGE_PATH)
45from waeup.kofa.browser.layout import (
46    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
47from waeup.kofa.browser.interfaces import ICaptchaManager
48from waeup.kofa.browser.breadcrumbs import Breadcrumb
49from waeup.kofa.browser.layout import (
50    NullValidator, jsaction, action, UtilityView)
51from waeup.kofa.browser.pages import (
52    add_local_role, del_local_roles, doll_up, ExportCSVView)
53from waeup.kofa.interfaces import (
54    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
55    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
56from waeup.kofa.interfaces import MessageFactory as _
57from waeup.kofa.permissions import get_users_with_local_roles
58from waeup.kofa.students.interfaces import IStudentsUtils
59from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
60from waeup.kofa.widgets.datewidget import (
61    FriendlyDateDisplayWidget,
62    FriendlyDatetimeDisplayWidget)
63
64grok.context(IKofaObject) # Make IKofaObject the default context
65
66WARNING = _('You can not edit your application records after final submission.'
67            ' You really want to submit?')
68
69class ApplicantsRootPage(KofaDisplayFormPage):
70    grok.context(IApplicantsRoot)
71    grok.name('index')
72    grok.require('waeup.Public')
73    form_fields = grok.AutoFields(IApplicantsRoot)
74    label = _('Application Section')
75    pnav = 3
76
77    def update(self):
78        super(ApplicantsRootPage, self).update()
79        return
80
81    @property
82    def introduction(self):
83        # Here we know that the cookie has been set
84        lang = self.request.cookies.get('kofa.language')
85        html = self.context.description_dict.get(lang,'')
86        if html == '':
87            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
88            html = self.context.description_dict.get(portal_language,'')
89        return html
90
91    @property
92    def containers(self):
93        if self.layout.isAuthenticated():
94            return self.context.values()
95        return [value for value in self.context.values() if not value.hidden]
96
97class ApplicantsSearchPage(KofaPage):
98    grok.context(IApplicantsRoot)
99    grok.name('search')
100    grok.require('waeup.viewApplication')
101    label = _('Find applicants')
102    search_button = _('Find applicant')
103    pnav = 3
104
105    def update(self, *args, **kw):
106        form = self.request.form
107        self.results = []
108        if 'searchterm' in form and form['searchterm']:
109            self.searchterm = form['searchterm']
110            self.searchtype = form['searchtype']
111        elif 'old_searchterm' in form:
112            self.searchterm = form['old_searchterm']
113            self.searchtype = form['old_searchtype']
114        else:
115            if 'search' in form:
116                self.flash(_('Empty search string'), type='warning')
117            return
118        self.results = search(query=self.searchterm,
119            searchtype=self.searchtype, view=self)
120        if not self.results:
121            self.flash(_('No applicant found.'), type='warning')
122        return
123
124class ApplicantsRootManageFormPage(KofaEditFormPage):
125    grok.context(IApplicantsRoot)
126    grok.name('manage')
127    grok.template('applicantsrootmanagepage')
128    form_fields = grok.AutoFields(IApplicantsRoot)
129    label = _('Manage application section')
130    pnav = 3
131    grok.require('waeup.manageApplication')
132    taboneactions = [_('Save')]
133    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
134    tabthreeactions1 = [_('Remove selected local roles')]
135    tabthreeactions2 = [_('Add local role')]
136    subunits = _('Applicants Containers')
137
138    def getLocalRoles(self):
139        roles = ILocalRolesAssignable(self.context)
140        return roles()
141
142    def getUsers(self):
143        """Get a list of all users.
144        """
145        for key, val in grok.getSite()['users'].items():
146            url = self.url(val)
147            yield(dict(url=url, name=key, val=val))
148
149    def getUsersWithLocalRoles(self):
150        return get_users_with_local_roles(self.context)
151
152    @jsaction(_('Remove selected'))
153    def delApplicantsContainers(self, **data):
154        form = self.request.form
155        if 'val_id' in form:
156            child_id = form['val_id']
157        else:
158            self.flash(_('No container selected!'), type='warning')
159            self.redirect(self.url(self.context, '@@manage')+'#tab2')
160            return
161        if not isinstance(child_id, list):
162            child_id = [child_id]
163        deleted = []
164        for id in child_id:
165            try:
166                del self.context[id]
167                deleted.append(id)
168            except:
169                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
170                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
171        if len(deleted):
172            self.flash(_('Successfully removed: ${a}',
173                mapping = {'a':', '.join(deleted)}))
174        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(_('Applicant 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
584    Only PortalManagers can do this.
585    """
586    #grok.context(IApplicantsContainer)
587    grok.name('createallstudents')
588    grok.require('waeup.managePortal')
589
590    def update(self):
591        cat = getUtility(ICatalog, name='applicants_catalog')
592        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
593        created = []
594        container_only = False
595        applicants_root = grok.getSite()['applicants']
596        if isinstance(self.context, ApplicantsContainer):
597            container_only = True
598        for result in results:
599            if container_only and result.__parent__ is not self.context:
600                continue
601            success, msg = result.createStudent(view=self)
602            if success:
603                created.append(result.applicant_id)
604            else:
605                ob_class = self.__implemented__.__name__.replace('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.flash(_('Payment ticket created.'))
659        self.redirect(self.url(payment))
660        return
661
662    def render(self):
663        return
664
665
666class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
667    """ Page to view an online payment ticket
668    """
669    grok.context(IApplicantOnlinePayment)
670    grok.name('index')
671    grok.require('waeup.viewApplication')
672    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
673    form_fields[
674        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
675    form_fields[
676        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
677    pnav = 3
678
679    @property
680    def label(self):
681        return _('${a}: Online Payment Ticket ${b}', mapping = {
682            'a':self.context.__parent__.display_fullname,
683            'b':self.context.p_id})
684
685class OnlinePaymentApprovePage(UtilityView, grok.View):
686    """ Approval view
687    """
688    grok.context(IApplicantOnlinePayment)
689    grok.name('approve')
690    grok.require('waeup.managePortal')
691
692    def update(self):
693        flashtype, msg, log = self.context.approveApplicantPayment()
694        if log is not None:
695            applicant = self.context.__parent__
696            # Add log message to applicants.log
697            applicant.writeLogMessage(self, log)
698            # Add log message to payments.log
699            self.context.logger.info(
700                '%s,%s,%s,%s,%s,,,,,,' % (
701                applicant.applicant_id,
702                self.context.p_id, self.context.p_category,
703                self.context.amount_auth, self.context.r_code))
704        self.flash(msg, type=flashtype)
705        return
706
707    def render(self):
708        self.redirect(self.url(self.context, '@@index'))
709        return
710
711class ExportPDFPaymentSlipPage(UtilityView, grok.View):
712    """Deliver a PDF slip of the context.
713    """
714    grok.context(IApplicantOnlinePayment)
715    grok.name('payment_slip.pdf')
716    grok.require('waeup.viewApplication')
717    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
718    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
719    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
720    prefix = 'form'
721    note = None
722
723    @property
724    def title(self):
725        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
726        return translate(_('Payment Data'), 'waeup.kofa',
727            target_language=portal_language)
728
729    @property
730    def label(self):
731        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
732        return translate(_('Online Payment Slip'),
733            'waeup.kofa', target_language=portal_language) \
734            + ' %s' % self.context.p_id
735
736    @property
737    def payment_slip_download_warning(self):
738        if self.context.__parent__.state != SUBMITTED:
739            return _('Please submit the application form before '
740                     'trying to download payment slips.')
741        return ''
742
743    def render(self):
744        if self.payment_slip_download_warning:
745            self.flash(self.payment_slip_download_warning, type='danger')
746            self.redirect(self.url(self.context))
747            return
748        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
749            self.request)
750        students_utils = getUtility(IStudentsUtils)
751        return students_utils.renderPDF(self,'payment_slip.pdf',
752            self.context.__parent__, applicantview, note=self.note)
753
754class ExportPDFPageApplicationSlip(UtilityView, grok.View):
755    """Deliver a PDF slip of the context.
756    """
757    grok.context(IApplicant)
758    grok.name('application_slip.pdf')
759    grok.require('waeup.viewApplication')
760    prefix = 'form'
761
762    def update(self):
763        if self.context.state in ('initialized', 'started', 'paid'):
764            self.flash(
765                _('Please pay and submit before trying to download '
766                  'the application slip.'), type='warning')
767            return self.redirect(self.url(self.context))
768        return
769
770    def render(self):
771        try:
772            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
773                view=self)
774        except IOError:
775            self.flash(
776                _('Your image file is corrupted. '
777                  'Please replace.'), type='danger')
778            return self.redirect(self.url(self.context))
779        self.response.setHeader(
780            'Content-Type', 'application/pdf')
781        return pdfstream
782
783def handle_img_upload(upload, context, view):
784    """Handle upload of applicant image.
785
786    Returns `True` in case of success or `False`.
787
788    Please note that file pointer passed in (`upload`) most probably
789    points to end of file when leaving this function.
790    """
791    size = file_size(upload)
792    if size > MAX_UPLOAD_SIZE:
793        view.flash(_('Uploaded image is too big!'), type='danger')
794        return False
795    dummy, ext = os.path.splitext(upload.filename)
796    ext.lower()
797    if ext != '.jpg':
798        view.flash(_('jpg file extension expected.'), type='danger')
799        return False
800    upload.seek(0) # file pointer moved when determining size
801    store = getUtility(IExtFileStore)
802    file_id = IFileStoreNameChooser(context).chooseName()
803    store.createFile(file_id, upload)
804    return True
805
806class ApplicantManageFormPage(KofaEditFormPage):
807    """A full edit view for applicant data.
808    """
809    grok.context(IApplicant)
810    grok.name('manage')
811    grok.require('waeup.manageApplication')
812    grok.template('applicanteditpage')
813    manage_applications = True
814    pnav = 3
815    display_actions = [[_('Save'), _('Finally Submit')],
816        [_('Add online payment ticket'),_('Remove selected tickets')]]
817
818    @property
819    def form_fields(self):
820        if self.context.special:
821            form_fields = grok.AutoFields(ISpecialApplicant)
822            form_fields['applicant_id'].for_display = True
823        else:
824            form_fields = grok.AutoFields(IApplicant)
825            form_fields['student_id'].for_display = True
826            form_fields['applicant_id'].for_display = True
827        return form_fields
828
829    @property
830    def target(self):
831        return getattr(self.context.__parent__, 'prefix', None)
832
833    @property
834    def separators(self):
835        return getUtility(IApplicantsUtils).SEPARATORS_DICT
836
837    @property
838    def custom_upload_requirements(self):
839        return ''
840
841    def update(self):
842        super(ApplicantManageFormPage, self).update()
843        self.wf_info = IWorkflowInfo(self.context)
844        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
845        self.upload_success = None
846        upload = self.request.form.get('form.passport', None)
847        if upload:
848            if self.custom_upload_requirements:
849                self.flash(
850                    self.custom_upload_requirements,
851                    type='danger')
852                self.redirect(self.url(self.context))
853                return
854            # We got a fresh upload, upload_success is
855            # either True or False
856            self.upload_success = handle_img_upload(
857                upload, self.context, self)
858            if self.upload_success:
859                self.context.writeLogMessage(self, 'saved: passport')
860        return
861
862    @property
863    def label(self):
864        container_title = self.context.__parent__.title
865        return _('${a} <br /> Application Form ${b}', mapping = {
866            'a':container_title, 'b':self.context.application_number})
867
868    def getTransitions(self):
869        """Return a list of dicts of allowed transition ids and titles.
870
871        Each list entry provides keys ``name`` and ``title`` for
872        internal name and (human readable) title of a single
873        transition.
874        """
875        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
876            if not t[0] in ('pay', 'create')]
877        return [dict(name='', title=_('No transition'))] +[
878            dict(name=x, title=y) for x, y in allowed_transitions]
879
880    @action(_('Save'), style='primary')
881    def save(self, **data):
882        form = self.request.form
883        password = form.get('password', None)
884        password_ctl = form.get('control_password', None)
885        if password:
886            validator = getUtility(IPasswordValidator)
887            errors = validator.validate_password(password, password_ctl)
888            if errors:
889                self.flash( ' '.join(errors), type='danger')
890                return
891        if self.upload_success is False:  # False is not None!
892            # Error during image upload. Ignore other values.
893            return
894        changed_fields = self.applyData(self.context, **data)
895        # Turn list of lists into single list
896        if changed_fields:
897            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
898        else:
899            changed_fields = []
900        if password:
901            # Now we know that the form has no errors and can set password ...
902            IUserAccount(self.context).setPassword(password)
903            changed_fields.append('password')
904        fields_string = ' + '.join(changed_fields)
905        trans_id = form.get('transition', None)
906        if trans_id:
907            self.wf_info.fireTransition(trans_id)
908        self.flash(_('Form has been saved.'))
909        if fields_string:
910            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
911        return
912
913    def unremovable(self, ticket):
914        return False
915
916    # This method is also used by the ApplicantEditFormPage
917    def delPaymentTickets(self, **data):
918        form = self.request.form
919        if 'val_id' in form:
920            child_id = form['val_id']
921        else:
922            self.flash(_('No payment selected.'), type='warning')
923            self.redirect(self.url(self.context))
924            return
925        if not isinstance(child_id, list):
926            child_id = [child_id]
927        deleted = []
928        for id in child_id:
929            # Applicants are not allowed to remove used payment tickets
930            if not self.unremovable(self.context[id]):
931                try:
932                    del self.context[id]
933                    deleted.append(id)
934                except:
935                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
936                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
937        if len(deleted):
938            self.flash(_('Successfully removed: ${a}',
939                mapping = {'a':', '.join(deleted)}))
940            self.context.writeLogMessage(
941                self, 'removed: % s' % ', '.join(deleted))
942        return
943
944    # We explicitely want the forms to be validated before payment tickets
945    # can be created. If no validation is requested, use
946    # 'validator=NullValidator' in the action directive
947    @action(_('Add online payment ticket'), style='primary')
948    def addPaymentTicket(self, **data):
949        self.redirect(self.url(self.context, '@@addafp'))
950        return
951
952    @jsaction(_('Remove selected tickets'))
953    def removePaymentTickets(self, **data):
954        self.delPaymentTickets(**data)
955        self.redirect(self.url(self.context) + '/@@manage')
956        return
957
958    # Not used in base package
959    def file_exists(self, attr):
960        file = getUtility(IExtFileStore).getFileByContext(
961            self.context, attr=attr)
962        if file:
963            return True
964        else:
965            return False
966
967class ApplicantEditFormPage(ApplicantManageFormPage):
968    """An applicant-centered edit view for applicant data.
969    """
970    grok.context(IApplicantEdit)
971    grok.name('edit')
972    grok.require('waeup.handleApplication')
973    grok.template('applicanteditpage')
974    manage_applications = False
975    submit_state = PAID
976
977    @property
978    def form_fields(self):
979        if self.context.special:
980            form_fields = grok.AutoFields(ISpecialApplicant).omit(
981                'locked', 'suspended')
982            form_fields['applicant_id'].for_display = True
983        else:
984            form_fields = grok.AutoFields(IApplicantEdit).omit(
985                'locked', 'course_admitted', 'student_id',
986                'suspended'
987                )
988            form_fields['applicant_id'].for_display = True
989            form_fields['reg_number'].for_display = True
990        return form_fields
991
992    @property
993    def display_actions(self):
994        state = IWorkflowState(self.context).getState()
995        actions = [[],[]]
996        if state == STARTED:
997            actions = [[_('Save')],
998                [_('Add online payment ticket'),_('Remove selected tickets')]]
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        state = IWorkflowState(self.context).getState()
1009        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
1010
1011    def emit_lock_message(self):
1012        self.flash(_('The requested form is locked (read-only).'),
1013                   type='warning')
1014        self.redirect(self.url(self.context))
1015        return
1016
1017    def update(self):
1018        if self.context.locked or (
1019            self.context.__parent__.expired and
1020            self.context.__parent__.strict_deadline):
1021            self.emit_lock_message()
1022            return
1023        super(ApplicantEditFormPage, self).update()
1024        return
1025
1026    def dataNotComplete(self):
1027        store = getUtility(IExtFileStore)
1028        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1029            return _('No passport picture uploaded.')
1030        if not self.request.form.get('confirm_passport', False):
1031            return _('Passport picture confirmation box not ticked.')
1032        return False
1033
1034    # We explicitely want the forms to be validated before payment tickets
1035    # can be created. If no validation is requested, use
1036    # 'validator=NullValidator' in the action directive
1037    @action(_('Add online payment ticket'), style='primary')
1038    def addPaymentTicket(self, **data):
1039        self.redirect(self.url(self.context, '@@addafp'))
1040        return
1041
1042    @jsaction(_('Remove selected tickets'))
1043    def removePaymentTickets(self, **data):
1044        self.delPaymentTickets(**data)
1045        self.redirect(self.url(self.context) + '/@@edit')
1046        return
1047
1048    @action(_('Save'), style='primary')
1049    def save(self, **data):
1050        if self.upload_success is False:  # False is not None!
1051            # Error during image upload. Ignore other values.
1052            return
1053        if data.get('course1', 1) == data.get('course2', 2):
1054            self.flash(_('1st and 2nd choice must be different.'),
1055                       type='warning')
1056            return
1057        self.applyData(self.context, **data)
1058        self.flash(_('Form has been saved.'))
1059        return
1060
1061    @action(_('Finally Submit'), warning=WARNING)
1062    def finalsubmit(self, **data):
1063        if self.upload_success is False:  # False is not None!
1064            return # error during image upload. Ignore other values
1065        if self.dataNotComplete():
1066            self.flash(self.dataNotComplete(), type='danger')
1067            return
1068        self.applyData(self.context, **data)
1069        state = IWorkflowState(self.context).getState()
1070        # This shouldn't happen, but the application officer
1071        # might have forgotten to lock the form after changing the state
1072        if state != self.submit_state:
1073            self.flash(_('The form cannot be submitted. Wrong state!'),
1074                       type='danger')
1075            return
1076        IWorkflowInfo(self.context).fireTransition('submit')
1077        # application_date is used in export files for sorting.
1078        # We can thus store utc.
1079        self.context.application_date = datetime.utcnow()
1080        self.flash(_('Form has been submitted.'))
1081        self.redirect(self.url(self.context))
1082        return
1083
1084class PassportImage(grok.View):
1085    """Renders the passport image for applicants.
1086    """
1087    grok.name('passport.jpg')
1088    grok.context(IApplicant)
1089    grok.require('waeup.viewApplication')
1090
1091    def render(self):
1092        # A filename chooser turns a context into a filename suitable
1093        # for file storage.
1094        image = getUtility(IExtFileStore).getFileByContext(self.context)
1095        self.response.setHeader(
1096            'Content-Type', 'image/jpeg')
1097        if image is None:
1098            # show placeholder image
1099            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1100        return image
1101
1102class ApplicantRegistrationPage(KofaAddFormPage):
1103    """Captcha'd registration page for applicants.
1104    """
1105    grok.context(IApplicantsContainer)
1106    grok.name('register')
1107    grok.require('waeup.Anonymous')
1108    grok.template('applicantregister')
1109
1110    @property
1111    def form_fields(self):
1112        form_fields = None
1113        if self.context.mode == 'update':
1114            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1115                'lastname','reg_number','email')
1116        else: #if self.context.mode == 'create':
1117            form_fields = grok.AutoFields(IApplicantEdit).select(
1118                'firstname', 'middlename', 'lastname', 'email', 'phone')
1119        return form_fields
1120
1121    @property
1122    def label(self):
1123        return _('Apply for ${a}',
1124            mapping = {'a':self.context.title})
1125
1126    def update(self):
1127        if self.context.expired:
1128            self.flash(_('Outside application period.'), type='warning')
1129            self.redirect(self.url(self.context))
1130            return
1131        # Handle captcha
1132        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1133        self.captcha_result = self.captcha.verify(self.request)
1134        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1135        return
1136
1137    def _redirect(self, email, password, applicant_id):
1138        # Forward only email to landing page in base package.
1139        self.redirect(self.url(self.context, 'registration_complete',
1140            data = dict(email=email)))
1141        return
1142
1143    @action(_('Send login credentials to email address'), style='primary')
1144    def register(self, **data):
1145        if not self.captcha_result.is_valid:
1146            # Captcha will display error messages automatically.
1147            # No need to flash something.
1148            return
1149        if self.context.mode == 'create':
1150            # Add applicant
1151            applicant = createObject(u'waeup.Applicant')
1152            self.applyData(applicant, **data)
1153            self.context.addApplicant(applicant)
1154            applicant.reg_number = applicant.applicant_id
1155            notify(grok.ObjectModifiedEvent(applicant))
1156        elif self.context.mode == 'update':
1157            # Update applicant
1158            reg_number = data.get('reg_number','')
1159            lastname = data.get('lastname','')
1160            cat = getUtility(ICatalog, name='applicants_catalog')
1161            results = list(
1162                cat.searchResults(reg_number=(reg_number, reg_number)))
1163            if results:
1164                applicant = results[0]
1165                if getattr(applicant,'lastname',None) is None:
1166                    self.flash(_('An error occurred.'), type='danger')
1167                    return
1168                elif applicant.lastname.lower() != lastname.lower():
1169                    # Don't tell the truth here. Anonymous must not
1170                    # know that a record was found and only the lastname
1171                    # verification failed.
1172                    self.flash(_('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.