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

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

Implement a special application procedure. This application is meant for supplementary payments by alumni and other persons who are not students of the portal.

Attention: All custom packages have to be adjusted.

  • Property svn:keywords set to Id
File size: 45.2 KB
Line 
1## $Id: browser.py 10831 2013-12-10 06:24:03Z 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
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(IApplicant).select(
532                'applicant_id', 'firstname', 'middlename', 'lastname',
533                'reg_number', 'date_of_birth', 'email', 'special_application')
534        else:
535            form_fields = grok.AutoFields(IApplicant).omit(
536                'locked', 'course_admitted', 'password', 'suspended',
537                'special_application')
538        return form_fields
539
540    @property
541    def target(self):
542        return getattr(self.context.__parent__, 'prefix', None)
543
544    @property
545    def separators(self):
546        return getUtility(IApplicantsUtils).SEPARATORS_DICT
547
548    def update(self):
549        self.passport_url = self.url(self.context, 'passport.jpg')
550        # Mark application as started if applicant logs in for the first time
551        usertype = getattr(self.request.principal, 'user_type', None)
552        if usertype == 'applicant' and \
553            IWorkflowState(self.context).getState() == INITIALIZED:
554            IWorkflowInfo(self.context).fireTransition('start')
555        return
556
557    @property
558    def hasPassword(self):
559        if self.context.password:
560            return _('set')
561        return _('unset')
562
563    @property
564    def label(self):
565        container_title = self.context.__parent__.title
566        return _('${a} <br /> Application Record ${b}', mapping = {
567            'a':container_title, 'b':self.context.application_number})
568
569    def getCourseAdmitted(self):
570        """Return link, title and code in html format to the certificate
571           admitted.
572        """
573        course_admitted = self.context.course_admitted
574        if getattr(course_admitted, '__parent__',None):
575            url = self.url(course_admitted)
576            title = course_admitted.title
577            code = course_admitted.code
578            return '<a href="%s">%s - %s</a>' %(url,code,title)
579        return ''
580
581class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
582    grok.context(IApplicant)
583    grok.name('base')
584    form_fields = grok.AutoFields(IApplicant).select(
585        'applicant_id','email', 'course1')
586
587class CreateStudentPage(UtilityView, grok.View):
588    """Create a student object from applicant data.
589    """
590    grok.context(IApplicant)
591    grok.name('createstudent')
592    grok.require('waeup.manageStudent')
593
594    def update(self):
595        msg = self.context.createStudent(view=self)[1]
596        self.flash(msg)
597        self.redirect(self.url(self.context))
598        return
599
600    def render(self):
601        return
602
603class CreateAllStudentsPage(UtilityView, grok.View):
604    """Create all student objects from applicant data
605    in a container.
606
607    This is a hidden page, no link or button will
608    be provided and only PortalManagers can do this.
609    """
610    #grok.context(IApplicantsContainer)
611    grok.name('createallstudents')
612    grok.require('waeup.managePortal')
613
614    def update(self):
615        cat = getUtility(ICatalog, name='applicants_catalog')
616        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
617        created = []
618        container_only = False
619        applicants_root = grok.getSite()['applicants']
620        if isinstance(self.context, ApplicantsContainer):
621            container_only = True
622        for result in results:
623            if container_only and result.__parent__ is not self.context:
624                continue
625            success, msg = result.createStudent(view=self)
626            if success:
627                created.append(result.applicant_id)
628            else:
629                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
630                applicants_root.logger.info(
631                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
632        if len(created):
633            self.flash(_('${a} students successfully created.',
634                mapping = {'a': len(created)}))
635        else:
636            self.flash(_('No student could be created.'))
637        self.redirect(self.url(self.context))
638        return
639
640    def render(self):
641        return
642
643class ApplicationFeePaymentAddPage(UtilityView, grok.View):
644    """ Page to add an online payment ticket
645    """
646    grok.context(IApplicant)
647    grok.name('addafp')
648    grok.require('waeup.payApplicant')
649    factory = u'waeup.ApplicantOnlinePayment'
650
651    def update(self):
652        for key in self.context.keys():
653            ticket = self.context[key]
654            if ticket.p_state == 'paid':
655                  self.flash(
656                      _('This type of payment has already been made.'))
657                  self.redirect(self.url(self.context))
658                  return
659        applicants_utils = getUtility(IApplicantsUtils)
660        container = self.context.__parent__
661        payment = createObject(self.factory)
662        error = applicants_utils.setPaymentDetails(
663            container, payment, self.context)
664        if error is not None:
665            self.flash(error)
666            self.redirect(self.url(self.context))
667            return
668        self.context[payment.p_id] = payment
669        self.flash(_('Payment ticket created.'))
670        self.redirect(self.url(payment))
671        return
672
673    def render(self):
674        return
675
676
677class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
678    """ Page to view an online payment ticket
679    """
680    grok.context(IApplicantOnlinePayment)
681    grok.name('index')
682    grok.require('waeup.viewApplication')
683    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
684    form_fields[
685        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
686    form_fields[
687        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
688    pnav = 3
689
690    @property
691    def label(self):
692        return _('${a}: Online Payment Ticket ${b}', mapping = {
693            'a':self.context.__parent__.display_fullname,
694            'b':self.context.p_id})
695
696class OnlinePaymentApprovePage(UtilityView, grok.View):
697    """ Approval view
698    """
699    grok.context(IApplicantOnlinePayment)
700    grok.name('approve')
701    grok.require('waeup.managePortal')
702
703    def update(self):
704        success, msg, log = self.context.approveApplicantPayment()
705        if log is not None:
706            applicant = self.context.__parent__
707            # Add log message to applicants.log
708            applicant.writeLogMessage(self, log)
709            # Add log message to payments.log
710            self.context.logger.info(
711                '%s,%s,%s,%s,%s,,,,,,' % (
712                applicant.applicant_id,
713                self.context.p_id, self.context.p_category,
714                self.context.amount_auth, self.context.r_code))
715        self.flash(msg)
716        return
717
718    def render(self):
719        self.redirect(self.url(self.context, '@@index'))
720        return
721
722class ExportPDFPaymentSlipPage(UtilityView, grok.View):
723    """Deliver a PDF slip of the context.
724    """
725    grok.context(IApplicantOnlinePayment)
726    grok.name('payment_slip.pdf')
727    grok.require('waeup.viewApplication')
728    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
729    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
730    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
731    prefix = 'form'
732    note = None
733
734    @property
735    def title(self):
736        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
737        return translate(_('Payment Data'), 'waeup.kofa',
738            target_language=portal_language)
739
740    @property
741    def label(self):
742        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
743        return translate(_('Online Payment Slip'),
744            'waeup.kofa', target_language=portal_language) \
745            + ' %s' % self.context.p_id
746
747    def render(self):
748        #if self.context.p_state != 'paid':
749        #    self.flash(_('Ticket not yet paid.'))
750        #    self.redirect(self.url(self.context))
751        #    return
752        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
753            self.request)
754        students_utils = getUtility(IStudentsUtils)
755        return students_utils.renderPDF(self,'payment_slip.pdf',
756            self.context.__parent__, applicantview, note=self.note)
757
758class ExportPDFPageApplicationSlip(UtilityView, grok.View):
759    """Deliver a PDF slip of the context.
760    """
761    grok.context(IApplicant)
762    grok.name('application_slip.pdf')
763    grok.require('waeup.viewApplication')
764    prefix = 'form'
765
766    def update(self):
767        if self.context.state in ('initialized', 'started', 'paid'):
768            self.flash(
769                _('Please pay and submit before trying to download the application slip.'))
770            return self.redirect(self.url(self.context))
771        return
772
773    def render(self):
774        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
775            view=self)
776        self.response.setHeader(
777            'Content-Type', 'application/pdf')
778        return pdfstream
779
780def handle_img_upload(upload, context, view):
781    """Handle upload of applicant image.
782
783    Returns `True` in case of success or `False`.
784
785    Please note that file pointer passed in (`upload`) most probably
786    points to end of file when leaving this function.
787    """
788    size = file_size(upload)
789    if size > MAX_UPLOAD_SIZE:
790        view.flash(_('Uploaded image is too big!'))
791        return False
792    dummy, ext = os.path.splitext(upload.filename)
793    ext.lower()
794    if ext != '.jpg':
795        view.flash(_('jpg file extension expected.'))
796        return False
797    upload.seek(0) # file pointer moved when determining size
798    store = getUtility(IExtFileStore)
799    file_id = IFileStoreNameChooser(context).chooseName()
800    store.createFile(file_id, upload)
801    return True
802
803class ApplicantManageFormPage(KofaEditFormPage):
804    """A full edit view for applicant data.
805    """
806    grok.context(IApplicant)
807    grok.name('manage')
808    grok.require('waeup.manageApplication')
809    grok.template('applicanteditpage')
810    manage_applications = True
811    pnav = 3
812    display_actions = [[_('Save'), _('Final Submit')],
813        [_('Add online payment ticket'),_('Remove selected tickets')]]
814
815    @property
816    def form_fields(self):
817        if self.context.special:
818            form_fields = grok.AutoFields(IApplicant).select(
819                'applicant_id', 'firstname', 'middlename', 'lastname',
820                'reg_number', 'date_of_birth', 'email', 'special_application',
821                'locked')
822        else:
823            form_fields = grok.AutoFields(IApplicant)
824            form_fields['student_id'].for_display = True
825            form_fields['applicant_id'].for_display = True
826        return form_fields
827
828    @property
829    def target(self):
830        return getattr(self.context.__parent__, 'prefix', None)
831
832    @property
833    def separators(self):
834        return getUtility(IApplicantsUtils).SEPARATORS_DICT
835
836    def update(self):
837        datepicker.need() # Enable jQuery datepicker in date fields.
838        warning.need()
839        super(ApplicantManageFormPage, self).update()
840        self.wf_info = IWorkflowInfo(self.context)
841        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
842        self.upload_success = None
843        upload = self.request.form.get('form.passport', None)
844        if upload:
845            # We got a fresh upload, upload_success is
846            # either True or False
847            self.upload_success = handle_img_upload(
848                upload, self.context, self)
849            if self.upload_success:
850                self.context.writeLogMessage(self, 'saved: passport')
851        return
852
853    @property
854    def label(self):
855        container_title = self.context.__parent__.title
856        return _('${a} <br /> Application Form ${b}', mapping = {
857            'a':container_title, 'b':self.context.application_number})
858
859    def getTransitions(self):
860        """Return a list of dicts of allowed transition ids and titles.
861
862        Each list entry provides keys ``name`` and ``title`` for
863        internal name and (human readable) title of a single
864        transition.
865        """
866        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
867            if not t[0] == 'pay']
868        return [dict(name='', title=_('No transition'))] +[
869            dict(name=x, title=y) for x, y in allowed_transitions]
870
871    @action(_('Save'), style='primary')
872    def save(self, **data):
873        form = self.request.form
874        password = form.get('password', None)
875        password_ctl = form.get('control_password', None)
876        if password:
877            validator = getUtility(IPasswordValidator)
878            errors = validator.validate_password(password, password_ctl)
879            if errors:
880                self.flash( ' '.join(errors))
881                return
882        if self.upload_success is False:  # False is not None!
883            # Error during image upload. Ignore other values.
884            return
885        changed_fields = self.applyData(self.context, **data)
886        # Turn list of lists into single list
887        if changed_fields:
888            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
889        else:
890            changed_fields = []
891        if password:
892            # Now we know that the form has no errors and can set password ...
893            IUserAccount(self.context).setPassword(password)
894            changed_fields.append('password')
895        fields_string = ' + '.join(changed_fields)
896        trans_id = form.get('transition', None)
897        if trans_id:
898            self.wf_info.fireTransition(trans_id)
899        self.flash(_('Form has been saved.'))
900        if fields_string:
901            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
902        return
903
904    def unremovable(self, ticket):
905        return False
906
907    # This method is also used by the ApplicantEditFormPage
908    def delPaymentTickets(self, **data):
909        form = self.request.form
910        if 'val_id' in form:
911            child_id = form['val_id']
912        else:
913            self.flash(_('No payment selected.'))
914            self.redirect(self.url(self.context))
915            return
916        if not isinstance(child_id, list):
917            child_id = [child_id]
918        deleted = []
919        for id in child_id:
920            # Applicants are not allowed to remove used payment tickets
921            if not self.unremovable(self.context[id]):
922                try:
923                    del self.context[id]
924                    deleted.append(id)
925                except:
926                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
927                            id, sys.exc_info()[0], sys.exc_info()[1]))
928        if len(deleted):
929            self.flash(_('Successfully removed: ${a}',
930                mapping = {'a':', '.join(deleted)}))
931            self.context.writeLogMessage(
932                self, 'removed: % s' % ', '.join(deleted))
933        return
934
935    # We explicitely want the forms to be validated before payment tickets
936    # can be created. If no validation is requested, use
937    # 'validator=NullValidator' in the action directive
938    @action(_('Add online payment ticket'))
939    def addPaymentTicket(self, **data):
940        self.redirect(self.url(self.context, '@@addafp'))
941        return
942
943    @jsaction(_('Remove selected tickets'))
944    def removePaymentTickets(self, **data):
945        self.delPaymentTickets(**data)
946        self.redirect(self.url(self.context) + '/@@manage')
947        return
948
949    # Not used in base package
950    def file_exists(self, attr):
951        file = getUtility(IExtFileStore).getFileByContext(
952            self.context, attr=attr)
953        if file:
954            return True
955        else:
956            return False
957
958class ApplicantEditFormPage(ApplicantManageFormPage):
959    """An applicant-centered edit view for applicant data.
960    """
961    grok.context(IApplicantEdit)
962    grok.name('edit')
963    grok.require('waeup.handleApplication')
964    grok.template('applicanteditpage')
965    manage_applications = False
966    submit_state = PAID
967
968    @property
969    def form_fields(self):
970        if self.context.special:
971            form_fields = grok.AutoFields(IApplicantEdit).select(
972                'applicant_id', 'firstname', 'middlename', 'lastname',
973                'reg_number', 'date_of_birth', 'email', 'special_application')
974        else:
975            form_fields = grok.AutoFields(IApplicantEdit).omit(
976                'locked', 'course_admitted', 'student_id',
977                'suspended', 'special_application'
978                )
979            form_fields['applicant_id'].for_display = True
980            form_fields['reg_number'].for_display = True
981        return form_fields
982
983    @property
984    def display_actions(self):
985        state = IWorkflowState(self.context).getState()
986        actions = [[],[]]
987        if state == STARTED:
988            actions = [[_('Save')],
989                [_('Add online payment ticket'),_('Remove selected tickets')]]
990        elif state == PAID:
991            actions = [[_('Save'), _('Final Submit')],
992                [_('Remove selected tickets')]]
993        return actions
994
995    def unremovable(self, ticket):
996        state = IWorkflowState(self.context).getState()
997        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
998
999    def emit_lock_message(self):
1000        self.flash(_('The requested form is locked (read-only).'))
1001        self.redirect(self.url(self.context))
1002        return
1003
1004    def update(self):
1005        if self.context.locked or (
1006            self.context.__parent__.expired and
1007            self.context.__parent__.strict_deadline):
1008            self.emit_lock_message()
1009            return
1010        super(ApplicantEditFormPage, self).update()
1011        return
1012
1013    def dataNotComplete(self):
1014        store = getUtility(IExtFileStore)
1015        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1016            return _('No passport picture uploaded.')
1017        if not self.request.form.get('confirm_passport', False):
1018            return _('Passport picture confirmation box not ticked.')
1019        return False
1020
1021    # We explicitely want the forms to be validated before payment tickets
1022    # can be created. If no validation is requested, use
1023    # 'validator=NullValidator' in the action directive
1024    @action(_('Add online payment ticket'))
1025    def addPaymentTicket(self, **data):
1026        self.redirect(self.url(self.context, '@@addafp'))
1027        return
1028
1029    @jsaction(_('Remove selected tickets'))
1030    def removePaymentTickets(self, **data):
1031        self.delPaymentTickets(**data)
1032        self.redirect(self.url(self.context) + '/@@edit')
1033        return
1034
1035    @action(_('Save'), style='primary')
1036    def save(self, **data):
1037        if self.upload_success is False:  # False is not None!
1038            # Error during image upload. Ignore other values.
1039            return
1040        if data.get('course1', 1) == data.get('course2', 2):
1041            self.flash(_('1st and 2nd choice must be different.'))
1042            return
1043        self.applyData(self.context, **data)
1044        self.flash(_('Form has been saved.'))
1045        return
1046
1047    @submitaction(_('Final Submit'))
1048    def finalsubmit(self, **data):
1049        if self.upload_success is False:  # False is not None!
1050            return # error during image upload. Ignore other values
1051        if self.dataNotComplete():
1052            self.flash(self.dataNotComplete())
1053            return
1054        self.applyData(self.context, **data)
1055        state = IWorkflowState(self.context).getState()
1056        # This shouldn't happen, but the application officer
1057        # might have forgotten to lock the form after changing the state
1058        if state != self.submit_state:
1059            self.flash(_('The form cannot be submitted. Wrong state!'))
1060            return
1061        IWorkflowInfo(self.context).fireTransition('submit')
1062        # application_date is used in export files for sorting.
1063        # We can thus store utc.
1064        self.context.application_date = datetime.utcnow()
1065        self.flash(_('Form has been submitted.'))
1066        self.redirect(self.url(self.context))
1067        return
1068
1069class PassportImage(grok.View):
1070    """Renders the passport image for applicants.
1071    """
1072    grok.name('passport.jpg')
1073    grok.context(IApplicant)
1074    grok.require('waeup.viewApplication')
1075
1076    def render(self):
1077        # A filename chooser turns a context into a filename suitable
1078        # for file storage.
1079        image = getUtility(IExtFileStore).getFileByContext(self.context)
1080        self.response.setHeader(
1081            'Content-Type', 'image/jpeg')
1082        if image is None:
1083            # show placeholder image
1084            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1085        return image
1086
1087class ApplicantRegistrationPage(KofaAddFormPage):
1088    """Captcha'd registration page for applicants.
1089    """
1090    grok.context(IApplicantsContainer)
1091    grok.name('register')
1092    grok.require('waeup.Anonymous')
1093    grok.template('applicantregister')
1094
1095    @property
1096    def form_fields(self):
1097        form_fields = None
1098        if self.context.mode == 'update':
1099            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1100                'firstname','reg_number','email')
1101        else: #if self.context.mode == 'create':
1102            form_fields = grok.AutoFields(IApplicantEdit).select(
1103                'firstname', 'middlename', 'lastname', 'email', 'phone')
1104        return form_fields
1105
1106    @property
1107    def label(self):
1108        return _('Apply for ${a}',
1109            mapping = {'a':self.context.title})
1110
1111    def update(self):
1112        if self.context.expired:
1113            self.flash(_('Outside application period.'))
1114            self.redirect(self.url(self.context))
1115            return
1116        # Handle captcha
1117        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1118        self.captcha_result = self.captcha.verify(self.request)
1119        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1120        return
1121
1122    def _redirect(self, email, password, applicant_id):
1123        # Forward only email to landing page in base package.
1124        self.redirect(self.url(self.context, 'registration_complete',
1125            data = dict(email=email)))
1126        return
1127
1128    @action(_('Send login credentials to email address'), style='primary')
1129    def register(self, **data):
1130        if not self.captcha_result.is_valid:
1131            # Captcha will display error messages automatically.
1132            # No need to flash something.
1133            return
1134        if self.context.mode == 'create':
1135            # Add applicant
1136            applicant = createObject(u'waeup.Applicant')
1137            self.applyData(applicant, **data)
1138            self.context.addApplicant(applicant)
1139            applicant.reg_number = applicant.applicant_id
1140            notify(grok.ObjectModifiedEvent(applicant))
1141        elif self.context.mode == 'update':
1142            # Update applicant
1143            reg_number = data.get('reg_number','')
1144            firstname = data.get('firstname','')
1145            cat = getUtility(ICatalog, name='applicants_catalog')
1146            results = list(
1147                cat.searchResults(reg_number=(reg_number, reg_number)))
1148            if results:
1149                applicant = results[0]
1150                if getattr(applicant,'firstname',None) is None:
1151                    self.flash(_('An error occurred.'))
1152                    return
1153                elif applicant.firstname.lower() != firstname.lower():
1154                    # Don't tell the truth here. Anonymous must not
1155                    # know that a record was found and only the firstname
1156                    # verification failed.
1157                    self.flash(_('No application record found.'))
1158                    return
1159                elif applicant.password is not None and \
1160                    applicant.state != INITIALIZED:
1161                    self.flash(_('Your password has already been set and used. '
1162                                 'Please proceed to the login page.'))
1163                    return
1164                # Store email address but nothing else.
1165                applicant.email = data['email']
1166                notify(grok.ObjectModifiedEvent(applicant))
1167            else:
1168                # No record found, this is the truth.
1169                self.flash(_('No application record found.'))
1170                return
1171        else:
1172            # Does not happen but anyway ...
1173            return
1174        kofa_utils = getUtility(IKofaUtils)
1175        password = kofa_utils.genPassword()
1176        IUserAccount(applicant).setPassword(password)
1177        # Send email with credentials
1178        login_url = self.url(grok.getSite(), 'login')
1179        url_info = u'Login: %s' % login_url
1180        msg = _('You have successfully been registered for the')
1181        if kofa_utils.sendCredentials(IUserAccount(applicant),
1182            password, url_info, msg):
1183            email_sent = applicant.email
1184        else:
1185            email_sent = None
1186        self._redirect(email=email_sent, password=password,
1187            applicant_id=applicant.applicant_id)
1188        return
1189
1190class ApplicantRegistrationEmailSent(KofaPage):
1191    """Landing page after successful registration.
1192
1193    """
1194    grok.name('registration_complete')
1195    grok.require('waeup.Public')
1196    grok.template('applicantregemailsent')
1197    label = _('Your registration was successful.')
1198
1199    def update(self, email=None, applicant_id=None, password=None):
1200        self.email = email
1201        self.password = password
1202        self.applicant_id = applicant_id
1203        return
1204
1205class ExportJobContainerOverview(KofaPage):
1206    """Page that lists active applicant data export jobs and provides links
1207    to discard or download CSV files.
1208
1209    """
1210    grok.context(VirtualApplicantsExportJobContainer)
1211    grok.require('waeup.manageApplication')
1212    grok.name('index.html')
1213    grok.template('exportjobsindex')
1214    label = _('Applicant Data Exports')
1215    pnav = 3
1216
1217    def update(self, CREATE=None, DISCARD=None, job_id=None):
1218        if CREATE:
1219            self.redirect(self.url('@@start_export'))
1220            return
1221        if DISCARD and job_id:
1222            entry = self.context.entry_from_job_id(job_id)
1223            self.context.delete_export_entry(entry)
1224            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1225            self.context.logger.info(
1226                '%s - discarded: job_id=%s' % (ob_class, job_id))
1227            self.flash(_('Discarded export') + ' %s' % job_id)
1228        self.entries = doll_up(self, user=self.request.principal.id)
1229        return
1230
1231class ExportJobContainerJobStart(KofaPage):
1232    """Page that starts an applicants export job.
1233
1234    """
1235    grok.context(VirtualApplicantsExportJobContainer)
1236    grok.require('waeup.manageApplication')
1237    grok.name('start_export')
1238
1239    def update(self):
1240        exporter = 'applicants'
1241        container_code = self.context.__parent__.code
1242        job_id = self.context.start_export_job(exporter,
1243                                      self.request.principal.id,
1244                                      container=container_code)
1245
1246        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1247        self.context.logger.info(
1248            '%s - exported: %s (%s), job_id=%s'
1249            % (ob_class, exporter, container_code, job_id))
1250        self.flash(_('Export started.'))
1251        self.redirect(self.url(self.context))
1252        return
1253
1254    def render(self):
1255        return
1256
1257class ExportJobContainerDownload(ExportCSVView):
1258    """Page that downloads a students export csv file.
1259
1260    """
1261    grok.context(VirtualApplicantsExportJobContainer)
1262    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.