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

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

Add expired property to be used in views and viewlets.

If strict_deadline is set, applicants are not allowed to open the edit form when application period has expired.

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