Projet

Général

Profil

0001-data-sources-add-error-management-parameters-44054.patch

Frédéric Péters, 14 mars 2021 16:30

Télécharger (26,4 ko)

Voir les différences:

Subject: [PATCH] data sources: add error management parameters (#44054)

 tests/form_pages/test_all.py                  |  37 ++++-
 tests/test_datasource.py                      | 128 +++++++++++-------
 tests/utilities.py                            |  21 ++-
 wcs/admin/data_sources.py                     |  29 ++--
 wcs/api.py                                    |  23 +++-
 wcs/data_sources.py                           |  69 ++++++++--
 wcs/templates/wcs/backoffice/data-source.html |   4 +-
 7 files changed, 229 insertions(+), 82 deletions(-)
tests/form_pages/test_all.py
24 24
from wcs.qommon import force_str
25 25
from wcs.qommon.emails import docutils
26 26
from wcs.qommon.ident.password_accounts import PasswordAccount
27
from wcs.qommon.misc import ConnectionError
27 28
from wcs.carddef import CardDef
28 29
from wcs.formdef import FormDef
29 30
from wcs.workflows import (
......
96 97
    return pub
97 98

  
98 99

  
100
@pytest.fixture
101
def error_email(pub):
102
    pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
103
    pub.write_cfg()
104
    pub.set_config()
105

  
106

  
99 107
def teardown_module(module):
100 108
    clean_temporary_pub()
101 109

  
......
5662 5670
        assert formdef.data_class().select()[0].data['0_display'] == 'world'
5663 5671

  
5664 5672

  
5665
def test_item_field_autocomplete_json_source(http_requests, pub):
5673
def test_item_field_autocomplete_json_source(http_requests, pub, error_email, emails):
5666 5674
    user = create_user(pub)
5667 5675
    formdef = create_formdef()
5668 5676
    formdef.data_class().wipe()
......
5752 5760
        # check unauthorized access
5753 5761
        resp2 = get_app(pub).get(select2_url + '?q=hell', status=403)
5754 5762

  
5763
    # check error handling in autocomplete endpoint
5764
    formdef.data_class().wipe()
5765

  
5766
    app = get_app(pub)
5767
    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
5768
        urlopen.side_effect = ConnectionError('...')
5769
        resp = app.get('/test/')
5770
        assert urlopen.call_count == 0
5771
        pq = resp.pyquery.remove_namespaces()
5772
        select2_url = pq('select').attr['data-select2-url']
5773

  
5774
        assert emails.count() == 0
5775
        resp2 = app.get(select2_url + '?q=hell')
5776
        assert urlopen.call_count == 1
5777
        assert urlopen.call_args[0][0] == 'http://remote.example.net/json?q=hell'
5778
        assert resp2.json == {'data': [], 'err': '1'}
5779
        assert emails.count() == 0
5780

  
5781
        data_source.notify_on_errors = True
5782
        data_source.store()
5783
        resp2 = app.get(select2_url + '?q=hell')
5784
        assert emails.count() == 1
5785
        assert 'wcs.qommon.errors.ConnectionError: ...' in emails.get_latest('subject')
5786

  
5787
        data_source.notify_on_errors = False
5788
        data_source.store()
5789

  
5755 5790
    # simulate select2 mode, with qommon.forms.js adding an extra hidden widget
5756 5791
    resp.form.fields['f0_display'] = Hidden(form=resp.form, tag='input', name='f0_display', pos=10)
5757 5792
    resp.form['f0'].force_value('1')
tests/test_datasource.py
53 53
    return req
54 54

  
55 55

  
56
@pytest.fixture
57
def error_email(pub):
58
    pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
59
    pub.write_cfg()
60
    pub.set_config()
61

  
62

  
56 63
def test_item_field_python_datasource(requests_pub):
57 64
    req = get_request()
58 65
    req.environ['REQUEST_METHOD'] = 'POST'
......
90 97
        {'id': '2', 'text': 'bar'},
91 98
    ]
92 99

  
93
    # invalid python expression
94
    datasource = {'type': 'formula', 'value': 'foobar'}
95
    assert data_sources.get_items(datasource) == []
96

  
97
    # expression not iterable
98
    datasource = {'type': 'formula', 'value': '2'}
99
    assert data_sources.get_items(datasource) == []
100

  
101 100
    # three-item tuples
102 101
    plain_list = [('1', 'foo', 'a'), ('2', 'bar', 'b')]
103 102
    datasource = {'type': 'formula', 'value': repr(plain_list)}
......
132 131
    ]
