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

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

Log changes when saving ApplicantsContainerManageFormPage?.

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