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

Last change on this file since 9948 was 9900, checked in by Henrik Bettermann, 12 years ago

Let's create all students from applicants root.

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