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

Last change on this file since 15539 was 15502, checked in by Henrik Bettermann, 5 years ago

Add passport picture switch to applicants containers.

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