source: main/waeup.kofa/branches/0.2/src/waeup/kofa/applicants/browser.py @ 15369

Last change on this file since 15369 was 11161, checked in by uli, 11 years ago

Merge current trunk into 0.2 maintenance branch. Now really

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