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

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

Implement ApplicantRefereeReportExporter.

  • Property svn:keywords set to Id
File size: 71.1 KB
Line 
1## $Id: browser.py 16064 2020-04-21 07:58:26Z 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 key in self.context.keys():
790                ticket = self.context[key]
791                if ticket.p_state == 'paid':
792                      self.flash(
793                          _('This type of payment has already been made.'),
794                          type='warning')
795                      self.redirect(self.url(self.context))
796                      return
797        applicants_utils = getUtility(IApplicantsUtils)
798        container = self.context.__parent__
799        payment = createObject(self.factory)
800        failure = applicants_utils.setPaymentDetails(
801            container, payment, self.context)
802        if failure is not None:
803            self.flash(failure, type='danger')
804            self.redirect(self.url(self.context))
805            return
806        self.context[payment.p_id] = payment
807        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
808        self.flash(_('Payment ticket created.'))
809        self.redirect(self.url(payment))
810        return
811
812    def render(self):
813        return
814
815
816class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
817    """ Page to view an online payment ticket
818    """
819    grok.context(IApplicantOnlinePayment)
820    grok.name('index')
821    grok.require('waeup.viewApplication')
822    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
823    form_fields[
824        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
825    form_fields[
826        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
827    pnav = 3
828
829    @property
830    def label(self):
831        return _('${a}: Online Payment Ticket ${b}', mapping = {
832            'a':self.context.__parent__.display_fullname,
833            'b':self.context.p_id})
834
835class OnlinePaymentApprovePage(UtilityView, grok.View):
836    """ Approval view
837    """
838    grok.context(IApplicantOnlinePayment)
839    grok.name('approve')
840    grok.require('waeup.managePortal')
841
842    def update(self):
843        flashtype, msg, log = self.context.approveApplicantPayment()
844        if log is not None:
845            applicant = self.context.__parent__
846            # Add log message to applicants.log
847            applicant.writeLogMessage(self, log)
848            # Add log message to payments.log
849            self.context.logger.info(
850                '%s,%s,%s,%s,%s,,,,,,' % (
851                applicant.applicant_id,
852                self.context.p_id, self.context.p_category,
853                self.context.amount_auth, self.context.r_code))
854        self.flash(msg, type=flashtype)
855        return
856
857    def render(self):
858        self.redirect(self.url(self.context, '@@index'))
859        return
860
861class ExportPDFPaymentSlipPage(UtilityView, grok.View):
862    """Deliver a PDF slip of the context.
863    """
864    grok.context(IApplicantOnlinePayment)
865    grok.name('payment_slip.pdf')
866    grok.require('waeup.viewApplication')
867    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
868    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
869    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
870    #prefix = 'form'
871    note = None
872
873    @property
874    def title(self):
875        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
876        return translate(_('Payment Data'), 'waeup.kofa',
877            target_language=portal_language)
878
879    @property
880    def label(self):
881        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
882        return translate(_('Online Payment Slip'),
883            'waeup.kofa', target_language=portal_language) \
884            + ' %s' % self.context.p_id
885
886    @property
887    def payment_slip_download_warning(self):
888        if self.context.__parent__.state not in (
889            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
890            return _('Please submit the application form before '
891                     'trying to download payment slips.')
892        return ''
893
894    def render(self):
895        if self.payment_slip_download_warning:
896            self.flash(self.payment_slip_download_warning, type='danger')
897            self.redirect(self.url(self.context))
898            return
899        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
900            self.request)
901        students_utils = getUtility(IStudentsUtils)
902        return students_utils.renderPDF(self,'payment_slip.pdf',
903            self.context.__parent__, applicantview, note=self.note)
904
905class ExportPDFPageApplicationSlip(UtilityView, grok.View):
906    """Deliver a PDF slip of the context.
907    """
908    grok.context(IApplicant)
909    grok.name('application_slip.pdf')
910    grok.require('waeup.viewApplication')
911    #prefix = 'form'
912
913    def update(self):
914        if self.context.state in ('initialized', 'started', 'paid'):
915            self.flash(
916                _('Please pay and submit before trying to download '
917                  'the application slip.'), type='warning')
918            return self.redirect(self.url(self.context))
919        return
920
921    def render(self):
922        try:
923            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
924                view=self)
925        except IOError:
926            self.flash(
927                _('Your image file is corrupted. '
928                  'Please replace.'), type='danger')
929            return self.redirect(self.url(self.context))
930        except LayoutError, err:
931            self.flash(
932                'PDF file could not be created. Reportlab error message: %s'
933                % escape(err.message),
934                type="danger")
935            return self.redirect(self.url(self.context))
936        self.response.setHeader(
937            'Content-Type', 'application/pdf')
938        return pdfstream
939
940def handle_img_upload(upload, context, view):
941    """Handle upload of applicant image.
942
943    Returns `True` in case of success or `False`.
944
945    Please note that file pointer passed in (`upload`) most probably
946    points to end of file when leaving this function.
947    """
948    max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
949    size = file_size(upload)
950    if size > max_upload_size:
951        view.flash(_('Uploaded image is too big!'), type='danger')
952        return False
953    dummy, ext = os.path.splitext(upload.filename)
954    ext.lower()
955    if ext != '.jpg':
956        view.flash(_('jpg file extension expected.'), type='danger')
957        return False
958    upload.seek(0) # file pointer moved when determining size
959    store = getUtility(IExtFileStore)
960    file_id = IFileStoreNameChooser(context).chooseName()
961    try:
962        store.createFile(file_id, upload)
963    except IOError:
964        view.flash(_('Image file cannot be changed.'), type='danger')
965        return False
966    return True
967
968def handle_file_upload(upload, context, view, attr=None):
969    """Handle upload of applicant files.
970
971    Returns `True` in case of success or `False`.
972
973    Please note that file pointer passed in (`upload`) most probably
974    points to end of file when leaving this function.
975    """
976    size = file_size(upload)
977    max_upload_size = 1024 * getUtility(IStudentsUtils).MAX_KB
978    if size > max_upload_size:
979        view.flash(_('Uploaded file is too big!'))
980        return False
981    dummy, ext = os.path.splitext(upload.filename)
982    ext.lower()
983    if ext != '.pdf':
984        view.flash(_('pdf file extension expected.'))
985        return False
986    upload.seek(0) # file pointer moved when determining size
987    store = getUtility(IExtFileStore)
988    file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
989    store.createFile(file_id, upload)
990    return True
991
992class ApplicantManageFormPage(KofaEditFormPage):
993    """A full edit view for applicant data.
994    """
995    grok.context(IApplicant)
996    grok.name('manage')
997    grok.require('waeup.manageApplication')
998    grok.template('applicanteditpage')
999    manage_applications = True
1000    pnav = 3
1001    display_actions = [[_('Save'), _('Finally Submit')],
1002        [_('Add online payment ticket'),_('Remove selected tickets')]]
1003
1004    @property
1005    def display_payments(self):
1006        if self.context.payments:
1007            return True
1008        if self.context.special:
1009            return True
1010        return getattr(self.context.__parent__, 'application_fee', None)
1011
1012    @property
1013    def display_refereereports(self):
1014        if self.context.refereereports:
1015            return True
1016        return False
1017
1018    def display_fileupload(self, filename):
1019        """This method can be used in custom packages to avoid unneccessary
1020        file uploads.
1021        """
1022        return True
1023
1024    @property
1025    def form_fields(self):
1026        if self.context.special:
1027            form_fields = grok.AutoFields(ISpecialApplicant)
1028            form_fields['applicant_id'].for_display = True
1029        else:
1030            form_fields = grok.AutoFields(IApplicant)
1031            form_fields['student_id'].for_display = True
1032            form_fields['applicant_id'].for_display = True
1033        return form_fields
1034
1035    @property
1036    def target(self):
1037        return getattr(self.context.__parent__, 'prefix', None)
1038
1039    @property
1040    def separators(self):
1041        return getUtility(IApplicantsUtils).SEPARATORS_DICT
1042
1043    @property
1044    def custom_upload_requirements(self):
1045        return ''
1046
1047    def update(self):
1048        super(ApplicantManageFormPage, self).update()
1049        max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
1050        self.wf_info = IWorkflowInfo(self.context)
1051        self.max_upload_size = string_from_bytes(max_upload_size)
1052        self.upload_success = None
1053        upload = self.request.form.get('form.passport', None)
1054        if upload:
1055            if self.custom_upload_requirements:
1056                self.flash(
1057                    self.custom_upload_requirements,
1058                    type='danger')
1059                self.redirect(self.url(self.context))
1060                return
1061            # We got a fresh upload, upload_success is
1062            # either True or False
1063            self.upload_success = handle_img_upload(
1064                upload, self.context, self)
1065            if self.upload_success:
1066                self.context.writeLogMessage(self, 'saved: passport')
1067        file_store = getUtility(IExtFileStore)
1068        self.additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
1069        for filename in self.additional_files:
1070            upload = self.request.form.get(filename[1], None)
1071            if upload:
1072                # We got a fresh file upload
1073                success = handle_file_upload(
1074                    upload, self.context, self, attr=filename[1])
1075                if success:
1076                    self.context.writeLogMessage(
1077                        self, 'saved: %s' % filename[1])
1078                else:
1079                    self.upload_success = False
1080        self.max_file_upload_size = string_from_bytes(
1081            1024*getUtility(IStudentsUtils).MAX_KB)
1082        return
1083
1084    @property
1085    def label(self):
1086        container_title = self.context.__parent__.title
1087        return _('${a} <br /> Application Form ${b}', mapping = {
1088            'a':container_title, 'b':self.context.application_number})
1089
1090    def getTransitions(self):
1091        """Return a list of dicts of allowed transition ids and titles.
1092
1093        Each list entry provides keys ``name`` and ``title`` for
1094        internal name and (human readable) title of a single
1095        transition.
1096        """
1097        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
1098            if not t[0] in ('pay', 'create')]
1099        return [dict(name='', title=_('No transition'))] +[
1100            dict(name=x, title=y) for x, y in allowed_transitions]
1101
1102    @action(_('Save'), style='primary')
1103    def save(self, **data):
1104        form = self.request.form
1105        password = form.get('password', None)
1106        password_ctl = form.get('control_password', None)
1107        if password:
1108            validator = getUtility(IPasswordValidator)
1109            errors = validator.validate_password(password, password_ctl)
1110            if errors:
1111                self.flash( ' '.join(errors), type='danger')
1112                return
1113        if self.upload_success is False:  # False is not None!
1114            # Error during image upload. Ignore other values.
1115            return
1116        changed_fields = self.applyData(self.context, **data)
1117        # Turn list of lists into single list
1118        if changed_fields:
1119            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1120        else:
1121            changed_fields = []
1122        if password:
1123            # Now we know that the form has no errors and can set password ...
1124            IUserAccount(self.context).setPassword(password)
1125            changed_fields.append('password')
1126        fields_string = ' + '.join(changed_fields)
1127        trans_id = form.get('transition', None)
1128        if trans_id:
1129            self.wf_info.fireTransition(trans_id)
1130        self.flash(_('Form has been saved.'))
1131        if fields_string:
1132            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
1133        return
1134
1135    def unremovable(self, ticket):
1136        return False
1137
1138    # This method is also used by the ApplicantEditFormPage
1139    def delPaymentTickets(self, **data):
1140        form = self.request.form
1141        if 'val_id' in form:
1142            child_id = form['val_id']
1143        else:
1144            self.flash(_('No payment selected.'), type='warning')
1145            self.redirect(self.url(self.context))
1146            return
1147        if not isinstance(child_id, list):
1148            child_id = [child_id]
1149        deleted = []
1150        for id in child_id:
1151            # Applicants are not allowed to remove used payment tickets
1152            if not self.unremovable(self.context[id]):
1153                try:
1154                    del self.context[id]
1155                    deleted.append(id)
1156                except:
1157                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1158                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1159        if len(deleted):
1160            self.flash(_('Successfully removed: ${a}',
1161                mapping = {'a':', '.join(deleted)}))
1162            self.context.writeLogMessage(
1163                self, 'removed: % s' % ', '.join(deleted))
1164        return
1165
1166    # We explicitely want the forms to be validated before payment tickets
1167    # can be created. If no validation is requested, use
1168    # 'validator=NullValidator' in the action directive
1169    @action(_('Add online payment ticket'), style='primary')
1170    def addPaymentTicket(self, **data):
1171        self.redirect(self.url(self.context, '@@addafp'))
1172        return
1173
1174    @jsaction(_('Remove selected tickets'))
1175    def removePaymentTickets(self, **data):
1176        self.delPaymentTickets(**data)
1177        self.redirect(self.url(self.context) + '/@@manage')
1178        return
1179
1180    # Not used in base package
1181    def file_exists(self, attr):
1182        file = getUtility(IExtFileStore).getFileByContext(
1183            self.context, attr=attr)
1184        if file:
1185            return True
1186        else:
1187            return False
1188
1189class ApplicantEditFormPage(ApplicantManageFormPage):
1190    """An applicant-centered edit view for applicant data.
1191    """
1192    grok.context(IApplicantEdit)
1193    grok.name('edit')
1194    grok.require('waeup.handleApplication')
1195    grok.template('applicanteditpage')
1196    manage_applications = False
1197    submit_state = PAID
1198    mandate_days = 7
1199
1200    @property
1201    def display_refereereports(self):
1202        return False
1203
1204    @property
1205    def form_fields(self):
1206        if self.context.special:
1207            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1208                'locked', 'suspended')
1209            form_fields['applicant_id'].for_display = True
1210        else:
1211            form_fields = grok.AutoFields(IApplicantEdit).omit(
1212                'locked', 'course_admitted', 'student_id',
1213                'suspended'
1214                )
1215            form_fields['applicant_id'].for_display = True
1216            form_fields['reg_number'].for_display = True
1217        return form_fields
1218
1219    @property
1220    def display_actions(self):
1221        state = IWorkflowState(self.context).getState()
1222        # If the form is unlocked, applicants are allowed to save the form
1223        # and remove unused tickets.
1224        actions = [[_('Save')], [_('Remove selected tickets')]]
1225        # Only in state started they can also add tickets.
1226        if state == STARTED:
1227            actions = [[_('Save')],
1228                [_('Add online payment ticket'),_('Remove selected tickets')]]
1229        # In state paid, they can submit the data and further add tickets
1230        # if the application is special.
1231        elif self.context.special and state == PAID:
1232            actions = [[_('Save'), _('Finally Submit')],
1233                [_('Add online payment ticket'),_('Remove selected tickets')]]
1234        elif state == PAID:
1235            actions = [[_('Save'), _('Finally Submit')],
1236                [_('Remove selected tickets')]]
1237        return actions
1238
1239    def unremovable(self, ticket):
1240        return ticket.r_code
1241
1242    def emit_lock_message(self):
1243        self.flash(_('The requested form is locked (read-only).'),
1244                   type='warning')
1245        self.redirect(self.url(self.context))
1246        return
1247
1248    def update(self):
1249        if self.context.locked or (
1250            self.context.__parent__.expired and
1251            self.context.__parent__.strict_deadline):
1252            self.emit_lock_message()
1253            return
1254        super(ApplicantEditFormPage, self).update()
1255        return
1256
1257    def dataNotComplete(self, data):
1258        if self.context.__parent__.with_picture:
1259            store = getUtility(IExtFileStore)
1260            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1261                return _('No passport picture uploaded.')
1262            if not self.request.form.get('confirm_passport', False):
1263                return _('Passport picture confirmation box not ticked.')
1264        return False
1265
1266    # We explicitely want the forms to be validated before payment tickets
1267    # can be created. If no validation is requested, use
1268    # 'validator=NullValidator' in the action directive
1269    @action(_('Add online payment ticket'), style='primary')
1270    def addPaymentTicket(self, **data):
1271        self.redirect(self.url(self.context, '@@addafp'))
1272        return
1273
1274    @jsaction(_('Remove selected tickets'))
1275    def removePaymentTickets(self, **data):
1276        self.delPaymentTickets(**data)
1277        self.redirect(self.url(self.context) + '/@@edit')
1278        return
1279
1280    @action(_('Save'), style='primary')
1281    def save(self, **data):
1282        if self.upload_success is False:  # False is not None!
1283            # Error during image upload. Ignore other values.
1284            return
1285        self.applyData(self.context, **data)
1286        self.flash(_('Form has been saved.'))
1287        return
1288
1289    def informReferees(self):
1290        site = grok.getSite()
1291        kofa_utils = getUtility(IKofaUtils)
1292        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1293        failed = ''
1294        emails_sent = 0
1295        for referee in self.context.referees:
1296            if referee.email_sent:
1297                continue
1298            mandate = RefereeReportMandate(days=self.mandate_days)
1299            mandate.params['name'] = referee.name
1300            mandate.params['email'] = referee.email
1301            mandate.params[
1302                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1303                    self.context.__parent__.code,
1304                    self.context.application_number)
1305            mandate.params['redirect_path2'] = ''
1306            mandate.params['applicant_id'] = self.context.applicant_id
1307            site['mandates'].addMandate(mandate)
1308            # Send invitation email
1309            args = {'mandate_id':mandate.mandate_id}
1310            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1311            url_info = u'Report link: %s' % mandate_url
1312            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1313            if success:
1314                emails_sent += 1
1315                self.context.writeLogMessage(
1316                    self, 'email sent: %s' % referee.email)
1317                referee.email_sent = True
1318            else:
1319                failed += '%s ' % referee.email
1320        return failed, emails_sent
1321
1322    @action(_('Finally Submit'), warning=WARNING)
1323    def finalsubmit(self, **data):
1324        if self.upload_success is False:  # False is not None!
1325            return # error during image upload. Ignore other values
1326        dnt = self.dataNotComplete(data)
1327        if dnt:
1328            self.flash(dnt, type='danger')
1329            return
1330        self.applyData(self.context, **data)
1331        state = IWorkflowState(self.context).getState()
1332        # This shouldn't happen, but the application officer
1333        # might have forgotten to lock the form after changing the state
1334        if state != self.submit_state:
1335            self.flash(_('The form cannot be submitted. Wrong state!'),
1336                       type='danger')
1337            return
1338        msg = _('Form has been submitted.')
1339        # Create mandates and send emails to referees
1340        if getattr(self.context, 'referees', None):
1341            failed, emails_sent = self.informReferees()
1342            if failed:
1343                self.flash(
1344                    _('Some invitation emails could not be sent:') + failed,
1345                    type='danger')
1346                return
1347            msg = _('Form has been successfully submitted and '
1348                    '${a} invitation emails were sent.',
1349                    mapping = {'a':  emails_sent})
1350        IWorkflowInfo(self.context).fireTransition('submit')
1351        # application_date is used in export files for sorting.
1352        # We can thus store utc.
1353        self.context.application_date = datetime.utcnow()
1354        self.flash(msg)
1355        self.redirect(self.url(self.context))
1356        return
1357
1358class PassportImage(grok.View):
1359    """Renders the passport image for applicants.
1360    """
1361    grok.name('passport.jpg')
1362    grok.context(IApplicant)
1363    grok.require('waeup.viewApplication')
1364
1365    def render(self):
1366        # A filename chooser turns a context into a filename suitable
1367        # for file storage.
1368        image = getUtility(IExtFileStore).getFileByContext(self.context)
1369        self.response.setHeader('Content-Type', 'image/jpeg')
1370        if image is None:
1371            # show placeholder image
1372            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1373        return image
1374
1375class PassportImageForReport(PassportImage):
1376    """Renders the passport image for applicants for referee reports.
1377    """
1378    grok.name('passport_for_report.jpg')
1379    grok.context(IApplicant)
1380    grok.require('waeup.Public')
1381
1382    def render(self):
1383        # Check mandate
1384        form = self.request.form
1385        self.mandate_id = form.get('mandate_id', None)
1386        self.mandates = grok.getSite()['mandates']
1387        mandate = self.mandates.get(self.mandate_id, None)
1388        if mandate is None:
1389            self.flash(_('No mandate.'), type='warning')
1390            self.redirect(self.application_url())
1391            return
1392        if mandate:
1393            # Check the mandate expiration date after redirect again
1394            if mandate.expires < datetime.utcnow():
1395                self.flash(_('Mandate expired.'),
1396                           type='warning')
1397                self.redirect(self.application_url())
1398                return
1399            # Check if mandate allows access
1400            if mandate.params.get('applicant_id') != self.context.applicant_id:
1401                self.flash(_('Wrong mandate.'),
1402                           type='warning')
1403                self.redirect(self.application_url())
1404                return
1405            return super(PassportImageForReport, self).render()
1406        return
1407
1408class ApplicantRegistrationPage(KofaAddFormPage):
1409    """Captcha'd registration page for applicants.
1410    """
1411    grok.context(IApplicantsContainer)
1412    grok.name('register')
1413    grok.require('waeup.Anonymous')
1414    grok.template('applicantregister')
1415
1416    @property
1417    def form_fields(self):
1418        form_fields = None
1419        if self.context.mode == 'update':
1420            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1421                'lastname','reg_number','email')
1422        else: #if self.context.mode == 'create':
1423            form_fields = grok.AutoFields(IApplicantEdit).select(
1424                'firstname', 'middlename', 'lastname', 'email', 'phone')
1425        return form_fields
1426
1427    @property
1428    def label(self):
1429        return _('Apply for ${a}',
1430            mapping = {'a':self.context.title})
1431
1432    def update(self):
1433        if self.context.expired:
1434            self.flash(_('Outside application period.'), type='warning')
1435            self.redirect(self.url(self.context))
1436            return
1437        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1438        if blocker:
1439            self.flash(_('The portal is in maintenance mode '
1440                        'and registration temporarily disabled.'),
1441                       type='warning')
1442            self.redirect(self.url(self.context))
1443            return
1444        # Handle captcha
1445        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1446        self.captcha_result = self.captcha.verify(self.request)
1447        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1448        return
1449
1450    def _redirect(self, email, password, applicant_id):
1451        # Forward only email to landing page in base package.
1452        self.redirect(self.url(self.context, 'registration_complete',
1453            data = dict(email=email)))
1454        return
1455
1456    @property
1457    def _postfix(self):
1458        """In customized packages we can add a container dependent string if
1459        applicants have been imported into several containers.
1460        """
1461        return ''
1462
1463    @action(_('Send login credentials to email address'), style='primary')
1464    def register(self, **data):
1465        if not self.captcha_result.is_valid:
1466            # Captcha will display error messages automatically.
1467            # No need to flash something.
1468            return
1469        if self.context.mode == 'create':
1470            # Check if there are unused records in this container which
1471            # can be taken
1472            applicant = self.context.first_unused
1473            if applicant is None:
1474                # Add applicant
1475                applicant = createObject(u'waeup.Applicant')
1476                self.context.addApplicant(applicant)
1477            else:
1478                applicants_root = grok.getSite()['applicants']
1479                ob_class = self.__implemented__.__name__.replace(
1480                    'waeup.kofa.','')
1481                applicants_root.logger.info('%s - used: %s' % (
1482                    ob_class, applicant.applicant_id))
1483            self.applyData(applicant, **data)
1484            # applicant.reg_number = applicant.applicant_id
1485            notify(grok.ObjectModifiedEvent(applicant))
1486        elif self.context.mode == 'update':
1487            # Update applicant
1488            reg_number = data.get('reg_number','')
1489            lastname = data.get('lastname','')
1490            cat = getUtility(ICatalog, name='applicants_catalog')
1491            searchstr = reg_number + self._postfix
1492            results = list(
1493                cat.searchResults(reg_number=(searchstr, searchstr)))
1494            if results:
1495                applicant = results[0]
1496                if getattr(applicant,'lastname',None) is None:
1497                    self.flash(_('An error occurred.'), type='danger')
1498                    return
1499                elif applicant.lastname.lower() != lastname.lower():
1500                    # Don't tell the truth here. Anonymous must not
1501                    # know that a record was found and only the lastname
1502                    # verification failed.
1503                    self.flash(
1504                        _('No application record found.'), type='warning')
1505                    return
1506                elif applicant.password is not None and \
1507                    applicant.state != INITIALIZED:
1508                    self.flash(_('Your password has already been set and used. '
1509                                 'Please proceed to the login page.'),
1510                               type='warning')
1511                    return
1512                # Store email address but nothing else.
1513                applicant.email = data['email']
1514                notify(grok.ObjectModifiedEvent(applicant))
1515            else:
1516                # No record found, this is the truth.
1517                self.flash(_('No application record found.'), type='warning')
1518                return
1519        else:
1520            # Does not happen but anyway ...
1521            return
1522        kofa_utils = getUtility(IKofaUtils)
1523        password = kofa_utils.genPassword()
1524        IUserAccount(applicant).setPassword(password)
1525        # Send email with credentials
1526        login_url = self.url(grok.getSite(), 'login')
1527        url_info = u'Login: %s' % login_url
1528        msg = _('You have successfully been registered for the')
1529        if kofa_utils.sendCredentials(IUserAccount(applicant),
1530            password, url_info, msg):
1531            email_sent = applicant.email
1532        else:
1533            email_sent = None
1534        self._redirect(email=email_sent, password=password,
1535            applicant_id=applicant.applicant_id)
1536        return
1537
1538class ApplicantRegistrationEmailSent(KofaPage):
1539    """Landing page after successful registration.
1540
1541    """
1542    grok.name('registration_complete')
1543    grok.require('waeup.Public')
1544    grok.template('applicantregemailsent')
1545    label = _('Your registration was successful.')
1546
1547    def update(self, email=None, applicant_id=None, password=None):
1548        self.email = email
1549        self.password = password
1550        self.applicant_id = applicant_id
1551        return
1552
1553class ApplicantCheckStatusPage(KofaPage):
1554    """Captcha'd status checking page for applicants.
1555    """
1556    grok.context(IApplicantsRoot)
1557    grok.name('checkstatus')
1558    grok.require('waeup.Anonymous')
1559    grok.template('applicantcheckstatus')
1560    buttonname = _('Submit')
1561
1562    def label(self):
1563        if self.result:
1564            return _('Admission status of ${a}',
1565                     mapping = {'a':self.applicant.applicant_id})
1566        return _('Check your admission status')
1567
1568    def update(self, SUBMIT=None):
1569        form = self.request.form
1570        self.result = False
1571        # Handle captcha
1572        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1573        self.captcha_result = self.captcha.verify(self.request)
1574        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1575        if SUBMIT:
1576            if not self.captcha_result.is_valid:
1577                # Captcha will display error messages automatically.
1578                # No need to flash something.
1579                return
1580            unique_id = form.get('unique_id', None)
1581            lastname = form.get('lastname', None)
1582            if not unique_id or not lastname:
1583                self.flash(
1584                    _('Required input missing.'), type='warning')
1585                return
1586            cat = getUtility(ICatalog, name='applicants_catalog')
1587            results = list(
1588                cat.searchResults(applicant_id=(unique_id, unique_id)))
1589            if not results:
1590                results = list(
1591                    cat.searchResults(reg_number=(unique_id, unique_id)))
1592            if results:
1593                applicant = results[0]
1594                if applicant.lastname.lower().strip() != lastname.lower():
1595                    # Don't tell the truth here. Anonymous must not
1596                    # know that a record was found and only the lastname
1597                    # verification failed.
1598                    self.flash(
1599                        _('No application record found.'), type='warning')
1600                    return
1601            else:
1602                self.flash(_('No application record found.'), type='warning')
1603                return
1604            self.applicant = applicant
1605            self.entry_session = "%s/%s" % (
1606                applicant.__parent__.year,
1607                applicant.__parent__.year+1)
1608            course_admitted = getattr(applicant, 'course_admitted', None)
1609            self.course_admitted = False
1610            if course_admitted is not None:
1611                try:
1612                    self.course_admitted = True
1613                    self.longtitle = course_admitted.longtitle
1614                    self.department = course_admitted.__parent__.__parent__.longtitle
1615                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1616                except AttributeError:
1617                    self.flash(_('Application record invalid.'), type='warning')
1618                    return
1619            self.result = True
1620            self.admitted = False
1621            self.not_admitted = False
1622            self.submitted = False
1623            self.not_submitted = False
1624            self.created = False
1625            if applicant.state in (ADMITTED, CREATED):
1626                self.admitted = True
1627            if applicant.state in (CREATED):
1628                self.created = True
1629                self.student_id = applicant.student_id
1630                self.password = applicant.application_number
1631            if applicant.state in (NOT_ADMITTED,):
1632                self.not_admitted = True
1633            if applicant.state in (SUBMITTED,):
1634                self.submitted = True
1635            if applicant.state in (INITIALIZED, STARTED, PAID):
1636                self.not_submitted = True
1637        return
1638
1639class ExportJobContainerOverview(KofaPage):
1640    """Page that lists active applicant data export jobs and provides links
1641    to discard or download CSV files.
1642
1643    """
1644    grok.context(VirtualApplicantsExportJobContainer)
1645    grok.require('waeup.manageApplication')
1646    grok.name('index.html')
1647    grok.template('exportjobsindex')
1648    label = _('Data Exports')
1649    pnav = 3
1650
1651    def update(self, CREATE=None, DISCARD=None, job_id=None):
1652        if CREATE:
1653            self.redirect(self.url('@@start_export'))
1654            return
1655        if DISCARD and job_id:
1656            entry = self.context.entry_from_job_id(job_id)
1657            self.context.delete_export_entry(entry)
1658            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1659            self.context.logger.info(
1660                '%s - discarded: job_id=%s' % (ob_class, job_id))
1661            self.flash(_('Discarded export') + ' %s' % job_id)
1662        self.entries = doll_up(self, user=self.request.principal.id)
1663        return
1664
1665class ExportJobContainerJobStart(UtilityView, grok.View):
1666    """View that starts three export jobs, one for applicants, a second
1667    one for applicant payments and a third for referee reports.
1668    """
1669    grok.context(VirtualApplicantsExportJobContainer)
1670    grok.require('waeup.manageApplication')
1671    grok.name('start_export')
1672
1673    def update(self):
1674        utils = queryUtility(IKofaUtils)
1675        if not utils.expensive_actions_allowed():
1676            self.flash(_(
1677                "Currently, exporters cannot be started due to high "
1678                "system load. Please try again later."), type='danger')
1679            self.entries = doll_up(self, user=None)
1680            return
1681
1682        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1683        container_code = self.context.__parent__.code
1684        # Start first exporter
1685        for exporter in ('applicants',
1686                         'applicantpayments',
1687                         'applicantrefereereports'):
1688            job_id = self.context.start_export_job(exporter,
1689                                          self.request.principal.id,
1690                                          container=container_code)
1691            self.context.logger.info(
1692                '%s - exported: %s (%s), job_id=%s'
1693                % (ob_class, exporter, container_code, job_id))
1694            # Commit transaction so that job is stored in the ZODB
1695            transaction.commit()
1696        self.flash(_('Exports started.'))
1697        self.redirect(self.url(self.context))
1698        return
1699
1700    def render(self):
1701        return
1702
1703class ExportJobContainerDownload(ExportCSVView):
1704    """Page that downloads a students export csv file.
1705
1706    """
1707    grok.context(VirtualApplicantsExportJobContainer)
1708    grok.require('waeup.manageApplication')
1709
1710class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1711    """A display view for referee reports.
1712    """
1713    grok.context(IApplicantRefereeReport)
1714    grok.name('index')
1715    grok.require('waeup.manageApplication')
1716    label = _('Referee Report')
1717    pnav = 3
1718    form_fields = grok.AutoFields(IApplicantRefereeReport)
1719    form_fields[
1720        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1721
1722class RemoveRefereeReportPage(UtilityView, grok.View):
1723    """
1724    """
1725    grok.context(IApplicantRefereeReport)
1726    grok.name('remove')
1727    grok.require('waeup.manageApplication')
1728
1729    def update(self):
1730        redirect_url = self.url(self.context.__parent__)
1731        self.context.__parent__.writeLogMessage(
1732            self, 'removed: %s' % self.context.r_id)
1733        del self.context.__parent__[self.context.r_id]
1734        self.flash(_('Referee report removed.'))
1735        self.redirect(redirect_url)
1736        return
1737
1738    def render(self):
1739        return
1740
1741class RefereeReportAddFormPage(KofaAddFormPage):
1742    """Add-form to add an referee report. This form
1743    is protected by a mandate.
1744    """
1745    grok.context(IApplicant)
1746    grok.require('waeup.Public')
1747    grok.name('addrefereereport')
1748    form_fields = grok.AutoFields(
1749        IApplicantRefereeReport).omit('creation_date')
1750    grok.template('refereereportpage')
1751    label = _('Referee Report Form')
1752    pnav = 3
1753    #doclink = DOCLINK + '/refereereports.html'
1754
1755    def update(self):
1756        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1757        if blocker:
1758            self.flash(_('The portal is in maintenance mode. '
1759                        'Referee report forms are temporarily disabled.'),
1760                       type='warning')
1761            self.redirect(self.application_url())
1762            return
1763        # Check mandate
1764        form = self.request.form
1765        self.mandate_id = form.get('mandate_id', None)
1766        self.mandates = grok.getSite()['mandates']
1767        mandate = self.mandates.get(self.mandate_id, None)
1768        if mandate is None and not self.request.form.get('form.actions.submit'):
1769            self.flash(_('No mandate.'), type='warning')
1770            self.redirect(self.application_url())
1771            return
1772        if mandate:
1773            # Check the mandate expiration date after redirect again
1774            if mandate.expires < datetime.utcnow():
1775                self.flash(_('Mandate expired.'),
1776                           type='warning')
1777                self.redirect(self.application_url())
1778                return
1779            args = {'mandate_id':mandate.mandate_id}
1780            # Check if report exists.
1781            # If so, redirect to the pdf file.
1782            if mandate.params.get('redirect_path2'):
1783                self.redirect(
1784                    self.application_url() +
1785                    mandate.params.get('redirect_path2') +
1786                    '?%s' % urlencode(args))
1787                return
1788            # Prefill form with mandate params
1789            self.form_fields.get(
1790                'name').field.default = mandate.params['name']
1791            self.form_fields.get(
1792                'email').field.default = mandate.params['email']
1793            self.passport_url = self.url(
1794                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
1795        super(RefereeReportAddFormPage, self).update()
1796        return
1797
1798    @action(_('Submit'),
1799              warning=_('Are you really sure? '
1800                        'Reports can neither be modified or added '
1801                        'after submission.'),
1802              style='primary')
1803    def addRefereeReport(self, **data):
1804        report = createObject(u'waeup.ApplicantRefereeReport')
1805        timestamp = ("%d" % int(time()*10000))[1:]
1806        report.r_id = "r%s" % timestamp
1807        self.applyData(report, **data)
1808        self.context[report.r_id] = report
1809        # self.flash(_('Referee report has been saved. Thank you!'))
1810        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1811        # Changed on 19/04/20: We do no longer delete the mandate
1812        # but set path to redirect to the pdf file
1813        self.mandates[self.mandate_id].params[
1814            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
1815                self.context.__parent__.code,
1816                self.context.application_number,
1817                report.r_id)
1818        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
1819        args = {'mandate_id':self.mandate_id}
1820        self.redirect(self.url(report, 'referee_report.pdf')
1821                      + '?%s' % urlencode(args))
1822        return
1823
1824class ExportPDFReportSlipPage(UtilityView, grok.View):
1825    """Deliver a PDF slip of the context.
1826    """
1827    grok.context(IApplicantRefereeReport)
1828    grok.name('referee_report_slip.pdf')
1829    grok.require('waeup.manageApplication')
1830    form_fields = grok.AutoFields(IApplicantRefereeReport)
1831    form_fields[
1832        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1833    #prefix = 'form'
1834    note = None
1835
1836    @property
1837    def title(self):
1838        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1839        return translate(_('Referee Report'), 'waeup.kofa',
1840            target_language=portal_language)
1841
1842    @property
1843    def label(self):
1844        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1845        return translate(_('Referee Report Slip'),
1846            'waeup.kofa', target_language=portal_language) \
1847            + ' %s' % self.context.r_id
1848
1849    def render(self):
1850        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
1851            self.request)
1852        students_utils = getUtility(IStudentsUtils)
1853        return students_utils.renderPDF(self,'referee_report_slip.pdf',
1854            self.context.__parent__, applicantview, note=self.note)
1855
1856class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
1857    """Deliver a PDF slip of the context to referees.
1858    """
1859    grok.name('referee_report.pdf')
1860    grok.require('waeup.Public')
1861
1862    def update(self):
1863        # Check mandate
1864        form = self.request.form
1865        self.mandate_id = form.get('mandate_id', None)
1866        self.mandates = grok.getSite()['mandates']
1867        mandate = self.mandates.get(self.mandate_id, None)
1868        if mandate is None:
1869            self.flash(_('No mandate.'), type='warning')
1870            self.redirect(self.application_url())
1871            return
1872        if mandate:
1873            # Check the mandate expiration date after redirect again
1874            if mandate.expires < datetime.utcnow():
1875                self.flash(_('Mandate expired.'),
1876                           type='warning')
1877                self.redirect(self.application_url())
1878                return
1879            # Check if form has really been submitted
1880            if not mandate.params.get('redirect_path2') \
1881                or mandate.params.get(
1882                    'applicant_id') != self.context.__parent__.applicant_id:
1883                self.flash(_('Wrong mandate.'),
1884                           type='warning')
1885                self.redirect(self.application_url())
1886                return
1887            super(ExportPDFReportSlipPage2, self).update()
1888        return
1889
1890class AdditionalFile(grok.View):
1891    """Renders additional pdf files for applicants.
1892    This is a baseclass.
1893    """
1894    grok.baseclass()
1895    grok.context(IApplicant)
1896    grok.require('waeup.viewApplication')
1897
1898    def render(self):
1899        pdf = getUtility(IExtFileStore).getFileByContext(
1900            self.context, attr=self.__name__)
1901        self.response.setHeader('Content-Type', 'application/pdf')
1902        return pdf
1903
1904class TestFile(AdditionalFile):
1905    """Renders testfile.pdf.
1906    """
1907    grok.name('testfile.pdf')
Note: See TracBrowser for help on using the repository browser.