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

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

Increase mandate expiration time.

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