Projet

Général

Profil

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

Frédéric Péters, 09 octobre 2016 20:37

Télécharger (10,6 ko)

Voir les différences:

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

 tests/test_api.py        | 49 ++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_form_pages.py | 12 +++++++++++-
 wcs/api.py               | 42 +++++++++++++++++++++++++++++++++++++++--
 wcs/data_sources.py      | 12 ++++++++----
 wcs/fields.py            | 19 ++++++++++++++++++-
 wcs/qommon/form.py       |  3 +--
 6 files changed, 127 insertions(+), 10 deletions(-)
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
......
1440 1441
    secret, orig = get_secret_and_orig('https://api.example.com/endpoint/')
1441 1442
    assert secret == '1234'
1442 1443
    assert orig == 'example.net'
1444

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

  
1453
    get_app(pub).get('/api/datasources/xxx', status=404)
1454
    get_app(pub).get('/api/datasources/xxx/', status=404)
1455
    get_app(pub).get('/api/datasources/foobar/', status=403)
1456

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

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

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

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

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

  
1487
    # test custom handling of jsonp sources (redirect)
1488
    data_source.data_source = {'type': 'jsonp', 'value': 'http://remote.example.net/json'}
1489
    data_source.store()
1490
    resp = app.get(url + '?q=fo&callback=cb123')
1491
    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
19 19
import urllib2
20 20
import sys
21 21

  
22
from quixote import get_request, get_publisher, get_response
22
from quixote import get_request, get_publisher, get_response, get_session, redirect
23 23
from quixote.directory import Directory
24 24
from qommon import misc
25 25
from qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
26 26
    UnknownNameIdAccessForbiddenError)
27
from qommon.tokens import Token
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
......
572 574
        return json.dumps(data)
573 575

  
574 576

  
577
class ApiDataSourceDirectory(Directory):
578
    def __init__(self, datasource):
579
        self.datasource = datasource
580

  
581
    def _q_lookup(self, component):
582
        try:
583
            token = Token.get(component)
584
        except KeyError:
585
            raise AccessForbiddenError()
586
        if not getattr(token, 'session_id', None):
587
            raise AccessForbiddenError()
588
        if token.session_id != get_session().session_id:
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

  
575 611
class ApiDirectory(Directory):
576 612
    _q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
577
            'formdefs', 'categories', 'user', 'users', 'code']
613
            'formdefs', 'categories', 'user', 'users', 'code',
614
            'datasources']
578 615

  
579 616
    forms = ApiFormsDirectory()
580 617
    formdefs = ApiFormdefsDirectory()
......
582 619
    user = ApiUserDirectory()
583 620
    users = ApiUsersDirectory()
584 621
    code = ApiTrackingCodeDirectory()
622
    datasources = ApiDataSourcesDirectory()
585 623

  
586 624
    def reverse_geocoding(self):
587 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 *
30 31
from qommon.misc import localstrftime, date_format, ellipsize
32
from qommon.tokens import Token
31 33
from qommon import get_cfg, get_logger
32 34

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

  
568 584
    def fill_admin_form(self, form):
569 585
        WidgetField.fill_admin_form(self, form)
......
573 589
                value=self.validation, advanced=(not self.validation))
574 590
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
575 591
                 value=self.data_source,
592
                 require_jsonp=True,
576 593
                 title=_('Data Source'),
577 594
                 hint=_('This will allow autocompletion from an external source.'),
578 595
                 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
-