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

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

Make sure that that 1st and 2nd choice course of study are different.

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