Projet

Général

Profil

0002-arcgis-add-feature-server-query-endpoint-63825.patch

Thomas Noël, 23 mai 2022 15:00

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH 2/3] arcgis: add feature server query endpoint (#63825)

 passerelle/apps/arcgis/models.py |  91 ++++++++++-
 tests/test_arcgis.py             | 270 +++++++++++++++++++++++++++++++
 2 files changed, 355 insertions(+), 6 deletions(-)
passerelle/apps/arcgis/models.py
76 76
            params['units'] = 'esriSRUnit_Meter'
77 77
        return params
78 78

  
79
    def get_query_response(self, uri, params, id_template, text_template, full):
79
    def get_query_response(self, uri, params, id_template, text_template, full, text_fieldname=None):
80 80
        url = urlparse.urljoin(self.base_url, uri)
81 81
        response = self.requests.get(url, params=params)
82 82

  
......
92 92

  
93 93
        features = infos.pop('features', [])
94 94
        id_fieldname = infos.get('objectIdFieldName') or 'OBJECTID'
95
        text_fieldname = infos.get('displayFieldName')
95
        text_fieldname = text_fieldname or infos.get('displayFieldName')
96
        aliases = {}
97
        for field in infos.get('fields') or []:
98
            if field.get('alias') and field.get('name'):
99
                aliases[field['alias']] = field['name']
96 100
        if infos.get('fieldAliases'):
97
            aliases = {v: k for k, v in infos['fieldAliases'].items()}
98
        else:
99
            aliases = {}
101
            for name, alias in infos['fieldAliases'].items():
102
                aliases[alias] = name
100 103

  
101 104
        data = []
102 105

  
......
108 111
        for n, feature in enumerate(features):
109 112
            if 'attributes' in feature:
110 113
                feature['id'] = '%s' % get_feature_attribute(feature, id_fieldname)
111
                feature['text'] = '%s' % get_feature_attribute(feature, text_fieldname)
114
                feature['text'] = (
115
                    '%s' % get_feature_attribute(feature, text_fieldname) if text_fieldname else feature['id']
116
                )
112 117
            else:
113 118
                feature['id'] = feature['text'] = '%d' % (n + 1)
114 119
            if id_template:
......
193 198
            uri, params, id_template=id_template, text_template=template, full=full
194 199
        )
195 200

  
201
    @endpoint(
202
        name='featureservice-query',
203
        description=_('Feature Service Query'),
204
        perm='can_access',
205
        parameters={
206
            'folder': {
207
                'description': _('Folder name'),
208
                'example_value': 'Specialty',
209
            },
210
            'service': {
211
                'description': _('Service name'),
212
                'example_value': 'ESRI_StateCityHighway_USA',
213
            },
214
            'layer': {
215
                'description': _('Layer or table name'),
216
                'example_value': '1',
217
            },
218
            'lat': {'description': _('Latitude')},
219
            'lon': {'description': _('Longitude')},
220
            'latmin': {'description': _('Minimal latitude (envelope)')},
221
            'lonmin': {'description': _('Minimal longitude (envelope)')},
222
            'latmax': {'description': _('Maximal latitude (envelope)')},
223
            'lonmax': {'description': _('Maximal longitude (envelope)')},
224
            'text_fieldname': {
225
                'description': _('Field name for text attribute'),
226
                'example_value': 'STATE_NAME',
227
            },
228
            'template': {
229
                'description': _('Django template for text attribute'),
230
                'example_value': '{{ attributes.STATE_NAME }} ({{ attributes.STATE_ABBR }})',
231
            },
232
            'id_template': {
233
                'description': _('Django template for id attribute'),
234
            },
235
            'full': {
236
                'description': _('Returns all ArcGIS informations (geometry, metadata)'),
237
                'type': 'bool',
238
            },
239
        },
240
    )
241
    def featureservice_query(
242
        self,
243
        request,
244
        service,
245
        layer='0',
246
        folder='',
247
        lat=None,
248
        lon=None,
249
        latmin=None,
250
        lonmin=None,
251
        latmax=None,
252
        lonmax=None,
253
        text_fieldname=None,
254
        template=None,
255
        id_template=None,
256
        full=False,
257
        **kwargs,
258
    ):
