Project

General

Profile

0001-forms-extend-json-autocomplete-support-for-be-asynch.patch

Frédéric Péters, 27 Mar 2019 04:13 PM

Download (29.2 KB)

View differences:

Subject: [PATCH] forms: extend json autocomplete support for be asynchronous
 if possible (#31492)

 tests/test_form_pages.py             | 180 ++++++++++++++++++++++++++-
 wcs/admin/data_sources.py            |  22 ++++
 wcs/api.py                           |  28 ++++-
 wcs/data_sources.py                  |  98 ++++++++++++++-
 wcs/fields.py                        |  81 +++++-------
 wcs/forms/root.py                    |  19 ++-
 wcs/qommon/static/js/qommon.forms.js |   4 +
 wcs/sessions.py                      |  24 +++-
 8 files changed, 396 insertions(+), 60 deletions(-)
tests/test_form_pages.py
11 11
from urlparse import urlparse
12 12
import zipfile
13 13
import base64
14
from webtest import Upload
14
from webtest import Upload, Hidden
15 15
import mock
16 16

  
17 17
try:
......
4785 4785
        assert formdef.data_class().select()[0].data['0'] == ['2']
4786 4786
        assert formdef.data_class().select()[0].data['0_display'] == 'world'
4787 4787

  
4788
def test_item_field_autocomplete_json_source(http_requests, pub):
4789
    user = create_user(pub)
4790
    formdef = create_formdef()
4791
    formdef.data_class().wipe()
4792

  
4793
    NamedDataSource.wipe()
4794
    data_source = NamedDataSource(name='foobar')
4795
    data_source.data_source = {'type': 'json', 'value': 'http://remote.example.net/json'}
4796
    data_source.store()
4797

  
4798
    formdef.fields = [
4799
        fields.ItemField(id='0', label='string', type='item',
4800
            data_source={'type': 'foobar'},
4801
            display_mode='autocomplete',
4802
        ),
4803
    ]
4804
    formdef.store()
4805

  
4806
    with mock.patch('qommon.misc.urlopen') as urlopen:
4807
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'},
4808
                         {'id': '2', 'text': 'world', 'extra': 'bar'}]}
4809
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4810
        resp = get_app(pub).get('/test/')
4811
        assert "$('#form_f0').select2();" in resp.body
4812
        resp.form['f0'] = '2'
4813
        resp = resp.form.submit('submit') # -> validation page
4814
        resp = resp.form.submit('submit') # -> submit
4815
        assert formdef.data_class().select()[0].data['0'] == '2'
4816
        assert formdef.data_class().select()[0].data['0_display'] == 'world'
4817
        assert formdef.data_class().select()[0].data['0_structured'] == data['data'][1]
4818

  
4819
    # check with possibility of remote query
4820
    data_source.query_parameter = 'q'
4821
    data_source.id_parameter = 'id'
4822
    data_source.store()
4823

  
4824
    formdef.data_class().wipe()
4825

  
4826
    app = get_app(pub)
4827
    with mock.patch('qommon.misc.urlopen') as urlopen:
4828
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'},
4829
                         {'id': '2', 'text': 'world', 'extra': 'bar'}]}
4830
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4831
        resp = app.get('/test/')
4832
        assert urlopen.call_count == 0
4833
        pq = resp.pyquery.remove_namespaces()
4834
        select2_url = pq('select').attr['data-select2-url']
4835

  
4836
    with mock.patch('qommon.misc.urlopen') as urlopen:
4837
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4838
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4839
        resp2 = app.get(select2_url + '?q=hell')
4840
        assert urlopen.call_count == 1
4841
        assert urlopen.call_args[0][0] == 'http://remote.example.net/json?q=hell'
4842
        assert resp2.json == data
4843

  
4844
        # check unauthorized access
4845
        resp2 = get_app(pub).get(select2_url + '?q=hell', status=403)
4846

  
4847
    # simulate select2 mode, with qommon.forms.js adding an extra hidden widget
4848
    resp.form.fields['f0_display'] = Hidden(form=resp.form, tag='input', name='f0_display', pos=10)