133 132

  
134 133

  
134
def test_python_datasource_errors(pub, error_email, http_requests, emails, caplog):
135
    # invalid python expression
136
    datasource = {'type': 'formula', 'value': 'foobar', 'notify_on_errors': True}
137
    assert data_sources.get_items(datasource) == []
138
    assert 'Failed to eval() Python data source' in emails.get_latest('subject')
139

  
140
    # expression not iterable
141
    datasource = {'type': 'formula', 'value': '2', 'notify_on_errors': True}
142
    assert data_sources.get_items(datasource) == []
143
    assert 'gave a non-iterable result' in emails.get_latest('subject')
144

  
145

  
135 146
def test_python_datasource_with_evalutils(pub):
136 147
    plain_list = [
137 148
        {'id': 'foo', 'text': 'Foo', 'value': '2017-01-01'},
......
365 376
    ]
366 377

  
367 378

  
368
def test_json_datasource_bad_url(pub, http_requests, caplog):
379
def test_json_datasource_bad_url(pub, error_email, http_requests, emails, caplog):
369 380
    datasource = {'type': 'json', 'value': 'http://remote.example.net/404'}
370 381
    assert data_sources.get_items(datasource) == []
371
    assert 'Error loading JSON data source' in caplog.records[-1].message
372
    assert 'status: 404' in caplog.records[-1].message
382
    assert emails.count() == 0
373 383

  
374
    datasource = {'type': 'json', 'value': 'http://remote.example.net/xml'}
384
    datasource = {'type': 'json', 'value': 'http://remote.example.net/404', 'notify_on_errors': True}
375 385
    assert data_sources.get_items(datasource) == []
376
    assert 'Error reading JSON data source output' in caplog.records[-1].message
377
    assert 'Expecting value:' in caplog.records[-1].message
386
    assert emails.count() == 1
387
    assert 'error in HTTP request to http://remote.example.net/404 (status: 404)' in emails.get_latest(
388
        'subject'
389
    )
378 390

  
379
    datasource = {'type': 'json', 'value': 'http://remote.example.net/connection-error'}
391
    datasource = {'type': 'json', 'value': 'http://remote.example.net/xml', 'notify_on_errors': True}
380 392
    assert data_sources.get_items(datasource) == []
381
    assert 'Error loading JSON data source' in caplog.records[-1].message
382
    assert 'error' in caplog.records[-1].message
393
    assert emails.count() == 2
394
    assert 'Error reading JSON data source' in emails.get_latest('subject')
383 395

  
384
    datasource = {'type': 'json', 'value': 'http://remote.example.net/json-list-err1'}
396
    datasource = {
397
        'type': 'json',
398
        'value': 'http://remote.example.net/connection-error',
399
        'notify_on_errors': True,
400
    }
385 401
    assert data_sources.get_items(datasource) == []
386
    assert 'Error reading JSON data source output (err 1)' in caplog.records[-1].message
402
    assert 'Error loading JSON data source' in emails.get_latest('subject')
387 403

  
404
    datasource = {
405
        'type': 'json',
406
        'value': 'http://remote.example.net/json-list-err1',
407
        'notify_on_errors': True,
408
    }
409
    assert data_sources.get_items(datasource) == []
410
    assert 'Error reading JSON data source output (err 1)' in emails.get_latest('subject')
388 411

  
389
def test_json_datasource_bad_url_scheme(pub, caplog):
390
    datasource = {'type': 'json', 'value': ''}
412

  
413
def test_json_datasource_bad_url_scheme(pub, error_email, emails):
414
    datasource = {'type': 'json', 'value': '', 'notify_on_errors': True}
391 415
    assert data_sources.get_items(datasource) == []
392
    assert caplog.records[-1].message == 'Empty URL in JSON data source'
416
    assert emails.count() == 0
393 417

  
394
    datasource = {'type': 'json', 'value': 'foo://bar'}
418
    datasource = {'type': 'json', 'value': 'foo://bar', 'notify_on_errors': True}
395 419
    assert data_sources.get_items(datasource) == []
396
    assert 'Error loading JSON data source' in caplog.records[-1].message
397
    assert 'invalid scheme in URL' in caplog.records[-1].message
420
    assert 'Error loading JSON data source' in emails.get_latest('subject')
