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

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

Switch test URL.

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