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

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

Make further adjustments for fileupload in the applicants section.

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