source: main/waeup.kofa/branches/uli-diazo-themed/src/waeup/kofa/applicants/browser.py @ 11011

Last change on this file since 11011 was 11011, checked in by Henrik Bettermann, 11 years ago

More adjustments in applicants module.

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