source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/applicants/browser.py @ 10209

Last change on this file since 10209 was 9211, checked in by uli, 12 years ago

Rollback r9209. Looks like multiple merges from trunk confuse svn when merging back into trunk.

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