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

Last change on this file since 11872 was 11870, checked in by Henrik Bettermann, 10 years ago

Hide application_slip_notice on ApplicantsContainerPage?.

  • Property svn:keywords set to Id
File size: 46.3 KB
Line 
1## $Id: browser.py 11870 2014-10-22 06:31:57Z 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
23from datetime import datetime, date
24from zope.event import notify
25from zope.component import getUtility, createObject, getAdapter
26from zope.catalog.interfaces import ICatalog
27from zope.i18n import translate
28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
30from waeup.kofa.applicants.interfaces import (
31    IApplicant, IApplicantEdit, IApplicantsRoot,
32    IApplicantsContainer, IApplicantsContainerAdd,
33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
34    IApplicantRegisterUpdate, ISpecialApplicant
35    )
36from waeup.kofa.applicants.container import (
37    ApplicantsContainer, VirtualApplicantsExportJobContainer)
38from waeup.kofa.applicants.applicant import search
39from waeup.kofa.applicants.workflow import (
40    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
41from waeup.kofa.browser import (
42#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
43    DEFAULT_PASSPORT_IMAGE_PATH)
44from waeup.kofa.browser.layout import (
45    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
46from waeup.kofa.browser.interfaces import ICaptchaManager
47from waeup.kofa.browser.breadcrumbs import Breadcrumb
48from waeup.kofa.browser.layout import (
49    NullValidator, jsaction, action, UtilityView)
50from waeup.kofa.browser.pages import (
51    add_local_role, del_local_roles, doll_up, ExportCSVView)
52from waeup.kofa.interfaces import (
53    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
54    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
55from waeup.kofa.interfaces import MessageFactory as _
56from waeup.kofa.permissions import get_users_with_local_roles
57from waeup.kofa.students.interfaces import IStudentsUtils
58from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
59from waeup.kofa.widgets.datewidget import (
60    FriendlyDateDisplayWidget,
61    FriendlyDatetimeDisplayWidget)
62from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
63
64grok.context(IKofaObject) # Make IKofaObject the default context
65
66WARNING = _('You can not edit your application records after final submission.'
67            ' You really want to submit?')
68
69class ApplicantsRootPage(KofaDisplayFormPage):
70    grok.context(IApplicantsRoot)
71    grok.name('index')
72    grok.require('waeup.Public')
73    form_fields = grok.AutoFields(IApplicantsRoot)
74    form_fields['description'].custom_widget = HTMLDisplayWidget
75    label = _('Application Section')
76    pnav = 3
77
78    def update(self):
79        super(ApplicantsRootPage, self).update()
80        return
81
82    @property
83    def introduction(self):
84        # Here we know that the cookie has been set
85        lang = self.request.cookies.get('kofa.language')
86        html = self.context.description_dict.get(lang,'')
87        if html == '':
88            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
89            html = self.context.description_dict.get(portal_language,'')
90        return html
91
92    @property
93    def containers(self):
94        if self.layout.isAuthenticated():
95            return self.context.values()
96        return [value for value in self.context.values() if not value.hidden]
97
98class ApplicantsSearchPage(KofaPage):
99    grok.context(IApplicantsRoot)
100    grok.name('search')
101    grok.require('waeup.viewApplication')
102    label = _('Find applicants')
103    search_button = _('Find applicant')
104    pnav = 3
105
106    def update(self, *args, **kw):
107        form = self.request.form
108        self.results = []
109        if 'searchterm' in form and form['searchterm']:
110            self.searchterm = form['searchterm']
111            self.searchtype = form['searchtype']
112        elif 'old_searchterm' in form:
113            self.searchterm = form['old_searchterm']
114            self.searchtype = form['old_searchtype']
115        else:
116            if 'search' in form:
117                self.flash(_('Empty search string'), type='warning')
118            return
119        self.results = search(query=self.searchterm,
120            searchtype=self.searchtype, view=self)
121        if not self.results:
122            self.flash(_('No applicant found.'), type='warning')
123        return
124
125class ApplicantsRootManageFormPage(KofaEditFormPage):
126    grok.context(IApplicantsRoot)
127    grok.name('manage')
128    grok.template('applicantsrootmanagepage')
129    form_fields = grok.AutoFields(IApplicantsRoot)
130    label = _('Manage application section')
131    pnav = 3
132    grok.require('waeup.manageApplication')
133    taboneactions = [_('Save')]
134    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
135    tabthreeactions1 = [_('Remove selected local roles')]
136    tabthreeactions2 = [_('Add local role')]
137    subunits = _('Applicants Containers')
138
139    def getLocalRoles(self):
140        roles = ILocalRolesAssignable(self.context)
141        return roles()
142
143    def getUsers(self):
144        """Get a list of all users.
145        """
146        for key, val in grok.getSite()['users'].items():
147            url = self.url(val)
148            yield(dict(url=url, name=key, val=val))
149
150    def getUsersWithLocalRoles(self):
151        return get_users_with_local_roles(self.context)
152
153    @jsaction(_('Remove selected'))
154    def delApplicantsContainers(self, **data):
155        form = self.request.form
156        if 'val_id' in form:
157            child_id = form['val_id']
158        else:
159            self.flash(_('No container selected!'), type='warning')
160            self.redirect(self.url(self.context, '@@manage')+'#tab2')
161            return
162        if not isinstance(child_id, list):
163            child_id = [child_id]
164        deleted = []
165        for id in child_id:
166            try:
167                del self.context[id]
168                deleted.append(id)
169            except:
170                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
171                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
172        if len(deleted):
173            self.flash(_('Successfully removed: ${a}',
174                mapping = {'a':', '.join(deleted)}))
175        self.redirect(self.url(self.context, '@@manage')+'#tab2')
176        return
177
178    @action(_('Add applicants container'), validator=NullValidator)
179    def addApplicantsContainer(self, **data):
180        self.redirect(self.url(self.context, '@@add'))
181        return
182
183    @action(_('Add local role'), validator=NullValidator)
184    def addLocalRole(self, **data):
185        return add_local_role(self,3, **data)
186
187    @action(_('Remove selected local roles'))
188    def delLocalRoles(self, **data):
189        return del_local_roles(self,3,**data)
190
191    def _description(self):
192        view = ApplicantsRootPage(
193            self.context,self.request)
194        view.setUpWidgets()
195        return view.widgets['description']()
196
197    @action(_('Save'), style='primary')
198    def save(self, **data):
199        self.applyData(self.context, **data)
200        self.context.description_dict = self._description()
201        self.flash(_('Form has been saved.'))
202        return
203
204class ApplicantsContainerAddFormPage(KofaAddFormPage):
205    grok.context(IApplicantsRoot)
206    grok.require('waeup.manageApplication')
207    grok.name('add')
208    grok.template('applicantscontaineraddpage')
209    label = _('Add applicants container')
210    pnav = 3
211
212    form_fields = grok.AutoFields(
213        IApplicantsContainerAdd).omit('code').omit('title')
214
215    @action(_('Add applicants container'))
216    def addApplicantsContainer(self, **data):
217        year = data['year']
218        code = u'%s%s' % (data['prefix'], year)
219        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
220        title = apptypes_dict[data['prefix']][0]
221        title = u'%s %s/%s' % (title, year, year + 1)
222        if code in self.context.keys():
223            self.flash(
224              _('An applicants container for the same application '
225                'type and entrance year exists already in the database.'),
226                type='warning')
227            return
228        # Add new applicants container...
229        container = createObject(u'waeup.ApplicantsContainer')
230        self.applyData(container, **data)
231        container.code = code
232        container.title = title
233        self.context[code] = container
234        self.flash(_('Added:') + ' "%s".' % code)
235        self.redirect(self.url(self.context, u'@@manage'))
236        return
237
238    @action(_('Cancel'), validator=NullValidator)
239    def cancel(self, **data):
240        self.redirect(self.url(self.context, '@@manage'))
241
242class ApplicantsRootBreadcrumb(Breadcrumb):
243    """A breadcrumb for applicantsroot.
244    """
245    grok.context(IApplicantsRoot)
246    title = _(u'Applicants')
247
248class ApplicantsContainerBreadcrumb(Breadcrumb):
249    """A breadcrumb for applicantscontainers.
250    """
251    grok.context(IApplicantsContainer)
252
253
254class ApplicantsExportsBreadcrumb(Breadcrumb):
255    """A breadcrumb for exports.
256    """
257    grok.context(VirtualApplicantsExportJobContainer)
258    title = _(u'Applicant Data Exports')
259    target = None
260
261class ApplicantBreadcrumb(Breadcrumb):
262    """A breadcrumb for applicants.
263    """
264    grok.context(IApplicant)
265
266    @property
267    def title(self):
268        """Get a title for a context.
269        """
270        return self.context.application_number
271
272class OnlinePaymentBreadcrumb(Breadcrumb):
273    """A breadcrumb for payments.
274    """
275    grok.context(IApplicantOnlinePayment)
276
277    @property
278    def title(self):
279        return self.context.p_id
280
281class ApplicantsStatisticsPage(KofaDisplayFormPage):
282    """Some statistics about applicants in a container.
283    """
284    grok.context(IApplicantsContainer)
285    grok.name('statistics')
286    grok.require('waeup.viewApplicationStatistics')
287    grok.template('applicantcontainerstatistics')
288
289    @property
290    def label(self):
291        return "%s" % self.context.title
292
293class ApplicantsContainerPage(KofaDisplayFormPage):
294    """The standard view for regular applicant containers.
295    """
296    grok.context(IApplicantsContainer)
297    grok.name('index')
298    grok.require('waeup.Public')
299    grok.template('applicantscontainerpage')
300    pnav = 3
301
302    @property
303    def form_fields(self):
304        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
305        form_fields['description'].custom_widget = HTMLDisplayWidget
306        form_fields[
307            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
308        form_fields[
309            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
310        if self.request.principal.id == 'zope.anybody':
311            form_fields = form_fields.omit(
312                'code', 'prefix', 'year', 'mode', 'hidden',
313                'strict_deadline', 'application_category',
314                'application_slip_notice')
315        return form_fields
316
317    @property
318    def introduction(self):
319        # Here we know that the cookie has been set
320        lang = self.request.cookies.get('kofa.language')
321        html = self.context.description_dict.get(lang,'')
322        if html == '':
323            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
324            html = self.context.description_dict.get(portal_language,'')
325        return html
326
327    @property
328    def label(self):
329        return "%s" % self.context.title
330
331class ApplicantsContainerManageFormPage(KofaEditFormPage):
332    grok.context(IApplicantsContainer)
333    grok.name('manage')
334    grok.template('applicantscontainermanagepage')
335    form_fields = grok.AutoFields(IApplicantsContainer)
336    taboneactions = [_('Save'),_('Cancel')]
337    tabtwoactions = [_('Remove selected'),_('Cancel'),
338        _('Create students from selected')]
339    tabthreeactions1 = [_('Remove selected local roles')]
340    tabthreeactions2 = [_('Add local role')]
341    # Use friendlier date widget...
342    grok.require('waeup.manageApplication')
343
344    @property
345    def label(self):
346        return _('Manage applicants container')
347
348    pnav = 3
349
350    @property
351    def showApplicants(self):
352        if len(self.context) < 5000:
353            return True
354        return False
355
356    def getLocalRoles(self):
357        roles = ILocalRolesAssignable(self.context)
358        return roles()
359
360    def getUsers(self):
361        """Get a list of all users.
362        """
363        for key, val in grok.getSite()['users'].items():
364            url = self.url(val)
365            yield(dict(url=url, name=key, val=val))
366
367    def getUsersWithLocalRoles(self):
368        return get_users_with_local_roles(self.context)
369
370    def _description(self):
371        view = ApplicantsContainerPage(
372            self.context,self.request)
373        view.setUpWidgets()
374        return view.widgets['description']()
375
376    @action(_('Save'), style='primary')
377    def save(self, **data):
378        changed_fields = self.applyData(self.context, **data)
379        if changed_fields:
380            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
381        else:
382            changed_fields = []
383        self.context.description_dict = self._description()
384        # Always refresh title. So we can change titles
385        # if APP_TYPES_DICT has been edited.
386        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
387        title = apptypes_dict[self.context.prefix][0]
388        #self.context.title = u'%s %s/%s' % (
389        #    title, self.context.year, self.context.year + 1)
390        self.flash(_('Form has been saved.'))
391        fields_string = ' + '.join(changed_fields)
392        self.context.writeLogMessage(self, 'saved: % s' % fields_string)
393        return
394
395    @jsaction(_('Remove selected'))
396    def delApplicant(self, **data):
397        form = self.request.form
398        if 'val_id' in form:
399            child_id = form['val_id']
400        else:
401            self.flash(_('No applicant selected!'), type='warning')
402            self.redirect(self.url(self.context, '@@manage')+'#tab2')
403            return
404        if not isinstance(child_id, list):
405            child_id = [child_id]
406        deleted = []
407        for id in child_id:
408            try:
409                del self.context[id]
410                deleted.append(id)
411            except:
412                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
413                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
414        if len(deleted):
415            self.flash(_('Successfully removed: ${a}',
416                mapping = {'a':', '.join(deleted)}))
417        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
418        return
419
420    @action(_('Create students from selected'))
421    def createStudents(self, **data):
422        form = self.request.form
423        if 'val_id' in form:
424            child_id = form['val_id']
425        else:
426            self.flash(_('No applicant selected!'), type='warning')
427            self.redirect(self.url(self.context, '@@manage')+'#tab2')
428            return
429        if not isinstance(child_id, list):
430            child_id = [child_id]
431        created = []
432        for id in child_id:
433            success, msg = self.context[id].createStudent(view=self)
434            if success:
435                created.append(id)
436        if len(created):
437            self.flash(_('${a} students successfully created.',
438                mapping = {'a': len(created)}))
439        else:
440            self.flash(_('No student could be created.'), type='warning')
441        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
442        return
443
444    @action(_('Cancel'), validator=NullValidator)
445    def cancel(self, **data):
446        self.redirect(self.url(self.context))
447        return
448
449    @action(_('Add local role'), validator=NullValidator)
450    def addLocalRole(self, **data):
451        return add_local_role(self,3, **data)
452
453    @action(_('Remove selected local roles'))
454    def delLocalRoles(self, **data):
455        return del_local_roles(self,3,**data)
456
457class ApplicantAddFormPage(KofaAddFormPage):
458    """Add-form to add an applicant.
459    """
460    grok.context(IApplicantsContainer)
461    grok.require('waeup.manageApplication')
462    grok.name('addapplicant')
463    #grok.template('applicantaddpage')
464    form_fields = grok.AutoFields(IApplicant).select(
465        'firstname', 'middlename', 'lastname',
466        'email', 'phone')
467    label = _('Add applicant')
468    pnav = 3
469
470    @action(_('Create application record'))
471    def addApplicant(self, **data):
472        applicant = createObject(u'waeup.Applicant')
473        self.applyData(applicant, **data)
474        self.context.addApplicant(applicant)
475        self.flash(_('Applicant record created.'))
476        self.redirect(
477            self.url(self.context[applicant.application_number], 'index'))
478        return
479
480class ApplicantDisplayFormPage(KofaDisplayFormPage):
481    """A display view for applicant data.
482    """
483    grok.context(IApplicant)
484    grok.name('index')
485    grok.require('waeup.viewApplication')
486    grok.template('applicantdisplaypage')
487    label = _('Applicant')
488    pnav = 3
489    hide_hint = False
490
491    @property
492    def form_fields(self):
493        if self.context.special:
494            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
495        else:
496            form_fields = grok.AutoFields(IApplicant).omit(
497                'locked', 'course_admitted', 'password', 'suspended')
498        return form_fields
499
500    @property
501    def target(self):
502        return getattr(self.context.__parent__, 'prefix', None)
503
504    @property
505    def separators(self):
506        return getUtility(IApplicantsUtils).SEPARATORS_DICT
507
508    def update(self):
509        self.passport_url = self.url(self.context, 'passport.jpg')
510        # Mark application as started if applicant logs in for the first time
511        usertype = getattr(self.request.principal, 'user_type', None)
512        if usertype == 'applicant' and \
513            IWorkflowState(self.context).getState() == INITIALIZED:
514            IWorkflowInfo(self.context).fireTransition('start')
515        if usertype == 'applicant' and self.context.state == 'created':
516            session = '%s/%s' % (self.context.__parent__.year,
517                                 self.context.__parent__.year+1)
518            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
519            msg = _(
520                '\n <strong>Congratulations!</strong>' +
521                ' You have been offered provisional admission into the' +
522                ' ${c} Academic Session of ${d}.'
523                ' Your student record has been created for you.' +
524                ' Please, logout again and proceed to the' +
525                ' login page of the portal.'
526                ' Then enter your new student credentials:' +
527                ' user name= ${a}, password = ${b}.' +
528                ' Change your password when you have logged in.',
529                mapping = {
530                    'a':self.context.student_id,
531                    'b':self.context.application_number,
532                    'c':session,
533                    'd':title}
534                )
535            self.flash(msg)
536        return
537
538    @property
539    def hasPassword(self):
540        if self.context.password:
541            return _('set')
542        return _('unset')
543
544    @property
545    def label(self):
546        container_title = self.context.__parent__.title
547        return _('${a} <br /> Application Record ${b}', mapping = {
548            'a':container_title, 'b':self.context.application_number})
549
550    def getCourseAdmitted(self):
551        """Return link, title and code in html format to the certificate
552           admitted.
553        """
554        course_admitted = self.context.course_admitted
555        if getattr(course_admitted, '__parent__',None):
556            url = self.url(course_admitted)
557            title = course_admitted.title
558            code = course_admitted.code
559            return '<a href="%s">%s - %s</a>' %(url,code,title)
560        return ''
561
562class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
563    grok.context(IApplicant)
564    grok.name('base')
565    form_fields = grok.AutoFields(IApplicant).select(
566        'applicant_id','email', 'course1')
567
568class CreateStudentPage(UtilityView, grok.View):
569    """Create a student object from applicant data.
570    """
571    grok.context(IApplicant)
572    grok.name('createstudent')
573    grok.require('waeup.manageStudent')
574
575    def update(self):
576        msg = self.context.createStudent(view=self)[1]
577        self.flash(msg, type='warning')
578        self.redirect(self.url(self.context))
579        return
580
581    def render(self):
582        return
583
584class CreateAllStudentsPage(UtilityView, grok.View):
585    """Create all student objects from applicant data
586    in a container.
587
588    Only PortalManagers can do this.
589    """
590    #grok.context(IApplicantsContainer)
591    grok.name('createallstudents')
592    grok.require('waeup.managePortal')
593
594    def update(self):
595        cat = getUtility(ICatalog, name='applicants_catalog')
596        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
597        created = []
598        container_only = False
599        applicants_root = grok.getSite()['applicants']
600        if isinstance(self.context, ApplicantsContainer):
601            container_only = True
602        for result in results:
603            if container_only and result.__parent__ is not self.context:
604                continue
605            success, msg = result.createStudent(view=self)
606            if success:
607                created.append(result.applicant_id)
608            else:
609                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
610                applicants_root.logger.info(
611                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
612        if len(created):
613            self.flash(_('${a} students successfully created.',
614                mapping = {'a': len(created)}))
615        else:
616            self.flash(_('No student could be created.'), type='warning')
617        self.redirect(self.url(self.context))
618        return
619
620    def render(self):
621        return
622
623class ApplicationFeePaymentAddPage(UtilityView, grok.View):
624    """ Page to add an online payment ticket
625    """
626    grok.context(IApplicant)
627    grok.name('addafp')
628    grok.require('waeup.payApplicant')
629    factory = u'waeup.ApplicantOnlinePayment'
630
631    @property
632    def custom_requirements(self):
633        return ''
634
635    def update(self):
636        # Additional requirements in custom packages.
637        if self.custom_requirements:
638            self.flash(
639                self.custom_requirements,
640                type='danger')
641            self.redirect(self.url(self.context))
642            return
643        if not self.context.special:
644            for key in self.context.keys():
645                ticket = self.context[key]
646                if ticket.p_state == 'paid':
647                      self.flash(
648                          _('This type of payment has already been made.'),
649                          type='warning')
650                      self.redirect(self.url(self.context))
651                      return
652        applicants_utils = getUtility(IApplicantsUtils)
653        container = self.context.__parent__
654        payment = createObject(self.factory)
655        failure = applicants_utils.setPaymentDetails(
656            container, payment, self.context)
657        if failure is not None:
658            self.flash(failure[0], type='danger')
659            self.redirect(self.url(self.context))
660            return
661        self.context[payment.p_id] = payment
662        self.flash(_('Payment ticket created.'))
663        self.redirect(self.url(payment))
664        return
665
666    def render(self):
667        return
668
669
670class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
671    """ Page to view an online payment ticket
672    """
673    grok.context(IApplicantOnlinePayment)
674    grok.name('index')
675    grok.require('waeup.viewApplication')
676    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
677    form_fields[
678        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
679    form_fields[
680        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
681    pnav = 3
682
683    @property
684    def label(self):
685        return _('${a}: Online Payment Ticket ${b}', mapping = {
686            'a':self.context.__parent__.display_fullname,
687            'b':self.context.p_id})
688
689class OnlinePaymentApprovePage(UtilityView, grok.View):
690    """ Approval view
691    """
692    grok.context(IApplicantOnlinePayment)
693    grok.name('approve')
694    grok.require('waeup.managePortal')
695
696    def update(self):
697        flashtype, msg, log = self.context.approveApplicantPayment()
698        if log is not None:
699            applicant = self.context.__parent__
700            # Add log message to applicants.log
701            applicant.writeLogMessage(self, log)
702            # Add log message to payments.log
703            self.context.logger.info(
704                '%s,%s,%s,%s,%s,,,,,,' % (
705                applicant.applicant_id,
706                self.context.p_id, self.context.p_category,
707                self.context.amount_auth, self.context.r_code))
708        self.flash(msg, type=flashtype)
709        return
710
711    def render(self):
712        self.redirect(self.url(self.context, '@@index'))
713        return
714
715class ExportPDFPaymentSlipPage(UtilityView, grok.View):
716    """Deliver a PDF slip of the context.
717    """
718    grok.context(IApplicantOnlinePayment)
719    grok.name('payment_slip.pdf')
720    grok.require('waeup.viewApplication')
721    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
722    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
723    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
724    prefix = 'form'
725    note = None
726
727    @property
728    def title(self):
729        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
730        return translate(_('Payment Data'), 'waeup.kofa',
731            target_language=portal_language)
732
733    @property
734    def label(self):
735        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
736        return translate(_('Online Payment Slip'),
737            'waeup.kofa', target_language=portal_language) \
738            + ' %s' % self.context.p_id
739
740    @property
741    def payment_slip_download_warning(self):
742        if self.context.__parent__.state != SUBMITTED:
743            return _('Please submit the application form before '
744                     'trying to download payment slips.')
745        return ''
746
747    def render(self):
748        if self.payment_slip_download_warning:
749            self.flash(self.payment_slip_download_warning, type='danger')
750            self.redirect(self.url(self.context))
751            return
752        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
753            self.request)
754        students_utils = getUtility(IStudentsUtils)
755        return students_utils.renderPDF(self,'payment_slip.pdf',
756            self.context.__parent__, applicantview, note=self.note)
757
758class ExportPDFPageApplicationSlip(UtilityView, grok.View):
759    """Deliver a PDF slip of the context.
760    """
761    grok.context(IApplicant)
762    grok.name('application_slip.pdf')
763    grok.require('waeup.viewApplication')
764    prefix = 'form'
765
766    def update(self):
767        if self.context.state in ('initialized', 'started', 'paid'):
768            self.flash(
769                _('Please pay and submit before trying to download '
770                  'the application slip.'), type='warning')
771            return self.redirect(self.url(self.context))
772        return
773
774    def render(self):
775        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
776            view=self)
777        self.response.setHeader(
778            'Content-Type', 'application/pdf')
779        return pdfstream
780
781def handle_img_upload(upload, context, view):
782    """Handle upload of applicant image.
783
784    Returns `True` in case of success or `False`.
785
786    Please note that file pointer passed in (`upload`) most probably
787    points to end of file when leaving this function.
788    """
789    size = file_size(upload)
790    if size > MAX_UPLOAD_SIZE:
791        view.flash(_('Uploaded image is too big!'), type='danger')
792        return False
793    dummy, ext = os.path.splitext(upload.filename)
794    ext.lower()
795    if ext != '.jpg':
796        view.flash(_('jpg file extension expected.'), type='danger')
797        return False
798    upload.seek(0) # file pointer moved when determining size
799    store = getUtility(IExtFileStore)
800    file_id = IFileStoreNameChooser(context).chooseName()
801    store.createFile(file_id, upload)
802    return True
803
804class ApplicantManageFormPage(KofaEditFormPage):
805    """A full edit view for applicant data.
806    """
807    grok.context(IApplicant)
808    grok.name('manage')
809    grok.require('waeup.manageApplication')
810    grok.template('applicanteditpage')
811    manage_applications = True
812    pnav = 3
813    display_actions = [[_('Save'), _('Final Submit')],
814        [_('Add online payment ticket'),_('Remove selected tickets')]]
815
816    @property
817    def form_fields(self):
818        if self.context.special:
819            form_fields = grok.AutoFields(ISpecialApplicant)
820            form_fields['applicant_id'].for_display = True
821        else:
822            form_fields = grok.AutoFields(IApplicant)
823            form_fields['student_id'].for_display = True
824            form_fields['applicant_id'].for_display = True
825        return form_fields
826
827    @property
828    def target(self):
829        return getattr(self.context.__parent__, 'prefix', None)
830
831    @property
832    def separators(self):
833        return getUtility(IApplicantsUtils).SEPARATORS_DICT
834
835    @property
836    def custom_upload_requirements(self):
837        return ''
838
839    def update(self):
840        super(ApplicantManageFormPage, self).update()
841        self.wf_info = IWorkflowInfo(self.context)
842        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
843        self.upload_success = None
844        upload = self.request.form.get('form.passport', None)
845        if upload:
846            if self.custom_upload_requirements:
847                self.flash(
848                    self.custom_upload_requirements,
849                    type='danger')
850                self.redirect(self.url(self.context))
851                return
852            # We got a fresh upload, upload_success is
853            # either True or False
854            self.upload_success = handle_img_upload(
855                upload, self.context, self)
856            if self.upload_success:
857                self.context.writeLogMessage(self, 'saved: passport')
858        return
859
860    @property
861    def label(self):
862        container_title = self.context.__parent__.title
863        return _('${a} <br /> Application Form ${b}', mapping = {
864            'a':container_title, 'b':self.context.application_number})
865
866    def getTransitions(self):
867        """Return a list of dicts of allowed transition ids and titles.
868
869        Each list entry provides keys ``name`` and ``title`` for
870        internal name and (human readable) title of a single
871        transition.
872        """
873        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
874            if not t[0] in ('pay', 'create')]
875        return [dict(name='', title=_('No transition'))] +[
876            dict(name=x, title=y) for x, y in allowed_transitions]
877
878    @action(_('Save'), style='primary')
879    def save(self, **data):
880        form = self.request.form
881        password = form.get('password', None)
882        password_ctl = form.get('control_password', None)
883        if password:
884            validator = getUtility(IPasswordValidator)
885            errors = validator.validate_password(password, password_ctl)
886            if errors:
887                self.flash( ' '.join(errors), type='danger')
888                return
889        if self.upload_success is False:  # False is not None!
890            # Error during image upload. Ignore other values.
891            return
892        changed_fields = self.applyData(self.context, **data)
893        # Turn list of lists into single list
894        if changed_fields:
895            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
896        else:
897            changed_fields = []
898        if password:
899            # Now we know that the form has no errors and can set password ...
900            IUserAccount(self.context).setPassword(password)
901            changed_fields.append('password')
902        fields_string = ' + '.join(changed_fields)
903        trans_id = form.get('transition', None)
904        if trans_id:
905            self.wf_info.fireTransition(trans_id)
906        self.flash(_('Form has been saved.'))
907        if fields_string:
908            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
909        return
910
911    def unremovable(self, ticket):
912        return False
913
914    # This method is also used by the ApplicantEditFormPage
915    def delPaymentTickets(self, **data):
916        form = self.request.form
917        if 'val_id' in form:
918            child_id = form['val_id']
919        else:
920            self.flash(_('No payment selected.'), type='warning')
921            self.redirect(self.url(self.context))
922            return
923        if not isinstance(child_id, list):
924            child_id = [child_id]
925        deleted = []
926        for id in child_id:
927            # Applicants are not allowed to remove used payment tickets
928            if not self.unremovable(self.context[id]):
929                try:
930                    del self.context[id]
931                    deleted.append(id)
932                except:
933                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
934                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
935        if len(deleted):
936            self.flash(_('Successfully removed: ${a}',
937                mapping = {'a':', '.join(deleted)}))
938            self.context.writeLogMessage(
939                self, 'removed: % s' % ', '.join(deleted))
940        return
941
942    # We explicitely want the forms to be validated before payment tickets
943    # can be created. If no validation is requested, use
944    # 'validator=NullValidator' in the action directive
945    @action(_('Add online payment ticket'), style='primary')
946    def addPaymentTicket(self, **data):
947        self.redirect(self.url(self.context, '@@addafp'))
948        return
949
950    @jsaction(_('Remove selected tickets'))
951    def removePaymentTickets(self, **data):
952        self.delPaymentTickets(**data)
953        self.redirect(self.url(self.context) + '/@@manage')
954        return
955
956    # Not used in base package
957    def file_exists(self, attr):
958        file = getUtility(IExtFileStore).getFileByContext(
959            self.context, attr=attr)
960        if file:
961            return True
962        else:
963            return False
964
965class ApplicantEditFormPage(ApplicantManageFormPage):
966    """An applicant-centered edit view for applicant data.
967    """
968    grok.context(IApplicantEdit)
969    grok.name('edit')
970    grok.require('waeup.handleApplication')
971    grok.template('applicanteditpage')
972    manage_applications = False
973    submit_state = PAID
974
975    @property
976    def form_fields(self):
977        if self.context.special:
978            form_fields = grok.AutoFields(ISpecialApplicant).omit(
979                'locked', 'suspended')
980            form_fields['applicant_id'].for_display = True
981        else:
982            form_fields = grok.AutoFields(IApplicantEdit).omit(
983                'locked', 'course_admitted', 'student_id',
984                'suspended'
985                )
986            form_fields['applicant_id'].for_display = True
987            form_fields['reg_number'].for_display = True
988        return form_fields
989
990    @property
991    def display_actions(self):
992        state = IWorkflowState(self.context).getState()
993        actions = [[],[]]
994        if state == STARTED:
995            actions = [[_('Save')],
996                [_('Add online payment ticket'),_('Remove selected tickets')]]
997        elif self.context.special and state == PAID:
998            actions = [[_('Save'), _('Final Submit')],
999                [_('Add online payment ticket'),_('Remove selected tickets')]]
1000        elif state == PAID:
1001            actions = [[_('Save'), _('Final Submit')],
1002                [_('Remove selected tickets')]]
1003        return actions
1004
1005    def unremovable(self, ticket):
1006        state = IWorkflowState(self.context).getState()
1007        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
1008
1009    def emit_lock_message(self):
1010        self.flash(_('The requested form is locked (read-only).'),
1011                   type='warning')
1012        self.redirect(self.url(self.context))
1013        return
1014
1015    def update(self):
1016        if self.context.locked or (
1017            self.context.__parent__.expired and
1018            self.context.__parent__.strict_deadline):
1019            self.emit_lock_message()
1020            return
1021        super(ApplicantEditFormPage, self).update()
1022        return
1023
1024    def dataNotComplete(self):
1025        store = getUtility(IExtFileStore)
1026        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1027            return _('No passport picture uploaded.')
1028        if not self.request.form.get('confirm_passport', False):
1029            return _('Passport picture confirmation box not ticked.')
1030        return False
1031
1032    # We explicitely want the forms to be validated before payment tickets
1033    # can be created. If no validation is requested, use
1034    # 'validator=NullValidator' in the action directive
1035    @action(_('Add online payment ticket'), style='primary')
1036    def addPaymentTicket(self, **data):
1037        self.redirect(self.url(self.context, '@@addafp'))
1038        return
1039
1040    @jsaction(_('Remove selected tickets'))
1041    def removePaymentTickets(self, **data):
1042        self.delPaymentTickets(**data)
1043        self.redirect(self.url(self.context) + '/@@edit')
1044        return
1045
1046    @action(_('Save'), style='primary')
1047    def save(self, **data):
1048        if self.upload_success is False:  # False is not None!
1049            # Error during image upload. Ignore other values.
1050            return
1051        if data.get('course1', 1) == data.get('course2', 2):
1052            self.flash(_('1st and 2nd choice must be different.'),
1053                       type='warning')
1054            return
1055        self.applyData(self.context, **data)
1056        self.flash(_('Form has been saved.'))
1057        return
1058
1059    @action(_('Final Submit'), warning=WARNING)
1060    def finalsubmit(self, **data):
1061        if self.upload_success is False:  # False is not None!
1062            return # error during image upload. Ignore other values
1063        if self.dataNotComplete():
1064            self.flash(self.dataNotComplete(), type='danger')
1065            return
1066        self.applyData(self.context, **data)
1067        state = IWorkflowState(self.context).getState()
1068        # This shouldn't happen, but the application officer
1069        # might have forgotten to lock the form after changing the state
1070        if state != self.submit_state:
1071            self.flash(_('The form cannot be submitted. Wrong state!'),
1072                       type='danger')
1073            return
1074        IWorkflowInfo(self.context).fireTransition('submit')
1075        # application_date is used in export files for sorting.
1076        # We can thus store utc.
1077        self.context.application_date = datetime.utcnow()
1078        self.flash(_('Form has been submitted.'))
1079        self.redirect(self.url(self.context))
1080        return
1081
1082class PassportImage(grok.View):
1083    """Renders the passport image for applicants.
1084    """
1085    grok.name('passport.jpg')
1086    grok.context(IApplicant)
1087    grok.require('waeup.viewApplication')
1088
1089    def render(self):
1090        # A filename chooser turns a context into a filename suitable
1091        # for file storage.
1092        image = getUtility(IExtFileStore).getFileByContext(self.context)
1093        self.response.setHeader(
1094            'Content-Type', 'image/jpeg')
1095        if image is None:
1096            # show placeholder image
1097            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1098        return image
1099
1100class ApplicantRegistrationPage(KofaAddFormPage):
1101    """Captcha'd registration page for applicants.
1102    """
1103    grok.context(IApplicantsContainer)
1104    grok.name('register')
1105    grok.require('waeup.Anonymous')
1106    grok.template('applicantregister')
1107
1108    @property
1109    def form_fields(self):
1110        form_fields = None
1111        if self.context.mode == 'update':
1112            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1113                'lastname','reg_number','email')
1114        else: #if self.context.mode == 'create':
1115            form_fields = grok.AutoFields(IApplicantEdit).select(
1116                'firstname', 'middlename', 'lastname', 'email', 'phone')
1117        return form_fields
1118
1119    @property
1120    def label(self):
1121        return _('Apply for ${a}',
1122            mapping = {'a':self.context.title})
1123
1124    def update(self):
1125        if self.context.expired:
1126            self.flash(_('Outside application period.'), type='warning')
1127            self.redirect(self.url(self.context))
1128            return
1129        # Handle captcha
1130        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1131        self.captcha_result = self.captcha.verify(self.request)
1132        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1133        return
1134
1135    def _redirect(self, email, password, applicant_id):
1136        # Forward only email to landing page in base package.
1137        self.redirect(self.url(self.context, 'registration_complete',
1138            data = dict(email=email)))
1139        return
1140
1141    @action(_('Send login credentials to email address'), style='primary')
1142    def register(self, **data):
1143        if not self.captcha_result.is_valid:
1144            # Captcha will display error messages automatically.
1145            # No need to flash something.
1146            return
1147        if self.context.mode == 'create':
1148            # Add applicant
1149            applicant = createObject(u'waeup.Applicant')
1150            self.applyData(applicant, **data)
1151            self.context.addApplicant(applicant)
1152            applicant.reg_number = applicant.applicant_id
1153            notify(grok.ObjectModifiedEvent(applicant))
1154        elif self.context.mode == 'update':
1155            # Update applicant
1156            reg_number = data.get('reg_number','')
1157            lastname = data.get('lastname','')
1158            cat = getUtility(ICatalog, name='applicants_catalog')
1159            results = list(
1160                cat.searchResults(reg_number=(reg_number, reg_number)))
1161            if results:
1162                applicant = results[0]
1163                if getattr(applicant,'lastname',None) is None:
1164                    self.flash(_('An error occurred.'), type='danger')
1165                    return
1166                elif applicant.lastname.lower() != lastname.lower():
1167                    # Don't tell the truth here. Anonymous must not
1168                    # know that a record was found and only the lastname
1169                    # verification failed.
1170                    self.flash(_('No application record found.'), type='warning')
1171                    return
1172                elif applicant.password is not None and \
1173                    applicant.state != INITIALIZED:
1174                    self.flash(_('Your password has already been set and used. '
1175                                 'Please proceed to the login page.'),
1176                               type='warning')
1177                    return
1178                # Store email address but nothing else.
1179                applicant.email = data['email']
1180                notify(grok.ObjectModifiedEvent(applicant))
1181            else:
1182                # No record found, this is the truth.
1183                self.flash(_('No application record found.'), type='warning')
1184                return
1185        else:
1186            # Does not happen but anyway ...
1187            return
1188        kofa_utils = getUtility(IKofaUtils)
1189        password = kofa_utils.genPassword()
1190        IUserAccount(applicant).setPassword(password)
1191        # Send email with credentials
1192        login_url = self.url(grok.getSite(), 'login')
1193        url_info = u'Login: %s' % login_url
1194        msg = _('You have successfully been registered for the')
1195        if kofa_utils.sendCredentials(IUserAccount(applicant),
1196            password, url_info, msg):
1197            email_sent = applicant.email
1198        else:
1199            email_sent = None
1200        self._redirect(email=email_sent, password=password,
1201            applicant_id=applicant.applicant_id)
1202        return
1203
1204class ApplicantRegistrationEmailSent(KofaPage):
1205    """Landing page after successful registration.
1206
1207    """
1208    grok.name('registration_complete')
1209    grok.require('waeup.Public')
1210    grok.template('applicantregemailsent')
1211    label = _('Your registration was successful.')
1212
1213    def update(self, email=None, applicant_id=None, password=None):
1214        self.email = email
1215        self.password = password
1216        self.applicant_id = applicant_id
1217        return
1218
1219class ExportJobContainerOverview(KofaPage):
1220    """Page that lists active applicant data export jobs and provides links
1221    to discard or download CSV files.
1222
1223    """
1224    grok.context(VirtualApplicantsExportJobContainer)
1225    grok.require('waeup.manageApplication')
1226    grok.name('index.html')
1227    grok.template('exportjobsindex')
1228    label = _('Data Exports')
1229    pnav = 3
1230
1231    def update(self, CREATE=None, DISCARD=None, job_id=None):
1232        if CREATE:
1233            self.redirect(self.url('@@start_export'))
1234            return
1235        if DISCARD and job_id:
1236            entry = self.context.entry_from_job_id(job_id)
1237            self.context.delete_export_entry(entry)
1238            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1239            self.context.logger.info(
1240                '%s - discarded: job_id=%s' % (ob_class, job_id))
1241            self.flash(_('Discarded export') + ' %s' % job_id)
1242        self.entries = doll_up(self, user=self.request.principal.id)
1243        return
1244
1245class ExportJobContainerJobStart(KofaPage):
1246    """Page that starts an applicants export job.
1247
1248    """
1249    grok.context(VirtualApplicantsExportJobContainer)
1250    grok.require('waeup.manageApplication')
1251    grok.name('start_export')
1252
1253    def update(self):
1254        exporter = 'applicants'
1255        container_code = self.context.__parent__.code
1256        job_id = self.context.start_export_job(exporter,
1257                                      self.request.principal.id,
1258                                      container=container_code)
1259
1260        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1261        self.context.logger.info(
1262            '%s - exported: %s (%s), job_id=%s'
1263            % (ob_class, exporter, container_code, job_id))
1264        self.flash(_('Export started.'))
1265        self.redirect(self.url(self.context))
1266        return
1267
1268    def render(self):
1269        return
1270
1271class ExportJobContainerDownload(ExportCSVView):
1272    """Page that downloads a students export csv file.
1273
1274    """
1275    grok.context(VirtualApplicantsExportJobContainer)
1276    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.