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

Last change on this file since 15395 was 15122, checked in by Henrik Bettermann, 6 years ago

Always display applicant payments if they exist.

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