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

Last change on this file since 16092 was 16074, checked in by Henrik Bettermann, 5 years ago

Only enable customization.

  • Property svn:keywords set to Id
File size: 71.6 KB
Line 
1## $Id: browser.py 16074 2020-04-26 09:21:39Z 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
23import transaction
24from cgi import escape
25from urllib import urlencode
26from datetime import datetime, date
27from time import time, sleep
28from zope.event import notify
29from zope.component import getUtility, queryUtility, createObject, getAdapter
30from zope.catalog.interfaces import ICatalog
31from zope.i18n import translate
32from zope.security import checkPermission
33from hurry.workflow.interfaces import (
34    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
35from reportlab.platypus.doctemplate import LayoutError
36from waeup.kofa.mandates.mandate import RefereeReportMandate
37from waeup.kofa.applicants.interfaces import (
38    IApplicant, IApplicantEdit, IApplicantsRoot,
39    IApplicantsContainer, IApplicantsContainerAdd,
40    IApplicantOnlinePayment, IApplicantsUtils,
41    IApplicantRegisterUpdate, ISpecialApplicant,
42    IApplicantRefereeReport
43    )
44from waeup.kofa.utils.helpers import html2dict
45from waeup.kofa.applicants.container import (
46    ApplicantsContainer, VirtualApplicantsExportJobContainer)
47from waeup.kofa.applicants.applicant import search
48from waeup.kofa.applicants.workflow import (
49    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED)
50from waeup.kofa.browser import (
51#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
52    DEFAULT_PASSPORT_IMAGE_PATH)
53from waeup.kofa.browser.layout import (
54    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
55from waeup.kofa.browser.interfaces import ICaptchaManager
56from waeup.kofa.browser.breadcrumbs import Breadcrumb
57from waeup.kofa.browser.layout import (
58    NullValidator, jsaction, action, UtilityView)
59from waeup.kofa.browser.pages import (
60    add_local_role, del_local_roles, doll_up, ExportCSVView)
61from waeup.kofa.interfaces import (
62    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF, DOCLINK,
63    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
64from waeup.kofa.interfaces import MessageFactory as _
65from waeup.kofa.permissions import get_users_with_local_roles
66from waeup.kofa.students.interfaces import IStudentsUtils
67from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
68from waeup.kofa.widgets.datewidget import (
69    FriendlyDateDisplayWidget,
70    FriendlyDatetimeDisplayWidget)
71
72grok.context(IKofaObject) # Make IKofaObject the default context
73
74WARNING = _('You can not edit your application records after final submission.'
75            ' You really want to submit?')
76
77class ApplicantsRootPage(KofaDisplayFormPage):
78    grok.context(IApplicantsRoot)
79    grok.name('index')
80    grok.require('waeup.Public')
81    form_fields = grok.AutoFields(IApplicantsRoot)
82    label = _('Applicants Section')
83    pnav = 3
84
85    def update(self):
86        super(ApplicantsRootPage, self).update()
87        return
88
89    @property
90    def introduction(self):
91        # Here we know that the cookie has been set
92        lang = self.request.cookies.get('kofa.language')
93        html = self.context.description_dict.get(lang,'')
94        if html == '':
95            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
96            html = self.context.description_dict.get(portal_language,'')
97        return html
98
99    @property
100    def containers(self):
101        if self.layout.isAuthenticated():
102            return self.context.values()
103        values = sorted([container for container in self.context.values()
104                         if not container.hidden and container.enddate],
105                        key=lambda value: value.enddate, reverse=True)
106        return values
107
108class ApplicantsSearchPage(KofaPage):
109    grok.context(IApplicantsRoot)
110    grok.name('search')
111    grok.require('waeup.viewApplication')
112    label = _('Find applicants')
113    search_button = _('Find applicant')
114    pnav = 3
115
116    def update(self, *args, **kw):
117        form = self.request.form
118        self.results = []
119        if 'searchterm' in form and form['searchterm']:
120            self.searchterm = form['searchterm']
121            self.searchtype = form['searchtype']
122        elif 'old_searchterm' in form:
123            self.searchterm = form['old_searchterm']
124            self.searchtype = form['old_searchtype']
125        else:
126            if 'search' in form:
127                self.flash(_('Empty search string'), type='warning')
128            return
129        self.results = search(query=self.searchterm,
130            searchtype=self.searchtype, view=self)
131        if not self.results:
132            self.flash(_('No applicant found.'), type='warning')
133        return
134
135class ApplicantsRootManageFormPage(KofaEditFormPage):
136    grok.context(IApplicantsRoot)
137    grok.name('manage')
138    grok.template('applicantsrootmanagepage')
139    form_fields = grok.AutoFields(IApplicantsRoot)
140    label = _('Manage applicants section')
141    pnav = 3
142    grok.require('waeup.manageApplication')
143    taboneactions = [_('Save')]
144    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
145    tabthreeactions1 = [_('Remove selected local roles')]
146    tabthreeactions2 = [_('Add local role')]
147    subunits = _('Applicants Containers')
148    doclink = DOCLINK + '/applicants.html'
149
150    def getLocalRoles(self):
151        roles = ILocalRolesAssignable(self.context)
152        return roles()
153
154    def getUsers(self):
155        return getUtility(IKofaUtils).getUsers()
156
157    #def getUsers(self):
158    #    """Get a list of all users.
159    #    """
160    #    for key, val in grok.getSite()['users'].items():
161    #        url = self.url(val)
162    #        yield(dict(url=url, name=key, val=val))
163
164    def getUsersWithLocalRoles(self):
165        return get_users_with_local_roles(self.context)
166
167    @jsaction(_('Remove selected'))
168    def delApplicantsContainers(self, **data):
169        form = self.request.form
170        if 'val_id' in form:
171            child_id = form['val_id']
172        else:
173            self.flash(_('No container selected!'), type='warning')
174            self.redirect(self.url(self.context, '@@manage')+'#tab2')
175            return
176        if not isinstance(child_id, list):
177            child_id = [child_id]
178        deleted = []
179        for id in child_id:
180            try:
181                del self.context[id]
182                deleted.append(id)
183            except:
184                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
185                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
186        if len(deleted):
187            self.flash(_('Successfully removed: ${a}',
188                mapping = {'a':', '.join(deleted)}))
189        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
190        self.context.logger.info(
191            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
192        self.redirect(self.url(self.context, '@@manage')+'#tab2')
193        return
194
195    @action(_('Add applicants container'), validator=NullValidator)
196    def addApplicantsContainer(self, **data):
197        self.redirect(self.url(self.context, '@@add'))
198        return
199
200    @action(_('Add local role'), validator=NullValidator)
201    def addLocalRole(self, **data):
202        return add_local_role(self,3, **data)
203
204    @action(_('Remove selected local roles'))
205    def delLocalRoles(self, **data):
206        return del_local_roles(self,3,**data)
207
208    @action(_('Save'), style='primary')
209    def save(self, **data):
210        self.applyData(self.context, **data)
211        description = getattr(self.context, 'description', None)
212        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
213        self.context.description_dict = html2dict(description, portal_language)
214        self.flash(_('Form has been saved.'))
215        return
216
217class ApplicantsContainerAddFormPage(KofaAddFormPage):
218    grok.context(IApplicantsRoot)
219    grok.require('waeup.manageApplication')
220    grok.name('add')
221    grok.template('applicantscontaineraddpage')
222    label = _('Add applicants container')
223    pnav = 3
224
225    form_fields = grok.AutoFields(
226        IApplicantsContainerAdd).omit('code').omit('title')
227
228    @action(_('Add applicants container'))
229    def addApplicantsContainer(self, **data):
230        year = data['year']
231        if not data['container_number']:
232            code = u'%s%s' % (data['prefix'], year)
233        else:
234            code = u'%s%s' % (data['prefix'], data['container_number'])
235        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
236        title = apptypes_dict[data['prefix']][0]
237        title = u'%s %s/%s' % (title, year, year + 1)
238        if code in self.context.keys():
239            self.flash(
240              _('An applicants container for the same application '
241                'type and entrance year exists already in the database.'),
242                type='warning')
243            return
244        # Add new applicants container...
245        container = createObject(u'waeup.ApplicantsContainer')
246        self.applyData(container, **data)
247        container.code = code
248        container.title = title
249        self.context[code] = container
250        self.flash(_('Added:') + ' "%s".' % code)
251        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
252        self.context.logger.info('%s - added: %s' % (ob_class, code))
253        self.redirect(self.url(self.context, u'@@manage'))
254        return
255
256    @action(_('Cancel'), validator=NullValidator)
257    def cancel(self, **data):
258        self.redirect(self.url(self.context, '@@manage'))
259
260class ApplicantsRootBreadcrumb(Breadcrumb):
261    """A breadcrumb for applicantsroot.
262    """
263    grok.context(IApplicantsRoot)
264    title = _(u'Applicants')
265
266class ApplicantsContainerBreadcrumb(Breadcrumb):
267    """A breadcrumb for applicantscontainers.
268    """
269    grok.context(IApplicantsContainer)
270
271
272class ApplicantsExportsBreadcrumb(Breadcrumb):
273    """A breadcrumb for exports.
274    """
275    grok.context(VirtualApplicantsExportJobContainer)
276    title = _(u'Applicant Data Exports')
277    target = None
278
279class ApplicantBreadcrumb(Breadcrumb):
280    """A breadcrumb for applicants.
281    """
282    grok.context(IApplicant)
283
284    @property
285    def title(self):
286        """Get a title for a context.
287        """
288        return self.context.application_number
289
290class OnlinePaymentBreadcrumb(Breadcrumb):
291    """A breadcrumb for payments.
292    """
293    grok.context(IApplicantOnlinePayment)
294
295    @property
296    def title(self):
297        return self.context.p_id
298
299class RefereeReportBreadcrumb(Breadcrumb):
300    """A breadcrumb for referee reports.
301    """
302    grok.context(IApplicantRefereeReport)
303
304    @property
305    def title(self):
306        return self.context.r_id
307
308class ApplicantsStatisticsPage(KofaDisplayFormPage):
309    """Some statistics about applicants in a container.
310    """
311    grok.context(IApplicantsContainer)
312    grok.name('statistics')
313    grok.require('waeup.viewApplicationStatistics')
314    grok.template('applicantcontainerstatistics')
315
316    @property
317    def label(self):
318        return "%s" % self.context.title
319
320class ApplicantsContainerPage(KofaDisplayFormPage):
321    """The standard view for regular applicant containers.
322    """
323    grok.context(IApplicantsContainer)
324    grok.name('index')
325    grok.require('waeup.Public')
326    grok.template('applicantscontainerpage')
327    pnav = 3
328
329    @property
330    def form_fields(self):
331        form_fields = grok.AutoFields(IApplicantsContainer).omit(
332            'title', 'description')
333        form_fields[
334            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
335        form_fields[
336            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
337        if self.request.principal.id == 'zope.anybody':
338            form_fields = form_fields.omit(
339                'code', 'prefix', 'year', 'mode', 'hidden',
340                'strict_deadline', 'application_category',
341                'application_slip_notice', 'with_picture')
342        return form_fields
343
344    @property
345    def introduction(self):
346        # Here we know that the cookie has been set
347        lang = self.request.cookies.get('kofa.language')
348        html = self.context.description_dict.get(lang,'')
349        if html == '':
350            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
351            html = self.context.description_dict.get(portal_language,'')
352        return html
353
354    @property
355    def label(self):
356        return "%s" % self.context.title
357
358class ApplicantsContainerManageFormPage(KofaEditFormPage):
359    grok.context(IApplicantsContainer)
360    grok.name('manage')
361    grok.template('applicantscontainermanagepage')
362    form_fields = grok.AutoFields(IApplicantsContainer)
363    taboneactions = [_('Save'),_('Cancel')]
364    tabtwoactions = [_('Remove selected'),_('Cancel'),
365        _('Create students from selected')]
366    tabthreeactions1 = [_('Remove selected local roles')]
367    tabthreeactions2 = [_('Add local role')]
368    # Use friendlier date widget...
369    grok.require('waeup.manageApplication')
370    doclink = DOCLINK + '/applicants.html'
371
372    @property
373    def label(self):
374        return _('Manage applicants container')
375
376    pnav = 3
377
378    @property
379    def showApplicants(self):
380        if self.context.counts[1] < 1000:
381            return True
382        return False
383
384    def getLocalRoles(self):
385        roles = ILocalRolesAssignable(self.context)
386        return roles()
387
388    #def getUsers(self):
389    #    """Get a list of all users.
390    #    """
391    #    for key, val in grok.getSite()['users'].items():
392    #        url = self.url(val)
393    #        yield(dict(url=url, name=key, val=val))
394
395    def getUsers(self):
396        return getUtility(IKofaUtils).getUsers()
397
398    def getUsersWithLocalRoles(self):
399        return get_users_with_local_roles(self.context)
400
401    @action(_('Save'), style='primary')
402    def save(self, **data):
403        changed_fields = self.applyData(self.context, **data)
404        if changed_fields:
405            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
406        else:
407            changed_fields = []
408        description = getattr(self.context, 'description', None)
409        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
410        self.context.description_dict = html2dict(description, portal_language)
411        self.flash(_('Form has been saved.'))
412        fields_string = ' + '.join(changed_fields)
413        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
414        return
415
416    @jsaction(_('Remove selected'))
417    def delApplicant(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        deleted = []
428        for id in child_id:
429            try:
430                del self.context[id]
431                deleted.append(id)
432            except:
433                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
434                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
435        if len(deleted):
436            self.flash(_('Successfully removed: ${a}',
437                mapping = {'a':', '.join(deleted)}))
438        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
439        return
440
441    @action(_('Create students from selected'))
442    def createStudents(self, **data):
443        if not checkPermission('waeup.createStudents', self.context):
444            self.flash(
445                _('You don\'t have permission to create student records.'),
446                type='warning')
447            self.redirect(self.url(self.context, '@@manage')+'#tab2')
448            return
449        form = self.request.form
450        if 'val_id' in form:
451            child_id = form['val_id']
452        else:
453            self.flash(_('No applicant selected!'), type='warning')
454            self.redirect(self.url(self.context, '@@manage')+'#tab2')
455            return
456        if not isinstance(child_id, list):
457            child_id = [child_id]
458        created = []
459        if len(child_id) > 10 and self.request.principal.id != 'admin':
460            self.flash(_('A maximum of 10 applicants can be selected!'),
461                       type='warning')
462            self.redirect(self.url(self.context, '@@manage')+'#tab2')
463            return
464        for id in child_id:
465            success, msg = self.context[id].createStudent(view=self)
466            if success:
467                created.append(id)
468        if len(created):
469            self.flash(_('${a} students successfully created.',
470                mapping = {'a': len(created)}))
471        else:
472            self.flash(_('No student could be created.'), type='warning')
473        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
474        return
475
476    @action(_('Cancel'), validator=NullValidator)
477    def cancel(self, **data):
478        self.redirect(self.url(self.context))
479        return
480
481    @action(_('Add local role'), validator=NullValidator)
482    def addLocalRole(self, **data):
483        return add_local_role(self,3, **data)
484
485    @action(_('Remove selected local roles'))
486    def delLocalRoles(self, **data):
487        return del_local_roles(self,3,**data)
488
489class ApplicantAddFormPage(KofaAddFormPage):
490    """Add-form to add an applicant.
491    """
492    grok.context(IApplicantsContainer)
493    grok.require('waeup.manageApplication')
494    grok.name('addapplicant')
495    #grok.template('applicantaddpage')
496    form_fields = grok.AutoFields(IApplicant).select(
497        'firstname', 'middlename', 'lastname',
498        'email', 'phone')
499    label = _('Add applicant')
500    pnav = 3
501    doclink = DOCLINK + '/applicants.html'
502
503    @action(_('Create application record'))
504    def addApplicant(self, **data):
505        applicant = createObject(u'waeup.Applicant')
506        self.applyData(applicant, **data)
507        self.context.addApplicant(applicant)
508        self.flash(_('Application record created.'))
509        self.redirect(
510            self.url(self.context[applicant.application_number], 'index'))
511        return
512
513class ApplicantsContainerPrefillFormPage(KofaAddFormPage):
514    """Form to pre-fill applicants containers.
515    """
516    grok.context(IApplicantsContainer)
517    grok.require('waeup.manageApplication')
518    grok.name('prefill')
519    grok.template('prefillcontainer')
520    label = _('Pre-fill container')
521    pnav = 3
522    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
523
524    def update(self):
525        if self.context.mode == 'update':
526            self.flash(_('Container must be in create mode to be pre-filled.'),
527                type='danger')
528            self.redirect(self.url(self.context))
529            return
530        super(ApplicantsContainerPrefillFormPage, self).update()
531        return
532
533    @action(_('Pre-fill now'), style='primary')
534    def addApplicants(self):
535        form = self.request.form
536        if 'number' in form and form['number']:
537            number = int(form['number'])
538        for i in range(number):
539            applicant = createObject(u'waeup.Applicant')
540            self.context.addApplicant(applicant)
541        self.flash(_('%s application records created.' % number))
542        self.context.writeLogMessage(self, '%s applicants created' % (number))
543        self.redirect(self.url(self.context, 'index'))
544        return
545
546    @action(_('Cancel'), validator=NullValidator)
547    def cancel(self, **data):
548        self.redirect(self.url(self.context))
549        return
550
551class ApplicantsContainerPurgeFormPage(KofaEditFormPage):
552    """Form to purge applicants containers.
553    """
554    grok.context(IApplicantsContainer)
555    grok.require('waeup.manageApplication')
556    grok.name('purge')
557    grok.template('purgecontainer')
558    label = _('Purge container')
559    pnav = 3
560    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
561
562    @action(_('Remove initialized records'),
563              tooltip=_('Don\'t use if application is in progress!'),
564              warning=_('Are you really sure?'),
565              style='primary')
566    def purgeInitialized(self):
567        form = self.request.form
568        purged = 0
569        keys = [key for key in self.context.keys()]
570        for key in keys:
571            if self.context[key].state == 'initialized':
572                del self.context[key]
573                purged += 1
574        self.flash(_('%s application records purged.' % purged))
575        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
576        self.redirect(self.url(self.context, 'index'))
577        return
578
579    @action(_('Cancel'), validator=NullValidator)
580    def cancel(self, **data):
581        self.redirect(self.url(self.context))
582        return
583
584class ApplicantDisplayFormPage(KofaDisplayFormPage):
585    """A display view for applicant data.
586    """
587    grok.context(IApplicant)
588    grok.name('index')
589    grok.require('waeup.viewApplication')
590    grok.template('applicantdisplaypage')
591    label = _('Applicant')
592    pnav = 3
593    hide_hint = False
594
595    @property
596    def display_refereereports(self):
597        if self.context.refereereports:
598            return True
599        return False
600
601    @property
602    def file_links(self):
603        html = ''
604        file_store = getUtility(IExtFileStore)
605        additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
606        for filename in additional_files:
607            pdf = getUtility(IExtFileStore).getFileByContext(
608                self.context, attr=filename[1])
609            if pdf:
610                html += '<a href="%s">%s</a>, ' % (self.url(
611                    self.context, filename[1]), filename[0])
612        html = html.strip(', ')
613        return html
614
615    @property
616    def display_payments(self):
617        if self.context.payments:
618            return True
619        if self.context.special:
620            return True
621        return getattr(self.context.__parent__, 'application_fee', None)
622
623    @property
624    def form_fields(self):
625        if self.context.special:
626            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
627        else:
628            form_fields = grok.AutoFields(IApplicant).omit(
629                'locked', 'course_admitted', 'password', 'suspended')
630        return form_fields
631
632    @property
633    def target(self):
634        return getattr(self.context.__parent__, 'prefix', None)
635
636    @property
637    def separators(self):
638        return getUtility(IApplicantsUtils).SEPARATORS_DICT
639
640    def update(self):
641        self.passport_url = self.url(self.context, 'passport.jpg')
642        # Mark application as started if applicant logs in for the first time
643        usertype = getattr(self.request.principal, 'user_type', None)
644        if usertype == 'applicant' and \
645            IWorkflowState(self.context).getState() == INITIALIZED:
646            IWorkflowInfo(self.context).fireTransition('start')
647        if usertype == 'applicant' and self.context.state == 'created':
648            session = '%s/%s' % (self.context.__parent__.year,
649                                 self.context.__parent__.year+1)
650            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
651            msg = _(
652                '\n <strong>Congratulations!</strong>' +
653                ' You have been offered provisional admission into the' +
654                ' ${c} Academic Session of ${d}.'
655                ' Your student record has been created for you.' +
656                ' Please, logout again and proceed to the' +
657                ' login page of the portal.'
658                ' Then enter your new student credentials:' +
659                ' user name= ${a}, password = ${b}.' +
660                ' Change your password when you have logged in.',
661                mapping = {
662                    'a':self.context.student_id,
663                    'b':self.context.application_number,
664                    'c':session,
665                    'd':title}
666                )
667            self.flash(msg)
668        return
669
670    @property
671    def hasPassword(self):
672        if self.context.password:
673            return _('set')
674        return _('unset')
675
676    @property
677    def label(self):
678        container_title = self.context.__parent__.title
679        return _('${a} <br /> Application Record ${b}', mapping = {
680            'a':container_title, 'b':self.context.application_number})
681
682    def getCourseAdmitted(self):
683        """Return link, title and code in html format to the certificate
684           admitted.
685        """
686        course_admitted = self.context.course_admitted
687        if getattr(course_admitted, '__parent__',None):
688            url = self.url(course_admitted)
689            title = course_admitted.title
690            code = course_admitted.code
691            return '<a href="%s">%s - %s</a>' %(url,code,title)
692        return ''
693
694class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
695    grok.context(IApplicant)
696    grok.name('base')
697
698    @property
699    def form_fields(self):
700        form_fields = grok.AutoFields(IApplicant).select(
701            'applicant_id', 'reg_number', 'email', 'course1')
702        if self.context.__parent__.prefix in ('special',):
703            form_fields['reg_number'].field.title = u'Identification Number'
704            return form_fields
705        return form_fields
706
707class CreateStudentPage(UtilityView, grok.View):
708    """Create a student object from applicant data.
709    """
710    grok.context(IApplicant)
711    grok.name('createstudent')
712    grok.require('waeup.createStudents')
713
714    def update(self):
715        success, msg = self.context.createStudent(view=self)
716        if success:
717            self.flash(msg)
718        else:
719            self.flash(msg, type='warning')
720        self.redirect(self.url(self.context))
721        return
722
723    def render(self):
724        return
725
726class CreateAllStudentsPage(KofaPage):
727    """Create all student objects from applicant data
728    in the root container or in a specific applicants container only.
729    Only PortalManagers or StudentCreators can do this.
730    """
731    #grok.context(IApplicantsContainer)
732    grok.name('createallstudents')
733    grok.require('waeup.createStudents')
734    label = _('Student Record Creation Report')
735
736    def update(self):
737        grok.getSite()['configuration'].maintmode_enabled_by = u'admin'
738        transaction.commit()
739        # Wait 10 seconds for all transactions to be finished.
740        # Do not wait in tests.
741        if not self.request.principal.id == 'zope.mgr':
742            sleep(10)
743        cat = getUtility(ICatalog, name='applicants_catalog')
744        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
745        created = []
746        failed = []
747        container_only = False
748        applicants_root = grok.getSite()['applicants']
749        if isinstance(self.context, ApplicantsContainer):
750            container_only = True
751        for result in results:
752            if container_only and result.__parent__ is not self.context:
753                continue
754            success, msg = result.createStudent(view=self)
755            if success:
756                created.append(result.applicant_id)
757            else:
758                failed.append(
759                    (result.applicant_id, self.url(result, 'manage'), msg))
760                ob_class = self.__implemented__.__name__.replace(
761                    'waeup.kofa.','')
762        grok.getSite()['configuration'].maintmode_enabled_by = None
763        self.successful = ', '.join(created)
764        self.failed = failed
765        return
766
767
768class ApplicationFeePaymentAddPage(UtilityView, grok.View):
769    """ Page to add an online payment ticket
770    """
771    grok.context(IApplicant)
772    grok.name('addafp')
773    grok.require('waeup.payApplicant')
774    factory = u'waeup.ApplicantOnlinePayment'
775
776    @property
777    def custom_requirements(self):
778        return ''
779
780    def update(self):
781        # Additional requirements in custom packages.
782        if self.custom_requirements:
783            self.flash(
784                self.custom_requirements,
785                type='danger')
786            self.redirect(self.url(self.context))
787            return
788        if not self.context.special:
789            for ticket in self.context.payments:
790                if ticket.p_state == 'paid':
791                      self.flash(
792                          _('This type of payment has already been made.'),
793                          type='warning')
794                      self.redirect(self.url(self.context))
795                      return
796        applicants_utils = getUtility(IApplicantsUtils)
797        container = self.context.__parent__
798        payment = createObject(self.factory)
799        failure = applicants_utils.setPaymentDetails(
800            container, payment, self.context)
801        if failure is not None:
802            self.flash(failure, type='danger')
803            self.redirect(self.url(self.context))
804            return
805        self.context[payment.p_id] = payment
806        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
807        self.flash(_('Payment ticket created.'))
808        self.redirect(self.url(payment))
809        return
810
811    def render(self):
812        return
813
814
815class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
816    """ Page to view an online payment ticket
817    """
818    grok.context(IApplicantOnlinePayment)
819    grok.name('index')
820    grok.require('waeup.viewApplication')
821    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
822    form_fields[
823        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
824    form_fields[
825        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
826    pnav = 3
827
828    @property
829    def label(self):
830        return _('${a}: Online Payment Ticket ${b}', mapping = {
831            'a':self.context.__parent__.display_fullname,
832            'b':self.context.p_id})
833
834class OnlinePaymentApprovePage(UtilityView, grok.View):
835    """ Approval view
836    """
837    grok.context(IApplicantOnlinePayment)
838    grok.name('approve')
839    grok.require('waeup.managePortal')
840
841    def update(self):
842        flashtype, msg, log = self.context.approveApplicantPayment()
843        if log is not None:
844            applicant = self.context.__parent__
845            # Add log message to applicants.log
846            applicant.writeLogMessage(self, log)
847            # Add log message to payments.log
848            self.context.logger.info(
849                '%s,%s,%s,%s,%s,,,,,,' % (
850                applicant.applicant_id,
851                self.context.p_id, self.context.p_category,
852                self.context.amount_auth, self.context.r_code))
853        self.flash(msg, type=flashtype)
854        return
855
856    def render(self):
857        self.redirect(self.url(self.context, '@@index'))
858        return
859
860class ExportPDFPaymentSlipPage(UtilityView, grok.View):
861    """Deliver a PDF slip of the context.
862    """
863    grok.context(IApplicantOnlinePayment)
864    grok.name('payment_slip.pdf')
865    grok.require('waeup.viewApplication')
866    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
867    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
868    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
869    #prefix = 'form'
870    note = None
871
872    @property
873    def title(self):
874        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
875        return translate(_('Payment Data'), 'waeup.kofa',
876            target_language=portal_language)
877
878    @property
879    def label(self):
880        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
881        return translate(_('Online Payment Slip'),
882            'waeup.kofa', target_language=portal_language) \
883            + ' %s' % self.context.p_id
884
885    @property
886    def payment_slip_download_warning(self):
887        if self.context.__parent__.state not in (
888            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
889            return _('Please submit the application form before '
890                     'trying to download payment slips.')
891        return ''
892
893    def render(self):
894        if self.payment_slip_download_warning:
895            self.flash(self.payment_slip_download_warning, type='danger')
896            self.redirect(self.url(self.context))
897            return
898        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
899            self.request)
900        students_utils = getUtility(IStudentsUtils)
901        return students_utils.renderPDF(self,'payment_slip.pdf',
902            self.context.__parent__, applicantview, note=self.note)
903
904class ExportPDFPageApplicationSlip(UtilityView, grok.View):
905    """Deliver a PDF slip of the context.
906    """
907    grok.context(IApplicant)
908    grok.name('application_slip.pdf')
909    grok.require('waeup.viewApplication')
910    #prefix = 'form'
911
912    def update(self):
913        if self.context.state in ('initialized', 'started', 'paid'):
914            self.flash(
915                _('Please pay and submit before trying to download '
916                  'the application slip.'), type='warning')
917            return self.redirect(self.url(self.context))
918        return
919
920    def render(self):
921        try:
922            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
923                view=self)
924        except IOError:
925            self.flash(
926                _('Your image file is corrupted. '
927                  'Please replace.'), type='danger')
928            return self.redirect(self.url(self.context))
929        except LayoutError, err:
930            self.flash(
931                'PDF file could not be created. Reportlab error message: %s'
932                % escape(err.message),
933                type="danger")
934            return self.redirect(self.url(self.context))
935        self.response.setHeader(
936            'Content-Type', 'application/pdf')
937        return pdfstream
938
939def handle_img_upload(upload, context, view):
940    """Handle upload of applicant image.
941
942    Returns `True` in case of success or `False`.
943
944    Please note that file pointer passed in (`upload`) most probably
945    points to end of file when leaving this function.
946    """
947    max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
948    size = file_size(upload)
949    if size > max_upload_size:
950        view.flash(_('Uploaded image is too big!'), type='danger')
951        return False
952    dummy, ext = os.path.splitext(upload.filename)
953    ext.lower()
954    if ext != '.jpg':
955        view.flash(_('jpg file extension expected.'), type='danger')
956        return False
957    upload.seek(0) # file pointer moved when determining size
958    store = getUtility(IExtFileStore)
959    file_id = IFileStoreNameChooser(context).chooseName()
960    try:
961        store.createFile(file_id, upload)
962    except IOError:
963        view.flash(_('Image file cannot be changed.'), type='danger')
964        return False
965    return True
966
967def handle_file_upload(upload, context, view, attr=None):
968    """Handle upload of applicant files.
969
970    Returns `True` in case of success or `False`.
971
972    Please note that file pointer passed in (`upload`) most probably
973    points to end of file when leaving this function.
974    """
975    size = file_size(upload)
976    max_upload_size = 1024 * getUtility(IStudentsUtils).MAX_KB
977    if size > max_upload_size:
978        view.flash(_('Uploaded file is too big!'))
979        return False
980    dummy, ext = os.path.splitext(upload.filename)
981    ext.lower()
982    if ext != '.pdf':
983        view.flash(_('pdf file extension expected.'))
984        return False
985    upload.seek(0) # file pointer moved when determining size
986    store = getUtility(IExtFileStore)
987    file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
988    store.createFile(file_id, upload)
989    return True
990
991class ApplicantManageFormPage(KofaEditFormPage):
992    """A full edit view for applicant data.
993    """
994    grok.context(IApplicant)
995    grok.name('manage')
996    grok.require('waeup.manageApplication')
997    grok.template('applicanteditpage')
998    manage_applications = True
999    pnav = 3
1000    display_actions = [[_('Save'), _('Finally Submit')],
1001        [_('Add online payment ticket'),_('Remove selected tickets')]]
1002
1003    @property
1004    def display_payments(self):
1005        if self.context.payments:
1006            return True
1007        if self.context.special:
1008            return True
1009        return getattr(self.context.__parent__, 'application_fee', None)
1010
1011    @property
1012    def display_refereereports(self):
1013        if self.context.refereereports:
1014            return True
1015        return False
1016
1017    def display_fileupload(self, filename):
1018        """This method can be used in custom packages to avoid unneccessary
1019        file uploads.
1020        """
1021        return True
1022
1023    @property
1024    def form_fields(self):
1025        if self.context.special:
1026            form_fields = grok.AutoFields(ISpecialApplicant)
1027            form_fields['applicant_id'].for_display = True
1028        else:
1029            form_fields = grok.AutoFields(IApplicant)
1030            form_fields['student_id'].for_display = True
1031            form_fields['applicant_id'].for_display = True
1032        return form_fields
1033
1034    @property
1035    def target(self):
1036        return getattr(self.context.__parent__, 'prefix', None)
1037
1038    @property
1039    def separators(self):
1040        return getUtility(IApplicantsUtils).SEPARATORS_DICT
1041
1042    @property
1043    def custom_upload_requirements(self):
1044        return ''
1045
1046    def update(self):
1047        super(ApplicantManageFormPage, self).update()
1048        max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
1049        self.wf_info = IWorkflowInfo(self.context)
1050        self.max_upload_size = string_from_bytes(max_upload_size)
1051        self.upload_success = None
1052        upload = self.request.form.get('form.passport', None)
1053        if upload:
1054            if self.custom_upload_requirements:
1055                self.flash(
1056                    self.custom_upload_requirements,
1057                    type='danger')
1058                self.redirect(self.url(self.context))
1059                return
1060            # We got a fresh upload, upload_success is
1061            # either True or False
1062            self.upload_success = handle_img_upload(
1063                upload, self.context, self)
1064            if self.upload_success:
1065                self.context.writeLogMessage(self, 'saved: passport')
1066        file_store = getUtility(IExtFileStore)
1067        self.additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
1068        for filename in self.additional_files:
1069            upload = self.request.form.get(filename[1], None)
1070            if upload:
1071                # We got a fresh file upload
1072                success = handle_file_upload(
1073                    upload, self.context, self, attr=filename[1])
1074                if success:
1075                    self.context.writeLogMessage(
1076                        self, 'saved: %s' % filename[1])
1077                else:
1078                    self.upload_success = False
1079        self.max_file_upload_size = string_from_bytes(
1080            1024*getUtility(IStudentsUtils).MAX_KB)
1081        return
1082
1083    @property
1084    def label(self):
1085        container_title = self.context.__parent__.title
1086        return _('${a} <br /> Application Form ${b}', mapping = {
1087            'a':container_title, 'b':self.context.application_number})
1088
1089    def getTransitions(self):
1090        """Return a list of dicts of allowed transition ids and titles.
1091
1092        Each list entry provides keys ``name`` and ``title`` for
1093        internal name and (human readable) title of a single
1094        transition.
1095        """
1096        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
1097            if not t[0] in ('pay', 'create')]
1098        return [dict(name='', title=_('No transition'))] +[
1099            dict(name=x, title=y) for x, y in allowed_transitions]
1100
1101    def saveCourses(self, changed_fields):
1102        """In custom packages we needed to customize the certificate
1103        select widget. We just save course1 and course2 if these customized
1104        fields appear in the form.
1105        """
1106        return changed_fields
1107
1108    @action(_('Save'), style='primary')
1109    def save(self, **data):
1110        form = self.request.form
1111        password = form.get('password', None)
1112        password_ctl = form.get('control_password', None)
1113        if password:
1114            validator = getUtility(IPasswordValidator)
1115            errors = validator.validate_password(password, password_ctl)
1116            if errors:
1117                self.flash( ' '.join(errors), type='danger')
1118                return
1119        if self.upload_success is False:  # False is not None!
1120            # Error during image upload. Ignore other values.
1121            return
1122        changed_fields = self.applyData(self.context, **data)
1123        # Turn list of lists into single list
1124        if changed_fields:
1125            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1126        else:
1127            changed_fields = []
1128        changed_fields = self.saveCourses(changed_fields)
1129        if password:
1130            # Now we know that the form has no errors and can set password ...
1131            IUserAccount(self.context).setPassword(password)
1132            changed_fields.append('password')
1133        fields_string = ' + '.join(changed_fields)
1134        trans_id = form.get('transition', None)
1135        if trans_id:
1136            self.wf_info.fireTransition(trans_id)
1137        self.flash(_('Form has been saved.'))
1138        if fields_string:
1139            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
1140        return
1141
1142    def unremovable(self, ticket):
1143        return False
1144
1145    # This method is also used by the ApplicantEditFormPage
1146    def delPaymentTickets(self, **data):
1147        form = self.request.form
1148        if 'val_id' in form:
1149            child_id = form['val_id']
1150        else:
1151            self.flash(_('No payment selected.'), type='warning')
1152            self.redirect(self.url(self.context))
1153            return
1154        if not isinstance(child_id, list):
1155            child_id = [child_id]
1156        deleted = []
1157        for id in child_id:
1158            # Applicants are not allowed to remove used payment tickets
1159            if not self.unremovable(self.context[id]):
1160                try:
1161                    del self.context[id]
1162                    deleted.append(id)
1163                except:
1164                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1165                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1166        if len(deleted):
1167            self.flash(_('Successfully removed: ${a}',
1168                mapping = {'a':', '.join(deleted)}))
1169            self.context.writeLogMessage(
1170                self, 'removed: % s' % ', '.join(deleted))
1171        return
1172
1173    # We explicitely want the forms to be validated before payment tickets
1174    # can be created. If no validation is requested, use
1175    # 'validator=NullValidator' in the action directive
1176    @action(_('Add online payment ticket'), style='primary')
1177    def addPaymentTicket(self, **data):
1178        self.redirect(self.url(self.context, '@@addafp'))
1179        return
1180
1181    @jsaction(_('Remove selected tickets'))
1182    def removePaymentTickets(self, **data):
1183        self.delPaymentTickets(**data)
1184        self.redirect(self.url(self.context) + '/@@manage')
1185        return
1186
1187    # Not used in base package
1188    def file_exists(self, attr):
1189        file = getUtility(IExtFileStore).getFileByContext(
1190            self.context, attr=attr)
1191        if file:
1192            return True
1193        else:
1194            return False
1195
1196class ApplicantEditFormPage(ApplicantManageFormPage):
1197    """An applicant-centered edit view for applicant data.
1198    """
1199    grok.context(IApplicantEdit)
1200    grok.name('edit')
1201    grok.require('waeup.handleApplication')
1202    grok.template('applicanteditpage')
1203    manage_applications = False
1204    submit_state = PAID
1205    mandate_days = 7
1206
1207    @property
1208    def display_refereereports(self):
1209        return False
1210
1211    @property
1212    def form_fields(self):
1213        if self.context.special:
1214            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1215                'locked', 'suspended')
1216            form_fields['applicant_id'].for_display = True
1217        else:
1218            form_fields = grok.AutoFields(IApplicantEdit).omit(
1219                'locked', 'course_admitted', 'student_id',
1220                'suspended'
1221                )
1222            form_fields['applicant_id'].for_display = True
1223            form_fields['reg_number'].for_display = True
1224        return form_fields
1225
1226    @property
1227    def display_actions(self):
1228        state = IWorkflowState(self.context).getState()
1229        # If the form is unlocked, applicants are allowed to save the form
1230        # and remove unused tickets.
1231        actions = [[_('Save')], [_('Remove selected tickets')]]
1232        # Only in state started they can also add tickets.
1233        if state == STARTED:
1234            actions = [[_('Save')],
1235                [_('Add online payment ticket'),_('Remove selected tickets')]]
1236        # In state paid, they can submit the data and further add tickets
1237        # if the application is special.
1238        elif self.context.special and state == PAID:
1239            actions = [[_('Save'), _('Finally Submit')],
1240                [_('Add online payment ticket'),_('Remove selected tickets')]]
1241        elif state == PAID:
1242            actions = [[_('Save'), _('Finally Submit')],
1243                [_('Remove selected tickets')]]
1244        return actions
1245
1246    def unremovable(self, ticket):
1247        return ticket.r_code
1248
1249    def emit_lock_message(self):
1250        self.flash(_('The requested form is locked (read-only).'),
1251                   type='warning')
1252        self.redirect(self.url(self.context))
1253        return
1254
1255    def update(self):
1256        if self.context.locked or (
1257            self.context.__parent__.expired and
1258            self.context.__parent__.strict_deadline):
1259            self.emit_lock_message()
1260            return
1261        super(ApplicantEditFormPage, self).update()
1262        return
1263
1264    def dataNotComplete(self, data):
1265        if self.context.__parent__.with_picture:
1266            store = getUtility(IExtFileStore)
1267            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1268                return _('No passport picture uploaded.')
1269            if not self.request.form.get('confirm_passport', False):
1270                return _('Passport picture confirmation box not ticked.')
1271        return False
1272
1273    # We explicitely want the forms to be validated before payment tickets
1274    # can be created. If no validation is requested, use
1275    # 'validator=NullValidator' in the action directive
1276    @action(_('Add online payment ticket'), style='primary')
1277    def addPaymentTicket(self, **data):
1278        self.redirect(self.url(self.context, '@@addafp'))
1279        return
1280
1281    @jsaction(_('Remove selected tickets'))
1282    def removePaymentTickets(self, **data):
1283        self.delPaymentTickets(**data)
1284        self.redirect(self.url(self.context) + '/@@edit')
1285        return
1286
1287    def saveCourses(self):
1288        """In custom packages we needed to customize the certificate
1289        select widget. We just save course1 and course2 if these customized
1290        fields appear in the form.
1291        """
1292        return
1293
1294    @action(_('Save'), style='primary')
1295    def save(self, **data):
1296        if self.upload_success is False:  # False is not None!
1297            # Error during image upload. Ignore other values.
1298            return
1299        self.applyData(self.context, **data)
1300        self.saveCourses()
1301        self.flash(_('Form has been saved.'))
1302        return
1303
1304    def informReferees(self):
1305        site = grok.getSite()
1306        kofa_utils = getUtility(IKofaUtils)
1307        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1308        failed = ''
1309        emails_sent = 0
1310        for referee in self.context.referees:
1311            if referee.email_sent:
1312                continue
1313            mandate = RefereeReportMandate(days=self.mandate_days)
1314            mandate.params['name'] = referee.name
1315            mandate.params['email'] = referee.email
1316            mandate.params[
1317                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1318                    self.context.__parent__.code,
1319                    self.context.application_number)
1320            mandate.params['redirect_path2'] = ''
1321            mandate.params['applicant_id'] = self.context.applicant_id
1322            site['mandates'].addMandate(mandate)
1323            # Send invitation email
1324            args = {'mandate_id':mandate.mandate_id}
1325            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1326            url_info = u'Report link: %s' % mandate_url
1327            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1328            if success:
1329                emails_sent += 1
1330                self.context.writeLogMessage(
1331                    self, 'email sent: %s' % referee.email)
1332                referee.email_sent = True
1333            else:
1334                failed += '%s ' % referee.email
1335        return failed, emails_sent
1336
1337    @action(_('Finally Submit'), warning=WARNING)
1338    def finalsubmit(self, **data):
1339        if self.upload_success is False:  # False is not None!
1340            return # error during image upload. Ignore other values
1341        dnt = self.dataNotComplete(data)
1342        if dnt:
1343            self.flash(dnt, type='danger')
1344            return
1345        self.applyData(self.context, **data)
1346        state = IWorkflowState(self.context).getState()
1347        # This shouldn't happen, but the application officer
1348        # might have forgotten to lock the form after changing the state
1349        if state != self.submit_state:
1350            self.flash(_('The form cannot be submitted. Wrong state!'),
1351                       type='danger')
1352            return
1353        msg = _('Form has been submitted.')
1354        # Create mandates and send emails to referees
1355        if getattr(self.context, 'referees', None):
1356            failed, emails_sent = self.informReferees()
1357            if failed:
1358                self.flash(
1359                    _('Some invitation emails could not be sent:') + failed,
1360                    type='danger')
1361                return
1362            msg = _('Form has been successfully submitted and '
1363                    '${a} invitation emails were sent.',
1364                    mapping = {'a':  emails_sent})
1365        IWorkflowInfo(self.context).fireTransition('submit')
1366        # application_date is used in export files for sorting.
1367        # We can thus store utc.
1368        self.context.application_date = datetime.utcnow()
1369        self.flash(msg)
1370        self.redirect(self.url(self.context))
1371        return
1372
1373class PassportImage(grok.View):
1374    """Renders the passport image for applicants.
1375    """
1376    grok.name('passport.jpg')
1377    grok.context(IApplicant)
1378    grok.require('waeup.viewApplication')
1379
1380    def render(self):
1381        # A filename chooser turns a context into a filename suitable
1382        # for file storage.
1383        image = getUtility(IExtFileStore).getFileByContext(self.context)
1384        self.response.setHeader('Content-Type', 'image/jpeg')
1385        if image is None:
1386            # show placeholder image
1387            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1388        return image
1389
1390class PassportImageForReport(PassportImage):
1391    """Renders the passport image for applicants for referee reports.
1392    """
1393    grok.name('passport_for_report.jpg')
1394    grok.context(IApplicant)
1395    grok.require('waeup.Public')
1396
1397    def render(self):
1398        # Check mandate
1399        form = self.request.form
1400        self.mandate_id = form.get('mandate_id', None)
1401        self.mandates = grok.getSite()['mandates']
1402        mandate = self.mandates.get(self.mandate_id, None)
1403        if mandate is None:
1404            self.flash(_('No mandate.'), type='warning')
1405            self.redirect(self.application_url())
1406            return
1407        if mandate:
1408            # Check the mandate expiration date after redirect again
1409            if mandate.expires < datetime.utcnow():
1410                self.flash(_('Mandate expired.'),
1411                           type='warning')
1412                self.redirect(self.application_url())
1413                return
1414            # Check if mandate allows access
1415            if mandate.params.get('applicant_id') != self.context.applicant_id:
1416                self.flash(_('Wrong mandate.'),
1417                           type='warning')
1418                self.redirect(self.application_url())
1419                return
1420            return super(PassportImageForReport, self).render()
1421        return
1422
1423class ApplicantRegistrationPage(KofaAddFormPage):
1424    """Captcha'd registration page for applicants.
1425    """
1426    grok.context(IApplicantsContainer)
1427    grok.name('register')
1428    grok.require('waeup.Anonymous')
1429    grok.template('applicantregister')
1430
1431    @property
1432    def form_fields(self):
1433        form_fields = None
1434        if self.context.mode == 'update':
1435            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1436                'lastname','reg_number','email')
1437        else: #if self.context.mode == 'create':
1438            form_fields = grok.AutoFields(IApplicantEdit).select(
1439                'firstname', 'middlename', 'lastname', 'email', 'phone')
1440        return form_fields
1441
1442    @property
1443    def label(self):
1444        return _('Apply for ${a}',
1445            mapping = {'a':self.context.title})
1446
1447    def update(self):
1448        if self.context.expired:
1449            self.flash(_('Outside application period.'), type='warning')
1450            self.redirect(self.url(self.context))
1451            return
1452        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1453        if blocker:
1454            self.flash(_('The portal is in maintenance mode '
1455                        'and registration temporarily disabled.'),
1456                       type='warning')
1457            self.redirect(self.url(self.context))
1458            return
1459        # Handle captcha
1460        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1461        self.captcha_result = self.captcha.verify(self.request)
1462        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1463        return
1464
1465    def _redirect(self, email, password, applicant_id):
1466        # Forward only email to landing page in base package.
1467        self.redirect(self.url(self.context, 'registration_complete',
1468            data = dict(email=email)))
1469        return
1470
1471    @property
1472    def _postfix(self):
1473        """In customized packages we can add a container dependent string if
1474        applicants have been imported into several containers.
1475        """
1476        return ''
1477
1478    @action(_('Send login credentials to email address'), style='primary')
1479    def register(self, **data):
1480        if not self.captcha_result.is_valid:
1481            # Captcha will display error messages automatically.
1482            # No need to flash something.
1483            return
1484        if self.context.mode == 'create':
1485            # Check if there are unused records in this container which
1486            # can be taken
1487            applicant = self.context.first_unused
1488            if applicant is None:
1489                # Add applicant
1490                applicant = createObject(u'waeup.Applicant')
1491                self.context.addApplicant(applicant)
1492            else:
1493                applicants_root = grok.getSite()['applicants']
1494                ob_class = self.__implemented__.__name__.replace(
1495                    'waeup.kofa.','')
1496                applicants_root.logger.info('%s - used: %s' % (
1497                    ob_class, applicant.applicant_id))
1498            self.applyData(applicant, **data)
1499            # applicant.reg_number = applicant.applicant_id
1500            notify(grok.ObjectModifiedEvent(applicant))
1501        elif self.context.mode == 'update':
1502            # Update applicant
1503            reg_number = data.get('reg_number','')
1504            lastname = data.get('lastname','')
1505            cat = getUtility(ICatalog, name='applicants_catalog')
1506            searchstr = reg_number + self._postfix
1507            results = list(
1508                cat.searchResults(reg_number=(searchstr, searchstr)))
1509            if results:
1510                applicant = results[0]
1511                if getattr(applicant,'lastname',None) is None:
1512                    self.flash(_('An error occurred.'), type='danger')
1513                    return
1514                elif applicant.lastname.lower() != lastname.lower():
1515                    # Don't tell the truth here. Anonymous must not
1516                    # know that a record was found and only the lastname
1517                    # verification failed.
1518                    self.flash(
1519                        _('No application record found.'), type='warning')
1520                    return
1521                elif applicant.password is not None and \
1522                    applicant.state != INITIALIZED:
1523                    self.flash(_('Your password has already been set and used. '
1524                                 'Please proceed to the login page.'),
1525                               type='warning')
1526                    return
1527                # Store email address but nothing else.
1528                applicant.email = data['email']
1529                notify(grok.ObjectModifiedEvent(applicant))
1530            else:
1531                # No record found, this is the truth.
1532                self.flash(_('No application record found.'), type='warning')
1533                return
1534        else:
1535            # Does not happen but anyway ...
1536            return
1537        kofa_utils = getUtility(IKofaUtils)
1538        password = kofa_utils.genPassword()
1539        IUserAccount(applicant).setPassword(password)
1540        # Send email with credentials
1541        login_url = self.url(grok.getSite(), 'login')
1542        url_info = u'Login: %s' % login_url
1543        msg = _('You have successfully been registered for the')
1544        if kofa_utils.sendCredentials(IUserAccount(applicant),
1545            password, url_info, msg):
1546            email_sent = applicant.email
1547        else:
1548            email_sent = None
1549        self._redirect(email=email_sent, password=password,
1550            applicant_id=applicant.applicant_id)
1551        return
1552
1553class ApplicantRegistrationEmailSent(KofaPage):
1554    """Landing page after successful registration.
1555
1556    """
1557    grok.name('registration_complete')
1558    grok.require('waeup.Public')
1559    grok.template('applicantregemailsent')
1560    label = _('Your registration was successful.')
1561
1562    def update(self, email=None, applicant_id=None, password=None):
1563        self.email = email
1564        self.password = password
1565        self.applicant_id = applicant_id
1566        return
1567
1568class ApplicantCheckStatusPage(KofaPage):
1569    """Captcha'd status checking page for applicants.
1570    """
1571    grok.context(IApplicantsRoot)
1572    grok.name('checkstatus')
1573    grok.require('waeup.Anonymous')
1574    grok.template('applicantcheckstatus')
1575    buttonname = _('Submit')
1576
1577    def label(self):
1578        if self.result:
1579            return _('Admission status of ${a}',
1580                     mapping = {'a':self.applicant.applicant_id})
1581        return _('Check your admission status')
1582
1583    def update(self, SUBMIT=None):
1584        form = self.request.form
1585        self.result = False
1586        # Handle captcha
1587        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1588        self.captcha_result = self.captcha.verify(self.request)
1589        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1590        if SUBMIT:
1591            if not self.captcha_result.is_valid:
1592                # Captcha will display error messages automatically.
1593                # No need to flash something.
1594                return
1595            unique_id = form.get('unique_id', None)
1596            lastname = form.get('lastname', None)
1597            if not unique_id or not lastname:
1598                self.flash(
1599                    _('Required input missing.'), type='warning')
1600                return
1601            cat = getUtility(ICatalog, name='applicants_catalog')
1602            results = list(
1603                cat.searchResults(applicant_id=(unique_id, unique_id)))
1604            if not results:
1605                results = list(
1606                    cat.searchResults(reg_number=(unique_id, unique_id)))
1607            if results:
1608                applicant = results[0]
1609                if applicant.lastname.lower().strip() != lastname.lower():
1610                    # Don't tell the truth here. Anonymous must not
1611                    # know that a record was found and only the lastname
1612                    # verification failed.
1613                    self.flash(
1614                        _('No application record found.'), type='warning')
1615                    return
1616            else:
1617                self.flash(_('No application record found.'), type='warning')
1618                return
1619            self.applicant = applicant
1620            self.entry_session = "%s/%s" % (
1621                applicant.__parent__.year,
1622                applicant.__parent__.year+1)
1623            course_admitted = getattr(applicant, 'course_admitted', None)
1624            self.course_admitted = False
1625            if course_admitted is not None:
1626                try:
1627                    self.course_admitted = True
1628                    self.longtitle = course_admitted.longtitle
1629                    self.department = course_admitted.__parent__.__parent__.longtitle
1630                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1631                except AttributeError:
1632                    self.flash(_('Application record invalid.'), type='warning')
1633                    return
1634            self.result = True
1635            self.admitted = False
1636            self.not_admitted = False
1637            self.submitted = False
1638            self.not_submitted = False
1639            self.created = False
1640            if applicant.state in (ADMITTED, CREATED):
1641                self.admitted = True
1642            if applicant.state in (CREATED):
1643                self.created = True
1644                self.student_id = applicant.student_id
1645                self.password = applicant.application_number
1646            if applicant.state in (NOT_ADMITTED,):
1647                self.not_admitted = True
1648            if applicant.state in (SUBMITTED,):
1649                self.submitted = True
1650            if applicant.state in (INITIALIZED, STARTED, PAID):
1651                self.not_submitted = True
1652        return
1653
1654class ExportJobContainerOverview(KofaPage):
1655    """Page that lists active applicant data export jobs and provides links
1656    to discard or download CSV files.
1657
1658    """
1659    grok.context(VirtualApplicantsExportJobContainer)
1660    grok.require('waeup.manageApplication')
1661    grok.name('index.html')
1662    grok.template('exportjobsindex')
1663    label = _('Data Exports')
1664    pnav = 3
1665
1666    def update(self, CREATE=None, DISCARD=None, job_id=None):
1667        if CREATE:
1668            self.redirect(self.url('@@start_export'))
1669            return
1670        if DISCARD and job_id:
1671            entry = self.context.entry_from_job_id(job_id)
1672            self.context.delete_export_entry(entry)
1673            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1674            self.context.logger.info(
1675                '%s - discarded: job_id=%s' % (ob_class, job_id))
1676            self.flash(_('Discarded export') + ' %s' % job_id)
1677        self.entries = doll_up(self, user=self.request.principal.id)
1678        return
1679
1680class ExportJobContainerJobStart(UtilityView, grok.View):
1681    """View that starts three export jobs, one for applicants, a second
1682    one for applicant payments and a third for referee reports.
1683    """
1684    grok.context(VirtualApplicantsExportJobContainer)
1685    grok.require('waeup.manageApplication')
1686    grok.name('start_export')
1687
1688    def update(self):
1689        utils = queryUtility(IKofaUtils)
1690        if not utils.expensive_actions_allowed():
1691            self.flash(_(
1692                "Currently, exporters cannot be started due to high "
1693                "system load. Please try again later."), type='danger')
1694            self.entries = doll_up(self, user=None)
1695            return
1696
1697        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1698        container_code = self.context.__parent__.code
1699        # Start first exporter
1700        for exporter in ('applicants',
1701                         'applicantpayments',
1702                         'applicantrefereereports'):
1703            job_id = self.context.start_export_job(exporter,
1704                                          self.request.principal.id,
1705                                          container=container_code)
1706            self.context.logger.info(
1707                '%s - exported: %s (%s), job_id=%s'
1708                % (ob_class, exporter, container_code, job_id))
1709            # Commit transaction so that job is stored in the ZODB
1710            transaction.commit()
1711        self.flash(_('Exports started.'))
1712        self.redirect(self.url(self.context))
1713        return
1714
1715    def render(self):
1716        return
1717
1718class ExportJobContainerDownload(ExportCSVView):
1719    """Page that downloads a students export csv file.
1720
1721    """
1722    grok.context(VirtualApplicantsExportJobContainer)
1723    grok.require('waeup.manageApplication')
1724
1725class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1726    """A display view for referee reports.
1727    """
1728    grok.context(IApplicantRefereeReport)
1729    grok.name('index')
1730    grok.require('waeup.manageApplication')
1731    label = _('Referee Report')
1732    pnav = 3
1733    form_fields = grok.AutoFields(IApplicantRefereeReport)
1734    form_fields[
1735        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1736
1737class RemoveRefereeReportPage(UtilityView, grok.View):
1738    """
1739    """
1740    grok.context(IApplicantRefereeReport)
1741    grok.name('remove')
1742    grok.require('waeup.manageApplication')
1743
1744    def update(self):
1745        redirect_url = self.url(self.context.__parent__)
1746        self.context.__parent__.writeLogMessage(
1747            self, 'removed: %s' % self.context.r_id)
1748        del self.context.__parent__[self.context.r_id]
1749        self.flash(_('Referee report removed.'))
1750        self.redirect(redirect_url)
1751        return
1752
1753    def render(self):
1754        return
1755
1756class RefereeReportAddFormPage(KofaAddFormPage):
1757    """Add-form to add an referee report. This form
1758    is protected by a mandate.
1759    """
1760    grok.context(IApplicant)
1761    grok.require('waeup.Public')
1762    grok.name('addrefereereport')
1763    form_fields = grok.AutoFields(
1764        IApplicantRefereeReport).omit('creation_date')
1765    grok.template('refereereportpage')
1766    label = _('Referee Report Form')
1767    pnav = 3
1768    #doclink = DOCLINK + '/refereereports.html'
1769
1770    def update(self):
1771        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1772        if blocker:
1773            self.flash(_('The portal is in maintenance mode. '
1774                        'Referee report forms are temporarily disabled.'),
1775                       type='warning')
1776            self.redirect(self.application_url())
1777            return
1778        # Check mandate
1779        form = self.request.form
1780        self.mandate_id = form.get('mandate_id', None)
1781        self.mandates = grok.getSite()['mandates']
1782        mandate = self.mandates.get(self.mandate_id, None)
1783        if mandate is None and not self.request.form.get('form.actions.submit'):
1784            self.flash(_('No mandate.'), type='warning')
1785            self.redirect(self.application_url())
1786            return
1787        if mandate:
1788            # Check the mandate expiration date after redirect again
1789            if mandate.expires < datetime.utcnow():
1790                self.flash(_('Mandate expired.'),
1791                           type='warning')
1792                self.redirect(self.application_url())
1793                return
1794            args = {'mandate_id':mandate.mandate_id}
1795            # Check if report exists.
1796            # If so, redirect to the pdf file.
1797            if mandate.params.get('redirect_path2'):
1798                self.redirect(
1799                    self.application_url() +
1800                    mandate.params.get('redirect_path2') +
1801                    '?%s' % urlencode(args))
1802                return
1803            # Prefill form with mandate params
1804            self.form_fields.get(
1805                'name').field.default = mandate.params['name']
1806            self.form_fields.get(
1807                'email').field.default = mandate.params['email']
1808            self.passport_url = self.url(
1809                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
1810        super(RefereeReportAddFormPage, self).update()
1811        return
1812
1813    @action(_('Submit'),
1814              warning=_('Are you really sure? '
1815                        'Reports can neither be modified or added '
1816                        'after submission.'),
1817              style='primary')
1818    def addRefereeReport(self, **data):
1819        report = createObject(u'waeup.ApplicantRefereeReport')
1820        timestamp = ("%d" % int(time()*10000))[1:]
1821        report.r_id = "r%s" % timestamp
1822        self.applyData(report, **data)
1823        self.context[report.r_id] = report
1824        # self.flash(_('Referee report has been saved. Thank you!'))
1825        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1826        # Changed on 19/04/20: We do no longer delete the mandate
1827        # but set path to redirect to the pdf file
1828        self.mandates[self.mandate_id].params[
1829            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
1830                self.context.__parent__.code,
1831                self.context.application_number,
1832                report.r_id)
1833        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
1834        args = {'mandate_id':self.mandate_id}
1835        self.redirect(self.url(report, 'referee_report.pdf')
1836                      + '?%s' % urlencode(args))
1837        return
1838
1839class ExportPDFReportSlipPage(UtilityView, grok.View):
1840    """Deliver a PDF slip of the context.
1841    """
1842    grok.context(IApplicantRefereeReport)
1843    grok.name('referee_report_slip.pdf')
1844    grok.require('waeup.manageApplication')
1845    form_fields = grok.AutoFields(IApplicantRefereeReport)
1846    form_fields[
1847        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1848    #prefix = 'form'
1849    note = None
1850
1851    @property
1852    def title(self):
1853        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1854        return translate(_('Referee Report'), 'waeup.kofa',
1855            target_language=portal_language)
1856
1857    @property
1858    def label(self):
1859        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1860        return translate(_('Referee Report Slip'),
1861            'waeup.kofa', target_language=portal_language) \
1862            + ' %s' % self.context.r_id
1863
1864    def render(self):
1865        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
1866            self.request)
1867        students_utils = getUtility(IStudentsUtils)
1868        return students_utils.renderPDF(self,'referee_report_slip.pdf',
1869            self.context.__parent__, applicantview, note=self.note)
1870
1871class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
1872    """Deliver a PDF slip of the context to referees.
1873    """
1874    grok.name('referee_report.pdf')
1875    grok.require('waeup.Public')
1876
1877    def update(self):
1878        # Check mandate
1879        form = self.request.form
1880        self.mandate_id = form.get('mandate_id', None)
1881        self.mandates = grok.getSite()['mandates']
1882        mandate = self.mandates.get(self.mandate_id, None)
1883        if mandate is None:
1884            self.flash(_('No mandate.'), type='warning')
1885            self.redirect(self.application_url())
1886            return
1887        if mandate:
1888            # Check the mandate expiration date after redirect again
1889            if mandate.expires < datetime.utcnow():
1890                self.flash(_('Mandate expired.'),
1891                           type='warning')
1892                self.redirect(self.application_url())
1893                return
1894            # Check if form has really been submitted
1895            if not mandate.params.get('redirect_path2') \
1896                or mandate.params.get(
1897                    'applicant_id') != self.context.__parent__.applicant_id:
1898                self.flash(_('Wrong mandate.'),
1899                           type='warning')
1900                self.redirect(self.application_url())
1901                return
1902            super(ExportPDFReportSlipPage2, self).update()
1903        return
1904
1905class AdditionalFile(grok.View):
1906    """Renders additional pdf files for applicants.
1907    This is a baseclass.
1908    """
1909    grok.baseclass()
1910    grok.context(IApplicant)
1911    grok.require('waeup.viewApplication')
1912
1913    def render(self):
1914        pdf = getUtility(IExtFileStore).getFileByContext(
1915            self.context, attr=self.__name__)
1916        self.response.setHeader('Content-Type', 'application/pdf')
1917        return pdf
1918
1919class TestFile(AdditionalFile):
1920    """Renders testfile.pdf.
1921    """
1922    grok.name('testfile.pdf')
Note: See TracBrowser for help on using the repository browser.