421
    assert 'invalid scheme in URL' in emails.get_latest('subject')
398 422

  
399
    datasource = {'type': 'json', 'value': '/bla/blo'}
423
    datasource = {'type': 'json', 'value': '/bla/blo', 'notify_on_errors': True}
400 424
    assert data_sources.get_items(datasource) == []
401
    assert 'Error loading JSON data source' in caplog.records[-1].message
402
    assert 'invalid scheme in URL' in caplog.records[-1].message
425
    assert 'Error loading JSON data source' in emails.get_latest('subject')
426
    assert 'invalid scheme in URL' in emails.get_latest('subject')
403 427

  
404 428

  
405 429
def test_geojson_datasource(pub, requests_pub, http_requests):
......
741 765
    ]
742 766

  
743 767

  
744
def test_geojson_datasource_bad_url(pub, http_requests, caplog):
745
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/404'}
768
def test_geojson_datasource_bad_url(pub, http_requests, error_email, emails):
769
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/404', 'notify_on_errors': True}
746 770
    assert data_sources.get_items(datasource) == []
747
    assert 'Error loading JSON data source' in caplog.records[-1].message
748
    assert 'status: 404' in caplog.records[-1].message
771
    assert 'Error loading JSON data source' in emails.get_latest('subject')
772
    assert 'status: 404' in emails.get_latest('subject')
749 773

  
750
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/xml'}
774
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/xml', 'notify_on_errors': True}
751 775
    assert data_sources.get_items(datasource) == []
752
    assert 'Error reading JSON data source output' in caplog.records[-1].message
753
    assert 'Expecting value:' in caplog.records[-1].message
776
    assert 'Error reading JSON data source output' in emails.get_latest('subject')
777
    assert 'Expecting value:' in emails.get_latest('subject')
754 778

  
755
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/connection-error'}
779
    datasource = {
780
        'type': 'geojson',
781
        'value': 'http://remote.example.net/connection-error',
782
        'notify_on_errors': True,
783
    }
756 784
    assert data_sources.get_items(datasource) == []
757
    assert 'Error loading JSON data source' in caplog.records[-1].message
758
    assert 'error' in caplog.records[-1].message
785
    assert 'Error loading JSON data source' in emails.get_latest('subject')
786
    assert 'error' in emails.get_latest('subject')
759 787

  
760
    datasource = {'type': 'geojson', 'value': 'http://remote.example.net/json-list-err1'}
788
    datasource = {
789
        'type': 'geojson',
790
        'value': 'http://remote.example.net/json-list-err1',
791
        'notify_on_errors': True,
792
    }
761 793
    assert data_sources.get_items(datasource) == []
762
    assert 'Error reading JSON data source output (err 1)' in caplog.records[-1].message
794
    assert 'Error reading JSON data source output (err 1)' in emails.get_latest('subject')
763 795

  
764 796

  
765
def test_geojson_datasource_bad_url_scheme(pub, caplog):
797
def test_geojson_datasource_bad_url_scheme(pub, error_email, emails):
766 798
    datasource = {'type': 'geojson', 'value': ''}
767 799
    assert data_sources.get_items(datasource) == []
768
    assert caplog.records[-1].message == 'Empty URL in GeoJSON data source'
800
    assert emails.count() == 0
769 801

  
770
    datasource = {'type': 'geojson', 'value': 'foo://bar'}
802
    datasource = {'type': 'geojson', 'value': 'foo://bar', 'notify_on_errors': True}
771 803
    assert data_sources.get_items(datasource) == []
772
    assert 'Error loading JSON data source' in caplog.records[-1].message
773
    assert 'invalid scheme in URL' in caplog.records[-1].message
804
    assert 'Error loading JSON data source' in emails.get_latest('subject')
805
    assert 'invalid scheme in URL' in emails.get_latest('subject')
774 806

  
775
    datasource = {'type': 'geojson', 'value': '/bla/blo'}
807
    datasource = {'type': 'geojson', 'value': '/bla/blo', 'notify_on_errors': True}
776 808
    assert data_sources.get_items(datasource) == []
777
    assert 'Error loading JSON data source' in caplog.records[-1].message
778
    assert 'invalid scheme in URL' in caplog.records[-1].message
809
    assert 'Error loading JSON data source' in emails.get_latest('subject')
810
    assert 'invalid scheme in URL' in emails.get_latest('subject')
