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

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

Lock applicant in handle_applicant_transition_event.

  • Property svn:keywords set to Id
File size: 40.0 KB
Line 
1## $Id: browser.py 9398 2012-10-23 19:22:22Z 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.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    @property
312    def form_fields(self):
313        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
314        form_fields['description'].custom_widget = HTMLDisplayWidget
315        form_fields[
316            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
317        form_fields[
318            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
319        if self.request.principal.id == 'zope.anybody':
320            form_fields = form_fields.omit(
321                'code', 'prefix', 'year', 'mode',
322                'strict_deadline', 'application_category')
323        return form_fields
324
325    @property
326    def introduction(self):
327        # Here we know that the cookie has been set
328        lang = self.request.cookies.get('kofa.language')
329        html = self.context.description_dict.get(lang,'')
330        if html == '':
331            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
332            html = self.context.description_dict.get(portal_language,'')
333        return html
334
335    @property
336    def label(self):
337        return "%s" % self.context.title
338
339class ApplicantsContainerManageFormPage(KofaEditFormPage):
340    grok.context(IApplicantsContainer)
341    grok.name('manage')
342    grok.template('applicantscontainermanagepage')
343    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
344    taboneactions = [_('Save'),_('Cancel')]
345    tabtwoactions = [_('Remove selected'),_('Cancel'),
346        _('Create students from selected')]
347    tabthreeactions1 = [_('Remove selected local roles')]
348    tabthreeactions2 = [_('Add local role')]
349    # Use friendlier date widget...
350    grok.require('waeup.manageApplication')
351
352    @property
353    def label(self):
354        return _('Manage applicants container')
355
356    pnav = 3
357
358    @property
359    def showApplicants(self):
360        if len(self.context) < 5000:
361            return True
362        return False
363
364    def update(self):
365        datepicker.need() # Enable jQuery datepicker in date fields.
366        tabs.need()
367        toggleall.need()
368        self.tab1 = self.tab2 = self.tab3 = ''
369        qs = self.request.get('QUERY_STRING', '')
370        if not qs:
371            qs = 'tab1'
372        setattr(self, qs, 'active')
373        warning.need()
374        datatable.need()  # Enable jQurey datatables for contents listing
375        return super(ApplicantsContainerManageFormPage, self).update()
376
377    def getLocalRoles(self):
378        roles = ILocalRolesAssignable(self.context)
379        return roles()
380
381    def getUsers(self):
382        """Get a list of all users.
383        """
384        for key, val in grok.getSite()['users'].items():
385            url = self.url(val)
386            yield(dict(url=url, name=key, val=val))
387
388    def getUsersWithLocalRoles(self):
389        return get_users_with_local_roles(self.context)
390
391    def _description(self):
392        view = ApplicantsContainerPage(
393            self.context,self.request)
394        view.setUpWidgets()
395        return view.widgets['description']()
396
397    @action(_('Save'), style='primary')
398    def save(self, **data):
399        self.applyData(self.context, **data)
400        self.context.description_dict = self._description()
401        # Always refresh title. So we can change titles
402        # if APP_TYPES_DICT has been edited.
403        appcats_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
404        title = appcats_dict[self.context.prefix][0]
405        self.context.title = u'%s %s/%s' % (
406            title, self.context.year, self.context.year + 1)
407        self.flash(_('Form has been saved.'))
408        return
409
410    @jsaction(_('Remove selected'))
411    def delApplicant(self, **data):
412        form = self.request.form
413        if form.has_key('val_id'):
414            child_id = form['val_id']
415        else:
416            self.flash(_('No applicant selected!'))
417            self.redirect(self.url(self.context, '@@manage')+'?tab2')
418            return
419        if not isinstance(child_id, list):
420            child_id = [child_id]
421        deleted = []
422        for id in child_id:
423            try:
424                del self.context[id]
425                deleted.append(id)
426            except:
427                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
428                        id, sys.exc_info()[0], sys.exc_info()[1]))
429        if len(deleted):
430            self.flash(_('Successfully removed: ${a}',
431                mapping = {'a':', '.join(deleted)}))
432        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
433        return
434
435    @action(_('Create students from selected'))
436    def createStudents(self, **data):
437        form = self.request.form
438        if form.has_key('val_id'):
439            child_id = form['val_id']
440        else:
441            self.flash(_('No applicant selected!'))
442            self.redirect(self.url(self.context, '@@manage')+'?tab2')
443            return
444        if not isinstance(child_id, list):
445            child_id = [child_id]
446        created = []
447        for id in child_id:
448            success, msg = self.context[id].createStudent(view=self)
449            if success:
450                created.append(id)
451        if len(created):
452            self.flash(_('${a} students successfully created.',
453                mapping = {'a': len(created)}))
454        else:
455            self.flash(_('No student could be created.'))
456        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
457        return
458
459    @action(_('Cancel'), validator=NullValidator)
460    def cancel(self, **data):
461        self.redirect(self.url(self.context))
462        return
463
464    @action(_('Add local role'), validator=NullValidator)
465    def addLocalRole(self, **data):
466        return add_local_role(self,3, **data)
467
468    @action(_('Remove selected local roles'))
469    def delLocalRoles(self, **data):
470        return del_local_roles(self,3,**data)
471
472class ApplicantAddFormPage(KofaAddFormPage):
473    """Add-form to add an applicant.
474    """
475    grok.context(IApplicantsContainer)
476    grok.require('waeup.manageApplication')
477    grok.name('addapplicant')
478    #grok.template('applicantaddpage')
479    form_fields = grok.AutoFields(IApplicant).select(
480        'firstname', 'middlename', 'lastname',
481        'email', 'phone')
482    label = _('Add applicant')
483    pnav = 3
484
485    @action(_('Create application record'))
486    def addApplicant(self, **data):
487        applicant = createObject(u'waeup.Applicant')
488        self.applyData(applicant, **data)
489        self.context.addApplicant(applicant)
490        self.flash(_('Applicant record created.'))
491        self.redirect(
492            self.url(self.context[applicant.application_number], 'index'))
493        return
494
495class ApplicantDisplayFormPage(KofaDisplayFormPage):
496    """A display view for applicant data.
497    """
498    grok.context(IApplicant)
499    grok.name('index')
500    grok.require('waeup.viewApplication')
501    grok.template('applicantdisplaypage')
502    form_fields = grok.AutoFields(IApplicant).omit(
503        'locked', 'course_admitted', 'password', 'suspended')
504    label = _('Applicant')
505    pnav = 3
506    hide_hint = False
507
508    @property
509    def separators(self):
510        return getUtility(IApplicantsUtils).SEPARATORS_DICT
511
512    def update(self):
513        self.passport_url = self.url(self.context, 'passport.jpg')
514        # Mark application as started if applicant logs in for the first time
515        usertype = getattr(self.request.principal, 'user_type', None)
516        if usertype == 'applicant' and \
517            IWorkflowState(self.context).getState() == INITIALIZED:
518            IWorkflowInfo(self.context).fireTransition('start')
519        return
520
521    @property
522    def hasPassword(self):
523        if self.context.password:
524            return _('set')
525        return _('unset')
526
527    @property
528    def label(self):
529        container_title = self.context.__parent__.title
530        return _('${a} <br /> Application Record ${b}', mapping = {
531            'a':container_title, 'b':self.context.application_number})
532
533    def getCourseAdmitted(self):
534        """Return link, title and code in html format to the certificate
535           admitted.
536        """
537        course_admitted = self.context.course_admitted
538        if getattr(course_admitted, '__parent__',None):
539            url = self.url(course_admitted)
540            title = course_admitted.title
541            code = course_admitted.code
542            return '<a href="%s">%s - %s</a>' %(url,code,title)
543        return ''
544
545class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
546    grok.context(IApplicant)
547    grok.name('base')
548    form_fields = grok.AutoFields(IApplicant).select(
549        'applicant_id','email', 'course1')
550
551class CreateStudentPage(UtilityView, grok.View):
552    """Create a student object from applicant data.
553    """
554    grok.context(IApplicant)
555    grok.name('createstudent')
556    grok.require('waeup.manageStudent')
557
558    def update(self):
559        msg = self.context.createStudent(view=self)[1]
560        self.flash(msg)
561        self.redirect(self.url(self.context))
562        return
563
564    def render(self):
565        return
566
567class CreateAllStudentsPage(UtilityView, grok.View):
568    """Create all student objects from applicant data
569    in a container.
570
571    This is a hidden page, no link or button will
572    be provided and only PortalManagers can do this.
573    """
574    grok.context(IApplicantsContainer)
575    grok.name('createallstudents')
576    grok.require('waeup.managePortal')
577
578    def update(self):
579        cat = getUtility(ICatalog, name='applicants_catalog')
580        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
581        created = []
582        for result in results:
583            if not self.context.has_key(result.application_number):
584                continue
585            success, msg = result.createStudent(view=self)
586            if success:
587                created.append(result.applicant_id)
588            else:
589                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
590                self.context.__parent__.logger.info(
591                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
592        if len(created):
593            self.flash(_('${a} students successfully created.',
594                mapping = {'a': len(created)}))
595        else:
596            self.flash(_('No student could be created.'))
597        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
598        return
599
600    def render(self):
601        return
602
603class ApplicationFeePaymentAddPage(UtilityView, grok.View):
604    """ Page to add an online payment ticket
605    """
606    grok.context(IApplicant)
607    grok.name('addafp')
608    grok.require('waeup.payApplicant')
609    factory = u'waeup.ApplicantOnlinePayment'
610
611    def update(self):
612        for key in self.context.keys():
613            ticket = self.context[key]
614            if ticket.p_state == 'paid':
615                  self.flash(
616                      _('This type of payment has already been made.'))
617                  self.redirect(self.url(self.context))
618                  return
619        applicants_utils = getUtility(IApplicantsUtils)
620        container = self.context.__parent__
621        payment = createObject(self.factory)
622        error = applicants_utils.setPaymentDetails(container, payment)
623        if error is not None:
624            self.flash(error)
625            self.redirect(self.url(self.context))
626            return
627        self.context[payment.p_id] = payment
628        self.flash(_('Payment ticket created.'))
629        self.redirect(self.url(payment))
630        return
631
632    def render(self):
633        return
634
635
636class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
637    """ Page to view an online payment ticket
638    """
639    grok.context(IApplicantOnlinePayment)
640    grok.name('index')
641    grok.require('waeup.viewApplication')
642    form_fields = grok.AutoFields(IApplicantOnlinePayment)
643    form_fields[
644        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
645    form_fields[
646        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
647    pnav = 3
648
649    @property
650    def label(self):
651        return _('${a}: Online Payment Ticket ${b}', mapping = {
652            'a':self.context.__parent__.display_fullname,
653            'b':self.context.p_id})
654
655class OnlinePaymentApprovePage(UtilityView, grok.View):
656    """ Approval view
657    """
658    grok.context(IApplicantOnlinePayment)
659    grok.name('approve')
660    grok.require('waeup.managePortal')
661
662    def update(self):
663        success, msg, log = self.context.approveApplicantPayment()
664        if log is not None:
665            self.context.__parent__.writeLogMessage(self, log)
666        self.flash(msg)
667        return
668
669    def render(self):
670        self.redirect(self.url(self.context, '@@index'))
671        return
672
673class ExportPDFPaymentSlipPage(UtilityView, grok.View):
674    """Deliver a PDF slip of the context.
675    """
676    grok.context(IApplicantOnlinePayment)
677    grok.name('payment_slip.pdf')
678    grok.require('waeup.viewApplication')
679    form_fields = grok.AutoFields(IApplicantOnlinePayment)
680    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
681    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
682    prefix = 'form'
683    note = None
684
685    @property
686    def title(self):
687        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
688        return translate(_('Payment Data'), 'waeup.kofa',
689            target_language=portal_language)
690
691    @property
692    def label(self):
693        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
694        return translate(_('Online Payment Slip'),
695            'waeup.kofa', target_language=portal_language) \
696            + ' %s' % self.context.p_id
697
698    def render(self):
699        #if self.context.p_state != 'paid':
700        #    self.flash(_('Ticket not yet paid.'))
701        #    self.redirect(self.url(self.context))
702        #    return
703        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
704            self.request)
705        students_utils = getUtility(IStudentsUtils)
706        return students_utils.renderPDF(self,'payment_slip.pdf',
707            self.context.__parent__, applicantview, note=self.note)
708
709class ExportPDFPage(UtilityView, grok.View):
710    """Deliver a PDF slip of the context.
711    """
712    grok.context(IApplicant)
713    grok.name('application_slip.pdf')
714    grok.require('waeup.viewApplication')
715    prefix = 'form'
716
717    def update(self):
718        if self.context.state in ('initialized', 'started', 'paid'):
719            self.flash(
720                _('Please pay and submit before trying to download the application slip.'))
721            return self.redirect(self.url(self.context))
722        return
723
724    def render(self):
725        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
726            view=self)
727        self.response.setHeader(
728            'Content-Type', 'application/pdf')
729        return pdfstream
730
731def handle_img_upload(upload, context, view):
732    """Handle upload of applicant image.
733
734    Returns `True` in case of success or `False`.
735
736    Please note that file pointer passed in (`upload`) most probably
737    points to end of file when leaving this function.
738    """
739    size = file_size(upload)
740    if size > MAX_UPLOAD_SIZE:
741        view.flash(_('Uploaded image is too big!'))
742        return False
743    dummy, ext = os.path.splitext(upload.filename)
744    ext.lower()
745    if ext != '.jpg':
746        view.flash(_('jpg file extension expected.'))
747        return False
748    upload.seek(0) # file pointer moved when determining size
749    store = getUtility(IExtFileStore)
750    file_id = IFileStoreNameChooser(context).chooseName()
751    store.createFile(file_id, upload)
752    return True
753
754class ApplicantManageFormPage(KofaEditFormPage):
755    """A full edit view for applicant data.
756    """
757    grok.context(IApplicant)
758    grok.name('manage')
759    grok.require('waeup.manageApplication')
760    form_fields = grok.AutoFields(IApplicant)
761    form_fields['student_id'].for_display = True
762    form_fields['applicant_id'].for_display = True
763    grok.template('applicanteditpage')
764    manage_applications = True
765    pnav = 3
766    display_actions = [[_('Save'), _('Final Submit')],
767        [_('Add online payment ticket'),_('Remove selected tickets')]]
768
769    @property
770    def separators(self):
771        return getUtility(IApplicantsUtils).SEPARATORS_DICT
772
773    def update(self):
774        datepicker.need() # Enable jQuery datepicker in date fields.
775        warning.need()
776        super(ApplicantManageFormPage, self).update()
777        self.wf_info = IWorkflowInfo(self.context)
778        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
779        self.passport_changed = None
780        upload = self.request.form.get('form.passport', None)
781        if upload:
782            # We got a fresh upload
783            self.passport_changed = handle_img_upload(
784                upload, self.context, self)
785        return
786
787    @property
788    def label(self):
789        container_title = self.context.__parent__.title
790        return _('${a} <br /> Application Form ${b}', mapping = {
791            'a':container_title, 'b':self.context.application_number})
792
793    def getTransitions(self):
794        """Return a list of dicts of allowed transition ids and titles.
795
796        Each list entry provides keys ``name`` and ``title`` for
797        internal name and (human readable) title of a single
798        transition.
799        """
800        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
801            if not t[0] == 'pay']
802        return [dict(name='', title=_('No transition'))] +[
803            dict(name=x, title=y) for x, y in allowed_transitions]
804
805    @action(_('Save'), style='primary')
806    def save(self, **data):
807        form = self.request.form
808        password = form.get('password', None)
809        password_ctl = form.get('control_password', None)
810        if password:
811            validator = getUtility(IPasswordValidator)
812            errors = validator.validate_password(password, password_ctl)
813            if errors:
814                self.flash( ' '.join(errors))
815                return
816        if self.passport_changed is False:  # False is not None!
817            return # error during image upload. Ignore other values
818        changed_fields = self.applyData(self.context, **data)
819        # Turn list of lists into single list
820        if changed_fields:
821            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
822        else:
823            changed_fields = []
824        if self.passport_changed:
825            changed_fields.append('passport')
826        if password:
827            # Now we know that the form has no errors and can set password ...
828            IUserAccount(self.context).setPassword(password)
829            changed_fields.append('password')
830        fields_string = ' + '.join(changed_fields)
831        trans_id = form.get('transition', None)
832        if trans_id:
833            self.wf_info.fireTransition(trans_id)
834        self.flash(_('Form has been saved.'))
835        if fields_string:
836            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
837        return
838
839    def unremovable(self, ticket):
840        return False
841
842    # This method is also used by the ApplicantEditFormPage
843    def delPaymentTickets(self, **data):
844        form = self.request.form
845        if form.has_key('val_id'):
846            child_id = form['val_id']
847        else:
848            self.flash(_('No payment selected.'))
849            self.redirect(self.url(self.context))
850            return
851        if not isinstance(child_id, list):
852            child_id = [child_id]
853        deleted = []
854        for id in child_id:
855            # Applicants are not allowed to remove used payment tickets
856            if not self.unremovable(self.context[id]):
857                try:
858                    del self.context[id]
859                    deleted.append(id)
860                except:
861                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
862                            id, sys.exc_info()[0], sys.exc_info()[1]))
863        if len(deleted):
864            self.flash(_('Successfully removed: ${a}',
865                mapping = {'a':', '.join(deleted)}))
866            self.context.writeLogMessage(
867                self, 'removed: % s' % ', '.join(deleted))
868        return
869
870    # We explicitely want the forms to be validated before payment tickets
871    # can be created. If no validation is requested, use
872    # 'validator=NullValidator' in the action directive
873    @action(_('Add online payment ticket'))
874    def addPaymentTicket(self, **data):
875        self.redirect(self.url(self.context, '@@addafp'))
876        return
877
878    @jsaction(_('Remove selected tickets'))
879    def removePaymentTickets(self, **data):
880        self.delPaymentTickets(**data)
881        self.redirect(self.url(self.context) + '/@@manage')
882        return
883
884class ApplicantEditFormPage(ApplicantManageFormPage):
885    """An applicant-centered edit view for applicant data.
886    """
887    grok.context(IApplicantEdit)
888    grok.name('edit')
889    grok.require('waeup.handleApplication')
890    form_fields = grok.AutoFields(IApplicantEdit).omit(
891        'locked', 'course_admitted', 'student_id',
892        'suspended'
893        )
894    form_fields['applicant_id'].for_display = True
895    form_fields['reg_number'].for_display = True
896    grok.template('applicanteditpage')
897    manage_applications = False
898
899    @property
900    def display_actions(self):
901        state = IWorkflowState(self.context).getState()
902        if state == INITIALIZED:
903            actions = [[],[]]
904        elif state == STARTED:
905            actions = [[_('Save')],
906                [_('Add online payment ticket'),_('Remove selected tickets')]]
907        elif state == PAID:
908            actions = [[_('Save'), _('Final Submit')],
909                [_('Remove selected tickets')]]
910        else:
911            actions = [[],[]]
912        return actions
913
914    def unremovable(self, ticket):
915        state = IWorkflowState(self.context).getState()
916        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
917
918    def emit_lock_message(self):
919        self.flash(_('The requested form is locked (read-only).'))
920        self.redirect(self.url(self.context))
921        return
922
923    def update(self):
924        if self.context.locked or (
925            self.context.__parent__.expired and
926            self.context.__parent__.strict_deadline):
927            self.emit_lock_message()
928            return
929        super(ApplicantEditFormPage, self).update()
930        return
931
932    def dataNotComplete(self):
933        store = getUtility(IExtFileStore)
934        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
935            return _('No passport picture uploaded.')
936        if not self.request.form.get('confirm_passport', False):
937            return _('Passport picture confirmation box not ticked.')
938        return False
939
940    # We explicitely want the forms to be validated before payment tickets
941    # can be created. If no validation is requested, use
942    # 'validator=NullValidator' in the action directive
943    @action(_('Add online payment ticket'))
944    def addPaymentTicket(self, **data):
945        self.redirect(self.url(self.context, '@@addafp'))
946        return
947
948    @jsaction(_('Remove selected tickets'))
949    def removePaymentTickets(self, **data):
950        self.delPaymentTickets(**data)
951        self.redirect(self.url(self.context) + '/@@edit')
952        return
953
954    @action(_('Save'), style='primary')
955    def save(self, **data):
956        if self.passport_changed is False:  # False is not None!
957            return # error during image upload. Ignore other values
958        self.applyData(self.context, **data)
959        self.flash('Form has been saved.')
960        return
961
962    @submitaction(_('Final Submit'))
963    def finalsubmit(self, **data):
964        if self.passport_changed is False:  # False is not None!
965            return # error during image upload. Ignore other values
966        if self.dataNotComplete():
967            self.flash(self.dataNotComplete())
968            return
969        self.applyData(self.context, **data)
970        state = IWorkflowState(self.context).getState()
971        # This shouldn't happen, but the application officer
972        # might have forgotten to lock the form after changing the state
973        if state != PAID:
974            self.flash(_('The form cannot be submitted. Wrong state!'))
975            return
976        IWorkflowInfo(self.context).fireTransition('submit')
977        # application_date is used in export files for sorting.
978        # We can thus store utc.
979        self.context.application_date = datetime.utcnow()
980        self.flash(_('Form has been submitted.'))
981        self.redirect(self.url(self.context))
982        return
983
984class PassportImage(grok.View):
985    """Renders the passport image for applicants.
986    """
987    grok.name('passport.jpg')
988    grok.context(IApplicant)
989    grok.require('waeup.viewApplication')
990
991    def render(self):
992        # A filename chooser turns a context into a filename suitable
993        # for file storage.
994        image = getUtility(IExtFileStore).getFileByContext(self.context)
995        self.response.setHeader(
996            'Content-Type', 'image/jpeg')
997        if image is None:
998            # show placeholder image
999            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1000        return image
1001
1002class ApplicantRegistrationPage(KofaAddFormPage):
1003    """Captcha'd registration page for applicants.
1004    """
1005    grok.context(IApplicantsContainer)
1006    grok.name('register')
1007    grok.require('waeup.Anonymous')
1008    grok.template('applicantregister')
1009
1010    @property
1011    def form_fields(self):
1012        form_fields = None
1013        if self.context.mode == 'update':
1014            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1015                'firstname','reg_number','email')
1016        else: #if self.context.mode == 'create':
1017            form_fields = grok.AutoFields(IApplicantEdit).select(
1018                'firstname', 'middlename', 'lastname', 'email', 'phone')
1019        return form_fields
1020
1021    @property
1022    def label(self):
1023        return _('Apply for ${a}',
1024            mapping = {'a':self.context.title})
1025
1026    def update(self):
1027        if self.context.expired:
1028            self.flash(_('Outside application period.'))
1029            self.redirect(self.url(self.context))
1030            return
1031        # Handle captcha
1032        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1033        self.captcha_result = self.captcha.verify(self.request)
1034        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1035        return
1036
1037    def _redirect(self, email, password, applicant_id):
1038        # Forward only email to landing page in base package.
1039        self.redirect(self.url(self.context, 'registration_complete',
1040            data = dict(email=email)))
1041        return
1042
1043    @action(_('Send login credentials to email address'), style='primary')
1044    def register(self, **data):
1045        if not self.captcha_result.is_valid:
1046            # Captcha will display error messages automatically.
1047            # No need to flash something.
1048            return
1049        if self.context.mode == 'create':
1050            # Add applicant
1051            applicant = createObject(u'waeup.Applicant')
1052            self.applyData(applicant, **data)
1053            self.context.addApplicant(applicant)
1054            applicant.reg_number = applicant.applicant_id
1055            notify(grok.ObjectModifiedEvent(applicant))
1056        elif self.context.mode == 'update':
1057            # Update applicant
1058            reg_number = data.get('reg_number','')
1059            firstname = data.get('firstname','')
1060            cat = getUtility(ICatalog, name='applicants_catalog')
1061            results = list(
1062                cat.searchResults(reg_number=(reg_number, reg_number)))
1063            if results:
1064                applicant = results[0]
1065                if getattr(applicant,'firstname',None) is None:
1066                    self.flash(_('An error occurred.'))
1067                    return
1068                elif applicant.firstname.lower() != firstname.lower():
1069                    # Don't tell the truth here. Anonymous must not
1070                    # know that a record was found and only the firstname
1071                    # verification failed.
1072                    self.flash(_('No application record found.'))
1073                    return
1074                elif applicant.password is not None and \
1075                    applicant.state != INITIALIZED:
1076                    self.flash(_('Your password has already been set and used. '
1077                                 'Please proceed to the login page.'))
1078                    return
1079                # Store email address but nothing else.
1080                applicant.email = data['email']
1081                notify(grok.ObjectModifiedEvent(applicant))
1082            else:
1083                # No record found, this is the truth.
1084                self.flash(_('No application record found.'))
1085                return
1086        else:
1087            # Does not happen but anyway ...
1088            return
1089        kofa_utils = getUtility(IKofaUtils)
1090        password = kofa_utils.genPassword()
1091        IUserAccount(applicant).setPassword(password)
1092        # Send email with credentials
1093        login_url = self.url(grok.getSite(), 'login')
1094        url_info = u'Login: %s' % login_url
1095        msg = _('You have successfully been registered for the')
1096        if kofa_utils.sendCredentials(IUserAccount(applicant),
1097            password, url_info, msg):
1098            email_sent = applicant.email
1099        else:
1100            email_sent = None
1101        self._redirect(email=email_sent, password=password,
1102            applicant_id=applicant.applicant_id)
1103        return
1104
1105class ApplicantRegistrationEmailSent(KofaPage):
1106    """Landing page after successful registration.
1107
1108    """
1109    grok.name('registration_complete')
1110    grok.require('waeup.Public')
1111    grok.template('applicantregemailsent')
1112    label = _('Your registration was successful.')
1113
1114    def update(self, email=None, applicant_id=None, password=None):
1115        self.email = email
1116        self.password = password
1117        self.applicant_id = applicant_id
1118        return
Note: See TracBrowser for help on using the repository browser.