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

Last change on this file since 15060 was 14952, checked in by Henrik Bettermann, 7 years ago

Do not log error messages.
Improve Student Creation Report.
Adjust tests.

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