779 811

  
780 812

  
781 813
def test_item_field_named_python_datasource(requests_pub):
tests/utilities.py
245 245
class EmailsMocking(object):
246 246
    def create_smtp_server(self, *args, **kwargs):
247 247
        class MockSmtplibSMTP(object):
248
            def __init__(self, emails):
249
                self.emails = emails
248
            def __init__(self, mocking):
249
                self.mocking = mocking
250 250

  
251 251
            def send_message(self, msg, msg_from, rcpts):
252 252
                return self.sendmail(msg_from, rcpts, msg.as_string())
......
260 260
                else:
261 261
                    payload = msg.get_payload(decode=True)
262 262
                    payloads = [payload]
263
                self.emails[force_text(subject)] = {
263
                self.mocking.emails[force_text(subject)] = {
264 264
                    'from': msg_from,
265 265
                    'to': email.header.decode_header(msg['To'])[0][0],
266 266
                    'payload': force_str(payload if payload else ''),
267 267
                    'payloads': payloads,
268 268
                    'msg': msg,
269
                    'subject': force_text(subject),
269 270
                }
270
                self.emails[force_text(subject)]['email_rcpt'] = rcpts
271
                self.mocking.emails[force_text(subject)]['email_rcpt'] = rcpts
272
                self.mocking.latest_subject = force_text(subject)
271 273

  
272 274
            def quit(self):
273 275
                pass
274 276

  
275
        return MockSmtplibSMTP(self.emails)
277
        return MockSmtplibSMTP(self)
276 278

  
277 279
    def get(self, subject):
278 280
        return self.emails.get(subject)
279 281

  
282
    def get_latest(self, part=None):
283
        email = self.emails.get(self.latest_subject, {})
284
        if part:
285
            return email.get(part) if email else None
286
        return email
287

  
280 288
    def empty(self):
281 289
        self.emails.clear()
282 290

  
......
287 295
        self.wcs_create_smtp_server = sys.modules['wcs.qommon.emails'].create_smtp_server
288 296
        sys.modules['wcs.qommon.emails'].create_smtp_server = self.create_smtp_server
289 297
        self.emails = {}
298
        self.latest_subject = None
290 299
        return self
291 300

  
292 301
    def __exit__(self, exc_type, exc_value, tb):
......
407 416
            raise ConnectionError('error')
408 417

  
409 418
        if raise_on_http_errors and not (200 <= status < 300):
410
            raise ConnectionError('error in HTTP request to (status: %s)' % status)
419
            raise ConnectionError('error in HTTP request to %s (status: %s)' % (url, status))
411 420

  
412 421
        return FakeResponse(status, data, headers), status, data, None
413 422

  
wcs/admin/data_sources.py
187 187
                'data-dynamic-display-value': 'json',
188 188
            },
189 189
        )
190
        form.add(
191
            CheckboxWidget,
192
            'notify_on_errors',
193
            title=_('Notify on errors'),
194
            value=self.datasource.notify_on_errors,
195
        )
196
        form.add(
197
            CheckboxWidget,
198
            'record_on_errors',
199
            title=_('Record on errors'),
200
            value=self.datasource.record_on_errors,
201
        )
202

  
190 203
        if not self.datasource.is_readonly():
191 204
            form.add_submit('submit', _('Submit'))
192 205
        form.add_submit('cancel', _('Cancel'))
......
209 222
            raise ValueError()
210 223

  
211 224
        self.datasource.name = name
212
        self.datasource.description = form.get_widget('description').parse()
213
        self.datasource.data_source = form.get_widget('data_source')
214
        self.datasource.cache_duration = form.get_widget('cache_duration').parse()
215
        self.datasource.query_parameter = form.get_widget('query_parameter').parse()
216
        self.datasource.id_parameter = form.get_widget('id_parameter').parse()
217
        self.datasource.data_attribute = form.get_widget('data_attribute').parse()
218
        self.datasource.id_attribute = form.get_widget('id_attribute').parse()
219
        self.datasource.text_attribute = form.get_widget('text_attribute').parse()
220
        self.datasource.id_property = form.get_widget('id_property').parse()
221
        self.datasource.label_template_property = form.get_widget('label_template_property').parse()
222 225
        if slug_widget:
223 226
            self.datasource.slug = slug
227

  
228
        for widget in form.widgets:
229
            if widget.name in ('name', 'slug'):
230
                continue
231
            setattr(self.datasource, widget.name, widget.parse())
