Projet

Général

Profil

0002-datasources-add-geojson-support-42010.patch

Lauréline Guérin, 28 juillet 2020 16:55

Télécharger (29,5 ko)

Voir les différences:

Subject: [PATCH 2/2] datasources: add geojson support (#42010)

 tests/test_admin_pages.py             |   4 +
 tests/test_datasource.py              | 256 +++++++++++++++++++++++++-
 tests/test_datasources_admin_pages.py |  16 ++
 wcs/admin/data_sources.py             |  35 +++-
 wcs/data_sources.py                   |  90 ++++++---
 5 files changed, 370 insertions(+), 31 deletions(-)
tests/test_admin_pages.py
1570 1570
        (u'None', True, u'None'),
1571 1571
        (u'json', False, u'JSON URL'),
1572 1572
        (u'jsonp', False, u'JSONP URL'),
1573
        (u'geojson', False, u'GeoJSON URL'),
1573 1574
        (u'python', False, u'Python Expression')
1574 1575
    ]
1575 1576
    resp = resp.form.submit('submit').follow()
......
1584 1585
        (u'foobar', False, u'Foobar'),
1585 1586
        (u'json', False, u'JSON URL'),
1586 1587
        (u'jsonp', False, u'JSONP URL'),
1588
        (u'geojson', False, u'GeoJSON URL'),
1587 1589
        (u'python', False, u'Python Expression')
1588 1590
    ]
1589 1591
    resp.form['data_source$type'].value = 'foobar'
......
1600 1602
        (u'foobar', True, u'Foobar'),
1601 1603
        (u'json', False, u'JSON URL'),
1602 1604
        (u'jsonp', False, u'JSONP URL'),
1605
        (u'geojson', False, u'GeoJSON URL'),
1603 1606
        (u'python', False, u'Python Expression')
1604 1607
    ]
1605 1608

  
......
1613 1616
        (u'foobar', True, u'Foobar'),
1614 1617
        (u'json', False, u'JSON URL'),
1615 1618
        (u'jsonp', False, u'JSONP URL'),
1619
        (u'geojson', False, u'GeoJSON URL'),
1616 1620
        (u'python', False, u'Python Expression')
1617 1621
    ]
1618 1622

  
tests/test_datasource.py
4 4
import pytest
5 5
import os
6 6
import json
7
import sys
8 7
import shutil
9 8

  
10
from django.utils import six
11 9
from django.utils.six import StringIO
12 10
from django.utils.six.moves.urllib import parse as urlparse
13 11

  
14 12
from quixote import cleanup
15
from wcs import publisher
16 13
from wcs.qommon.http_request import HTTPRequest
17
from wcs.qommon.form import *
14
from wcs.qommon.form import get_request, Form
18 15
from wcs import fields, data_sources
19 16
from wcs.data_sources import NamedDataSource, register_data_source_function
20 17

  
......
140 137
            ('foo', 'Foo', 'foo', {'id': 'foo', 'text': 'Foo', 'value': '2017-01-01'})]
141 138

  
142 139

  
143
def test_json_datasource(http_requests):
140
def test_json_datasource(requests_pub, http_requests):
144 141
    req = get_request()
145 142
    get_request().datasources_cache = {}
146 143
    datasource = {'type': 'json', 'value': ''}
......
317 314
    assert 'invalid scheme in URL' in caplog.records[-1].message
318 315

  
319 316

  
317
def test_geojson_datasource(requests_pub, http_requests):
318
    get_request()
319
    get_request().datasources_cache = {}
320
    datasource = {'type': 'geojson', 'value': ''}
321
    assert data_sources.get_items(datasource) == []
322

  
323
    # missing file
324
    get_request().datasources_cache = {}
325
    geojson_file_path = os.path.join(pub.app_dir, 'test.geojson')
326
    datasource = {'type': 'geojson', 'value': 'file://%s' % geojson_file_path}
327
    assert data_sources.get_items(datasource) == []
328

  
329
    # invalid geojson file
330
    get_request().datasources_cache = {}
331
    geojson_file = open(geojson_file_path, 'wb')
332
    geojson_file.write(codecs.encode(b'foobar', 'zlib_codec'))