259
        uri = 'services/'
260
        if folder:
261
            uri += folder + '/'
262
        uri = uri + service + '/FeatureServer/' + layer + '/query'
263

  
264
        params = self.build_common_params(lat, lon, latmin, lonmin, latmax, lonmax, **kwargs)
265

  
266
        return self.get_query_response(
267
            uri,
268
            params,
269
            id_template=id_template,
270
            text_template=template,
271
            full=full,
272
            text_fieldname=text_fieldname,
273
        )
274

  
196 275
    @endpoint(
197 276
        name='tile',
198 277
        description=_('Tiles layer'),
tests/test_arcgis.py
81 81
   "displayFieldName" : "STATE_NAME"
82 82
}'''
83 83

  
84
FEATURES = '''{
85
    "features": [
86
        {
87
            "attributes": {
88
                "nom": "Le Nom",
89
                "id": "onze",
90
                "objectid": 11
91
            },
92
            "geometry" : {
93
               "rings" : [
94
                  [
95
                     [-111.475425113078, 44.7021622250113],
96
                     [-111.480804007084, 44.6914159859524]
97
                  ]
98
               ]
99
            }
100
        },
101
        {
102
            "attributes": {
103
                "nom": "Autre Nom",
104
                "id": "dix",
105
                "objectid": 10
106
            },
107
            "geometry" : {
108
               "rings" : [
109
                  [
110
                     [-111.475425113078, 44.7021622250113],
111
                     [-111.480804007084, 44.6914159859524]
112
                  ]
113
               ]
114
            }
115
        }
116
    ],
117
    "fields": [
118
        {
119
            "alias": "OBJECTID",
120
            "name": "objectid",
121
            "type": "esriFieldTypeOID"
122
        },
123
        {
124
            "alias": "quartier",
125
            "name": "nom",
126
            "type": "esriFieldTypeString"
127
        },
128
        {
129
            "alias": "code",
130
            "length": 5,
131
            "name": "id",
132
            "type": "esriFieldTypeString"
133
        }
134
    ],
135
    "geometryType": "esriGeometryPolygon",
136
    "globalIdFieldName": "",
137
    "objectIdFieldName": "objectid",
138
    "spatialReference": {
139
        "latestWkid": 3947,
140
        "wkid": 3947
141
    }
142
}'''
143

  
84 144

  
85 145
@pytest.fixture
86 146
def arcgis():
......
264 324
        assert resp.json['err_desc'] == '<lonmin> <latmin> <lonmax> and <latmax> must be floats'
265 325

  
266 326

  
327
def test_arcgis_featureservice_query(app, arcgis):
328
    endpoint = tests.utils.generic_endpoint_url('arcgis', 'featureservice-query', slug=arcgis.slug)
329
    assert endpoint == '/arcgis/test/featureservice-query'
330
    params = {'folder': 'fold', 'service': 'serv', 'layer': '42'}
331

  
332
    with mock.patch('passerelle.utils.Request.get') as requests_get:
333
        requests_get.return_value = tests.utils.FakedResponse(content=FEATURES, status_code=200)
334

  
335
        resp = app.get(endpoint, params=params, status=403)
336
        assert requests_get.call_count == 0
337
        assert resp.json['err'] == 1
338
        assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
339

  
340
        # open access
341
        api = ApiUser.objects.create(username='all', keytype='', key='')
342
        obj_type = ContentType.objects.get_for_model(arcgis)
343
        AccessRight.objects.create(
344
            codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=arcgis.pk
345
        )
346
        resp = app.get(endpoint, params=params, status=200)
347
        assert requests_get.call_count == 1
348
        assert (
349
            requests_get.call_args[0][0]
350
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
351
        )
352
        args = requests_get.call_args[1]['params']
353
        assert args['f'] == 'json'
354
        assert args['outFields'] == '*'
355
        assert 'data' in resp.json
356
        assert resp.json['err'] == 0
357
        assert len(resp.json['data']) == 2
358
        assert resp.json['data'][0]['id'] == '11'
359
        assert resp.json['data'][0]['text'] == '11'
360
        assert 'geometry' not in resp.json['data'][0]
361
        assert 'metadata' not in resp.json
362

  
363
        params['full'] = 'on'
364
        resp = app.get(endpoint, params=params, status=200)
365
        assert requests_get.call_count == 2
366
        assert (
367
            requests_get.call_args[0][0]
368
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
369
        )
370
        args = requests_get.call_args[1]['params']
371
        assert args['f'] == 'json'
372
        assert args['outFields'] == '*'
373
        assert 'data' in resp.json
374
        assert resp.json['err'] == 0
375
        assert len(resp.json['data']) == 2
376
        assert resp.json['data'][0]['id'] == '11'
377
        assert resp.json['data'][0]['text'] == '11'
378
        assert resp.json['data'][0]['geometry']
379
        assert resp.json['metadata']
380

  
381
        params['lat'] = '9.87654'
382
        params['lon'] = '1.12345'
383
        resp = app.get(endpoint, params=params, status=200)
384
        assert requests_get.call_count == 3
385
        assert (
386
            requests_get.call_args[0][0]
387
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
388
        )
389
        args = requests_get.call_args[1]['params']
390
        assert args['geometry'] == '1.12345,9.87654'
391
        assert args['geometryType'] == 'esriGeometryPoint'
392

  
393
        del params['lat']  # missing lat, do not search by geometry
394
        resp = app.get(endpoint, params=params, status=200)
395
        assert requests_get.call_count == 4
396
        assert (
397
            requests_get.call_args[0][0]
398
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
399
        )
400
        args = requests_get.call_args[1]['params']
401
        assert 'geometry' not in args
402
        assert 'geometryType' not in args
403

  
404
        params.update({'latmin': '1', 'lonmin': '2', 'latmax': '3', 'lonmax': '4'})
405
        resp = app.get(endpoint, params=params, status=200)
406
        assert requests_get.call_count == 5
407
        assert (
408
            requests_get.call_args[0][0]
409
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
410
        )
411
        args = requests_get.call_args[1]['params']
412
        assert args['geometry'] == '2.0,1.0,4.0,3.0'
413
        assert args['geometryType'] == 'esriGeometryEnvelope'
414

  
415
        del params['latmin']  # incomplete box, do not search by geometry
416
        resp = app.get(endpoint, params=params, status=200)
417
        assert requests_get.call_count == 6
418
        assert (
419
            requests_get.call_args[0][0]
420
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
421
        )
422
        args = requests_get.call_args[1]['params']
423
        assert 'geometry' not in args
424
        assert 'geometryType' not in args
425

  
426
        # others params are directly sent to ArcGIS
427
        params['spatialRel'] = 'esriSpatialRelContains'
428
        params.update({'latmin': '1', 'lonmin': '2', 'latmax': '3', 'lonmax': '4'})
429
        resp = app.get(endpoint, params=params, status=200)
430
        assert requests_get.call_count == 7
431
        assert (
432
            requests_get.call_args[0][0]
433
            == 'https://arcgis.example.net/services/fold/serv/FeatureServer/42/query'
434
        )
435
        args = requests_get.call_args[1]['params']
436
        assert args['geometry'] == '2.0,1.0,4.0,3.0'
437
        assert args['geometryType'] == 'esriGeometryEnvelope'
438
        assert args['spatialRel'] == 'esriSpatialRelContains'
439

  
440
        # folder
441
        params['folder'] = 'foo/bar'
442
        resp = app.get(endpoint, params=params, status=200)
443
        assert (
444
            requests_get.call_args[0][0]
445
            == 'https://arcgis.example.net/services/foo/bar/serv/FeatureServer/42/query'
446
        )
447
        del params['folder']
448
        resp = app.get(endpoint, params=params, status=200)
449
        assert (
450
            requests_get.call_args[0][0] == 'https://arcgis.example.net/services/serv/FeatureServer/42/query'
451
        )
452

  
453
        # minimal call
454
        resp = app.get(endpoint, params={'service': 'srv'}, status=200)
455
        assert requests_get.call_args[0][0] == 'https://arcgis.example.net/services/srv/FeatureServer/0/query'
456
        args = requests_get.call_args[1]['params']
457
        assert args == {'f': 'json', 'inSR': '4326', 'outSR': '4326', 'outFields': '*'}
458

  
459
        # distance
460
        resp = app.get(endpoint, params={'service': 'srv', 'distance': '100'}, status=200)
461
        assert requests_get.call_args[0][0] == 'https://arcgis.example.net/services/srv/FeatureServer/0/query'
462
        args = requests_get.call_args[1]['params']
463
        assert args['distance'] == '100'
464
        assert args['units'] == 'esriSRUnit_Meter'  # default unit
465
        resp = app.get(
466
            endpoint,
467
            params={'service': 'srv', 'distance': '5', 'units': 'esriSRUnit_NauticalMile'},
468
            status=200,
469
        )
470
        assert requests_get.call_args[0][0] == 'https://arcgis.example.net/services/srv/FeatureServer/0/query'
471
        args = requests_get.call_args[1]['params']
472
        assert args['distance'] == '5'
473
        assert args['units'] == 'esriSRUnit_NauticalMile'
474

  
475
        # text_fieldname
476
        for text_fieldname in ('nom', 'quartier'):  # 'nom': real name, 'quartier': its alias
477
            resp = app.get(
478
                endpoint,
479
                params={
480
                    'service': 'srv',
481
                    'text_fieldname': text_fieldname,
482
                },
483
                status=200,
484
            )
485
            assert 'data' in resp.json
486
            assert resp.json['err'] == 0
487
            assert len(resp.json['data']) == 2
488
            assert resp.json['data'][0]['id'] == '11'
489
            assert resp.json['data'][0]['text'] == 'Le Nom'
490
            assert resp.json['data'][1]['id'] == '10'
491
            assert resp.json['data'][1]['text'] == 'Autre Nom'
492

  
493
        # templates
494
        resp = app.get(
495
            endpoint,
496
            params={
497
                'service': 'srv',
498
                'template': '{{ attributes.nom }}',
499
                'id_template': '{{ attributes.id }}',
500
            },
501
            status=200,
502
        )
503
        assert 'data' in resp.json
504
        assert resp.json['err'] == 0
505
        assert len(resp.json['data']) == 2
506
        assert resp.json['data'][0]['id'] == 'onze'
507
        assert resp.json['data'][0]['text'] == 'Le Nom'
508
        assert resp.json['data'][1]['id'] == 'dix'
509
        assert resp.json['data'][1]['text'] == 'Autre Nom'
510

  
511
    # call errors
512
    with mock.patch('passerelle.utils.Request.get') as requests_get:
513
        requests_get.return_value = tests.utils.FakedResponse(content=FEATURES, status_code=200)
514
        resp = app.get(endpoint, params={}, status=400)
515
        assert requests_get.call_count == 0
516
        assert resp.json['err'] == 1
517
        assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
518
        assert resp.json['err_desc'] == "missing parameters: 'service'."
519

  
520
        resp = app.get(endpoint, params={'service': 'src', 'lat': '0', 'lon': 'y'}, status=400)
521
        assert requests_get.call_count == 0
522
        assert resp.json['err'] == 1
523
        assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
524
        assert resp.json['err_desc'] == '<lon> and <lat> must be floats'
525

  
526
        resp = app.get(
527
            endpoint,
528
            params={'service': 'src', 'latmin': '0', 'lonmin': 'y', 'latmax': '0', 'lonmax': '1'},
529
            status=400,
530
        )
531
        assert requests_get.call_count == 0
532
        assert resp.json['err'] == 1
533
        assert resp.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
534
        assert resp.json['err_desc'] == '<lonmin> <latmin> <lonmax> and <latmax> must be floats'
535

  
536

  
267 537
@pytest.mark.parametrize(
268 538
    'format_string,fail',
269 539
    [
270
-