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

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

In customized packages we have to add a container dependent string to reg_number if
applicants have been imported into several containers.

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