source: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser.py @ 17403

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

Sort export of "Data for Lecturer" by fcode, dcode and matric_number.

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