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

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

Import missing escape function.

  • Property svn:keywords set to Id
File size: 62.2 KB
Line 
1## $Id: browser.py 15584 2019-09-11 07:21:28Z 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    MAX_UPLOAD_SIZE, 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')
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 display_payments(self):
591        if self.context.payments:
592            return True
593        if self.context.special:
594            return True
595        return getattr(self.context.__parent__, 'application_fee', None)
596
597    @property
598    def form_fields(self):
599        if self.context.special:
600            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
601        else:
602            form_fields = grok.AutoFields(IApplicant).omit(
603                'locked', 'course_admitted', 'password', 'suspended')
604        return form_fields
605
606    @property
607    def target(self):
608        return getattr(self.context.__parent__, 'prefix', None)
609
610    @property
611    def separators(self):
612        return getUtility(IApplicantsUtils).SEPARATORS_DICT
613
614    def update(self):
615        self.passport_url = self.url(self.context, 'passport.jpg')
616        # Mark application as started if applicant logs in for the first time
617        usertype = getattr(self.request.principal, 'user_type', None)
618        if usertype == 'applicant' and \
619            IWorkflowState(self.context).getState() == INITIALIZED:
620            IWorkflowInfo(self.context).fireTransition('start')
621        if usertype == 'applicant' and self.context.state == 'created':
622            session = '%s/%s' % (self.context.__parent__.year,
623                                 self.context.__parent__.year+1)
624            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
625            msg = _(
626                '\n <strong>Congratulations!</strong>' +
627                ' You have been offered provisional admission into the' +
628                ' ${c} Academic Session of ${d}.'
629                ' Your student record has been created for you.' +
630                ' Please, logout again and proceed to the' +
631                ' login page of the portal.'
632                ' Then enter your new student credentials:' +
633                ' user name= ${a}, password = ${b}.' +
634                ' Change your password when you have logged in.',
635                mapping = {
636                    'a':self.context.student_id,
637                    'b':self.context.application_number,
638                    'c':session,
639                    'd':title}
640                )
641            self.flash(msg)
642        return
643
644    @property
645    def hasPassword(self):
646        if self.context.password:
647            return _('set')
648        return _('unset')
649
650    @property
651    def label(self):
652        container_title = self.context.__parent__.title
653        return _('${a} <br /> Application Record ${b}', mapping = {
654            'a':container_title, 'b':self.context.application_number})
655
656    def getCourseAdmitted(self):
657        """Return link, title and code in html format to the certificate
658           admitted.
659        """
660        course_admitted = self.context.course_admitted
661        if getattr(course_admitted, '__parent__',None):
662            url = self.url(course_admitted)
663            title = course_admitted.title
664            code = course_admitted.code
665            return '<a href="%s">%s - %s</a>' %(url,code,title)
666        return ''
667
668class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
669    grok.context(IApplicant)
670    grok.name('base')
671    @property
672    def form_fields(self):
673        form_fields = grok.AutoFields(IApplicant).select(
674            'applicant_id', 'reg_number', 'email', 'course1')
675        if self.context.__parent__.prefix in ('special',):
676            form_fields['reg_number'].field.title = u'Identification Number'
677            return form_fields
678        return form_fields
679
680class CreateStudentPage(UtilityView, grok.View):
681    """Create a student object from applicant data.
682    """
683    grok.context(IApplicant)
684    grok.name('createstudent')
685    grok.require('waeup.createStudents')
686
687    def update(self):
688        success, msg = self.context.createStudent(view=self)
689        if success:
690            self.flash(msg)
691        else:
692            self.flash(msg, type='warning')
693        self.redirect(self.url(self.context))
694        return
695
696    def render(self):
697        return
698
699class CreateAllStudentsPage(KofaPage):
700    """Create all student objects from applicant data
701    in the root container or in a specific applicants container only.
702    Only PortalManagers can do this.
703    """
704    #grok.context(IApplicantsContainer)
705    grok.name('createallstudents')
706    grok.require('waeup.createStudents')
707    label = _('Student Record Creation Report')
708
709    def update(self):
710        grok.getSite()['configuration'].maintmode_enabled_by = u'admin'
711        transaction.commit()
712        # Wait 10 seconds for all transactions to be finished.
713        # Do not wait in tests.
714        if not self.request.principal.id == 'zope.mgr':
715            sleep(10)
716        cat = getUtility(ICatalog, name='applicants_catalog')
717        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
718        created = []
719        failed = []
720        container_only = False
721        applicants_root = grok.getSite()['applicants']
722        if isinstance(self.context, ApplicantsContainer):
723            container_only = True
724        for result in results:
725            if container_only and result.__parent__ is not self.context:
726                continue
727            success, msg = result.createStudent(view=self)
728            if success:
729                created.append(result.applicant_id)
730            else:
731                failed.append(
732                    (result.applicant_id, self.url(result, 'manage'), msg))
733                ob_class = self.__implemented__.__name__.replace(
734                    'waeup.kofa.','')
735        grok.getSite()['configuration'].maintmode_enabled_by = None
736        self.successful = ', '.join(created)
737        self.failed = failed
738        return
739
740
741class ApplicationFeePaymentAddPage(UtilityView, grok.View):
742    """ Page to add an online payment ticket
743    """
744    grok.context(IApplicant)
745    grok.name('addafp')
746    grok.require('waeup.payApplicant')
747    factory = u'waeup.ApplicantOnlinePayment'
748
749    @property
750    def custom_requirements(self):
751        return ''
752
753    def update(self):
754        # Additional requirements in custom packages.
755        if self.custom_requirements:
756            self.flash(
757                self.custom_requirements,
758                type='danger')
759            self.redirect(self.url(self.context))
760            return
761        if not self.context.special:
762            for key in self.context.keys():
763                ticket = self.context[key]
764                if ticket.p_state == 'paid':
765                      self.flash(
766                          _('This type of payment has already been made.'),
767                          type='warning')
768                      self.redirect(self.url(self.context))
769                      return
770        applicants_utils = getUtility(IApplicantsUtils)
771        container = self.context.__parent__
772        payment = createObject(self.factory)
773        failure = applicants_utils.setPaymentDetails(
774            container, payment, self.context)
775        if failure is not None:
776            self.flash(failure, type='danger')
777            self.redirect(self.url(self.context))
778            return
779        self.context[payment.p_id] = payment
780        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
781        self.flash(_('Payment ticket created.'))
782        self.redirect(self.url(payment))
783        return
784
785    def render(self):
786        return
787
788
789class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
790    """ Page to view an online payment ticket
791    """
792    grok.context(IApplicantOnlinePayment)
793    grok.name('index')
794    grok.require('waeup.viewApplication')
795    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
796    form_fields[
797        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
798    form_fields[
799        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
800    pnav = 3
801
802    @property
803    def label(self):
804        return _('${a}: Online Payment Ticket ${b}', mapping = {
805            'a':self.context.__parent__.display_fullname,
806            'b':self.context.p_id})
807
808class OnlinePaymentApprovePage(UtilityView, grok.View):
809    """ Approval view
810    """
811    grok.context(IApplicantOnlinePayment)
812    grok.name('approve')
813    grok.require('waeup.managePortal')
814
815    def update(self):
816        flashtype, msg, log = self.context.approveApplicantPayment()
817        if log is not None:
818            applicant = self.context.__parent__
819            # Add log message to applicants.log
820            applicant.writeLogMessage(self, log)
821            # Add log message to payments.log
822            self.context.logger.info(
823                '%s,%s,%s,%s,%s,,,,,,' % (
824                applicant.applicant_id,
825                self.context.p_id, self.context.p_category,
826                self.context.amount_auth, self.context.r_code))
827        self.flash(msg, type=flashtype)
828        return
829
830    def render(self):
831        self.redirect(self.url(self.context, '@@index'))
832        return
833
834class ExportPDFPaymentSlipPage(UtilityView, grok.View):
835    """Deliver a PDF slip of the context.
836    """
837    grok.context(IApplicantOnlinePayment)
838    grok.name('payment_slip.pdf')
839    grok.require('waeup.viewApplication')
840    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
841    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
842    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
843    prefix = 'form'
844    note = None
845
846    @property
847    def title(self):
848        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
849        return translate(_('Payment Data'), 'waeup.kofa',
850            target_language=portal_language)
851
852    @property
853    def label(self):
854        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
855        return translate(_('Online Payment Slip'),
856            'waeup.kofa', target_language=portal_language) \
857            + ' %s' % self.context.p_id
858
859    @property
860    def payment_slip_download_warning(self):
861        if self.context.__parent__.state not in (
862            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
863            return _('Please submit the application form before '
864                     'trying to download payment slips.')
865        return ''
866
867    def render(self):
868        if self.payment_slip_download_warning:
869            self.flash(self.payment_slip_download_warning, type='danger')
870            self.redirect(self.url(self.context))
871            return
872        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
873            self.request)
874        students_utils = getUtility(IStudentsUtils)
875        return students_utils.renderPDF(self,'payment_slip.pdf',
876            self.context.__parent__, applicantview, note=self.note)
877
878class ExportPDFPageApplicationSlip(UtilityView, grok.View):
879    """Deliver a PDF slip of the context.
880    """
881    grok.context(IApplicant)
882    grok.name('application_slip.pdf')
883    grok.require('waeup.viewApplication')
884    prefix = 'form'
885
886    def update(self):
887        if self.context.state in ('initialized', 'started', 'paid'):
888            self.flash(
889                _('Please pay and submit before trying to download '
890                  'the application slip.'), type='warning')
891            return self.redirect(self.url(self.context))
892        return
893
894    def render(self):
895        try:
896            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
897                view=self)
898        except IOError:
899            self.flash(
900                _('Your image file is corrupted. '
901                  'Please replace.'), type='danger')
902            return self.redirect(self.url(self.context))
903        except LayoutError, err:
904            self.flash(
905                'PDF file could not be created. Reportlab error message: %s'
906                % escape(err.message),
907                type="danger")
908            return self.redirect(self.url(self.context))
909        self.response.setHeader(
910            'Content-Type', 'application/pdf')
911        return pdfstream
912
913def handle_img_upload(upload, context, view):
914    """Handle upload of applicant image.
915
916    Returns `True` in case of success or `False`.
917
918    Please note that file pointer passed in (`upload`) most probably
919    points to end of file when leaving this function.
920    """
921    size = file_size(upload)
922    if size > MAX_UPLOAD_SIZE:
923        view.flash(_('Uploaded image is too big!'), type='danger')
924        return False
925    dummy, ext = os.path.splitext(upload.filename)
926    ext.lower()
927    if ext != '.jpg':
928        view.flash(_('jpg file extension expected.'), type='danger')
929        return False
930    upload.seek(0) # file pointer moved when determining size
931    store = getUtility(IExtFileStore)
932    file_id = IFileStoreNameChooser(context).chooseName()
933    try:
934        store.createFile(file_id, upload)
935    except IOError:
936        view.flash(_('Image file cannot be changed.'), type='danger')
937        return False
938    return True
939
940class ApplicantManageFormPage(KofaEditFormPage):
941    """A full edit view for applicant data.
942    """
943    grok.context(IApplicant)
944    grok.name('manage')
945    grok.require('waeup.manageApplication')
946    grok.template('applicanteditpage')
947    manage_applications = True
948    pnav = 3
949    display_actions = [[_('Save'), _('Finally Submit')],
950        [_('Add online payment ticket'),_('Remove selected tickets')]]
951
952    @property
953    def display_payments(self):
954        if self.context.payments:
955            return True
956        if self.context.special:
957            return True
958        return getattr(self.context.__parent__, 'application_fee', None)
959
960    @property
961    def display_refereereports(self):
962        if self.context.refereereports:
963            return True
964        return False
965
966    @property
967    def form_fields(self):
968        if self.context.special:
969            form_fields = grok.AutoFields(ISpecialApplicant)
970            form_fields['applicant_id'].for_display = True
971        else:
972            form_fields = grok.AutoFields(IApplicant)
973            form_fields['student_id'].for_display = True
974            form_fields['applicant_id'].for_display = True
975        return form_fields
976
977    @property
978    def target(self):
979        return getattr(self.context.__parent__, 'prefix', None)
980
981    @property
982    def separators(self):
983        return getUtility(IApplicantsUtils).SEPARATORS_DICT
984
985    @property
986    def custom_upload_requirements(self):
987        return ''
988
989    def update(self):
990        super(ApplicantManageFormPage, self).update()
991        self.wf_info = IWorkflowInfo(self.context)
992        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
993        self.upload_success = None
994        upload = self.request.form.get('form.passport', None)
995        if upload:
996            if self.custom_upload_requirements:
997                self.flash(
998                    self.custom_upload_requirements,
999                    type='danger')
1000                self.redirect(self.url(self.context))
1001                return
1002            # We got a fresh upload, upload_success is
1003            # either True or False
1004            self.upload_success = handle_img_upload(
1005                upload, self.context, self)
1006            if self.upload_success:
1007                self.context.writeLogMessage(self, 'saved: passport')
1008        return
1009
1010    @property
1011    def label(self):
1012        container_title = self.context.__parent__.title
1013        return _('${a} <br /> Application Form ${b}', mapping = {
1014            'a':container_title, 'b':self.context.application_number})
1015
1016    def getTransitions(self):
1017        """Return a list of dicts of allowed transition ids and titles.
1018
1019        Each list entry provides keys ``name`` and ``title`` for
1020        internal name and (human readable) title of a single
1021        transition.
1022        """
1023        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
1024            if not t[0] in ('pay', 'create')]
1025        return [dict(name='', title=_('No transition'))] +[
1026            dict(name=x, title=y) for x, y in allowed_transitions]
1027
1028    @action(_('Save'), style='primary')
1029    def save(self, **data):
1030        form = self.request.form
1031        password = form.get('password', None)
1032        password_ctl = form.get('control_password', None)
1033        if password:
1034            validator = getUtility(IPasswordValidator)
1035            errors = validator.validate_password(password, password_ctl)
1036            if errors:
1037                self.flash( ' '.join(errors), type='danger')
1038                return
1039        if self.upload_success is False:  # False is not None!
1040            # Error during image upload. Ignore other values.
1041            return
1042        changed_fields = self.applyData(self.context, **data)
1043        # Turn list of lists into single list
1044        if changed_fields:
1045            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
1046        else:
1047            changed_fields = []
1048        if password:
1049            # Now we know that the form has no errors and can set password ...
1050            IUserAccount(self.context).setPassword(password)
1051            changed_fields.append('password')
1052        fields_string = ' + '.join(changed_fields)
1053        trans_id = form.get('transition', None)
1054        if trans_id:
1055            self.wf_info.fireTransition(trans_id)
1056        self.flash(_('Form has been saved.'))
1057        if fields_string:
1058            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
1059        return
1060
1061    def unremovable(self, ticket):
1062        return False
1063
1064    # This method is also used by the ApplicantEditFormPage
1065    def delPaymentTickets(self, **data):
1066        form = self.request.form
1067        if 'val_id' in form:
1068            child_id = form['val_id']
1069        else:
1070            self.flash(_('No payment selected.'), type='warning')
1071            self.redirect(self.url(self.context))
1072            return
1073        if not isinstance(child_id, list):
1074            child_id = [child_id]
1075        deleted = []
1076        for id in child_id:
1077            # Applicants are not allowed to remove used payment tickets
1078            if not self.unremovable(self.context[id]):
1079                try:
1080                    del self.context[id]
1081                    deleted.append(id)
1082                except:
1083                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
1084                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
1085        if len(deleted):
1086            self.flash(_('Successfully removed: ${a}',
1087                mapping = {'a':', '.join(deleted)}))
1088            self.context.writeLogMessage(
1089                self, 'removed: % s' % ', '.join(deleted))
1090        return
1091
1092    # We explicitely want the forms to be validated before payment tickets
1093    # can be created. If no validation is requested, use
1094    # 'validator=NullValidator' in the action directive
1095    @action(_('Add online payment ticket'), style='primary')
1096    def addPaymentTicket(self, **data):
1097        self.redirect(self.url(self.context, '@@addafp'))
1098        return
1099
1100    @jsaction(_('Remove selected tickets'))
1101    def removePaymentTickets(self, **data):
1102        self.delPaymentTickets(**data)
1103        self.redirect(self.url(self.context) + '/@@manage')
1104        return
1105
1106    # Not used in base package
1107    def file_exists(self, attr):
1108        file = getUtility(IExtFileStore).getFileByContext(
1109            self.context, attr=attr)
1110        if file:
1111            return True
1112        else:
1113            return False
1114
1115class ApplicantEditFormPage(ApplicantManageFormPage):
1116    """An applicant-centered edit view for applicant data.
1117    """
1118    grok.context(IApplicantEdit)
1119    grok.name('edit')
1120    grok.require('waeup.handleApplication')
1121    grok.template('applicanteditpage')
1122    manage_applications = False
1123    submit_state = PAID
1124
1125    @property
1126    def display_refereereports(self):
1127        return False
1128
1129    @property
1130    def form_fields(self):
1131        if self.context.special:
1132            form_fields = grok.AutoFields(ISpecialApplicant).omit(
1133                'locked', 'suspended')
1134            form_fields['applicant_id'].for_display = True
1135        else:
1136            form_fields = grok.AutoFields(IApplicantEdit).omit(
1137                'locked', 'course_admitted', 'student_id',
1138                'suspended'
1139                )
1140            form_fields['applicant_id'].for_display = True
1141            form_fields['reg_number'].for_display = True
1142        return form_fields
1143
1144    @property
1145    def display_actions(self):
1146        state = IWorkflowState(self.context).getState()
1147        # If the form is unlocked, applicants are allowed to save the form
1148        # and remove unused tickets.
1149        actions = [[_('Save')], [_('Remove selected tickets')]]
1150        # Only in state started they can also add tickets.
1151        if state == STARTED:
1152            actions = [[_('Save')],
1153                [_('Add online payment ticket'),_('Remove selected tickets')]]
1154        # In state paid, they can submit the data and further add tickets
1155        # if the application is special.
1156        elif self.context.special and state == PAID:
1157            actions = [[_('Save'), _('Finally Submit')],
1158                [_('Add online payment ticket'),_('Remove selected tickets')]]
1159        elif state == PAID:
1160            actions = [[_('Save'), _('Finally Submit')],
1161                [_('Remove selected tickets')]]
1162        return actions
1163
1164    def unremovable(self, ticket):
1165        return ticket.r_code
1166
1167    def emit_lock_message(self):
1168        self.flash(_('The requested form is locked (read-only).'),
1169                   type='warning')
1170        self.redirect(self.url(self.context))
1171        return
1172
1173    def update(self):
1174        if self.context.locked or (
1175            self.context.__parent__.expired and
1176            self.context.__parent__.strict_deadline):
1177            self.emit_lock_message()
1178            return
1179        super(ApplicantEditFormPage, self).update()
1180        return
1181
1182    def dataNotComplete(self):
1183        if self.context.__parent__.with_picture:
1184            store = getUtility(IExtFileStore)
1185            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1186                return _('No passport picture uploaded.')
1187            if not self.request.form.get('confirm_passport', False):
1188                return _('Passport picture confirmation box not ticked.')
1189        return False
1190
1191    # We explicitely want the forms to be validated before payment tickets
1192    # can be created. If no validation is requested, use
1193    # 'validator=NullValidator' in the action directive
1194    @action(_('Add online payment ticket'), style='primary')
1195    def addPaymentTicket(self, **data):
1196        self.redirect(self.url(self.context, '@@addafp'))
1197        return
1198
1199    @jsaction(_('Remove selected tickets'))
1200    def removePaymentTickets(self, **data):
1201        self.delPaymentTickets(**data)
1202        self.redirect(self.url(self.context) + '/@@edit')
1203        return
1204
1205    @action(_('Save'), style='primary')
1206    def save(self, **data):
1207        if self.upload_success is False:  # False is not None!
1208            # Error during image upload. Ignore other values.
1209            return
1210        self.applyData(self.context, **data)
1211        self.flash(_('Form has been saved.'))
1212        return
1213
1214    def informReferees(self):
1215        site = grok.getSite()
1216        kofa_utils = getUtility(IKofaUtils)
1217        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1218        failed = ''
1219        emails_sent = 0
1220        for referee in self.context.referees:
1221            if referee.email_sent:
1222                continue
1223            mandate = RefereeReportMandate()
1224            mandate.params['name'] = referee.name
1225            mandate.params['email'] = referee.email
1226            mandate.params[
1227                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1228                    self.context.__parent__.code,
1229                    self.context.application_number)
1230            site['mandates'].addMandate(mandate)
1231            # Send invitation email
1232            args = {'mandate_id':mandate.mandate_id}
1233            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1234            url_info = u'Report link: %s' % mandate_url
1235            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1236            if success:
1237                emails_sent += 1
1238                self.context.writeLogMessage(
1239                    self, 'email sent: %s' % referee.email)
1240                referee.email_sent = True
1241            else:
1242                failed += '%s ' % referee.email
1243        return failed, emails_sent
1244
1245    @action(_('Finally Submit'), warning=WARNING)
1246    def finalsubmit(self, **data):
1247        if self.upload_success is False:  # False is not None!
1248            return # error during image upload. Ignore other values
1249        if self.dataNotComplete():
1250            self.flash(self.dataNotComplete(), type='danger')
1251            return
1252        self.applyData(self.context, **data)
1253        state = IWorkflowState(self.context).getState()
1254        # This shouldn't happen, but the application officer
1255        # might have forgotten to lock the form after changing the state
1256        if state != self.submit_state:
1257            self.flash(_('The form cannot be submitted. Wrong state!'),
1258                       type='danger')
1259            return
1260        msg = _('Form has been submitted.')
1261        # Create mandates and send emails to referees
1262        if getattr(self.context, 'referees', None):
1263            failed, emails_sent = self.informReferees()
1264            if failed:
1265                self.flash(
1266                    _('Some invitation emails could not be sent:') + failed,
1267                    type='danger')
1268                return
1269            msg = _('Form has been successfully submitted and '
1270                    '${a} invitation emails were sent.',
1271                    mapping = {'a':  emails_sent})
1272        IWorkflowInfo(self.context).fireTransition('submit')
1273        # application_date is used in export files for sorting.
1274        # We can thus store utc.
1275        self.context.application_date = datetime.utcnow()
1276        self.flash(msg)
1277        self.redirect(self.url(self.context))
1278        return
1279
1280class PassportImage(grok.View):
1281    """Renders the passport image for applicants.
1282    """
1283    grok.name('passport.jpg')
1284    grok.context(IApplicant)
1285    grok.require('waeup.viewApplication')
1286
1287    def render(self):
1288        # A filename chooser turns a context into a filename suitable
1289        # for file storage.
1290        image = getUtility(IExtFileStore).getFileByContext(self.context)
1291        self.response.setHeader(
1292            'Content-Type', 'image/jpeg')
1293        if image is None:
1294            # show placeholder image
1295            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1296        return image
1297
1298class ApplicantRegistrationPage(KofaAddFormPage):
1299    """Captcha'd registration page for applicants.
1300    """
1301    grok.context(IApplicantsContainer)
1302    grok.name('register')
1303    grok.require('waeup.Anonymous')
1304    grok.template('applicantregister')
1305
1306    @property
1307    def form_fields(self):
1308        form_fields = None
1309        if self.context.mode == 'update':
1310            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1311                'lastname','reg_number','email')
1312        else: #if self.context.mode == 'create':
1313            form_fields = grok.AutoFields(IApplicantEdit).select(
1314                'firstname', 'middlename', 'lastname', 'email', 'phone')
1315        return form_fields
1316
1317    @property
1318    def label(self):
1319        return _('Apply for ${a}',
1320            mapping = {'a':self.context.title})
1321
1322    def update(self):
1323        if self.context.expired:
1324            self.flash(_('Outside application period.'), type='warning')
1325            self.redirect(self.url(self.context))
1326            return
1327        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1328        if blocker:
1329            self.flash(_('The portal is in maintenance mode '
1330                        'and registration temporarily disabled.'),
1331                       type='warning')
1332            self.redirect(self.url(self.context))
1333            return
1334        # Handle captcha
1335        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1336        self.captcha_result = self.captcha.verify(self.request)
1337        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1338        return
1339
1340    def _redirect(self, email, password, applicant_id):
1341        # Forward only email to landing page in base package.
1342        self.redirect(self.url(self.context, 'registration_complete',
1343            data = dict(email=email)))
1344        return
1345
1346    @property
1347    def _postfix(self):
1348        """In customized packages we can add a container dependent string if
1349        applicants have been imported into several containers.
1350        """
1351        return ''
1352
1353    @action(_('Send login credentials to email address'), style='primary')
1354    def register(self, **data):
1355        if not self.captcha_result.is_valid:
1356            # Captcha will display error messages automatically.
1357            # No need to flash something.
1358            return
1359        if self.context.mode == 'create':
1360            # Check if there are unused records in this container which
1361            # can be taken
1362            applicant = self.context.first_unused
1363            if applicant is None:
1364                # Add applicant
1365                applicant = createObject(u'waeup.Applicant')
1366                self.context.addApplicant(applicant)
1367            else:
1368                applicants_root = grok.getSite()['applicants']
1369                ob_class = self.__implemented__.__name__.replace(
1370                    'waeup.kofa.','')
1371                applicants_root.logger.info('%s - used: %s' % (
1372                    ob_class, applicant.applicant_id))
1373            self.applyData(applicant, **data)
1374            # applicant.reg_number = applicant.applicant_id
1375            notify(grok.ObjectModifiedEvent(applicant))
1376        elif self.context.mode == 'update':
1377            # Update applicant
1378            reg_number = data.get('reg_number','')
1379            lastname = data.get('lastname','')
1380            cat = getUtility(ICatalog, name='applicants_catalog')
1381            searchstr = reg_number + self._postfix
1382            results = list(
1383                cat.searchResults(reg_number=(searchstr, searchstr)))
1384            if results:
1385                applicant = results[0]
1386                if getattr(applicant,'lastname',None) is None:
1387                    self.flash(_('An error occurred.'), type='danger')
1388                    return
1389                elif applicant.lastname.lower() != lastname.lower():
1390                    # Don't tell the truth here. Anonymous must not
1391                    # know that a record was found and only the lastname
1392                    # verification failed.
1393                    self.flash(
1394                        _('No application record found.'), type='warning')
1395                    return
1396                elif applicant.password is not None and \
1397                    applicant.state != INITIALIZED:
1398                    self.flash(_('Your password has already been set and used. '
1399                                 'Please proceed to the login page.'),
1400                               type='warning')
1401                    return
1402                # Store email address but nothing else.
1403                applicant.email = data['email']
1404                notify(grok.ObjectModifiedEvent(applicant))
1405            else:
1406                # No record found, this is the truth.
1407                self.flash(_('No application record found.'), type='warning')
1408                return
1409        else:
1410            # Does not happen but anyway ...
1411            return
1412        kofa_utils = getUtility(IKofaUtils)
1413        password = kofa_utils.genPassword()
1414        IUserAccount(applicant).setPassword(password)
1415        # Send email with credentials
1416        login_url = self.url(grok.getSite(), 'login')
1417        url_info = u'Login: %s' % login_url
1418        msg = _('You have successfully been registered for the')
1419        if kofa_utils.sendCredentials(IUserAccount(applicant),
1420            password, url_info, msg):
1421            email_sent = applicant.email
1422        else:
1423            email_sent = None
1424        self._redirect(email=email_sent, password=password,
1425            applicant_id=applicant.applicant_id)
1426        return
1427
1428class ApplicantRegistrationEmailSent(KofaPage):
1429    """Landing page after successful registration.
1430
1431    """
1432    grok.name('registration_complete')
1433    grok.require('waeup.Public')
1434    grok.template('applicantregemailsent')
1435    label = _('Your registration was successful.')
1436
1437    def update(self, email=None, applicant_id=None, password=None):
1438        self.email = email
1439        self.password = password
1440        self.applicant_id = applicant_id
1441        return
1442
1443class ApplicantCheckStatusPage(KofaPage):
1444    """Captcha'd status checking page for applicants.
1445    """
1446    grok.context(IApplicantsRoot)
1447    grok.name('checkstatus')
1448    grok.require('waeup.Anonymous')
1449    grok.template('applicantcheckstatus')
1450    buttonname = _('Submit')
1451
1452    def label(self):
1453        if self.result:
1454            return _('Admission status of ${a}',
1455                     mapping = {'a':self.applicant.applicant_id})
1456        return _('Check your admission status')
1457
1458    def update(self, SUBMIT=None):
1459        form = self.request.form
1460        self.result = False
1461        # Handle captcha
1462        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1463        self.captcha_result = self.captcha.verify(self.request)
1464        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1465        if SUBMIT:
1466            if not self.captcha_result.is_valid:
1467                # Captcha will display error messages automatically.
1468                # No need to flash something.
1469                return
1470            unique_id = form.get('unique_id', None)
1471            lastname = form.get('lastname', None)
1472            if not unique_id or not lastname:
1473                self.flash(
1474                    _('Required input missing.'), type='warning')
1475                return
1476            cat = getUtility(ICatalog, name='applicants_catalog')
1477            results = list(
1478                cat.searchResults(applicant_id=(unique_id, unique_id)))
1479            if not results:
1480                results = list(
1481                    cat.searchResults(reg_number=(unique_id, unique_id)))
1482            if results:
1483                applicant = results[0]
1484                if applicant.lastname.lower().strip() != lastname.lower():
1485                    # Don't tell the truth here. Anonymous must not
1486                    # know that a record was found and only the lastname
1487                    # verification failed.
1488                    self.flash(
1489                        _('No application record found.'), type='warning')
1490                    return
1491            else:
1492                self.flash(_('No application record found.'), type='warning')
1493                return
1494            self.applicant = applicant
1495            self.entry_session = "%s/%s" % (
1496                applicant.__parent__.year,
1497                applicant.__parent__.year+1)
1498            course_admitted = getattr(applicant, 'course_admitted', None)
1499            self.course_admitted = False
1500            if course_admitted is not None:
1501                try:
1502                    self.course_admitted = True
1503                    self.longtitle = course_admitted.longtitle
1504                    self.department = course_admitted.__parent__.__parent__.longtitle
1505                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1506                except AttributeError:
1507                    self.flash(_('Application record invalid.'), type='warning')
1508                    return
1509            self.result = True
1510            self.admitted = False
1511            self.not_admitted = False
1512            self.submitted = False
1513            self.not_submitted = False
1514            self.created = False
1515            if applicant.state in (ADMITTED, CREATED):
1516                self.admitted = True
1517            if applicant.state in (CREATED):
1518                self.created = True
1519                self.student_id = applicant.student_id
1520                self.password = applicant.application_number
1521            if applicant.state in (NOT_ADMITTED,):
1522                self.not_admitted = True
1523            if applicant.state in (SUBMITTED,):
1524                self.submitted = True
1525            if applicant.state in (INITIALIZED, STARTED, PAID):
1526                self.not_submitted = True
1527        return
1528
1529class ExportJobContainerOverview(KofaPage):
1530    """Page that lists active applicant data export jobs and provides links
1531    to discard or download CSV files.
1532
1533    """
1534    grok.context(VirtualApplicantsExportJobContainer)
1535    grok.require('waeup.manageApplication')
1536    grok.name('index.html')
1537    grok.template('exportjobsindex')
1538    label = _('Data Exports')
1539    pnav = 3
1540
1541    def update(self, CREATE=None, DISCARD=None, job_id=None):
1542        if CREATE:
1543            self.redirect(self.url('@@start_export'))
1544            return
1545        if DISCARD and job_id:
1546            entry = self.context.entry_from_job_id(job_id)
1547            self.context.delete_export_entry(entry)
1548            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1549            self.context.logger.info(
1550                '%s - discarded: job_id=%s' % (ob_class, job_id))
1551            self.flash(_('Discarded export') + ' %s' % job_id)
1552        self.entries = doll_up(self, user=self.request.principal.id)
1553        return
1554
1555class ExportJobContainerJobStart(UtilityView, grok.View):
1556    """View that starts two export jobs, one for applicants and a second
1557    one for applicant payments.
1558    """
1559    grok.context(VirtualApplicantsExportJobContainer)
1560    grok.require('waeup.manageApplication')
1561    grok.name('start_export')
1562
1563    def update(self):
1564        utils = queryUtility(IKofaUtils)
1565        if not utils.expensive_actions_allowed():
1566            self.flash(_(
1567                "Currently, exporters cannot be started due to high "
1568                "system load. Please try again later."), type='danger')
1569            self.entries = doll_up(self, user=None)
1570            return
1571
1572        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1573        container_code = self.context.__parent__.code
1574        # Start first exporter
1575        exporter = 'applicants'
1576        job_id = self.context.start_export_job(exporter,
1577                                      self.request.principal.id,
1578                                      container=container_code)
1579        self.context.logger.info(
1580            '%s - exported: %s (%s), job_id=%s'
1581            % (ob_class, exporter, container_code, job_id))
1582        # Commit transaction so that job is stored in the ZODB
1583        transaction.commit()
1584        # Start second exporter
1585        exporter = 'applicantpayments'
1586        job_id = self.context.start_export_job(exporter,
1587                                      self.request.principal.id,
1588                                      container=container_code)
1589        self.context.logger.info(
1590            '%s - exported: %s (%s), job_id=%s'
1591            % (ob_class, exporter, container_code, job_id))
1592
1593        self.flash(_('Exports started.'))
1594        self.redirect(self.url(self.context))
1595        return
1596
1597    def render(self):
1598        return
1599
1600class ExportJobContainerDownload(ExportCSVView):
1601    """Page that downloads a students export csv file.
1602
1603    """
1604    grok.context(VirtualApplicantsExportJobContainer)
1605    grok.require('waeup.manageApplication')
1606
1607class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1608    """A display view for referee reports.
1609    """
1610    grok.context(IApplicantRefereeReport)
1611    grok.name('index')
1612    grok.require('waeup.manageApplication')
1613    label = _('Referee Report')
1614    pnav = 3
1615
1616class RefereeReportAddFormPage(KofaAddFormPage):
1617    """Add-form to add an referee report. This form
1618    is protected by a mandate.
1619    """
1620    grok.context(IApplicant)
1621    grok.require('waeup.Public')
1622    grok.name('addrefereereport')
1623    form_fields = grok.AutoFields(
1624        IApplicantRefereeReport).omit('creation_date')
1625    grok.template('refereereportpage')
1626    label = _('Add referee report')
1627    pnav = 3
1628    #doclink = DOCLINK + '/refereereports.html'
1629
1630    def update(self):
1631        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1632        if blocker:
1633            self.flash(_('The portal is in maintenance mode. '
1634                        'Referee report forms are temporarily disabled.'),
1635                       type='warning')
1636            self.redirect(self.application_url())
1637            return
1638        # Check mandate
1639        form = self.request.form
1640        self.mandate_id = form.get('mandate_id', None)
1641        self.mandates = grok.getSite()['mandates']
1642        mandate = self.mandates.get(self.mandate_id, None)
1643        if mandate is None and not self.request.form.get('form.actions.submit'):
1644            self.flash(_('No mandate.'), type='warning')
1645            self.redirect(self.application_url())
1646            return
1647        if mandate:
1648            # Prefill form with mandate params
1649            self.form_fields.get(
1650                'name').field.default = mandate.params['name']
1651            self.form_fields.get(
1652                'email').field.default = mandate.params['email']
1653        super(RefereeReportAddFormPage, self).update()
1654        return
1655
1656    @action(_('Submit'),
1657              warning=_('Are you really sure? '
1658                        'Reports can neither be modified or added '
1659                        'after submission.'),
1660              style='primary')
1661    def addRefereeReport(self, **data):
1662        report = createObject(u'waeup.ApplicantRefereeReport')
1663        timestamp = ("%d" % int(time()*10000))[1:]
1664        report.r_id = "r%s" % timestamp
1665        self.applyData(report, **data)
1666        self.context[report.r_id] = report
1667        self.flash(_('Referee report has been saved. Thank you!'))
1668        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1669        # Delete mandate
1670        del self.mandates[self.mandate_id]
1671        self.redirect(self.application_url())
1672        return
Note: See TracBrowser for help on using the repository browser.