333
    geojson_file.close()
334
    assert data_sources.get_items(datasource) == []
335

  
336
    # empty geojson file
337
    get_request().datasources_cache = {}
338
    geojson_file = open(geojson_file_path, 'w')
339
    json.dump({}, geojson_file)
340
    geojson_file.close()
341
    assert data_sources.get_items(datasource) == []
342

  
343
    # unrelated geojson file
344
    get_request().datasources_cache = {}
345
    geojson_file = open(geojson_file_path, 'w')
346
    json.dump('foobar', geojson_file)
347
    geojson_file.close()
348
    assert data_sources.get_items(datasource) == []
349

  
350
    # another unrelated geojson file
351
    get_request().datasources_cache = {}
352
    geojson_file = open(geojson_file_path, 'w')
353
    json.dump({'features': 'foobar'}, geojson_file)
354
    geojson_file.close()
355
    assert data_sources.get_items(datasource) == []
356

  
357
    # a good geojson file
358
    get_request().datasources_cache = {}
359
    geojson_file = open(geojson_file_path, 'w')
360
    json.dump({'features': [
361
        {'properties': {'id': '1', 'text': 'foo'}},
362
        {'properties': {'id': '2', 'text': 'bar'}}]}, geojson_file)
363
    geojson_file.close()
364
    assert data_sources.get_items(datasource) == [
365
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo'}}),
366
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar'}})]
367
    assert data_sources.get_structured_items(datasource) == [
368
        {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo'}},
369
        {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar'}}]
370

  
371
    # a geojson file with additional keys
372
    get_request().datasources_cache = {}
373
    geojson_file = open(geojson_file_path, 'w')
374
    json.dump({'features': [
375
        {'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}},
376
        {'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}}]}, geojson_file)
377
    geojson_file.close()
378
    assert data_sources.get_items(datasource) == [
379
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}}),
380
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}})]
381
    assert data_sources.get_structured_items(datasource) == [
382
        {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}},
383
        {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}}]
384

  
385
    # geojson specified with a variadic url
386
    get_request().datasources_cache = {}
387

  
388
    class GeoJSONUrlPath(object):
389
        def get_substitution_variables(self):
390
            return {'geojson_url': 'file://%s' % geojson_file_path}
391

  
392
    pub.substitutions.feed(GeoJSONUrlPath())
393
    datasource = {'type': 'geojson', 'value': '[geojson_url]'}
394
    assert data_sources.get_items(datasource) == [
395
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}}),
396
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}})]
397

  
398
    # same with django templated url
399
    get_request().datasources_cache = {}
400

  
401
    class GeoJSONUrlPath(object):
402
        def get_substitution_variables(self):
403
            return {'geojson_url': 'file://%s' % geojson_file_path}
404

  
405
    pub.substitutions.feed(GeoJSONUrlPath())
406
    datasource = {'type': 'geojson', 'value': '{{ geojson_url }}'}
407
    assert data_sources.get_items(datasource) == [
408
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}}),
409
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}})]
410

  
411
    # geojson specified with a variadic url with an erroneous space
412
    get_request().datasources_cache = {}
413

  
414
    class GeoJSONUrlPath(object):
415
        def get_substitution_variables(self):
416
            return {'geojson_url': 'file://%s' % geojson_file_path}
417

  
418
    pub.substitutions.feed(GeoJSONUrlPath())
419
    datasource = {'type': 'geojson', 'value': ' [geojson_url]'}
420
    assert data_sources.get_items(datasource) == [
421
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}}),
422
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}})]
423

  
424
    # same with django templated url
425
    get_request().datasources_cache = {}
426

  
427
    class GeoJSONUrlPath(object):
428
        def get_substitution_variables(self):
429
            return {'geojson_url': 'file://%s' % geojson_file_path}
430

  
431
    pub.substitutions.feed(GeoJSONUrlPath())
432
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}'}
433
    assert data_sources.get_items(datasource) == [
434
        ('1', 'foo', '1', {'id': '1', 'text': 'foo', 'properties': {'id': '1', 'text': 'foo', 'more': 'xxx'}}),
435
        ('2', 'bar', '2', {'id': '2', 'text': 'bar', 'properties': {'id': '2', 'text': 'bar', 'more': 'yyy'}})]
