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

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

After careful consideration: plural is more appropriate than singular.

A student section has another meaning: https://en.wikipedia.org/wiki/Student_section

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