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

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

Do not allow to create more than 10 students with a single request to
avoid a timeout of Nginx/Apache?.

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