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

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

Catch traceback.

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