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

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

Make getUsers customizable.

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