232

  
224 233
        self.datasource.store()
225 234

  
226 235

  
wcs/api.py
17 17
import datetime
18 18
import json
19 19
import re
20
import sys
20 21
import time
21 22
import urllib.parse
22 23

  
......
29 30

  
30 31
from .qommon import _
31 32
from .qommon import misc
32
from .qommon.errors import AccessForbiddenError, TraversalError, UnknownNameIdAccessForbiddenError
33
from .qommon.errors import (
34
    AccessForbiddenError,
35
    TraversalError,
36
    UnknownNameIdAccessForbiddenError,
37
    ConnectionError,
38
)
33 39
from .qommon.form import ComputedExpressionWidget
34 40
from .qommon.storage import Equal, NotEqual
35 41

  
......
37 43
from wcs.conditions import Condition, ValidationError
38 44
from wcs.carddef import CardDef
39 45
from wcs.formdef import FormDef
46
from wcs.data_sources import NamedDataSource
40 47
from wcs.data_sources import get_object as get_data_source_object
41 48
from wcs.roles import Role, logged_users_role
42 49
from wcs.forms.common import FormStatusPage
......
1022 1029
            url += urllib.parse.quote(get_request().form['q'])
1023 1030
            url = sign_url_auto_orig(url)
1024 1031
            get_response().set_content_type('application/json')
1025
            return misc.urlopen(url).read()
1032
            try:
1033
                return misc.urlopen(url).read()
1034
            except ConnectionError:
1035
                if 'data_source' in info:
1036
                    data_source = NamedDataSource.get(info['data_source'])
1037
                    exc_info = sys.exc_info()
1038
                    get_publisher().notify_of_exception(
1039
                        exc_info,
1040
                        context='[DATASOURCE]',
1041
                        notify=data_source.notify_on_errors,
1042
                        record=data_source.record_on_errors,
1043
                    )
1044
                return json.dumps({'data': [], 'err': '1'})
1026 1045

  
1027 1046
        # carddef_ref in info
1028 1047
        carddef_ref = info['carddef_ref']
wcs/data_sources.py
16 16

  
17 17
import collections
18 18
import hashlib
19
import sys
19 20
import urllib.parse
20 21
import xml.etree.ElementTree as ET
21 22

  
......
165 166
    data_source = data_source or {}
166 167
    data_key = data_source.get('data_attribute') or 'data'
167 168
    geojson = data_source.get('type') == 'geojson'
169
    error_summary = None
170
    exc = None
168 171
    try:
169 172
        entries = misc.json_loads(misc.urlopen(url).read())
170 173
        if not isinstance(entries, dict):
......
177 180
        else:
178 181
            if not isinstance(entries.get(data_key), list):
179 182
                raise ValueError('not a json dict with a %s list attribute' % data_key)
183
        return entries
180 184
    except misc.ConnectionError as e:
181
        get_logger().warning('Error loading %s (%s)' % (log_message_part, str(e)))
182
        return None
185
        error_summary = 'Error loading %s (%s)' % (log_message_part, str(e))
186
        exc = e
183 187
    except (ValueError, TypeError) as e:
184
        get_logger().warning('Error reading %s output (%s)' % (log_message_part, str(e)))
185
        return None
186
    return entries
188
        error_summary = 'Error reading %s output (%s)' % (log_message_part, str(e))
189
        exc = e
190

  
191
    if data_source and (data_source.get('record_on_errors') or data_source.get('notify_on_errors')):
192
        try:
193
            raise Exception(error_summary) from exc
194
        except Exception:
195
            exc_info = sys.exc_info()
196
        get_publisher().notify_of_exception(
197
            exc_info,
198
            context='[DATASOURCE]',
199
            notify=data_source.get('notify_on_errors'),
200
            record=data_source.get('record_on_errors'),
201
        )
202

  
203
    return None
187 204

  
188 205

  
189 206
def request_json_items(url, data_source):
......
271 288
        try:
272 289
            value = eval(data_source.get('value'), global_eval_dict, variables)
273 290
            if not isinstance(value, collections.Iterable):
