source: main/waeup.kofa/branches/uli-diazo-themed/src/waeup/kofa/applicants/browser.py @ 11012

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

Merge with changes made in trunk till r10944.

  • Property svn:keywords set to Id
File size: 46.3 KB
Line 
1## $Id: browser.py 11012 2014-01-30 14:13:41Z 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.resources import toggleall
49from waeup.kofa.browser.layout import (
50    NullValidator, jsaction, action, UtilityView, JSAction)
51from waeup.kofa.browser.pages import (
52    add_local_role, del_local_roles, doll_up, ExportCSVView)
53from waeup.kofa.browser.resources import datepicker, tabs, warning
54from waeup.kofa.interfaces import (
55    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
56    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
57from waeup.kofa.interfaces import MessageFactory as _
58from waeup.kofa.permissions import get_users_with_local_roles
59from waeup.kofa.students.interfaces import IStudentsUtils
60from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
61from waeup.kofa.widgets.datewidget import (
62    FriendlyDateDisplayWidget,
63    FriendlyDatetimeDisplayWidget)
64from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
65
66grok.context(IKofaObject) # Make IKofaObject the default context
67
68class SubmitJSAction(JSAction):
69
70    msg = _('\'You can not edit your application records after final submission.'
71            ' You really want to submit?\'')
72
73class submitaction(grok.action):
74
75    def __call__(self, success):
76        action = SubmitJSAction(self.label, success=success, **self.options)
77        self.actions.append(action)
78        return action
79
80class ApplicantsRootPage(KofaDisplayFormPage):
81    grok.context(IApplicantsRoot)
82    grok.name('index')
83    grok.require('waeup.Public')
84    form_fields = grok.AutoFields(IApplicantsRoot)
85    form_fields['description'].custom_widget = HTMLDisplayWidget
86    label = _('Application Section')
87    pnav = 3
88
89    def update(self):
90        super(ApplicantsRootPage, self).update()
91        return
92
93    @property
94    def introduction(self):
95        # Here we know that the cookie has been set
96        lang = self.request.cookies.get('kofa.language')
97        html = self.context.description_dict.get(lang,'')
98        if html == '':
99            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
100            html = self.context.description_dict.get(portal_language,'')
101        return html
102
103    @property
104    def containers(self):
105        if self.layout.isAuthenticated():
106            return self.context.values()
107        return [value for value in self.context.values() if not value.hidden]
108
109class ApplicantsSearchPage(KofaPage):
110    grok.context(IApplicantsRoot)
111    grok.name('search')
112    grok.require('waeup.viewApplication')
113    label = _('Find applicants')
114    search_button = _('Find applicant')
115    pnav = 3
116
117    def update(self, *args, **kw):
118        form = self.request.form
119        self.results = []
120        if 'searchterm' in form and form['searchterm']:
121            self.searchterm = form['searchterm']
122            self.searchtype = form['searchtype']
123        elif 'old_searchterm' in form:
124            self.searchterm = form['old_searchterm']
125            self.searchtype = form['old_searchtype']
126        else:
127            if 'search' in form:
128                self.flash(_('Empty search string'), type='warning')
129            return
130        self.results = search(query=self.searchterm,
131            searchtype=self.searchtype, view=self)
132        if not self.results:
133            self.flash(_('No applicant found.'), type='warning')
134        return
135
136class ApplicantsRootManageFormPage(KofaEditFormPage):
137    grok.context(IApplicantsRoot)
138    grok.name('manage')
139    grok.template('applicantsrootmanagepage')
140    form_fields = grok.AutoFields(IApplicantsRoot)
141    label = _('Manage application section')
142    pnav = 3
143    grok.require('waeup.manageApplication')
144    taboneactions = [_('Save')]
145    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
146    tabthreeactions1 = [_('Remove selected local roles')]
147    tabthreeactions2 = [_('Add local role')]
148    subunits = _('Applicants Containers')
149
150    def update(self):
151        warning.need()
152        self.tab1 = self.tab2 = self.tab3 = ''
153        qs = self.request.get('QUERY_STRING', '')
154        if not qs:
155            qs = 'tab1'
156        setattr(self, qs, 'active')
157        return super(ApplicantsRootManageFormPage, self).update()
158
159    def getLocalRoles(self):
160        roles = ILocalRolesAssignable(self.context)
161        return roles()
162
163    def getUsers(self):
164        """Get a list of all users.
165        """
166        for key, val in grok.getSite()['users'].items():
167            url = self.url(val)
168            yield(dict(url=url, name=key, val=val))
169
170    def getUsersWithLocalRoles(self):
171        return get_users_with_local_roles(self.context)
172
173    @jsaction(_('Remove selected'))
174    def delApplicantsContainers(self, **data):
175        form = self.request.form
176        if 'val_id' in form:
177            child_id = form['val_id']
178        else:
179            self.flash(_('No container selected!'), type='warning')
180            self.redirect(self.url(self.context, '@@manage')+'#tab2')
181            return
182        if not isinstance(child_id, list):
183            child_id = [child_id]
184        deleted = []
185        for id in child_id:
186            try:
187                del self.context[id]
188                deleted.append(id)
189            except:
190                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
191                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
192        if len(deleted):
193            self.flash(_('Successfully removed: ${a}',
194                mapping = {'a':', '.join(deleted)}))
195        self.redirect(self.url(self.context, '@@manage')+'#tab2')
196        return
197
198    @action(_('Add applicants container'), validator=NullValidator)
199    def addApplicantsContainer(self, **data):
200        self.redirect(self.url(self.context, '@@add'))
201        return
202
203    @action(_('Add local role'), validator=NullValidator)
204    def addLocalRole(self, **data):
205        return add_local_role(self,3, **data)
206
207    @action(_('Remove selected local roles'))
208    def delLocalRoles(self, **data):
209        return del_local_roles(self,3,**data)
210
211    def _description(self):
212        view = ApplicantsRootPage(
213            self.context,self.request)
214        view.setUpWidgets()
215        return view.widgets['description']()
216
217    @action(_('Save'), style='primary')
218    def save(self, **data):
219        self.applyData(self.context, **data)
220        self.context.description_dict = self._description()
221        self.flash(_('Form has been saved.'))
222        return
223
224class ApplicantsContainerAddFormPage(KofaAddFormPage):
225    grok.context(IApplicantsRoot)
226    grok.require('waeup.manageApplication')
227    grok.name('add')
228    grok.template('applicantscontaineraddpage')
229    label = _('Add applicants container')
230    pnav = 3
231
232    form_fields = grok.AutoFields(
233        IApplicantsContainerAdd).omit('code').omit('title')
234
235    def update(self):
236        datepicker.need() # Enable jQuery datepicker in date fields.
237        return super(ApplicantsContainerAddFormPage, self).update()
238
239    @action(_('Add applicants container'))
240    def addApplicantsContainer(self, **data):
241        year = data['year']
242        code = u'%s%s' % (data['prefix'], year)
243        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
244        title = apptypes_dict[data['prefix']][0]
245        title = u'%s %s/%s' % (title, year, year + 1)
246        if code in self.context.keys():
247            self.flash(
248              _('An applicants container for the same application '
249                'type and entrance year exists already in the database.'),
250                type='warning')
251            return
252        # Add new applicants container...
253        container = createObject(u'waeup.ApplicantsContainer')
254        self.applyData(container, **data)
255        container.code = code
256        container.title = title
257        self.context[code] = container
258        self.flash(_('Added:') + ' "%s".' % code)
259        self.redirect(self.url(self.context, u'@@manage'))
260        return
261
262    @action(_('Cancel'), validator=NullValidator)
263    def cancel(self, **data):
264        self.redirect(self.url(self.context, '@@manage'))
265
266class ApplicantsRootBreadcrumb(Breadcrumb):
267    """A breadcrumb for applicantsroot.
268    """
269    grok.context(IApplicantsRoot)
270    title = _(u'Applicants')
271
272class ApplicantsContainerBreadcrumb(Breadcrumb):
273    """A breadcrumb for applicantscontainers.
274    """
275    grok.context(IApplicantsContainer)
276
277
278class ApplicantsExportsBreadcrumb(Breadcrumb):
279    """A breadcrumb for exports.
280    """
281    grok.context(VirtualApplicantsExportJobContainer)
282    title = _(u'Applicant Data Exports')
283    target = None
284
285class ApplicantBreadcrumb(Breadcrumb):
286    """A breadcrumb for applicants.
287    """
288    grok.context(IApplicant)
289
290    @property
291    def title(self):
292        """Get a title for a context.
293        """
294        return self.context.application_number
295
296class OnlinePaymentBreadcrumb(Breadcrumb):
297    """A breadcrumb for payments.
298    """
299    grok.context(IApplicantOnlinePayment)
300
301    @property
302    def title(self):
303        return self.context.p_id
304
305class ApplicantsStatisticsPage(KofaDisplayFormPage):
306    """Some statistics about applicants in a container.
307    """
308    grok.context(IApplicantsContainer)
309    grok.name('statistics')
310    grok.require('waeup.viewApplicationStatistics')
311    grok.template('applicantcontainerstatistics')
312
313    @property
314    def label(self):
315        return "%s" % self.context.title
316
317class ApplicantsContainerPage(KofaDisplayFormPage):
318    """The standard view for regular applicant containers.
319    """
320    grok.context(IApplicantsContainer)
321    grok.name('index')
322    grok.require('waeup.Public')
323    grok.template('applicantscontainerpage')
324    pnav = 3
325
326    @property
327    def form_fields(self):
328        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
329        form_fields['description'].custom_widget = HTMLDisplayWidget
330        form_fields[
331            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
332        form_fields[
333            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
334        if self.request.principal.id == 'zope.anybody':
335            form_fields = form_fields.omit(
336                'code', 'prefix', 'year', 'mode', 'hidden',
337                'strict_deadline', 'application_category')
338        return form_fields
339
340    @property
341    def introduction(self):
342        # Here we know that the cookie has been set
343        lang = self.request.cookies.get('kofa.language')
344        html = self.context.description_dict.get(lang,'')
345        if html == '':
346            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
347            html = self.context.description_dict.get(portal_language,'')
348        return html
349
350    @property
351    def label(self):
352        return "%s" % self.context.title
353
354class ApplicantsContainerManageFormPage(KofaEditFormPage):
355    grok.context(IApplicantsContainer)
356    grok.name('manage')
357    grok.template('applicantscontainermanagepage')
358    form_fields = grok.AutoFields(IApplicantsContainer)
359    taboneactions = [_('Save'),_('Cancel')]
360    tabtwoactions = [_('Remove selected'),_('Cancel'),
361        _('Create students from selected')]
362    tabthreeactions1 = [_('Remove selected local roles')]
363    tabthreeactions2 = [_('Add local role')]
364    # Use friendlier date widget...
365    grok.require('waeup.manageApplication')
366
367    @property
368    def label(self):
369        return _('Manage applicants container')
370
371    pnav = 3
372
373    @property
374    def showApplicants(self):
375        if len(self.context) < 5000:
376            return True
377        return False
378
379    def update(self):
380        datepicker.need() # Enable jQuery datepicker in date fields.
381        toggleall.need()
382        self.tab1 = self.tab2 = self.tab3 = ''
383        qs = self.request.get('QUERY_STRING', '')
384        if not qs:
385            qs = 'tab1'
386        setattr(self, qs, 'active')
387        warning.need()
388        return super(ApplicantsContainerManageFormPage, self).update()
389
390    def getLocalRoles(self):
391        roles = ILocalRolesAssignable(self.context)
392        return roles()
393
394    def getUsers(self):
395        """Get a list of all users.
396        """
397        for key, val in grok.getSite()['users'].items():
398            url = self.url(val)
399            yield(dict(url=url, name=key, val=val))
400
401    def getUsersWithLocalRoles(self):
402        return get_users_with_local_roles(self.context)
403
404    def _description(self):
405        view = ApplicantsContainerPage(
406            self.context,self.request)
407        view.setUpWidgets()
408        return view.widgets['description']()
409
410    @action(_('Save'), style='primary')
411    def save(self, **data):
412        changed_fields = self.applyData(self.context, **data)
413        if changed_fields:
414            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
415        else:
416            changed_fields = []
417        self.context.description_dict = self._description()
418        # Always refresh title. So we can change titles
419        # if APP_TYPES_DICT has been edited.
420        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
421        title = apptypes_dict[self.context.prefix][0]
422        #self.context.title = u'%s %s/%s' % (
423        #    title, self.context.year, self.context.year + 1)
424        self.flash(_('Form has been saved.'))
425        fields_string = ' + '.join(changed_fields)
426        self.context.writeLogMessage(self, 'saved: % s' % fields_string)
427        return
428
429    @jsaction(_('Remove selected'))
430    def delApplicant(self, **data):
431        form = self.request.form
432        if 'val_id' in form:
433            child_id = form['val_id']
434        else:
435            self.flash(_('No applicant selected!'), type='warning')
436            self.redirect(self.url(self.context, '@@manage')+'#tab2')
437            return
438        if not isinstance(child_id, list):
439            child_id = [child_id]
440        deleted = []
441        for id in child_id:
442            try:
443                del self.context[id]
444                deleted.append(id)
445            except:
446                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
447                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
448        if len(deleted):
449            self.flash(_('Successfully removed: ${a}',
450                mapping = {'a':', '.join(deleted)}))
451        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
452        return
453
454    @action(_('Create students from selected'))
455    def createStudents(self, **data):
456        form = self.request.form
457        if 'val_id' in form:
458            child_id = form['val_id']
459        else:
460            self.flash(_('No applicant selected!'), type='warning')
461            self.redirect(self.url(self.context, '@@manage')+'#tab2')
462            return
463        if not isinstance(child_id, list):
464            child_id = [child_id]
465        created = []
466        for id in child_id:
467            success, msg = self.context[id].createStudent(view=self)
468            if success:
469                created.append(id)
470        if len(created):
471            self.flash(_('${a} students successfully created.',
472                mapping = {'a': len(created)}))
473        else:
474            self.flash(_('No student could be created.'), type='warning')
475        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
476        return
477
478    @action(_('Cancel'), validator=NullValidator)
479    def cancel(self, **data):
480        self.redirect(self.url(self.context))
481        return
482
483    @action(_('Add local role'), validator=NullValidator)
484    def addLocalRole(self, **data):
485        return add_local_role(self,3, **data)
486
487    @action(_('Remove selected local roles'))
488    def delLocalRoles(self, **data):
489        return del_local_roles(self,3,**data)
490
491class ApplicantAddFormPage(KofaAddFormPage):
492    """Add-form to add an applicant.
493    """
494    grok.context(IApplicantsContainer)
495    grok.require('waeup.manageApplication')
496    grok.name('addapplicant')
497    #grok.template('applicantaddpage')
498    form_fields = grok.AutoFields(IApplicant).select(
499        'firstname', 'middlename', 'lastname',
500        'email', 'phone')
501    label = _('Add applicant')
502    pnav = 3
503
504    @action(_('Create application record'))
505    def addApplicant(self, **data):
506        applicant = createObject(u'waeup.Applicant')
507        self.applyData(applicant, **data)
508        self.context.addApplicant(applicant)
509        self.flash(_('Applicant record created.'))
510        self.redirect(
511            self.url(self.context[applicant.application_number], 'index'))
512        return
513
514class ApplicantDisplayFormPage(KofaDisplayFormPage):
515    """A display view for applicant data.
516    """
517    grok.context(IApplicant)
518    grok.name('index')
519    grok.require('waeup.viewApplication')
520    grok.template('applicantdisplaypage')
521    label = _('Applicant')
522    pnav = 3
523    hide_hint = False
524
525    @property
526    def form_fields(self):
527        if self.context.special:
528            form_fields = grok.AutoFields(ISpecialApplicant)
529        else:
530            form_fields = grok.AutoFields(IApplicant).omit(
531                'locked', 'course_admitted', 'password', 'suspended')
532        return form_fields
533
534    @property
535    def target(self):
536        return getattr(self.context.__parent__, 'prefix', None)
537
538    @property
539    def separators(self):
540        return getUtility(IApplicantsUtils).SEPARATORS_DICT
541
542    def update(self):
543        self.passport_url = self.url(self.context, 'passport.jpg')
544        # Mark application as started if applicant logs in for the first time
545        usertype = getattr(self.request.principal, 'user_type', None)
546        if usertype == 'applicant' and \
547            IWorkflowState(self.context).getState() == INITIALIZED:
548            IWorkflowInfo(self.context).fireTransition('start')
549        if usertype == 'applicant' and self.context.state == 'created':
550            session = '%s/%s' % (self.context.__parent__.year,
551                                 self.context.__parent__.year+1)
552            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
553            msg = _(
554                '\n <strong>Congratulations!</strong>' +
555                ' You have been offered provisional admission into the' +
556                ' ${c} Academic Session of ${d}.'
557                ' Your student record has been created for you.' +
558                ' Please, logout again and proceed to the' +
559                ' login page of the portal.'
560                ' Then enter your new student credentials:' +
561                ' user name= ${a}, password = ${b}.' +
562                ' Change your password when you have logged in.',
563                mapping = {
564                    'a':self.context.student_id,
565                    'b':self.context.application_number,
566                    'c':session,
567                    'd':title}
568                )
569            self.flash(msg)
570        return
571
572    @property
573    def hasPassword(self):
574        if self.context.password:
575            return _('set')
576        return _('unset')
577
578    @property
579    def label(self):
580        container_title = self.context.__parent__.title
581        return _('${a} <br /> Application Record ${b}', mapping = {
582            'a':container_title, 'b':self.context.application_number})
583
584    def getCourseAdmitted(self):
585        """Return link, title and code in html format to the certificate
586           admitted.
587        """
588        course_admitted = self.context.course_admitted
589        if getattr(course_admitted, '__parent__',None):
590            url = self.url(course_admitted)
591            title = course_admitted.title
592            code = course_admitted.code
593            return '<a href="%s">%s - %s</a>' %(url,code,title)
594        return ''
595
596class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
597    grok.context(IApplicant)
598    grok.name('base')
599    form_fields = grok.AutoFields(IApplicant).select(
600        'applicant_id','email', 'course1')
601
602class CreateStudentPage(UtilityView, grok.View):
603    """Create a student object from applicant data.
604    """
605    grok.context(IApplicant)
606    grok.name('createstudent')
607    grok.require('waeup.manageStudent')
608
609    def update(self):
610        msg = self.context.createStudent(view=self)[1]
611        self.flash(msg, type='warning')
612        self.redirect(self.url(self.context))
613        return
614
615    def render(self):
616        return
617
618class CreateAllStudentsPage(UtilityView, grok.View):
619    """Create all student objects from applicant data
620    in a container.
621
622    This is a hidden page, no link or button will
623    be provided and only PortalManagers can do this.
624    """
625    #grok.context(IApplicantsContainer)
626    grok.name('createallstudents')
627    grok.require('waeup.managePortal')
628
629    def update(self):
630        cat = getUtility(ICatalog, name='applicants_catalog')
631        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
632        created = []
633        container_only = False
634        applicants_root = grok.getSite()['applicants']
635        if isinstance(self.context, ApplicantsContainer):
636            container_only = True
637        for result in results:
638            if container_only and result.__parent__ is not self.context:
639                continue
640            success, msg = result.createStudent(view=self)
641            if success:
642                created.append(result.applicant_id)
643            else:
644                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
645                applicants_root.logger.info(
646                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
647        if len(created):
648            self.flash(_('${a} students successfully created.',
649                mapping = {'a': len(created)}))
650        else:
651            self.flash(_('No student could be created.'), type='warning')
652        self.redirect(self.url(self.context))
653        return
654
655    def render(self):
656        return
657
658class ApplicationFeePaymentAddPage(UtilityView, grok.View):
659    """ Page to add an online payment ticket
660    """
661    grok.context(IApplicant)
662    grok.name('addafp')
663    grok.require('waeup.payApplicant')
664    factory = u'waeup.ApplicantOnlinePayment'
665
666    def update(self):
667        for key in self.context.keys():
668            ticket = self.context[key]
669            if ticket.p_state == 'paid':
670                  self.flash(
671                      _('This type of payment has already been made.'),
672                      type='warning')
673                  self.redirect(self.url(self.context))
674                  return
675        applicants_utils = getUtility(IApplicantsUtils)
676        container = self.context.__parent__
677        payment = createObject(self.factory)
678        failure = applicants_utils.setPaymentDetails(
679            container, payment, self.context)
680        if failure is not None:
681            self.flash(failure[0], type='danger')
682            self.redirect(self.url(self.context))
683            return
684        self.context[payment.p_id] = payment
685        self.flash(_('Payment ticket created.'))
686        self.redirect(self.url(payment))
687        return
688
689    def render(self):
690        return
691
692
693class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
694    """ Page to view an online payment ticket
695    """
696    grok.context(IApplicantOnlinePayment)
697    grok.name('index')
698    grok.require('waeup.viewApplication')
699    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
700    form_fields[
701        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
702    form_fields[
703        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
704    pnav = 3
705
706    @property
707    def label(self):
708        return _('${a}: Online Payment Ticket ${b}', mapping = {
709            'a':self.context.__parent__.display_fullname,
710            'b':self.context.p_id})
711
712class OnlinePaymentApprovePage(UtilityView, grok.View):
713    """ Approval view
714    """
715    grok.context(IApplicantOnlinePayment)
716    grok.name('approve')
717    grok.require('waeup.managePortal')
718
719    def update(self):
720        success, msg, log = self.context.approveApplicantPayment()
721        if log is not None:
722            applicant = self.context.__parent__
723            # Add log message to applicants.log
724            applicant.writeLogMessage(self, log)
725            # Add log message to payments.log
726            self.context.logger.info(
727                '%s,%s,%s,%s,%s,,,,,,' % (
728                applicant.applicant_id,
729                self.context.p_id, self.context.p_category,
730                self.context.amount_auth, self.context.r_code))
731        self.flash(msg, type='warning')
732        return
733
734    def render(self):
735        self.redirect(self.url(self.context, '@@index'))
736        return
737
738class ExportPDFPaymentSlipPage(UtilityView, grok.View):
739    """Deliver a PDF slip of the context.
740    """
741    grok.context(IApplicantOnlinePayment)
742    grok.name('payment_slip.pdf')
743    grok.require('waeup.viewApplication')
744    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
745    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
746    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
747    prefix = 'form'
748    note = None
749
750    @property
751    def title(self):
752        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
753        return translate(_('Payment Data'), 'waeup.kofa',
754            target_language=portal_language)
755
756    @property
757    def label(self):
758        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
759        return translate(_('Online Payment Slip'),
760            'waeup.kofa', target_language=portal_language) \
761            + ' %s' % self.context.p_id
762
763    def render(self):
764        #if self.context.p_state != 'paid':
765        #    self.flash(_('Ticket not yet paid.'))
766        #    self.redirect(self.url(self.context))
767        #    return
768        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
769            self.request)
770        students_utils = getUtility(IStudentsUtils)
771        return students_utils.renderPDF(self,'payment_slip.pdf',
772            self.context.__parent__, applicantview, note=self.note)
773
774class ExportPDFPageApplicationSlip(UtilityView, grok.View):
775    """Deliver a PDF slip of the context.
776    """
777    grok.context(IApplicant)
778    grok.name('application_slip.pdf')
779    grok.require('waeup.viewApplication')
780    prefix = 'form'
781
782    def update(self):
783        if self.context.state in ('initialized', 'started', 'paid'):
784            self.flash(
785                _('Please pay and submit before trying to download '
786                  'the application slip.'), type='warning')
787            return self.redirect(self.url(self.context))
788        return
789
790    def render(self):
791        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
792            view=self)
793        self.response.setHeader(
794            'Content-Type', 'application/pdf')
795        return pdfstream
796
797def handle_img_upload(upload, context, view):
798    """Handle upload of applicant image.
799
800    Returns `True` in case of success or `False`.
801
802    Please note that file pointer passed in (`upload`) most probably
803    points to end of file when leaving this function.
804    """
805    size = file_size(upload)
806    if size > MAX_UPLOAD_SIZE:
807        view.flash(_('Uploaded image is too big!'), type='danger')
808        return False
809    dummy, ext = os.path.splitext(upload.filename)
810    ext.lower()
811    if ext != '.jpg':
812        view.flash(_('jpg file extension expected.'), type='danger')
813        return False
814    upload.seek(0) # file pointer moved when determining size
815    store = getUtility(IExtFileStore)
816    file_id = IFileStoreNameChooser(context).chooseName()
817    store.createFile(file_id, upload)
818    return True
819
820class ApplicantManageFormPage(KofaEditFormPage):
821    """A full edit view for applicant data.
822    """
823    grok.context(IApplicant)
824    grok.name('manage')
825    grok.require('waeup.manageApplication')
826    grok.template('applicanteditpage')
827    manage_applications = True
828    pnav = 3
829    display_actions = [[_('Save'), _('Final Submit')],
830        [_('Add online payment ticket'),_('Remove selected tickets')]]
831
832    @property
833    def form_fields(self):
834        if self.context.special:
835            form_fields = grok.AutoFields(ISpecialApplicant)
836            form_fields['applicant_id'].for_display = True
837        else:
838            form_fields = grok.AutoFields(IApplicant)
839            form_fields['student_id'].for_display = True
840            form_fields['applicant_id'].for_display = True
841        return form_fields
842
843    @property
844    def target(self):
845        return getattr(self.context.__parent__, 'prefix', None)
846
847    @property
848    def separators(self):
849        return getUtility(IApplicantsUtils).SEPARATORS_DICT
850
851    def update(self):
852        datepicker.need() # Enable jQuery datepicker in date fields.
853        warning.need()
854        super(ApplicantManageFormPage, self).update()
855        self.wf_info = IWorkflowInfo(self.context)
856        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
857        self.upload_success = None
858        upload = self.request.form.get('form.passport', None)
859        if upload:
860            # We got a fresh upload, upload_success is
861            # either True or False
862            self.upload_success = handle_img_upload(
863                upload, self.context, self)
864            if self.upload_success:
865                self.context.writeLogMessage(self, 'saved: passport')
866        return
867
868    @property
869    def label(self):
870        container_title = self.context.__parent__.title
871        return _('${a} <br /> Application Form ${b}', mapping = {
872            'a':container_title, 'b':self.context.application_number})
873
874    def getTransitions(self):
875        """Return a list of dicts of allowed transition ids and titles.
876
877        Each list entry provides keys ``name`` and ``title`` for
878        internal name and (human readable) title of a single
879        transition.
880        """
881        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
882            if not t[0] == 'pay']
883        return [dict(name='', title=_('No transition'))] +[
884            dict(name=x, title=y) for x, y in allowed_transitions]
885
886    @action(_('Save'), style='primary')
887    def save(self, **data):
888        form = self.request.form
889        password = form.get('password', None)
890        password_ctl = form.get('control_password', None)
891        if password:
892            validator = getUtility(IPasswordValidator)
893            errors = validator.validate_password(password, password_ctl)
894            if errors:
895                self.flash( ' '.join(errors), type='danger')
896                return
897        if self.upload_success is False:  # False is not None!
898            # Error during image upload. Ignore other values.
899            return
900        changed_fields = self.applyData(self.context, **data)
901        # Turn list of lists into single list
902        if changed_fields:
903            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
904        else:
905            changed_fields = []
906        if password:
907            # Now we know that the form has no errors and can set password ...
908            IUserAccount(self.context).setPassword(password)
909            changed_fields.append('password')
910        fields_string = ' + '.join(changed_fields)
911        trans_id = form.get('transition', None)
912        if trans_id:
913            self.wf_info.fireTransition(trans_id)
914        self.flash(_('Form has been saved.'))
915        if fields_string:
916            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
917        return
918
919    def unremovable(self, ticket):
920        return False
921
922    # This method is also used by the ApplicantEditFormPage
923    def delPaymentTickets(self, **data):
924        form = self.request.form
925        if 'val_id' in form:
926            child_id = form['val_id']
927        else:
928            self.flash(_('No payment selected.'), type='warning')
929            self.redirect(self.url(self.context))
930            return
931        if not isinstance(child_id, list):
932            child_id = [child_id]
933        deleted = []
934        for id in child_id:
935            # Applicants are not allowed to remove used payment tickets
936            if not self.unremovable(self.context[id]):
937                try:
938                    del self.context[id]
939                    deleted.append(id)
940                except:
941                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
942                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
943        if len(deleted):
944            self.flash(_('Successfully removed: ${a}',
945                mapping = {'a':', '.join(deleted)}))
946            self.context.writeLogMessage(
947                self, 'removed: % s' % ', '.join(deleted))
948        return
949
950    # We explicitely want the forms to be validated before payment tickets
951    # can be created. If no validation is requested, use
952    # 'validator=NullValidator' in the action directive
953    @action(_('Add online payment ticket'))
954    def addPaymentTicket(self, **data):
955        self.redirect(self.url(self.context, '@@addafp'))
956        return
957
958    @jsaction(_('Remove selected tickets'))
959    def removePaymentTickets(self, **data):
960        self.delPaymentTickets(**data)
961        self.redirect(self.url(self.context) + '/@@manage')
962        return
963
964    # Not used in base package
965    def file_exists(self, attr):
966        file = getUtility(IExtFileStore).getFileByContext(
967            self.context, attr=attr)
968        if file:
969            return True
970        else:
971            return False
972
973class ApplicantEditFormPage(ApplicantManageFormPage):
974    """An applicant-centered edit view for applicant data.
975    """
976    grok.context(IApplicantEdit)
977    grok.name('edit')
978    grok.require('waeup.handleApplication')
979    grok.template('applicanteditpage')
980    manage_applications = False
981    submit_state = PAID
982
983    @property
984    def form_fields(self):
985        if self.context.special:
986            form_fields = grok.AutoFields(ISpecialApplicant)
987            form_fields['applicant_id'].for_display = True
988        else:
989            form_fields = grok.AutoFields(IApplicantEdit).omit(
990                'locked', 'course_admitted', 'student_id',
991                'suspended'
992                )
993            form_fields['applicant_id'].for_display = True
994            form_fields['reg_number'].for_display = True
995        return form_fields
996
997    @property
998    def display_actions(self):
999        state = IWorkflowState(self.context).getState()
1000        actions = [[],[]]
1001        if state == STARTED:
1002            actions = [[_('Save')],
1003                [_('Add online payment ticket'),_('Remove selected tickets')]]
1004        elif state == PAID:
1005            actions = [[_('Save'), _('Final 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'))
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    @submitaction(_('Final Submit'))
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                'firstname','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            firstname = data.get('firstname','')
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,'firstname',None) is None:
1168                    self.flash(_('An error occurred.'), type='danger')
1169                    return
1170                elif applicant.firstname.lower() != firstname.lower():
1171                    # Don't tell the truth here. Anonymous must not
1172                    # know that a record was found and only the firstname
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.