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

Last change on this file since 14282 was 14256, checked in by Henrik Bettermann, 8 years ago

Catch Reportlab LayoutError.

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