436

  
437
    # a geojson file with integer as 'id'
438
    get_request().datasources_cache = {}
439
    geojson_file = open(geojson_file_path, 'w')
440
    json.dump({'features': [
441
        {'properties': {'id': 1, 'text': 'foo'}},
442
        {'properties': {'id': 2, 'text': 'bar'}}]}, geojson_file)
443
    geojson_file.close()
444
    assert data_sources.get_items(datasource) == [
445
        ('1', 'foo', '1', {'id': 1, 'text': 'foo', 'properties': {'id': 1, 'text': 'foo'}}),
446
        ('2', 'bar', '2', {'id': 2, 'text': 'bar', 'properties': {'id': 2, 'text': 'bar'}})]
447
    assert data_sources.get_structured_items(datasource) == [
448
        {'id': 1, 'text': 'foo', 'properties': {'id': 1, 'text': 'foo'}},
449
        {'id': 2, 'text': 'bar', 'properties': {'id': 2, 'text': 'bar'}}]
450

  
451
    # a geojson file with empty or no text values
452
    get_request().datasources_cache = {}
453
    geojson_file = open(geojson_file_path, 'w')
454
    json.dump({'features': [
455
        {'properties': {'id': '1', 'text': ''}},
456
        {'properties': {'id': '2'}}]}, geojson_file)
457
    geojson_file.close()
458
    assert data_sources.get_items(datasource) == [
459
        ('1', '1', '1', {'id': '1', 'text': '1', 'properties': {'id': '1', 'text': ''}}),
460
        ('2', '2', '2', {'id': '2', 'text': '2', 'properties': {'id': '2'}})]
461
    assert data_sources.get_structured_items(datasource) == [
462
        {'id': '1', 'text': '1', 'properties': {'id': '1', 'text': ''}},
463
        {'id': '2', 'text': '2', 'properties': {'id': '2'}}]
464

  
465
    # a geojson file with empty or no id
466
    get_request().datasources_cache = {}
467
    geojson_file = open(geojson_file_path, 'w')
468
    json.dump({'features': [
469
        {'properties': {'id': '', 'text': 'foo'}},
470
        {'properties': {'text': 'bar'}},
471
        {'properties': {'id': None}}]}, geojson_file)
472
    geojson_file.close()
473
    assert data_sources.get_items(datasource) == []
474
    assert data_sources.get_structured_items(datasource) == []
475

  
476
    # specify id_property
477
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'id_property': 'gid'}
478
    get_request().datasources_cache = {}
479
    geojson_file = open(geojson_file_path, 'w')
480
    json.dump({'features': [
481
        {'properties': {'gid': '1', 'text': 'foo'}},
482
        {'properties': {'gid': '2', 'text': 'bar'}}]}, geojson_file)
483
    geojson_file.close()
484
    assert data_sources.get_structured_items(datasource) == [
485
        {'id': '1', 'text': 'foo', 'properties': {'gid': '1', 'text': 'foo'}},
486
        {'id': '2', 'text': 'bar', 'properties': {'gid': '2', 'text': 'bar'}}]
487

  
488
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'id_property': 'id'}
489
    get_request().datasources_cache = {}
490
    assert data_sources.get_structured_items(datasource) == []
491

  
492
    # specify label_template_property
493
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'label_template_property': '{{ id }}: {{ text }}'}
494
    get_request().datasources_cache = {}
495
    geojson_file = open(geojson_file_path, 'w')
496
    json.dump({'features': [
497
        {'properties': {'id': '1', 'text': 'foo'}},
498
        {'properties': {'id': '2', 'text': 'bar'}}]}, geojson_file)
499
    geojson_file.close()
500
    assert data_sources.get_structured_items(datasource) == [
501
        {'id': '1', 'text': '1: foo', 'properties': {'id': '1', 'text': 'foo'}},
502
        {'id': '2', 'text': '2: bar', 'properties': {'id': '2', 'text': 'bar'}}]
503

  
504
    # wrong template
505
    get_request().datasources_cache = {}
