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

Last change on this file since 12388 was 12247, checked in by Henrik Bettermann, 10 years ago

Remove deprecated HTML and REST widgets and use html2dict instead.

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