Projet

Général

Profil

0001-misc-add-a-jsonp-endpoint-for-datasources-use-it-for.patch

Frédéric Péters, 12 octobre 2016 17:18

Télécharger (11,1 ko)

Voir les différences:

Subject: [PATCH] misc: add a jsonp endpoint for datasources, use it for
 autocomplete (#10990)

 tests/test_admin_pages.py |  2 +-
 tests/test_api.py         | 49 +++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_form_pages.py  | 12 +++++++++++-
 wcs/api.py                | 38 ++++++++++++++++++++++++++++++++++--
 wcs/data_sources.py       | 12 ++++++++----
 wcs/fields.py             | 16 +++++++++++++++-
 wcs/qommon/form.py        |  3 +--
 7 files changed, 121 insertions(+), 11 deletions(-)
tests/test_admin_pages.py
942 942
            resp.body.index('<label for="form_data_source">Data Source</label>')
943 943

  
944 944
    # start filling the "data source" field
945
    resp.forms[0]['data_source$type'] = 'JSON URL'
945
    resp.forms[0]['data_source$type'] = 'JSONP URL'
946 946
    resp = resp.forms[0].submit('data_source$apply')
947 947

  
948 948
    # it should now appear before the additional parameters section
tests/test_api.py
5 5
import hmac
6 6
import base64
7 7
import hashlib
8
import re
8 9
import urllib
9 10
import urlparse
10 11
import datetime
......
1443 1444
    secret, orig = get_secret_and_orig('https://api.example.com/endpoint/')
1444 1445
    assert secret == '1234'
1445 1446
    assert orig == 'example.net'
1447

  
1448
def test_datasources_jsonp(pub):
1449
    NamedDataSource.wipe()
1450
    data_source = NamedDataSource(name='foobar')
1451
    source = [{'id': '1', 'text': 'foo', 'more': 'XXX'},
1452
              {'id': '2', 'text': 'bar', 'more': 'YYY'}]
1453
    data_source.data_source = {'type': 'formula', 'value': repr(source)}
1454
    data_source.store()
1455

  
1456
    get_app(pub).get('/api/datasources/xxx', status=404)
1457
    get_app(pub).get('/api/datasources/xxx/', status=404)
1458
    get_app(pub).get('/api/datasources/foobar/', status=403)
1459

  
1460
    FormDef.wipe()
1461
    formdef = FormDef()
1462
    formdef.name = 'test'
1463
    formdef.fields = [
1464
        fields.StringField(id='0', label='foobar0', varname='foobar0',
1465
            data_source={'type': 'foobar'}),
1466
    ]
1467
    formdef.store()
1468

  
1469
    get_app(pub).get('/api/datasources/foobar/12122', status=403)
1470

  
1471
    app = get_app(pub)
1472
    resp = app.get('/test/')
1473
    url = re.findall(r"'(/api/datasou.*)'", resp.body)[0]
1474

  
1475
    resp = app.get(url)
1476
    assert len(resp.json['data']) == 2
1477
    resp = app.get(url + '?q=fo')
1478
    resp_data = resp.body
1479
    assert len(resp.json['data']) == 1
1480
    resp = app.get(url + '?q=fo&callback=cb123')
1481
    assert resp_data in resp.body
1482
    assert resp.body.startswith('cb123(')
1483

  
1484
    # check accessing the URL from another session
1485
    get_app(pub).get(url, status=403)
1486
    app2 = get_app(pub)
1487
    resp2 = app2.get('/test/')
1488
    app2.get(url, status=403)
1489

  
1490
    # test custom handling of jsonp sources (redirect)
1491
    data_source.data_source = {'type': 'jsonp', 'value': 'http://remote.example.net/json'}
1492
    data_source.store()
1493
    resp = app.get(url + '?q=fo&callback=cb123')
1494
    assert resp.location == 'http://remote.example.net/json?q=fo&callback=cb123'
tests/test_form_pages.py
3080 3080
    assert not hasattr(formdata.data['0'], 'metadata')
3081 3081
    assert not '0_structured' in formdata.data
3082 3082

  
3083

  
3084 3083
def test_form_string_field_autocomplete(pub):
3085 3084
    formdef = create_formdef()
3086 3085
    formdef.fields = [fields.StringField(id='0', label='string', type='string', required=False)]
......
3105 3104
    assert ').autocomplete({' in resp.body
3106 3105
    assert 'http://example.net' in resp.body
3107 3106

  
3107
    # named data source
3108
    NamedDataSource.wipe()
3109
    data_source = NamedDataSource(name='foobar')
3110
    data_source.data_source = {'type': 'formula', 'value': repr([])}
3111
    data_source.store()
3112
    formdef.fields[0].data_source = {'type': 'foobar', 'value': ''}
3113
    formdef.store()
3114
    resp = get_app(pub).get('/test/')
3115
    assert ').autocomplete({' in resp.body
3116
    assert '/api/datasources/foobar/' in resp.body
3117

  
3108 3118
def test_form_workflow_trigger(pub):
3109 3119
    user = create_user(pub)
3110 3120

  
wcs/api.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import hashlib
17 18
import json
18 19
import time
19 20
import urllib2
20 21
import sys
21 22

  
22
from quixote import get_request, get_publisher, get_response
23
from quixote import get_request, get_publisher, get_response, get_session, redirect
23 24
from quixote.directory import Directory
24 25
from qommon import misc
25 26
from qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
26 27
    UnknownNameIdAccessForbiddenError)