506
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'label_template_property': '{{ text }'}
507
    assert data_sources.get_structured_items(datasource) == [
508
        {'id': '1', 'text': '{{ text }', 'properties': {'id': '1', 'text': 'foo'}},
509
        {'id': '2', 'text': '{{ text }', 'properties': {'id': '2', 'text': 'bar'}}]
510
    get_request().datasources_cache = {}
511
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'label_template_property': 'text'}
512
    assert data_sources.get_structured_items(datasource) == [
513
        {'id': '1', 'text': 'text', 'properties': {'id': '1', 'text': 'foo'}},
514
        {'id': '2', 'text': 'text', 'properties': {'id': '2', 'text': 'bar'}}]
515

  
516
    # unknown property or empty value
517
    datasource = {'type': 'geojson', 'value': ' {{ geojson_url }}', 'label_template_property': '{{ label }}'}
518
    get_request().datasources_cache = {}
519
    geojson_file = open(geojson_file_path, 'w')
520
    json.dump({'features': [
521
        {'properties': {'id': '1', 'text': 'foo', 'label': ''}},
522
        {'properties': {'id': '2', 'text': 'bar'}}]}, geojson_file)
523
    geojson_file.close()
524
    assert data_sources.get_structured_items(datasource) == [
525
        {'id': '1', 'text': '1', 'properties': {'id': '1', 'text': 'foo', 'label': ''}},
526
        {'id': '2', 'text': '2', 'properties': {'id': '2', 'text': 'bar'}}]
527

  
528

  
529
def test_geojson_datasource_bad_url(http_requests, caplog):
530
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/404'}
531
    assert data_sources.get_items(datasource) == []
532
    assert 'Error loading GeoJSON data source' in caplog.records[-1].message
533
    assert 'status: 404' in caplog.records[-1].message
534

  
535
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/xml'}
536
    assert data_sources.get_items(datasource) == []
537
    assert 'Error reading GeoJSON data source output' in caplog.records[-1].message
538
    assert 'Expecting value:' in caplog.records[-1].message
539

  
540
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/connection-error'}
541
    assert data_sources.get_items(datasource) == []
542
    assert 'Error loading GeoJSON data source' in caplog.records[-1].message
543
    assert 'error' in caplog.records[-1].message
544

  
545
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/json-list-err1'}
546
    assert data_sources.get_items(datasource) == []
547
    assert 'Error reading GeoJSON data source output (err 1)' in caplog.records[-1].message
548

  
549

  
550
def test_geojson_datasource_bad_url_scheme(caplog):
551
    datasource = {'type': 'geojson', 'value': ''}
552
    assert data_sources.get_items(datasource) == []
553
    assert caplog.records[-1].message == 'Empty URL in GeoJSON data source'
554

  
555
    datasource = {'type': 'geojson', 'value': 'foo://bar'}
556
    assert data_sources.get_items(datasource) == []
557
    assert 'Error loading GeoJSON data source' in caplog.records[-1].message
558
    assert 'invalid scheme in URL' in caplog.records[-1].message
559

  
560
    datasource = {'type': 'geojson', 'value': '/bla/blo'}
561
    assert data_sources.get_items(datasource) == []
562
    assert 'Error loading GeoJSON data source' in caplog.records[-1].message
563
    assert 'invalid scheme in URL' in caplog.records[-1].message
564

  
565

  
320 566
def test_item_field_named_python_datasource():
321 567
    NamedDataSource.wipe()
322 568
    data_source = NamedDataSource(name='foobar')
tests/test_datasources_admin_pages.py
160 160
        resp = app.get('/backoffice/settings/data-sources/%s/' % data_source.id)
161 161
    assert '<a href="http://example.net/foo/bar"' in resp.text
162 162

  
163
    # check geojson
164
    geojson_file_path = os.path.join(pub.app_dir, 'test.geojson')
165
    geojson_file = open(geojson_file_path, 'w')
166
    json.dump({'features': [
167
        {'properties': {'id': '1', 'text': 'foo', 'label': 'foo'}},
168
        {'properties': {'id': '2', 'text': 'bar', 'label': 'bar'}}]}, geojson_file)
169
    geojson_file.close()
170
    data_source.data_source = {'type': 'geojson', 'value': 'file://%s' % geojson_file_path}
