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

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

Jason is requesting customization of registration number field. This can't be done with the IApplicantBaseData based interfaces. It requires a new interface ISpecialApplicant.

Attention: The changes here are not yet compatible with the custom packages. Do not checkout!

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