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

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

Do not copy applicant_id into reg_number field after applicant creation.

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