171
    data_source.store()
172
    with HttpRequestsMocking():
173
        resp = app.get('/backoffice/settings/data-sources/%s/' % data_source.id)
174
    assert 'Preview' in resp.text
175
    assert 'foo' in resp.text
176
    assert 'bar' in resp.text
177
    assert 'Additional keys are available: label' in resp.text
178

  
163 179
    data_source.data_source = {'type': 'formula', 'value': '[str(x) for x in range(100)]'}
164 180
    data_source.store()
165 181
    resp = app.get('/backoffice/settings/data-sources/%s/' % data_source.id)
wcs/admin/data_sources.py
70 70
                advanced=False,
71 71
                attrs={
72 72
                    'data-dynamic-display-child-of': 'data_source$type',
73
                    'data-dynamic-display-value': 'json',
73
                    'data-dynamic-display-value-in': 'json|geojson',
74 74
                })
75 75
        form.add(StringWidget, 'query_parameter',
76 76
                value=self.datasource.query_parameter,
......
92 92
                    'data-dynamic-display-child-of': 'data_source$type',
93 93
                    'data-dynamic-display-value': 'json',
94 94
                })
95
        form.add(StringWidget, 'id_property',
96
                 value=self.datasource.id_property,
97
                 title=_('Id Property'),
98
                 hint=_('Name of the property to use to get a given entry from data source (default: id)'),
99
                 required=False,
100
                 advanced=False,
101
                 attrs={
102
                     'data-dynamic-display-child-of': 'data_source$type',
103
                     'data-dynamic-display-value': 'geojson',
104
                 })
105
        form.add(StringWidget, 'label_template_property',
106
                 value=self.datasource.label_template_property,
107
                 title=_('Label template'),
108
                 hint=_('Django expression to build label of each value (default: {{ text }})'),
109
                 required=False,
110
                 advanced=False,
111
                 size=80,
112
                 attrs={
113
                     'data-dynamic-display-child-of': 'data_source$type',
114
                     'data-dynamic-display-value': 'geojson',
115
                 })
95 116
        if self.datasource.slug and not self.is_used():
96 117
            form.add(StringWidget, 'slug',
97 118
                    value=self.datasource.slug,
......
124 145
        self.datasource.cache_duration = form.get_widget('cache_duration').parse()
125 146
        self.datasource.query_parameter = form.get_widget('query_parameter').parse()
126 147
        self.datasource.id_parameter = form.get_widget('id_parameter').parse()
148
        self.datasource.id_property = form.get_widget('id_property').parse()
149
        self.datasource.label_template_property = form.get_widget('label_template_property').parse()
127 150
        if slug_widget:
128 151
            self.datasource.slug = slug
129 152
        self.datasource.store()
......
156 179
        return formdefs
157 180

  
158 181
    def preview_block(self):
159
        if self.datasource.data_source.get('type') not in ('json', 'formula'):
182
        data_source = self.datasource.extended_data_source
183
        if data_source.get('type') not in ('json', 'geojson', 'formula'):
160 184
            return ''
161
        items = get_structured_items(self.datasource.data_source)
185
        items = get_structured_items(data_source)
162 186
        if not items:
163 187
            return ''
164 188
        r = TemplateIO(html=True)
......
175 199
            else:
176 200
                r += htmltext('<li><tt>%s</tt>: %s</li>') % (
177 201
                        item.get('id'), item.get('text'))
178
                additional_keys |= set(item.keys())
202
                if data_source.get('type') == 'geojson':
203
                    additional_keys |= set(item.get('properties', {}).keys())
204
                else:
205
                    additional_keys |= set(item.keys())
179 206
        if len(items) > 10:
180 207
            r += htmltext('<li>...</li>')
181 208
        r += htmltext('</ul>')
wcs/data_sources.py
18 18
import hashlib
19 19
import xml.etree.ElementTree as ET
20 20

  
21
from django.template import TemplateSyntaxError, VariableDoesNotExist
21 22
from django.utils import six
22 23
from django.utils.encoding import force_text, force_bytes
23 24
from django.utils.six.moves.urllib import parse as urllib
......
71 72
        options.append(('json', _('JSON URL'), 'json'))
72 73
        if allow_jsonp:
73 74
            options.append(('jsonp', _('JSONP URL'), 'jsonp'))
75
        options.append(('geojson', _('GeoJSON URL'), 'geojson'))
74 76
        options.append(('formula', _('Python Expression'), 'python'))
75 77

  
76 78
        self.add(SingleSelectWidget, 'type', options=options, value=value.get('type'),
......
82 84

  
83 85
        self.add(StringWidget, 'value', value=value.get('value'), size=80,
84 86
             attrs={'data-dynamic-display-child-of': 'data_source$type',
85
                    'data-dynamic-display-value-in': 'json|jsonp|python'})
87
                    'data-dynamic-display-value-in': 'json|jsonp|geojson|python'})
