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

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

suspended field must not be edited by special applicants.

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