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

Last change on this file since 10650 was 10645, checked in by Henrik Bettermann, 11 years ago

Rename buttons.

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