86 88

  
87 89
        self._parsed = False
88 90

  
......
116 118
    return tupled_items
117 119

  
118 120

  
119
def request_json_items(url):
121
def request_json_items(url, data_source):
120 122
    url = sign_url_auto_orig(url)
123
    geojson = data_source.get('type') == 'geojson'
121 124
    try:
122 125
        entries = misc.json_loads(misc.urlopen(url).read())
123 126
        if not isinstance(entries, dict):
124 127
            raise ValueError('not a json dict')
125 128
        if entries.get('err') not in (None, 0, "0"):
126 129
            raise ValueError('err %s' % entries['err'])
127
        if not isinstance(entries.get('data'), list):
130
        if not geojson and not isinstance(entries.get('data'), list):
128 131
            raise ValueError('not a json dict with a data list attribute')
132
        if geojson and not isinstance(entries.get('features'), list):
133
            raise ValueError('bad geojson format')
129 134
    except misc.ConnectionError as e:
130
        get_logger().warn('Error loading JSON data source (%s)' % str(e))
135
        if geojson:
136
            get_logger().warning('Error loading GeoJSON data source (%s)' % str(e))
137
        else:
138
            get_logger().warning('Error loading JSON data source (%s)' % str(e))
131 139
        return None
132 140
    except (ValueError, TypeError) as e:
133
        get_logger().warn('Error reading JSON data source output (%s)' % str(e))
141
        if geojson:
142
            get_logger().warning('Error reading GeoJSON data source output (%s)' % str(e))
143
        else:
144
            get_logger().warning('Error reading JSON data source output (%s)' % str(e))
134 145
        return None
135 146
    items = []
136
    for item in entries.get('data'):
137
        # skip malformed items
138
        if item.get('id') is None or item.get('id') == '':
139
            continue
140
        if 'text' not in item:
141
            item['text'] = item['id']
142
        items.append(item)
147
    if geojson:
148
        id_property = data_source.get('id_property') or 'id'
149
        for item in entries.get('features'):
150
            if not item.get('properties', {}).get(id_property):
151
                continue
152
            item['id'] = item['properties'][id_property]
153
            try:
154
                item['text'] = Template(
155
                    data_source.get('label_template_property') or '{{ text }}').render(item['properties'])
156
            except (TemplateSyntaxError, VariableDoesNotExist):
157
                pass
158
            if not item.get('text'):
159
                item['text'] = item['id']
160
            items.append(item)
161
    else:
162
        for item in entries.get('data'):
163
            # skip malformed items
164
            if item.get('id') is None or item.get('id') == '':
165
                continue
166
            if 'text' not in item:
167
                item['text'] = item['id']
168
            items.append(item)
143 169
    return items
144 170

  
145 171

  
......
159 185
        items.sort(key=lambda x: misc.simplify(x['text']))
160 186
        return items
161 187

  
162
    if data_source.get('type') not in ('json', 'jsonp', 'formula'):
188
    if data_source.get('type') not in ('json', 'jsonp', 'geojson', 'formula'):
163 189
        # named data source
164 190
        named_data_source = NamedDataSource.get_by_slug(data_source['type'])
165 191
        if named_data_source.cache_duration:
166 192
            cache_duration = int(named_data_source.cache_duration)
167
        data_source = named_data_source.data_source
193
        data_source = named_data_source.extended_data_source
168 194

  
169 195
    if data_source.get('type') == 'formula':
