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

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

Don't call dataNotComplete twice.

  • Property svn:keywords set to Id
File size: 62.2 KB
Line 
1## $Id: browser.py 15636 2019-10-03 20:51:02Z 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, data):
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        dnt = self.dataNotComplete(data)
1250        if dnt:
1251            self.flash(dnt, type='danger')
1252            return
1253        self.applyData(self.context, **data)
1254        state = IWorkflowState(self.context).getState()
1255        # This shouldn't happen, but the application officer
1256        # might have forgotten to lock the form after changing the state
1257        if state != self.submit_state:
1258            self.flash(_('The form cannot be submitted. Wrong state!'),
1259                       type='danger')
1260            return
1261        msg = _('Form has been submitted.')
1262        # Create mandates and send emails to referees
1263        if getattr(self.context, 'referees', None):
1264            failed, emails_sent = self.informReferees()
1265            if failed:
1266                self.flash(
1267                    _('Some invitation emails could not be sent:') + failed,
1268                    type='danger')
1269                return
1270            msg = _('Form has been successfully submitted and '
1271                    '${a} invitation emails were sent.',
1272                    mapping = {'a':  emails_sent})
1273        IWorkflowInfo(self.context).fireTransition('submit')
1274        # application_date is used in export files for sorting.
1275        # We can thus store utc.
1276        self.context.application_date = datetime.utcnow()
1277        self.flash(msg)
1278        self.redirect(self.url(self.context))
1279        return
1280
1281class PassportImage(grok.View):
1282    """Renders the passport image for applicants.
1283    """
1284    grok.name('passport.jpg')
1285    grok.context(IApplicant)
1286    grok.require('waeup.viewApplication')
1287
1288    def render(self):
1289        # A filename chooser turns a context into a filename suitable
1290        # for file storage.
1291        image = getUtility(IExtFileStore).getFileByContext(self.context)
1292        self.response.setHeader(
1293            'Content-Type', 'image/jpeg')
1294        if image is None:
1295            # show placeholder image
1296            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1297        return image
1298
1299class ApplicantRegistrationPage(KofaAddFormPage):
1300    """Captcha'd registration page for applicants.
1301    """
1302    grok.context(IApplicantsContainer)
1303    grok.name('register')
1304    grok.require('waeup.Anonymous')
1305    grok.template('applicantregister')
1306
1307    @property
1308    def form_fields(self):
1309        form_fields = None
1310        if self.context.mode == 'update':
1311            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1312                'lastname','reg_number','email')
1313        else: #if self.context.mode == 'create':
1314            form_fields = grok.AutoFields(IApplicantEdit).select(
1315                'firstname', 'middlename', 'lastname', 'email', 'phone')
1316        return form_fields
1317
1318    @property
1319    def label(self):
1320        return _('Apply for ${a}',
1321            mapping = {'a':self.context.title})
1322
1323    def update(self):
1324        if self.context.expired:
1325            self.flash(_('Outside application period.'), type='warning')
1326            self.redirect(self.url(self.context))
1327            return
1328        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1329        if blocker:
1330            self.flash(_('The portal is in maintenance mode '
1331                        'and registration temporarily disabled.'),
1332                       type='warning')
1333            self.redirect(self.url(self.context))
1334            return
1335        # Handle captcha
1336        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1337        self.captcha_result = self.captcha.verify(self.request)
1338        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1339        return
1340
1341    def _redirect(self, email, password, applicant_id):
1342        # Forward only email to landing page in base package.
1343        self.redirect(self.url(self.context, 'registration_complete',
1344            data = dict(email=email)))
1345        return
1346
1347    @property
1348    def _postfix(self):
1349        """In customized packages we can add a container dependent string if
1350        applicants have been imported into several containers.
1351        """
1352        return ''
1353
1354    @action(_('Send login credentials to email address'), style='primary')
1355    def register(self, **data):
1356        if not self.captcha_result.is_valid:
1357            # Captcha will display error messages automatically.
1358            # No need to flash something.
1359            return
1360        if self.context.mode == 'create':
1361            # Check if there are unused records in this container which
1362            # can be taken
1363            applicant = self.context.first_unused
1364            if applicant is None:
1365                # Add applicant
1366                applicant = createObject(u'waeup.Applicant')
1367                self.context.addApplicant(applicant)
1368            else:
1369                applicants_root = grok.getSite()['applicants']
1370                ob_class = self.__implemented__.__name__.replace(
1371                    'waeup.kofa.','')
1372                applicants_root.logger.info('%s - used: %s' % (
1373                    ob_class, applicant.applicant_id))
1374            self.applyData(applicant, **data)
1375            # applicant.reg_number = applicant.applicant_id
1376            notify(grok.ObjectModifiedEvent(applicant))
1377        elif self.context.mode == 'update':
1378            # Update applicant
1379            reg_number = data.get('reg_number','')
1380            lastname = data.get('lastname','')
1381            cat = getUtility(ICatalog, name='applicants_catalog')
1382            searchstr = reg_number + self._postfix
1383            results = list(
1384                cat.searchResults(reg_number=(searchstr, searchstr)))
1385            if results:
1386                applicant = results[0]
1387                if getattr(applicant,'lastname',None) is None:
1388                    self.flash(_('An error occurred.'), type='danger')
1389                    return
1390                elif applicant.lastname.lower() != lastname.lower():
1391                    # Don't tell the truth here. Anonymous must not
1392                    # know that a record was found and only the lastname
1393                    # verification failed.
1394                    self.flash(
1395                        _('No application record found.'), type='warning')
1396                    return
1397                elif applicant.password is not None and \
1398                    applicant.state != INITIALIZED:
1399                    self.flash(_('Your password has already been set and used. '
1400                                 'Please proceed to the login page.'),
1401                               type='warning')
1402                    return
1403                # Store email address but nothing else.
1404                applicant.email = data['email']
1405                notify(grok.ObjectModifiedEvent(applicant))
1406            else:
1407                # No record found, this is the truth.
1408                self.flash(_('No application record found.'), type='warning')
1409                return
1410        else:
1411            # Does not happen but anyway ...
1412            return
1413        kofa_utils = getUtility(IKofaUtils)
1414        password = kofa_utils.genPassword()
1415        IUserAccount(applicant).setPassword(password)
1416        # Send email with credentials
1417        login_url = self.url(grok.getSite(), 'login')
1418        url_info = u'Login: %s' % login_url
1419        msg = _('You have successfully been registered for the')
1420        if kofa_utils.sendCredentials(IUserAccount(applicant),
1421            password, url_info, msg):
1422            email_sent = applicant.email
1423        else:
1424            email_sent = None
1425        self._redirect(email=email_sent, password=password,
1426            applicant_id=applicant.applicant_id)
1427        return
1428
1429class ApplicantRegistrationEmailSent(KofaPage):
1430    """Landing page after successful registration.
1431
1432    """
1433    grok.name('registration_complete')
1434    grok.require('waeup.Public')
1435    grok.template('applicantregemailsent')
1436    label = _('Your registration was successful.')
1437
1438    def update(self, email=None, applicant_id=None, password=None):
1439        self.email = email
1440        self.password = password
1441        self.applicant_id = applicant_id
1442        return
1443
1444class ApplicantCheckStatusPage(KofaPage):
1445    """Captcha'd status checking page for applicants.
1446    """
1447    grok.context(IApplicantsRoot)
1448    grok.name('checkstatus')
1449    grok.require('waeup.Anonymous')
1450    grok.template('applicantcheckstatus')
1451    buttonname = _('Submit')
1452
1453    def label(self):
1454        if self.result:
1455            return _('Admission status of ${a}',
1456                     mapping = {'a':self.applicant.applicant_id})
1457        return _('Check your admission status')
1458
1459    def update(self, SUBMIT=None):
1460        form = self.request.form
1461        self.result = False
1462        # Handle captcha
1463        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1464        self.captcha_result = self.captcha.verify(self.request)
1465        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1466        if SUBMIT:
1467            if not self.captcha_result.is_valid:
1468                # Captcha will display error messages automatically.
1469                # No need to flash something.
1470                return
1471            unique_id = form.get('unique_id', None)
1472            lastname = form.get('lastname', None)
1473            if not unique_id or not lastname:
1474                self.flash(
1475                    _('Required input missing.'), type='warning')
1476                return
1477            cat = getUtility(ICatalog, name='applicants_catalog')
1478            results = list(
1479                cat.searchResults(applicant_id=(unique_id, unique_id)))
1480            if not results:
1481                results = list(
1482                    cat.searchResults(reg_number=(unique_id, unique_id)))
1483            if results:
1484                applicant = results[0]
1485                if applicant.lastname.lower().strip() != lastname.lower():
1486                    # Don't tell the truth here. Anonymous must not
1487                    # know that a record was found and only the lastname
1488                    # verification failed.
1489                    self.flash(
1490                        _('No application record found.'), type='warning')
1491                    return
1492            else:
1493                self.flash(_('No application record found.'), type='warning')
1494                return
1495            self.applicant = applicant
1496            self.entry_session = "%s/%s" % (
1497                applicant.__parent__.year,
1498                applicant.__parent__.year+1)
1499            course_admitted = getattr(applicant, 'course_admitted', None)
1500            self.course_admitted = False
1501            if course_admitted is not None:
1502                try:
1503                    self.course_admitted = True
1504                    self.longtitle = course_admitted.longtitle
1505                    self.department = course_admitted.__parent__.__parent__.longtitle
1506                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1507                except AttributeError:
1508                    self.flash(_('Application record invalid.'), type='warning')
1509                    return
1510            self.result = True
1511            self.admitted = False
1512            self.not_admitted = False
1513            self.submitted = False
1514            self.not_submitted = False
1515            self.created = False
1516            if applicant.state in (ADMITTED, CREATED):
1517                self.admitted = True
1518            if applicant.state in (CREATED):
1519                self.created = True
1520                self.student_id = applicant.student_id
1521                self.password = applicant.application_number
1522            if applicant.state in (NOT_ADMITTED,):
1523                self.not_admitted = True
1524            if applicant.state in (SUBMITTED,):
1525                self.submitted = True
1526            if applicant.state in (INITIALIZED, STARTED, PAID):
1527                self.not_submitted = True
1528        return
1529
1530class ExportJobContainerOverview(KofaPage):
1531    """Page that lists active applicant data export jobs and provides links
1532    to discard or download CSV files.
1533
1534    """
1535    grok.context(VirtualApplicantsExportJobContainer)
1536    grok.require('waeup.manageApplication')
1537    grok.name('index.html')
1538    grok.template('exportjobsindex')
1539    label = _('Data Exports')
1540    pnav = 3
1541
1542    def update(self, CREATE=None, DISCARD=None, job_id=None):
1543        if CREATE:
1544            self.redirect(self.url('@@start_export'))
1545            return
1546        if DISCARD and job_id:
1547            entry = self.context.entry_from_job_id(job_id)
1548            self.context.delete_export_entry(entry)
1549            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1550            self.context.logger.info(
1551                '%s - discarded: job_id=%s' % (ob_class, job_id))
1552            self.flash(_('Discarded export') + ' %s' % job_id)
1553        self.entries = doll_up(self, user=self.request.principal.id)
1554        return
1555
1556class ExportJobContainerJobStart(UtilityView, grok.View):
1557    """View that starts two export jobs, one for applicants and a second
1558    one for applicant payments.
1559    """
1560    grok.context(VirtualApplicantsExportJobContainer)
1561    grok.require('waeup.manageApplication')
1562    grok.name('start_export')
1563
1564    def update(self):
1565        utils = queryUtility(IKofaUtils)
1566        if not utils.expensive_actions_allowed():
1567            self.flash(_(
1568                "Currently, exporters cannot be started due to high "
1569                "system load. Please try again later."), type='danger')
1570            self.entries = doll_up(self, user=None)
1571            return
1572
1573        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1574        container_code = self.context.__parent__.code
1575        # Start first exporter
1576        exporter = 'applicants'
1577        job_id = self.context.start_export_job(exporter,
1578                                      self.request.principal.id,
1579                                      container=container_code)
1580        self.context.logger.info(
1581            '%s - exported: %s (%s), job_id=%s'
1582            % (ob_class, exporter, container_code, job_id))
1583        # Commit transaction so that job is stored in the ZODB
1584        transaction.commit()
1585        # Start second exporter
1586        exporter = 'applicantpayments'
1587        job_id = self.context.start_export_job(exporter,
1588                                      self.request.principal.id,
1589                                      container=container_code)
1590        self.context.logger.info(
1591            '%s - exported: %s (%s), job_id=%s'
1592            % (ob_class, exporter, container_code, job_id))
1593
1594        self.flash(_('Exports started.'))
1595        self.redirect(self.url(self.context))
1596        return
1597
1598    def render(self):
1599        return
1600
1601class ExportJobContainerDownload(ExportCSVView):
1602    """Page that downloads a students export csv file.
1603
1604    """
1605    grok.context(VirtualApplicantsExportJobContainer)
1606    grok.require('waeup.manageApplication')
1607
1608class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1609    """A display view for referee reports.
1610    """
1611    grok.context(IApplicantRefereeReport)
1612    grok.name('index')
1613    grok.require('waeup.manageApplication')
1614    label = _('Referee Report')
1615    pnav = 3
1616
1617class RefereeReportAddFormPage(KofaAddFormPage):
1618    """Add-form to add an referee report. This form
1619    is protected by a mandate.
1620    """
1621    grok.context(IApplicant)
1622    grok.require('waeup.Public')
1623    grok.name('addrefereereport')
1624    form_fields = grok.AutoFields(
1625        IApplicantRefereeReport).omit('creation_date')
1626    grok.template('refereereportpage')
1627    label = _('Add referee report')
1628    pnav = 3
1629    #doclink = DOCLINK + '/refereereports.html'
1630
1631    def update(self):
1632        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1633        if blocker:
1634            self.flash(_('The portal is in maintenance mode. '
1635                        'Referee report forms are temporarily disabled.'),
1636                       type='warning')
1637            self.redirect(self.application_url())
1638            return
1639        # Check mandate
1640        form = self.request.form
1641        self.mandate_id = form.get('mandate_id', None)
1642        self.mandates = grok.getSite()['mandates']
1643        mandate = self.mandates.get(self.mandate_id, None)
1644        if mandate is None and not self.request.form.get('form.actions.submit'):
1645            self.flash(_('No mandate.'), type='warning')
1646            self.redirect(self.application_url())
1647            return
1648        if mandate:
1649            # Prefill form with mandate params
1650            self.form_fields.get(
1651                'name').field.default = mandate.params['name']
1652            self.form_fields.get(
1653                'email').field.default = mandate.params['email']
1654        super(RefereeReportAddFormPage, self).update()
1655        return
1656
1657    @action(_('Submit'),
1658              warning=_('Are you really sure? '
1659                        'Reports can neither be modified or added '
1660                        'after submission.'),
1661              style='primary')
1662    def addRefereeReport(self, **data):
1663        report = createObject(u'waeup.ApplicantRefereeReport')
1664        timestamp = ("%d" % int(time()*10000))[1:]
1665        report.r_id = "r%s" % timestamp
1666        self.applyData(report, **data)
1667        self.context[report.r_id] = report
1668        self.flash(_('Referee report has been saved. Thank you!'))
1669        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1670        # Delete mandate
1671        del self.mandates[self.mandate_id]
1672        self.redirect(self.application_url())
1673        return
Note: See TracBrowser for help on using the repository browser.