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

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

Add log entry if pre-filled application record is used.

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