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

Last change on this file since 16094 was 16094, checked in by Henrik Bettermann, 4 years ago

ApplicantCheckStatusPage? must get its own pnav value.

  • Property svn:keywords set to Id
File size: 71.6 KB
Line 
1## $Id: browser.py 16094 2020-05-25 10:00:48Z 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    pnav = 6
1577
1578    def label(self):
1579        if self.result:
1580            return _('Admission status of ${a}',
1581                     mapping = {'a':self.applicant.applicant_id})
1582        return _('Check your admission status')
1583
1584    def update(self, SUBMIT=None):
1585        form = self.request.form
1586        self.result = False
1587        # Handle captcha
1588        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1589        self.captcha_result = self.captcha.verify(self.request)
1590        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1591        if SUBMIT:
1592            if not self.captcha_result.is_valid:
1593                # Captcha will display error messages automatically.
1594                # No need to flash something.
1595                return
1596            unique_id = form.get('unique_id', None)
1597            lastname = form.get('lastname', None)
1598            if not unique_id or not lastname:
1599                self.flash(
1600                    _('Required input missing.'), type='warning')
1601                return
1602            cat = getUtility(ICatalog, name='applicants_catalog')
1603            results = list(
1604                cat.searchResults(applicant_id=(unique_id, unique_id)))
1605            if not results:
1606                results = list(
1607                    cat.searchResults(reg_number=(unique_id, unique_id)))
1608            if results:
1609                applicant = results[0]
1610                if applicant.lastname.lower().strip() != lastname.lower():
1611                    # Don't tell the truth here. Anonymous must not
1612                    # know that a record was found and only the lastname
1613                    # verification failed.
1614                    self.flash(
1615                        _('No application record found.'), type='warning')
1616                    return
1617            else:
1618                self.flash(_('No application record found.'), type='warning')
1619                return
1620            self.applicant = applicant
1621            self.entry_session = "%s/%s" % (
1622                applicant.__parent__.year,
1623                applicant.__parent__.year+1)
1624            course_admitted = getattr(applicant, 'course_admitted', None)
1625            self.course_admitted = False
1626            if course_admitted is not None:
1627                try:
1628                    self.course_admitted = True
1629                    self.longtitle = course_admitted.longtitle
1630                    self.department = course_admitted.__parent__.__parent__.longtitle
1631                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1632                except AttributeError:
1633                    self.flash(_('Application record invalid.'), type='warning')
1634                    return
1635            self.result = True
1636            self.admitted = False
1637            self.not_admitted = False
1638            self.submitted = False
1639            self.not_submitted = False
1640            self.created = False
1641            if applicant.state in (ADMITTED, CREATED):
1642                self.admitted = True
1643            if applicant.state in (CREATED):
1644                self.created = True
1645                self.student_id = applicant.student_id
1646                self.password = applicant.application_number
1647            if applicant.state in (NOT_ADMITTED,):
1648                self.not_admitted = True
1649            if applicant.state in (SUBMITTED,):
1650                self.submitted = True
1651            if applicant.state in (INITIALIZED, STARTED, PAID):
1652                self.not_submitted = True
1653        return
1654
1655class ExportJobContainerOverview(KofaPage):
1656    """Page that lists active applicant data export jobs and provides links
1657    to discard or download CSV files.
1658
1659    """
1660    grok.context(VirtualApplicantsExportJobContainer)
1661    grok.require('waeup.manageApplication')
1662    grok.name('index.html')
1663    grok.template('exportjobsindex')
1664    label = _('Data Exports')
1665    pnav = 3
1666
1667    def update(self, CREATE=None, DISCARD=None, job_id=None):
1668        if CREATE:
1669            self.redirect(self.url('@@start_export'))
1670            return
1671        if DISCARD and job_id:
1672            entry = self.context.entry_from_job_id(job_id)
1673            self.context.delete_export_entry(entry)
1674            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1675            self.context.logger.info(
1676                '%s - discarded: job_id=%s' % (ob_class, job_id))
1677            self.flash(_('Discarded export') + ' %s' % job_id)
1678        self.entries = doll_up(self, user=self.request.principal.id)
1679        return
1680
1681class ExportJobContainerJobStart(UtilityView, grok.View):
1682    """View that starts three export jobs, one for applicants, a second
1683    one for applicant payments and a third for referee reports.
1684    """
1685    grok.context(VirtualApplicantsExportJobContainer)
1686    grok.require('waeup.manageApplication')
1687    grok.name('start_export')
1688
1689    def update(self):
1690        utils = queryUtility(IKofaUtils)
1691        if not utils.expensive_actions_allowed():
1692            self.flash(_(
1693                "Currently, exporters cannot be started due to high "
1694                "system load. Please try again later."), type='danger')
1695            self.entries = doll_up(self, user=None)
1696            return
1697
1698        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1699        container_code = self.context.__parent__.code
1700        # Start first exporter
1701        for exporter in ('applicants',
1702                         'applicantpayments',
1703                         'applicantrefereereports'):
1704            job_id = self.context.start_export_job(exporter,
1705                                          self.request.principal.id,
1706                                          container=container_code)
1707            self.context.logger.info(
1708                '%s - exported: %s (%s), job_id=%s'
1709                % (ob_class, exporter, container_code, job_id))
1710            # Commit transaction so that job is stored in the ZODB
1711            transaction.commit()
1712        self.flash(_('Exports started.'))
1713        self.redirect(self.url(self.context))
1714        return
1715
1716    def render(self):
1717        return
1718
1719class ExportJobContainerDownload(ExportCSVView):
1720    """Page that downloads a students export csv file.
1721
1722    """
1723    grok.context(VirtualApplicantsExportJobContainer)
1724    grok.require('waeup.manageApplication')
1725
1726class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1727    """A display view for referee reports.
1728    """
1729    grok.context(IApplicantRefereeReport)
1730    grok.name('index')
1731    grok.require('waeup.manageApplication')
1732    label = _('Referee Report')
1733    pnav = 3
1734    form_fields = grok.AutoFields(IApplicantRefereeReport)
1735    form_fields[
1736        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1737
1738class RemoveRefereeReportPage(UtilityView, grok.View):
1739    """
1740    """
1741    grok.context(IApplicantRefereeReport)
1742    grok.name('remove')
1743    grok.require('waeup.manageApplication')
1744
1745    def update(self):
1746        redirect_url = self.url(self.context.__parent__)
1747        self.context.__parent__.writeLogMessage(
1748            self, 'removed: %s' % self.context.r_id)
1749        del self.context.__parent__[self.context.r_id]
1750        self.flash(_('Referee report removed.'))
1751        self.redirect(redirect_url)
1752        return
1753
1754    def render(self):
1755        return
1756
1757class RefereeReportAddFormPage(KofaAddFormPage):
1758    """Add-form to add an referee report. This form
1759    is protected by a mandate.
1760    """
1761    grok.context(IApplicant)
1762    grok.require('waeup.Public')
1763    grok.name('addrefereereport')
1764    form_fields = grok.AutoFields(
1765        IApplicantRefereeReport).omit('creation_date')
1766    grok.template('refereereportpage')
1767    label = _('Referee Report Form')
1768    pnav = 3
1769    #doclink = DOCLINK + '/refereereports.html'
1770
1771    def update(self):
1772        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1773        if blocker:
1774            self.flash(_('The portal is in maintenance mode. '
1775                        'Referee report forms are temporarily disabled.'),
1776                       type='warning')
1777            self.redirect(self.application_url())
1778            return
1779        # Check mandate
1780        form = self.request.form
1781        self.mandate_id = form.get('mandate_id', None)
1782        self.mandates = grok.getSite()['mandates']
1783        mandate = self.mandates.get(self.mandate_id, None)
1784        if mandate is None and not self.request.form.get('form.actions.submit'):
1785            self.flash(_('No mandate.'), type='warning')
1786            self.redirect(self.application_url())
1787            return
1788        if mandate:
1789            # Check the mandate expiration date after redirect again
1790            if mandate.expires < datetime.utcnow():
1791                self.flash(_('Mandate expired.'),
1792                           type='warning')
1793                self.redirect(self.application_url())
1794                return
1795            args = {'mandate_id':mandate.mandate_id}
1796            # Check if report exists.
1797            # If so, redirect to the pdf file.
1798            if mandate.params.get('redirect_path2'):
1799                self.redirect(
1800                    self.application_url() +
1801                    mandate.params.get('redirect_path2') +
1802                    '?%s' % urlencode(args))
1803                return
1804            # Prefill form with mandate params
1805            self.form_fields.get(
1806                'name').field.default = mandate.params['name']
1807            self.form_fields.get(
1808                'email').field.default = mandate.params['email']
1809            self.passport_url = self.url(
1810                self.context, 'passport_for_report.jpg') + '?%s' % urlencode(args)
1811        super(RefereeReportAddFormPage, self).update()
1812        return
1813
1814    @action(_('Submit'),
1815              warning=_('Are you really sure? '
1816                        'Reports can neither be modified or added '
1817                        'after submission.'),
1818              style='primary')
1819    def addRefereeReport(self, **data):
1820        report = createObject(u'waeup.ApplicantRefereeReport')
1821        timestamp = ("%d" % int(time()*10000))[1:]
1822        report.r_id = "r%s" % timestamp
1823        self.applyData(report, **data)
1824        self.context[report.r_id] = report
1825        # self.flash(_('Referee report has been saved. Thank you!'))
1826        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1827        # Changed on 19/04/20: We do no longer delete the mandate
1828        # but set path to redirect to the pdf file
1829        self.mandates[self.mandate_id].params[
1830            'redirect_path2'] = '/applicants/%s/%s/%s/referee_report.pdf' % (
1831                self.context.__parent__.code,
1832                self.context.application_number,
1833                report.r_id)
1834        notify(grok.ObjectModifiedEvent(self.mandates[self.mandate_id]))
1835        args = {'mandate_id':self.mandate_id}
1836        self.redirect(self.url(report, 'referee_report.pdf')
1837                      + '?%s' % urlencode(args))
1838        return
1839
1840class ExportPDFReportSlipPage(UtilityView, grok.View):
1841    """Deliver a PDF slip of the context.
1842    """
1843    grok.context(IApplicantRefereeReport)
1844    grok.name('referee_report_slip.pdf')
1845    grok.require('waeup.manageApplication')
1846    form_fields = grok.AutoFields(IApplicantRefereeReport)
1847    form_fields[
1848        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1849    #prefix = 'form'
1850    note = None
1851
1852    @property
1853    def title(self):
1854        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1855        return translate(_('Referee Report'), 'waeup.kofa',
1856            target_language=portal_language)
1857
1858    @property
1859    def label(self):
1860        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1861        return translate(_('Referee Report Slip'),
1862            'waeup.kofa', target_language=portal_language) \
1863            + ' %s' % self.context.r_id
1864
1865    def render(self):
1866        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
1867            self.request)
1868        students_utils = getUtility(IStudentsUtils)
1869        return students_utils.renderPDF(self,'referee_report_slip.pdf',
1870            self.context.__parent__, applicantview, note=self.note)
1871
1872class ExportPDFReportSlipPage2(ExportPDFReportSlipPage):
1873    """Deliver a PDF slip of the context to referees.
1874    """
1875    grok.name('referee_report.pdf')
1876    grok.require('waeup.Public')
1877
1878    def update(self):
1879        # Check mandate
1880        form = self.request.form
1881        self.mandate_id = form.get('mandate_id', None)
1882        self.mandates = grok.getSite()['mandates']
1883        mandate = self.mandates.get(self.mandate_id, None)
1884        if mandate is None:
1885            self.flash(_('No mandate.'), type='warning')
1886            self.redirect(self.application_url())
1887            return
1888        if mandate:
1889            # Check the mandate expiration date after redirect again
1890            if mandate.expires < datetime.utcnow():
1891                self.flash(_('Mandate expired.'),
1892                           type='warning')
1893                self.redirect(self.application_url())
1894                return
1895            # Check if form has really been submitted
1896            if not mandate.params.get('redirect_path2') \
1897                or mandate.params.get(
1898                    'applicant_id') != self.context.__parent__.applicant_id:
1899                self.flash(_('Wrong mandate.'),
1900                           type='warning')
1901                self.redirect(self.application_url())
1902                return
1903            super(ExportPDFReportSlipPage2, self).update()
1904        return
1905
1906class AdditionalFile(grok.View):
1907    """Renders additional pdf files for applicants.
1908    This is a baseclass.
1909    """
1910    grok.baseclass()
1911    grok.context(IApplicant)
1912    grok.require('waeup.viewApplication')
1913
1914    def render(self):
1915        pdf = getUtility(IExtFileStore).getFileByContext(
1916            self.context, attr=self.__name__)
1917        self.response.setHeader('Content-Type', 'application/pdf')
1918        return pdf
1919
1920class TestFile(AdditionalFile):
1921    """Renders testfile.pdf.
1922    """
1923    grok.name('testfile.pdf')
Note: See TracBrowser for help on using the repository browser.