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

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

Add CreateStudents permission and StudentsCreator role.

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