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

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

Make max passport picture size customizable.

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