274
                get_logger().warning(
275
                    'Python data source (%r) gave a non-iterable result' % data_source.get('value')
291
                try:
292
                    raise Exception(
293
                        'Python data source (%r) gave a non-iterable result' % data_source.get('value')
294
                    )
295
                except Exception:
296
                    exc_info = sys.exc_info()
297
                get_publisher().notify_of_exception(
298
                    exc_info,
299
                    context='[DATASOURCE]',
300
                    notify=data_source.get('notify_on_errors'),
301
                    record=data_source.get('record_on_errors'),
276 302
                )
277 303
                return []
278 304
            if len(value) == 0:
......
289 315
            elif isinstance(value[0], str):
290 316
                return [{'id': x, 'text': x} for x in value]
291 317
            return value
292
        except:
293
            get_logger().warning('Failed to eval() Python data source (%r)' % data_source.get('value'))
318
        except Exception as exc:
319
            try:
320
                raise Exception(
321
                    'Failed to eval() Python data source (%r)' % data_source.get('value')
322
                ) from exc
323
            except Exception:
324
                exc_info = sys.exc_info()
325
            get_publisher().notify_of_exception(
326
                exc_info,
327
                context='[DATASOURCE]',
328
                notify=data_source.get('notify_on_errors'),
329
                record=data_source.get('record_on_errors'),
330
            )
294 331
            return []
295 332
    elif data_source.get('type') in ['json', 'geojson']:
296 333
        # the content available at a json URL, it must answer with a dict with
......
299 336
        geojson = data_source.get('type') == 'geojson'
300 337
        url = data_source.get('value')
301 338
        if not url:
302
            if geojson:
303
                get_logger().warning('Empty URL in GeoJSON data source')
304
            else:
305
                get_logger().warning('Empty URL in JSON data source')
306 339
            return []
307 340
        url = url.strip()
308 341
        if Template.is_template_string(url):
......
382 415
    label_template_property = None
383 416
    external = None
384 417
    external_status = None
418
    notify_on_errors = False
419
    record_on_errors = False
385 420

  
386 421
    # declarations for serialization
387 422
    XML_NODES = [
......
399 434
        ('external', 'str'),
400 435
        ('external_status', 'str'),
401 436
        ('data_source', 'data_source'),
437
        ('notify_on_errors', 'bool'),
438
        ('record_on_errors', 'bool'),
402 439
    ]
403 440

  
404 441
    def __init__(self, name=None):
......
417 454
                {
418 455
                    'id_property': self.id_property,
419 456
                    'label_template_property': self.label_template_property,
457
                    'notify_on_errors': self.notify_on_errors,
458
                    'record_on_errors': self.record_on_errors,
420 459
                }
421 460
            )
422 461
            return data_source
......
427 466
                    'data_attribute': self.data_attribute,
428 467
                    'id_attribute': self.id_attribute,
429 468
                    'text_attribute': self.text_attribute,
469
                    'notify_on_errors': self.notify_on_errors,
470
                    'record_on_errors': self.record_on_errors,
430 471
                }
431 472
            )
432 473
            return data_source
......
527 568
            json_url = self.get_json_query_url()
528 569
            info = None
529 570
            if json_url:
530
                info = {'url': json_url}
571
                info = {'url': json_url, 'data_source': self.id}
531 572
            return '/api/autocomplete/%s' % (get_session().get_data_source_query_info_token(info))
532 573
        if self.type and self.type.startswith('carddef:'):
533 574
            parts = self.type.split(':')
wcs/templates/wcs/backoffice/data-source.html
19 19
<h3>{% trans "Configuration" %}</h3>
20 20
<ul>
21 21
  <li>{% trans "Type of source:" %} {{ datasource.type_label }}</li>
22
  {% if datasource.data_source.type == 'json' or datasource.data_source.type == 'jsonp' %}
22
  {% if datasource.data_source.type == 'json' or datasource.data_source.type == 'jsonp' or datasource.data_source.type == 'geojson' %}
23 23
  <li>{% trans "URL:" %} <a href="{{ datasource.get_variadic_url }}">{{ datasource.data_source.value }}</a></li>
24 24
  {% elif datasource.data_source.type == 'formula' %}
25 25
  <li>{% trans "Python Expression:" %} {{ datasource.data_source.value }}</li>
......
27 27
  {% if datasource.cache_duration %}
28 28
  <li>{% trans "Cache Duration:" %} {{ datasource.humanized_cache_duration }}
29 29
  {% endif %}
30
  <li>{% trans "Notify on errors:" %} {{ datasource.notify_on_errors|yesno }}</li>
31
  <li>{% trans "Record on errors:" %} {{ datasource.record_on_errors|yesno }}</li>
30 32
</ul>
31 33
</div>
32 34

  
33
-