4849
    resp.form['f0'].force_value('1')
4850
    resp.form.fields['f0_display'].force_value('hello')
4851

  
4852
    with mock.patch('qommon.misc.urlopen') as urlopen:
4853
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4854
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4855
        resp = resp.form.submit('submit') # -> validation page
4856
        assert urlopen.call_count == 1
4857
        assert urlopen.call_args[0][0] == 'http://remote.example.net/json?id=1'
4858
        assert resp.form['f0'].value == '1'
4859
        assert resp.form['f0_label'].value == 'hello'
4860

  
4861
    with mock.patch('qommon.misc.urlopen') as urlopen:
4862
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4863
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4864
        resp = resp.form.submit('submit') # -> submit
4865
        assert urlopen.call_count == 1
4866
        assert urlopen.call_args[0][0] == 'http://remote.example.net/json?id=1'
4867
        assert formdef.data_class().select()[0].data['0'] == '1'
4868
        assert formdef.data_class().select()[0].data['0_display'] == 'hello'
4869
        assert formdef.data_class().select()[0].data['0_structured'] == data['data'][0]
4870

  
4871
    # same thing with signed URLs
4872
    formdef.data_class().wipe()
4873
    open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w').write('''\
4874
[wscall-secrets]
4875
remote.example.net = 1234
4876
''')
4877

  
4878
    app = get_app(pub)
4879
    with mock.patch('qommon.misc.urlopen') as urlopen:
4880
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'},
4881
                         {'id': '2', 'text': 'world', 'extra': 'bar'}]}
4882
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4883
        resp = app.get('/test/')
4884
        assert urlopen.call_count == 0
4885
        pq = resp.pyquery.remove_namespaces()
4886
        select2_url = pq('select').attr['data-select2-url']
4887

  
4888
    with mock.patch('qommon.misc.urlopen') as urlopen:
4889
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4890
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4891
        resp2 = app.get(select2_url + '?q=hell')
4892
        assert urlopen.call_count == 1
4893
        assert urlopen.call_args[0][0].startswith('http://remote.example.net/json?q=hell&orig=example.net&')
4894
        assert resp2.json == data
4895

  
4896
    # simulate select2 mode, with qommon.forms.js adding an extra hidden widget
4897
    resp.form.fields['f0_display'] = Hidden(form=resp.form, tag='input', name='f0_display', pos=10)
4898
    resp.form['f0'].force_value('1')
4899
    resp.form.fields['f0_display'].force_value('hello')
4900

  
4901
    with mock.patch('qommon.misc.urlopen') as urlopen:
4902
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4903
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4904
        resp = resp.form.submit('submit') # -> validation page
4905
        assert urlopen.call_count == 1
4906
        assert urlopen.call_args[0][0].startswith('http://remote.example.net/json?id=1&orig=example.net&')
4907
        assert resp.form['f0'].value == '1'
4908
        assert resp.form['f0_label'].value == 'hello'
4909

  
4910
    with mock.patch('qommon.misc.urlopen') as urlopen:
4911
        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
4912
        urlopen.side_effect = lambda *args: StringIO.StringIO(json.dumps(data))
4913
        resp = resp.form.submit('submit') # -> submit
4914
        assert urlopen.call_count == 1
4915
        assert urlopen.call_args[0][0].startswith('http://remote.example.net/json?id=1&orig=example.net&')
4916
        assert formdef.data_class().select()[0].data['0'] == '1'
4917
        assert formdef.data_class().select()[0].data['0_display'] == 'hello'
4918
        assert formdef.data_class().select()[0].data['0_structured'] == data['data'][0]
4919

  
4920
def test_item_field_autocomplete_jsonp_source(http_requests, pub):
4921
    user = create_user(pub)
4922
    formdef = create_formdef()
4923
    formdef.data_class().wipe()
4924

  
4925
    NamedDataSource.wipe()
4926
    data_source = NamedDataSource(name='foobar')
4927
    data_source.data_source = {'type': 'jsonp', 'value': 'http://remote.example.net/jsonp'}
4928
    data_source.store()
4929

  
4930
    formdef.fields = [
4931
        fields.ItemField(id='0', label='string', type='item',
4932
            data_source={'type': 'foobar'},
4933
            display_mode='autocomplete',
4934
        ),
4935
    ]
4936
    formdef.store()
4937

  
4938
    app = get_app(pub)
4939
    with mock.patch('qommon.misc.urlopen') as urlopen:
4940
        resp = app.get('/test/')
4941
        assert urlopen.call_count == 0
4942
        pq = resp.pyquery.remove_namespaces()
4943
        select2_url = pq('select').attr['data-select2-url']
4944
        assert select2_url == 'http://remote.example.net/jsonp'
4945

  
4946
    # simulate select2 mode, with qommon.forms.js adding an extra hidden widget
4947
    resp.form.fields['f0_display'] = [Hidden(form=resp.form, tag='input', name='f0_display', pos=10)]
4948
    resp.form.field_order.append(('f0_display', resp.form.fields['f0_display'][0]))
4949
    resp.form['f0'].force_value('1')
4950
    resp.form['f0_display'].force_value('hello')
4951

  
4952
    with mock.patch('qommon.misc.urlopen') as urlopen:
4953
        resp = resp.form.submit('submit') # -> validation page
4954
        assert urlopen.call_count == 0
4955
        assert resp.form['f0'].value == '1'
4956
        assert resp.form['f0_label'].value == 'hello'
4957

  
4958
    with mock.patch('qommon.misc.urlopen') as urlopen:
4959
        resp = resp.form.submit('submit') # -> submit
4960
        assert urlopen.call_count == 0
4961
        assert formdef.data_class().select()[0].data['0'] == '1'
4962
        assert formdef.data_class().select()[0].data['0_display'] == 'hello'
4963
        # no _structured data for pure jsonp sources
4964
        assert '0_structured' not in formdef.data_class().select()[0].data
4965

  
4788 4966
def test_form_data_keywords(pub):
4789 4967
    formdef = create_formdef()
4790 4968
    formdef.keywords = 'hello,world'
wcs/admin/data_sources.py
56 56
                    'data-dynamic-display-child-of': 'data_source$type',
57 57
                    'data-dynamic-display-value': _('JSON URL'),
58 58
                })
59
        form.add(StringWidget, 'query_parameter',
60
                value=self.datasource.query_parameter,
61
                title=_('Query Parameter'),
62
                hint=_('Name of the parameter to use for querying source (typically, q)'),
63
                required=False,
64
                advanced=False,
65
                attrs={
66
                    'data-dynamic-display-child-of': 'data_source$type',
67
                    'data-dynamic-display-value': _('JSON URL'),
68
                })
69
        form.add(StringWidget, 'id_parameter',
70
                value=self.datasource.id_parameter,
71
                title=_('Id Parameter'),
72
                hint=_('Name of the parameter to use to get a given entry from data source (typically, id)'),
73
                required=False,
74
                advanced=False,
75
                attrs={
76
                    'data-dynamic-display-child-of': 'data_source$type',
77
                    'data-dynamic-display-value': _('JSON URL'),
78
                })
59 79
        if self.datasource.slug:
60 80
            form.add(StringWidget, 'slug',
61 81
                    value=self.datasource.slug,
......
88 108
        self.datasource.description = form.get_widget('description').parse()
89 109
        self.datasource.data_source = form.get_widget('data_source')
90 110
        self.datasource.cache_duration = form.get_widget('cache_duration').parse()
111
        self.datasource.query_parameter = form.get_widget('query_parameter').parse()
112
        self.datasource.id_parameter = form.get_widget('id_parameter').parse()
91 113
        if self.datasource.slug:
92 114
            self.datasource.slug = slug
93 115
        self.datasource.store()
wcs/api.py
19 19
import time
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
23 23
from quixote.directory import Directory
24 24

  
25
from django.utils.six.moves.urllib import parse as urllib
26

  
25 27
from qommon import _
26 28
from qommon import misc
27 29
from qommon.evalutils import make_datetime
......
31 33

  
32 34
from wcs.categories import Category
33 35
from wcs.conditions import Condition, ValidationError
36
from wcs.data_sources import NamedDataSource
34 37
from wcs.formdef import FormDef
35 38
from wcs.roles import Role, logged_users_role
36 39
from wcs.forms.common import FormStatusPage
37 40
import wcs.qommon.storage as st
38
from wcs.api_utils import is_url_signed, get_user_from_api_query_string
41
from wcs.api_utils import MissingSecret, sign_url, get_secret_and_orig, is_url_signed, get_user_from_api_query_string
39 42

  
40 43
from backoffice.management import FormPage as BackofficeFormPage
41 44
from backoffice.management import ManagementDirectory
......
741 744
        return json.dumps(data)
742 745

  
743 746

  
747
class AutocompleteDirectory(Directory):
748
    def _q_lookup(self, component):
749
        url = get_session().get_data_source_query_url_from_token(component)
750
        if not url:
751
            raise AccessForbiddenError()
752
        url += urllib.quote(get_request().form['q'])
753
        unsigned_url = url
754
        try:
755
            signature_key, orig = get_secret_and_orig(url)
756
        except MissingSecret:
757
            pass
758
        else:
759
            url += '&orig=%s' % orig
760
            url = sign_url(url, signature_key)
761
        get_response().set_content_type('application/json')
762
        return misc.urlopen(url).read()
763

  
764

  
744 765
class ApiDirectory(Directory):
745 766
    _q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
746
            'formdefs', 'categories', 'user', 'users', 'code',
767
            'formdefs', 'categories', 'user', 'users', 'code', 'autocomplete',
747 768
            ('validate-expression', 'validate_expression'),
748 769
            ('validate-condition', 'validate_condition')]
749 770

  
......
753 774
    user = ApiUserDirectory()
754 775
    users = ApiUsersDirectory()
755 776
    code = ApiTrackingCodeDirectory()
777
    autocomplete = AutocompleteDirectory()
756 778

  
757 779
    def reverse_geocoding(self):
758 780
        get_response().set_content_type('application/json')
wcs/data_sources.py
21 21
from django.utils.six.moves.urllib import parse as urllib
22 22
from django.utils.six.moves.urllib import parse as urlparse
23 23

  
24
from quixote import get_publisher, get_request
24
from quixote import get_publisher, get_request, get_session
25 25
from quixote.html import TemplateIO
26 26

  
27 27
from qommon import _
......
226 226
    return NamedDataSource.get_by_slug(ds_type).data_source
227 227

  
228 228

  
229
def get_object(data_source):
230
    if not data_source:
231
        return None
232
    ds_type = data_source.get('type')
233
    if ds_type in ('json', 'jsonp', 'formula'):
234
        named_data_source = NamedDataSource()
235
        named_data_source.data_source = data_source
236
        return named_data_source
237
    return NamedDataSource.get_by_slug(ds_type)
238

  
239

  
229 240
class NamedDataSource(XmlStorableObject):
230 241
    _names = 'datasources'
231 242
    _xml_tagname = 'datasources'
......
235 246
    description = None
236 247
    data_source = None
237 248
    cache_duration = None
249
    query_parameter = None
250
    id_parameter = None
238 251

  
239 252
    # declarations for serialization
240 253
    XML_NODES = [('name', 'str'), ('slug', 'str'), ('description', 'str'),
241 254
            ('cache_duration', 'str'),
255
            ('query_parameter', 'str'),
256
            ('id_parameter', 'str'),
242 257
            ('data_source', 'data_source'),
243 258
            ]
244 259

  
......
246 261
        StorableObject.__init__(self)
247 262
        self.name = name
248 263

  
264
    @property
265
    def type(self):
266
        return self.data_source.get('type')
267

  
268
    def can_jsonp(self):
269
        if self.type == 'jsonp':
270
            return True
271
        if self.type == 'json' and self.query_parameter:
272
            return True
273
        return False
274

  
249 275
    def migrate(self):
250 276
        changed = False
251 277

  
......
294 320
            return objects[0]
295 321
        raise KeyError("data source '%s' does not exist" % slug)
296 322

  
323
    def get_json_query_url(self):
324
        url = self.data_source.get('value').strip()
325
        if Template.is_template_string(url):
326
            vars = get_publisher().substitutions.get_context_variables(mode='lazy')
327
            url = get_variadic_url(url, vars)
328
        if not '?' in url:
329
            url += '?' + self.query_parameter + '='
330
        else:
331
            url += '&' + self.query_parameter + '='
332
        return url
333

  
334
    def get_jsonp_url(self):
335
        if self.type == 'jsonp':
336
            return self.data_source.get('value')
337
        if self.type == 'json' and self.query_parameter:
338
            return '/api/autocomplete/%s' % (
339
                    get_session().get_data_source_query_url_token(self.get_json_query_url()))
340
        return None
341

  
342
    def load_json(self, param_name, param_value):
343
        url = self.data_source.get('value').strip()
344
        if Template.is_template_string(url):
345
            vars = get_publisher().substitutions.get_context_variables(mode='lazy')
346
            url = get_variadic_url(url, vars)
347

  
348
        if not '?' in url:
349
            url += '?'
350
        else:
351
            url += '&'
352
        url += param_name + '=' + urllib.quote(param_value)
353

  
354
        request = get_request()
355
        if hasattr(request, 'datasources_cache') and url in request.datasources_cache:
356
            return request.datasources_cache[url]
357

  
358
        unsigned_url = url
359
        try:
360
            signature_key, orig = get_secret_and_orig(url)
361
        except MissingSecret:
362
            pass
363
        else:
364
            url += '&orig=%s' % orig
365
            url = sign_url(url, signature_key)
366
        resp = qommon.misc.urlopen(url).read()
367
        if hasattr(request, 'datasources_cache'):
368
            request.datasources_cache[unsigned_url] = resp
369
        return resp
370

  
371
    def get_display_value(self, option_id):
372
        value = self.get_structured_value(option_id)
373
        if value:
374
            return value.get('text')
375
        return None
376

  
377
    def get_structured_value(self, option_id):
378
        value = None
379
        if self.type == 'json' and self.id_parameter:
380
            resp = self.load_json(self.id_parameter, option_id)
381
            response = qommon.misc.json_loads(resp)
382
            if response['data']:
383
                value = response['data'][0]
384
        else:
385
            for item in get_structured_items(self.data_source, mode='lazy'):
386
                if str(item['id']) == str(option_id):
387
                    value = item
388
                    break
389
        if value is None:
390
            return None
391
        return value
392

  
297 393
    @classmethod
298 394
    def get_substitution_variables(cls):
299 395
        return {'data_source': DataSourcesSubstitutionProxy()}
wcs/fields.py
1224 1224
            return [(x, x) for x in self.items]
1225 1225
        return []
1226 1226

  
1227
    def perform_more_widget_changes(self, form, kwargs, edit = True):
1228
        real_data_source = data_sources.get_real(self.data_source)
1229
        if real_data_source and real_data_source.get('type') == 'jsonp':
1230
            kwargs['url'] = real_data_source.get('value')
1227
    def perform_more_widget_changes(self, form, kwargs, edit=True):
1228
        data_source = data_sources.get_object(self.data_source)
1229

  
1230
        if data_source and data_source.type == 'jsonp':
1231
            # a source defined as JSONP can only be used in autocomplete mode
1232
            self.display_mode = 'autocomplete'
1233

  
1234
        if self.display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
1235
            self.url = kwargs['url'] = data_source.get_jsonp_url()
1231 1236
            self.widget_class = JsonpSingleSelectWidget
1232
        elif self.items:
1237
            return
1238

  
1239
        if self.items:
1233 1240
            kwargs['options'] = self.get_options()
1234 1241
        elif self.data_source:
1235 1242
            items = data_sources.get_items(self.data_source,
......
1254 1261
            kwargs['select2'] = True
1255 1262

  
1256 1263
    def get_display_value(self, value):
1257
        real_value = value
1258
        label_value = str(value or '')
1259
        kwargs = {}
1260
        self.perform_more_widget_changes(None, kwargs, False)
1261
        real_data_source = data_sources.get_real(self.data_source)
1262
        if real_data_source and real_data_source.get('type') == 'jsonp':
1264
        data_source = data_sources.get_object(self.data_source)
1265
        if data_source is None:
1266
            return value or ''
1267

  
1268
        if data_source.type == 'jsonp':
1263 1269
            if not get_session().jsonp_display_values:
1264 1270
                get_session().jsonp_display_values = {}
1265
            label_value = get_session().jsonp_display_values.get(
1266
                            '%s_%s' % (real_data_source.get('value'), value))
1267
        elif type(kwargs['options'][0]) in (tuple, list):
1268
            if len(kwargs['options'][0]) == 2:
1269
                for key, value in kwargs['options']:
1270
                    if str(key) == str(real_value):
1271
                        label_value = value
1272
                        break
1273
            elif len(kwargs['options'][0]) == 3:
1274
                for key, value, key2 in kwargs['options']:
1275
                    if str(key) == str(real_value):
1276
                        label_value = value
1277
                        break
1278
        return label_value
1271
            return get_session().jsonp_display_values.get(
1272
                            '%s_%s' % (data_source.get_jsonp_url(), value))
1273

  
1274
        return data_source.get_display_value(value)
1279 1275

  
1280 1276
    def add_to_view_form(self, form, value = None):
1281 1277
        real_value = value
......
1295 1291

  
1296 1292
    def store_display_value(self, data, field_id):
1297 1293
        value = data.get(field_id)
1298
        kwargs = {}
1299
        self.perform_more_widget_changes(None, kwargs, False)
1300
        real_data_source = data_sources.get_real(self.data_source)
1301
        if real_data_source and real_data_source.get('type') == 'jsonp':
1294
        data_source = data_sources.get_object(self.data_source)
1295
        if data_source and data_source.type == 'jsonp':
1302 1296
            if get_request():
1303 1297
                display_value = get_request().form.get('f%s_display' % field_id)
1298
                real_data_source = data_source.data_source
1304 1299
                if display_value is None:
1305 1300
                    if not get_session().jsonp_display_values:
1306 1301
                        get_session().jsonp_display_values = {}
......
1312 1307
                    get_session().jsonp_display_values[
1313 1308
                            '%s_%s' % (real_data_source.get('value'), value)] = display_value
1314 1309
                return display_value
1315
        elif type(kwargs['options'][0]) in (tuple, list):
1316
            if len(kwargs['options'][0]) == 2:
1317
                for key, option_value in kwargs['options']:
1318
                    if str(key) == str(value):
1319
                        return option_value
1320
            elif len(kwargs['options'][0]) == 3:
1321
                for key, option_value, key_repeat in kwargs['options']:
1322
                    if str(key) == str(value):
1323
                        return option_value
1324
        return str(value)
1310
        return self.get_display_value(value)
1325 1311

  
1326 1312
    def store_structured_value(self, data, field_id):
1327
        value = data.get(field_id)
1328
        if not self.data_source:
1313
        data_source = data_sources.get_object(self.data_source)
1314
        if data_source is None:
1329 1315
            return
1330
        structured_options = data_sources.get_structured_items(self.data_source)
1331
        if not structured_options:
1316

  
1317
        if data_source.type == 'jsonp':
1332 1318
            return
1333
        if not set(structured_options[0].keys()) != set(['id', 'text']):
1319

  
1320
        value = data_source.get_structured_value(data.get(field_id))
1321
        if value is None or set(value.keys()) == set(['id', 'text']):
1334 1322
            return
1335
        for structured_option in structured_options:
1336
            if str(structured_option.get('id')) == str(value):
1337
                return structured_option
1338
        return None
1323
        return value
1339 1324

  
1340 1325
    def fill_admin_form(self, form):
1341 1326
        WidgetField.fill_admin_form(self, form)
wcs/forms/root.py
766 766
                return self.removedraft()
767 767

  
768 768
            form_data = session.get_by_magictoken(magictoken, {})
769
            data = self.formdef.get_data(form)
769
            with get_publisher().substitutions.temporary_feed(
770
                    transient_formdata, force_mode='lazy'):
771
                data = self.formdef.get_data(form)
770 772
            form_data.update(data)
771 773

  
772 774
            if self.has_draft_support() and form.get_submit() == 'savedraft':
......
809 811
                        page_error_messages=page_error_messages)
810 812

  
811 813
            form_data = session.get_by_magictoken(magictoken, {})
812
            data = self.formdef.get_data(form)
814
            with get_publisher().substitutions.temporary_feed(
815
                    transient_formdata, force_mode='lazy'):
816
                data = self.formdef.get_data(form)
813 817
            form_data.update(data)
814 818

  
815 819
            session.add_magictoken(magictoken, form_data)
......
1092 1096

  
1093 1097
        for field in displayed_fields:
1094 1098
            if field.key == 'item' and field.data_source:
1095
                real_data_source = data_sources.get_real(field.data_source)
1096
                if real_data_source.get('type') != 'json':
1099
                data_source = data_sources.get_object(field.data_source)
1100
                if data_source.type != 'json':
1097 1101
                    continue
1098 1102
                varnames = re.findall(r'\bform[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)',
1099
                        real_data_source.get('value'))
1103
                        data_source.data_source.get('value'))
1104
                if (modified_field_varname is None or modified_field_varname in varnames) and (
1105
                        field.display_mode == 'autocomplete' and data_source.query_parameter):
1106
                    # computed earlier, in perform_more_widget_changes, when the field
1107
                    # was added to the form
1108
                    result[field.id]['source_url'] = field.url
1100 1109
                if modified_field_varname in varnames:
1101 1110
                    result[field.id]['items'] = [
1102 1111
                            {'id': x[2], 'text': x[1]} for x in field.get_options(mode='lazy')]
wcs/qommon/static/js/qommon.forms.js
115 115
            var $widget = $('[data-field-id="' + key + '"]');
116 116
            $widget.html(value.content);
117 117
          }
118
          if (value.source_url) {
119
            // json change of URL
120
            $widget.find('[data-select2-url]').data('select2-url', value.source_url);
121
          }
118 122
        });
119 123
      }
120 124
    });
wcs/sessions.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 random
18 17
import time
18
import uuid
19 19

  
20 20
import qommon.sessions
21 21
from qommon.sessions import Session
......
25 25
    magictokens = None
26 26
    anonymous_formdata_keys = None
27 27
    visiting_objects = None
28
    data_source_query_url_tokens = None
28 29

  
29 30
    def has_info(self):
30 31
        return (self.anonymous_formdata_keys or
31
                self.magictokens or self.visiting_objects or Session.has_info(self))
32
                self.magictokens or
33
                self.visiting_objects or
34
                self.data_source_query_url_tokens or
35
                Session.has_info(self))
32 36
    is_dirty = has_info
33 37

  
34 38
    def add_magictoken(self, token, data):
......
120 124
                del session.visiting_objects[object_key]
121 125
                session.store()
122 126

  
127
    def get_data_source_query_url_token(self, url):
128
        if not self.data_source_query_url_tokens:
129
            self.data_source_query_url_tokens = {}
130
        for key, value in self.data_source_query_url_tokens.items():
131
            if value == url:
132
                return key
133
        key = str(uuid.uuid4())
134
        self.data_source_query_url_tokens[key] = url
135
        self.store()
136
        return key
137

  
138
    def get_data_source_query_url_from_token(self, token):
139
        if not self.data_source_query_url_tokens:
140
            return None
141
        return self.data_source_query_url_tokens.get(token)
142

  
123 143
qommon.sessions.BasicSession = BasicSession
124 144
StorageSessionManager = qommon.sessions.StorageSessionManager
125
-