27 28

  
28 29
from wcs.categories import Category
30
from wcs.data_sources import NamedDataSource
29 31
from wcs.formdef import FormDef
30 32
from wcs.roles import Role, logged_users_role
31 33
from wcs.forms.common import FormStatusPage
......
576 578
        return json.dumps(data)
577 579

  
578 580

  
581
class ApiDataSourceDirectory(Directory):
582
    def __init__(self, datasource):
583
        self.datasource = datasource
584

  
585
    def _q_lookup(self, component):
586
        if not get_session() or not get_session().session_id:
587
            raise AccessForbiddenError()
588
        if component != hashlib.md5('%s:%s' % (self.datasource.slug, get_session().session_id)).hexdigest():
589
            raise AccessForbiddenError()
590
        dtype = self.datasource.data_source.get('type')
591
        if dtype == 'jsonp':
592
            # redirect to the source
593
            url = self.datasource.data_source.get('value')
594
            if not '?' in url:
595
                url += '?'
596
            url += get_request().get_query()
597
            return redirect(url)
598
        query = get_request().form.get('q', '').lower()
599
        items = [x[-1] for x in self.datasource.get_items() if query in x[1].lower()]
600
        return misc.json_response({'data': items})
601

  
602

  
603
class ApiDataSourcesDirectory(Directory):
604
    def _q_lookup(self, component):
605
        try:
606
            return ApiDataSourceDirectory(NamedDataSource.get_by_slug(component))
607
        except KeyError:
608
            raise TraversalError()
609

  
610

  
579 611
class ApiDirectory(Directory):
580 612
    _q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
581
            'formdefs', 'categories', 'user', 'users', 'code']
613
            'formdefs', 'categories', 'user', 'users', 'code',
614
            'datasources']
582 615

  
583 616
    forms = ApiFormsDirectory()
584 617
    formdefs = ApiFormdefsDirectory()
......
586 619
    user = ApiUserDirectory()
587 620
    users = ApiUsersDirectory()
588 621
    code = ApiTrackingCodeDirectory()
622
    datasources = ApiDataSourcesDirectory()
589 623

  
590 624
    def reverse_geocoding(self):
591 625
        try:
wcs/data_sources.py
39 39

  
40 40
class DataSourceSelectionWidget(CompositeWidget):
41 41
    def __init__(self, name, value=None, allow_jsonp=True,
42
            allow_named_sources=True, **kwargs):
42
            allow_named_sources=True, require_jsonp=False, **kwargs):
43 43
        CompositeWidget.__init__(self, name, value, **kwargs)
44 44

  
45 45
        if not value:
46 46
            value = {}
47 47

  
48
        options = [('none', _('None')),
49
                   ('formula', _('Python Expression')),
50
                   ('json', _('JSON URL'))]
48
        options = [('none', _('None'))]
49
        if not require_jsonp:
50
            options.append(('formula', _('Python Expression')))
51
            options.append(('json', _('JSON URL')))
51 52
        if allow_jsonp:
52 53
            options.append(('jsonp', _('JSONP URL')))
53 54
        if allow_named_sources:
......
245 246
    def get_substitution_variables(cls):
246 247
        return {'data_source': DataSourcesSubstitutionProxy()}
247 248

  
249
    def get_items(self):
250
        return get_items(self.data_source)
251

  
248 252

  
249 253
class DataSourcesSubstitutionProxy(object):
250 254
    def __getattr__(self, attr):
wcs/fields.py
19 19
import random
20 20
import re
21 21
import base64
22
import hashlib
22 23
import xml.etree.ElementTree as ET
23 24
import collections
24 25
from HTMLParser import HTMLParser
25 26

  
26
from quixote import get_request, get_publisher
27
from quixote import get_request, get_publisher, get_session_manager
27 28
from quixote.html import htmltext, TemplateIO
28 29

  
29 30
from qommon.form import *
......
564 565
            if real_data_source.get('type') == 'jsonp':
565 566
                kwargs['url'] = real_data_source.get('value')
566 567
                self.widget_class = AutocompleteStringWidget
568
            elif self.data_source.get('type') not in ('none', 'formula', 'json'):
569
                # named data source, pass a token to assert the user is
570
                # allowed to access the data.
571
                session = get_session()
572
                if not session.session_id:
573
                    # this require a session to exist
574
                    session.get_anonymous_key(generate=True)
575
                    get_session_manager().maintain_session(session)
576
                token = hashlib.md5('%s:%s' % (self.data_source.get('type'), get_session().session_id)).hexdigest()
577
                kwargs['url'] = get_publisher().get_root_url() + 'api/datasources/%s/%s' % (
578
                        self.data_source.get('type'), token)
579
                self.widget_class = AutocompleteStringWidget
567 580

  
568 581
    def fill_admin_form(self, form):
569 582
        WidgetField.fill_admin_form(self, form)
......
573 586
                value=self.validation, advanced=(not self.validation))
574 587
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
575 588
                 value=self.data_source,
589
                 require_jsonp=True,
576 590
                 title=_('Data Source'),
577 591
                 hint=_('This will allow autocompletion from an external source.'),
578 592
                 advanced=is_datasource_advanced(self.data_source),
wcs/qommon/form.py
1972 1972
    url = None
1973 1973

  
1974 1974
    def __init__(self, *args, **kwargs):
1975
        self.url = kwargs.pop('url', None)
1975 1976
        WcsExtraStringWidget.__init__(self, *args, **kwargs)
1976
        if kwargs.get('url'):
1977
            self.url = kwargs.get('url')
1978 1977

  
1979 1978
    def render_content(self):
1980 1979
        get_response().add_javascript(['jquery.js', 'jquery-ui.js'])
1981
-