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

Last change on this file since 14014 was 14014, checked in by Henrik Bettermann, 9 years ago

Send invitation email to referees when applicant finally submits the form.

  • Property svn:keywords set to Id
File size: 59.6 KB
Line 
1## $Id: browser.py 14014 2016-07-04 09:02:52Z 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 pre-fill 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        for referee in self.context.referees:
1179            if referee.email_sent:
1180                continue
1181            mandate = RefereeReportMandate()
1182            mandate.params['name'] = referee.name
1183            mandate.params['email'] = referee.email
1184            mandate.params[
1185                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
1186                    self.context.__parent__.code,
1187                    self.context.application_number)
1188            site['mandates'].addMandate(mandate)
1189            # Send invitation email
1190            args = {'mandate_id':mandate.mandate_id}
1191            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1192            url_info = u'Report link: %s' % mandate_url
1193            success = kofa_utils.inviteReferee(referee, self.context, url_info)
1194            if success:
1195                self.context.writeLogMessage(
1196                    self, 'email sent: %s' % referee.email)
1197                referee.email_sent = True
1198            else:
1199                failed += '%s ' % referee.email
1200        return failed
1201
1202    @action(_('Finally Submit'), warning=WARNING)
1203    def finalsubmit(self, **data):
1204        if self.upload_success is False:  # False is not None!
1205            return # error during image upload. Ignore other values
1206        if self.dataNotComplete():
1207            self.flash(self.dataNotComplete(), type='danger')
1208            return
1209        self.applyData(self.context, **data)
1210        state = IWorkflowState(self.context).getState()
1211        # This shouldn't happen, but the application officer
1212        # might have forgotten to lock the form after changing the state
1213        if state != self.submit_state:
1214            self.flash(_('The form cannot be submitted. Wrong state!'),
1215                       type='danger')
1216            return
1217        msg = _('Form has been submitted.')
1218        # Create mandates and send emails to referees
1219        if getattr(self.context, 'referees', None):
1220            failed = self.informReferees()
1221            if failed:
1222                self.flash(
1223                    _('Some invitation emails could not be sent:') + failed,
1224                    type='danger')
1225                return
1226            msg = _('Form has been successfully submitted and '
1227                    'all invitation emails were sent.')
1228        IWorkflowInfo(self.context).fireTransition('submit')
1229        # application_date is used in export files for sorting.
1230        # We can thus store utc.
1231        self.context.application_date = datetime.utcnow()
1232        self.flash(msg)
1233        self.redirect(self.url(self.context))
1234        return
1235
1236class PassportImage(grok.View):
1237    """Renders the passport image for applicants.
1238    """
1239    grok.name('passport.jpg')
1240    grok.context(IApplicant)
1241    grok.require('waeup.viewApplication')
1242
1243    def render(self):
1244        # A filename chooser turns a context into a filename suitable
1245        # for file storage.
1246        image = getUtility(IExtFileStore).getFileByContext(self.context)
1247        self.response.setHeader(
1248            'Content-Type', 'image/jpeg')
1249        if image is None:
1250            # show placeholder image
1251            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1252        return image
1253
1254class ApplicantRegistrationPage(KofaAddFormPage):
1255    """Captcha'd registration page for applicants.
1256    """
1257    grok.context(IApplicantsContainer)
1258    grok.name('register')
1259    grok.require('waeup.Anonymous')
1260    grok.template('applicantregister')
1261
1262    @property
1263    def form_fields(self):
1264        form_fields = None
1265        if self.context.mode == 'update':
1266            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1267                'lastname','reg_number','email')
1268        else: #if self.context.mode == 'create':
1269            form_fields = grok.AutoFields(IApplicantEdit).select(
1270                'firstname', 'middlename', 'lastname', 'email', 'phone')
1271        return form_fields
1272
1273    @property
1274    def label(self):
1275        return _('Apply for ${a}',
1276            mapping = {'a':self.context.title})
1277
1278    def update(self):
1279        if self.context.expired:
1280            self.flash(_('Outside application period.'), type='warning')
1281            self.redirect(self.url(self.context))
1282            return
1283        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1284        if blocker:
1285            self.flash(_('The portal is in maintenance mode '
1286                        'and registration temporarily disabled.'),
1287                       type='warning')
1288            self.redirect(self.url(self.context))
1289            return
1290        # Handle captcha
1291        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1292        self.captcha_result = self.captcha.verify(self.request)
1293        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1294        return
1295
1296    def _redirect(self, email, password, applicant_id):
1297        # Forward only email to landing page in base package.
1298        self.redirect(self.url(self.context, 'registration_complete',
1299            data = dict(email=email)))
1300        return
1301
1302    @action(_('Send login credentials to email address'), style='primary')
1303    def register(self, **data):
1304        if not self.captcha_result.is_valid:
1305            # Captcha will display error messages automatically.
1306            # No need to flash something.
1307            return
1308        if self.context.mode == 'create':
1309            # Check if there are unused records in this container which
1310            # can be taken
1311            applicant = self.context.first_unused
1312            if applicant is None:
1313                # Add applicant
1314                applicant = createObject(u'waeup.Applicant')
1315                self.context.addApplicant(applicant)
1316            self.applyData(applicant, **data)
1317            applicant.reg_number = applicant.applicant_id
1318            notify(grok.ObjectModifiedEvent(applicant))
1319        elif self.context.mode == 'update':
1320            # Update applicant
1321            reg_number = data.get('reg_number','')
1322            lastname = data.get('lastname','')
1323            cat = getUtility(ICatalog, name='applicants_catalog')
1324            results = list(
1325                cat.searchResults(reg_number=(reg_number, reg_number)))
1326            if results:
1327                applicant = results[0]
1328                if getattr(applicant,'lastname',None) is None:
1329                    self.flash(_('An error occurred.'), type='danger')
1330                    return
1331                elif applicant.lastname.lower() != lastname.lower():
1332                    # Don't tell the truth here. Anonymous must not
1333                    # know that a record was found and only the lastname
1334                    # verification failed.
1335                    self.flash(
1336                        _('No application record found.'), type='warning')
1337                    return
1338                elif applicant.password is not None and \
1339                    applicant.state != INITIALIZED:
1340                    self.flash(_('Your password has already been set and used. '
1341                                 'Please proceed to the login page.'),
1342                               type='warning')
1343                    return
1344                # Store email address but nothing else.
1345                applicant.email = data['email']
1346                notify(grok.ObjectModifiedEvent(applicant))
1347            else:
1348                # No record found, this is the truth.
1349                self.flash(_('No application record found.'), type='warning')
1350                return
1351        else:
1352            # Does not happen but anyway ...
1353            return
1354        kofa_utils = getUtility(IKofaUtils)
1355        password = kofa_utils.genPassword()
1356        IUserAccount(applicant).setPassword(password)
1357        # Send email with credentials
1358        login_url = self.url(grok.getSite(), 'login')
1359        url_info = u'Login: %s' % login_url
1360        msg = _('You have successfully been registered for the')
1361        if kofa_utils.sendCredentials(IUserAccount(applicant),
1362            password, url_info, msg):
1363            email_sent = applicant.email
1364        else:
1365            email_sent = None
1366        self._redirect(email=email_sent, password=password,
1367            applicant_id=applicant.applicant_id)
1368        return
1369
1370class ApplicantRegistrationEmailSent(KofaPage):
1371    """Landing page after successful registration.
1372
1373    """
1374    grok.name('registration_complete')
1375    grok.require('waeup.Public')
1376    grok.template('applicantregemailsent')
1377    label = _('Your registration was successful.')
1378
1379    def update(self, email=None, applicant_id=None, password=None):
1380        self.email = email
1381        self.password = password
1382        self.applicant_id = applicant_id
1383        return
1384
1385class ApplicantCheckStatusPage(KofaPage):
1386    """Captcha'd status checking page for applicants.
1387    """
1388    grok.context(IApplicantsRoot)
1389    grok.name('checkstatus')
1390    grok.require('waeup.Anonymous')
1391    grok.template('applicantcheckstatus')
1392    buttonname = _('Submit')
1393
1394    def label(self):
1395        if self.result:
1396            return _('Admission status of ${a}',
1397                     mapping = {'a':self.applicant.applicant_id})
1398        return _('Check your admission status')
1399
1400    def update(self, SUBMIT=None):
1401        form = self.request.form
1402        self.result = False
1403        # Handle captcha
1404        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1405        self.captcha_result = self.captcha.verify(self.request)
1406        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1407        if SUBMIT:
1408            if not self.captcha_result.is_valid:
1409                # Captcha will display error messages automatically.
1410                # No need to flash something.
1411                return
1412            unique_id = form.get('unique_id', None)
1413            lastname = form.get('lastname', None)
1414            if not unique_id or not lastname:
1415                self.flash(
1416                    _('Required input missing.'), type='warning')
1417                return
1418            cat = getUtility(ICatalog, name='applicants_catalog')
1419            results = list(
1420                cat.searchResults(applicant_id=(unique_id, unique_id)))
1421            if not results:
1422                results = list(
1423                    cat.searchResults(reg_number=(unique_id, unique_id)))
1424            if results:
1425                applicant = results[0]
1426                if applicant.lastname.lower().strip() != lastname.lower():
1427                    # Don't tell the truth here. Anonymous must not
1428                    # know that a record was found and only the lastname
1429                    # verification failed.
1430                    self.flash(
1431                        _('No application record found.'), type='warning')
1432                    return
1433            else:
1434                self.flash(_('No application record found.'), type='warning')
1435                return
1436            self.applicant = applicant
1437            self.entry_session = "%s/%s" % (
1438                applicant.__parent__.year,
1439                applicant.__parent__.year+1)
1440            course_admitted = getattr(applicant, 'course_admitted', None)
1441            self.course_admitted = False
1442            if course_admitted is not None:
1443                self.course_admitted = True
1444                self.longtitle = course_admitted.longtitle
1445                self.department = course_admitted.__parent__.__parent__.longtitle
1446                self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
1447            self.result = True
1448            self.admitted = False
1449            self.not_admitted = False
1450            self.submitted = False
1451            self.not_submitted = False
1452            self.created = False
1453            if applicant.state in (ADMITTED, CREATED):
1454                self.admitted = True
1455            if applicant.state in (CREATED):
1456                self.created = True
1457                self.student_id = applicant.student_id
1458                self.password = applicant.application_number
1459            if applicant.state in (NOT_ADMITTED,):
1460                self.not_admitted = True
1461            if applicant.state in (SUBMITTED,):
1462                self.submitted = True
1463            if applicant.state in (INITIALIZED, STARTED, PAID):
1464                self.not_submitted = True
1465        return
1466
1467class ExportJobContainerOverview(KofaPage):
1468    """Page that lists active applicant data export jobs and provides links
1469    to discard or download CSV files.
1470
1471    """
1472    grok.context(VirtualApplicantsExportJobContainer)
1473    grok.require('waeup.manageApplication')
1474    grok.name('index.html')
1475    grok.template('exportjobsindex')
1476    label = _('Data Exports')
1477    pnav = 3
1478
1479    def update(self, CREATE=None, DISCARD=None, job_id=None):
1480        if CREATE:
1481            self.redirect(self.url('@@start_export'))
1482            return
1483        if DISCARD and job_id:
1484            entry = self.context.entry_from_job_id(job_id)
1485            self.context.delete_export_entry(entry)
1486            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1487            self.context.logger.info(
1488                '%s - discarded: job_id=%s' % (ob_class, job_id))
1489            self.flash(_('Discarded export') + ' %s' % job_id)
1490        self.entries = doll_up(self, user=self.request.principal.id)
1491        return
1492
1493class ExportJobContainerJobStart(UtilityView, grok.View):
1494    """View that starts two export jobs, one for applicants and a second
1495    one for applicant payments.
1496    """
1497    grok.context(VirtualApplicantsExportJobContainer)
1498    grok.require('waeup.manageApplication')
1499    grok.name('start_export')
1500
1501    def update(self):
1502        utils = queryUtility(IKofaUtils)
1503        if not utils.expensive_actions_allowed():
1504            self.flash(_(
1505                "Currently, exporters cannot be started due to high "
1506                "system load. Please try again later."), type='danger')
1507            self.entries = doll_up(self, user=None)
1508            return
1509
1510        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1511        container_code = self.context.__parent__.code
1512        # Start first exporter
1513        exporter = 'applicants'
1514        job_id = self.context.start_export_job(exporter,
1515                                      self.request.principal.id,
1516                                      container=container_code)
1517        self.context.logger.info(
1518            '%s - exported: %s (%s), job_id=%s'
1519            % (ob_class, exporter, container_code, job_id))
1520        # Commit transaction so that job is stored in the ZODB
1521        transaction.commit()
1522        # Start second exporter
1523        exporter = 'applicantpayments'
1524        job_id = self.context.start_export_job(exporter,
1525                                      self.request.principal.id,
1526                                      container=container_code)
1527        self.context.logger.info(
1528            '%s - exported: %s (%s), job_id=%s'
1529            % (ob_class, exporter, container_code, job_id))
1530
1531        self.flash(_('Exports started.'))
1532        self.redirect(self.url(self.context))
1533        return
1534
1535    def render(self):
1536        return
1537
1538class ExportJobContainerDownload(ExportCSVView):
1539    """Page that downloads a students export csv file.
1540
1541    """
1542    grok.context(VirtualApplicantsExportJobContainer)
1543    grok.require('waeup.manageApplication')
1544
1545class RefereeReportDisplayFormPage(KofaDisplayFormPage):
1546    """A display view for referee reports.
1547    """
1548    grok.context(IApplicantRefereeReport)
1549    grok.name('index')
1550    grok.require('waeup.manageApplication')
1551    label = _('Referee Report')
1552    pnav = 3
1553
1554class RefereeReportAddFormPage(KofaAddFormPage):
1555    """Add-form to add an referee report. This form
1556    is protected by a mandate.
1557    """
1558    grok.context(IApplicant)
1559    grok.require('waeup.Public')
1560    grok.name('addrefereereport')
1561    form_fields = grok.AutoFields(
1562        IApplicantRefereeReport).omit('creation_date')
1563    grok.template('refereereportpage')
1564    label = _('Add referee report')
1565    pnav = 3
1566    #doclink = DOCLINK + '/refereereports.html'
1567
1568    def update(self):
1569        blocker = grok.getSite()['configuration'].maintmode_enabled_by
1570        if blocker:
1571            self.flash(_('The portal is in maintenance mode. '
1572                        'Referee report forms are temporarily disabled.'),
1573                       type='warning')
1574            self.redirect(self.application_url())
1575            return
1576        # Check mandate
1577        form = self.request.form
1578        self.mandate_id = form.get('mandate_id', None)
1579        self.mandates = grok.getSite()['mandates']
1580        mandate = self.mandates.get(self.mandate_id, None)
1581        if mandate is None and not self.request.form.get('form.actions.submit'):
1582            self.flash(_('No mandate.'), type='warning')
1583            self.redirect(self.application_url())
1584            return
1585        if mandate:
1586            # Prefill form with mandate params
1587            self.form_fields.get(
1588                'name').field.default = mandate.params['name']
1589            self.form_fields.get(
1590                'email').field.default = mandate.params['email']
1591        super(RefereeReportAddFormPage, self).update()
1592        return
1593
1594    @action(_('Submit'),
1595              warning=_('Are you really sure? '
1596                        'Reports can neither be modified or added '
1597                        'after submission.'),
1598              style='primary')
1599    def addRefereeReport(self, **data):
1600        report = createObject(u'waeup.ApplicantRefereeReport')
1601        timestamp = ("%d" % int(time()*10000))[1:]
1602        report.r_id = "r%s" % timestamp
1603        self.applyData(report, **data)
1604        self.context[report.r_id] = report
1605        self.flash(_('Referee report has been saved. Thank you!'))
1606        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
1607        # Delete mandate
1608        del self.mandates[self.mandate_id]
1609        self.redirect(self.application_url())
1610        return
Note: See TracBrowser for help on using the repository browser.