170 196
        # the result of a python expression, it must be a list.
......
181 207
        try:
182 208
            value = eval(data_source.get('value'), global_eval_dict, variables)
183 209
            if not isinstance(value, collections.Iterable):
184
                get_logger().warn('Python data source (%r) gave a non-iterable result' % \
185
                                data_source.get('value'))
210
                get_logger().warn('Python data source (%r) gave a non-iterable result' %
211
                                  data_source.get('value'))
186 212
                return []
187 213
            if len(value) == 0:
188 214
                return []
......
201 227
        except:
202 228
            get_logger().warn('Failed to eval() Python data source (%r)' % data_source.get('value'))
203 229
            return []
204
    elif data_source.get('type') == 'json':
230
    elif data_source.get('type') in ['json', 'geojson']:
205 231
        # the content available at a json URL, it must answer with a dict with
206 232
        # a 'data' key holding the list of items, each of them being a dict
207 233
        # with at least both an "id" and a "text" key.
234
        geojson = data_source.get('type') == 'geojson'
208 235
        url = data_source.get('value')
209 236
        if not url:
210
            get_logger().warn('Empty URL in JSON data source')
237
            if geojson:
238
                get_logger().warning('Empty URL in GeoJSON data source')
239
            else:
240
                get_logger().warning('Empty URL in JSON data source')
211 241
            return []
212 242
        url = url.strip()
213 243
        if Template.is_template_string(url):
......
225 255
            if items is not None:
226 256
                return items
227 257

  
228
        items = request_json_items(url)
258
        items = request_json_items(url, data_source)
229 259
        if items is None:
230 260
            return []
231 261
        if hasattr(request, 'datasources_cache'):
......
240 270
    if not data_source:
241 271
        return None
242 272
    ds_type = data_source.get('type')
243
    if ds_type in ('json', 'jsonp', 'formula'):
273
    if ds_type in ('json', 'jsonp', 'geojson', 'formula'):
244 274
        return data_source
245 275
    if ds_type and ds_type.startswith('carddef:'):
246 276
        return data_source
......
251 281
    if not data_source:
252 282
        return None
253 283
    ds_type = data_source.get('type')
254
    if ds_type in ('json', 'jsonp', 'formula'):
284
    if ds_type in ('json', 'jsonp', 'geojson', 'formula'):
255 285
        named_data_source = NamedDataSource()
256 286
        named_data_source.data_source = data_source
257 287
        return named_data_source
......
274 304
    cache_duration = None
275 305
    query_parameter = None
276 306
    id_parameter = None
307
    id_property = None
308
    label_template_property = None
277 309

  
278 310
    # declarations for serialization
279 311
    XML_NODES = [('name', 'str'), ('slug', 'str'), ('description', 'str'),
280 312
            ('cache_duration', 'str'),
281 313
            ('query_parameter', 'str'),
282 314
            ('id_parameter', 'str'),
315
            ('id_property', 'str'),
316
            ('label_template_property', 'str'),
283 317
            ('data_source', 'data_source'),
284 318
            ]
285 319

  
......
291 325
    def type(self):
292 326
        return self.data_source.get('type')
293 327

  
328
    @property
329
    def extended_data_source(self):
330
        if self.type != 'geojson':
331
            return self.data_source
332
        data_source = self.data_source.copy()
333
        data_source.update({
334
            'id_property': self.id_property,
335
            'label_template_property': self.label_template_property,
336
        })
337
        return data_source
338

  
294 339
    def can_jsonp(self):
295 340
        if self.type == 'jsonp':
296 341
            return True
......
387 432
                return None
388 433
            return items[0]
389 434

  
390
        items = request_json_items(url)
435
        items = request_json_items(url, self.data_source)
391 436
        if not items:  # None or empty list are not valid
392 437
            return None
393 438
        if hasattr(request, 'datasources_cache'):
......
428 473
        data_source_labels = {
429 474
            'json': _('JSON'),
430 475
            'jsonp': _('JSONP'),
476
            'geojson': _('GeoJSON'),
431 477
            'formula': _('Python Expression'),
432 478
        }
433 479
        data_source_type = self.data_source.get('type')
434
-