Projet

Général

Profil

0005-opengis-cache-custom-queries-20535.patch

Valentin Deniaud, 26 mars 2020 17:59

Télécharger (11,7 ko)

Voir les différences:

Subject: [PATCH 5/6] opengis: cache custom queries (#20535)

 .../opengis/migrations/0008_featurecache.py   |  27 ++++
 passerelle/apps/opengis/models.py             |  62 ++++++-
 tests/test_opengis.py                         | 151 +++++++++++++++++-
 3 files changed, 236 insertions(+), 4 deletions(-)
 create mode 100644 passerelle/apps/opengis/migrations/0008_featurecache.py
passerelle/apps/opengis/migrations/0008_featurecache.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-03-25 17:39
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import jsonfield.fields
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('opengis', '0007_auto_20200324_1019'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='FeatureCache',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('lat', models.FloatField()),
22
                ('lon', models.FloatField()),
23
                ('data', jsonfield.fields.JSONField(default=dict)),
24
                ('query', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='opengis.Query', verbose_name='Query')),
25
            ],
26
        ),
27
    ]
passerelle/apps/opengis/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import datetime
17 18
import math
18 19
import xml.etree.ElementTree as ET
19 20

  
20 21
import six
21 22

  
22 23
import pyproj
24
from jsonfield import JSONField
23 25

  
24 26
from django.core.cache import cache
25
from django.db import models
27
from django.db import models, transaction
26 28
from django.http import HttpResponse
27 29
from django.shortcuts import get_object_or_404
28 30
from django.urls import reverse
31
from django.utils import timezone
29 32
from django.utils.six.moves.html_parser import HTMLParser
30 33
from django.utils.text import slugify
31 34
from django.utils.translation import ugettext_lazy as _
......
382 385
        Query.objects.bulk_create(new)
383 386
        return instance
384 387

  
388
    def daily(self):
389
        super(OpenGIS, self).daily()
390
        self.update_queries()
391

  
392
    def update_queries(self, query_id=None):
393
        queries = self.query_set.all()
394
        if query_id:
395
            queries = queries.filter(pk=query_id)
396
        for query in queries:
397
            query.update_cache()
398

  
385 399
    def create_query_url(self):
386 400
        return reverse('opengis-query-new', kwargs={'slug': self.slug})
387 401

  
402
    def save(self, *args, **kwargs):
403
        super(OpenGIS, self).save(*args, **kwargs)
404
        if self.query_set.exists():
405
            self.add_job('update_queries')
406

  
388 407

  
389 408
class Query(BaseQuery):
390 409
    resource = models.ForeignKey(
......
416 435
        return endpoint
417 436

  
418 437
    def q(self, request):
419
        return self.endpoint(request, self.typename, property_name=None,
420
                             xml_filter=self.filter_expression)
438
        features = self.features.all()
439
        if not features.exists():
440
            raise APIError('Data is not synchronized yet. Retry in a few minutes.')
441
        data = {
442
            'type': 'FeatureCollection',
443
            'name': self.typename
444
        }
445
        data['features'] = list(features.values_list('data', flat=True))
446
        return data
447

  
448
    def update_cache(self):
449
        data = self.endpoint(None, self.typename, None, xml_filter=self.filter_expression)
450
        features = []
451
        for feature in data['data']:
452
            geometry = feature.get('geometry') or {}
453
            try:
454
                lon, lat = geometry['coordinates']
455
            except (KeyError, TypeError):
456
                self.resource.logger.warning('invalid coordinates in geometry: %s', geometry)
457
                continue
458
            features.append(FeatureCache(query=self, lat=lat, lon=lon, data=feature))
459
        with transaction.atomic():
460
            self.features.all().delete()
461
            FeatureCache.objects.bulk_create(features)
462

  
463
    def save(self, *args, **kwargs):
464
        super(Query, self).save(*args, **kwargs)
465
        self.resource.add_job('update_queries', query_id=self.pk)
466

  
467

  
468
class FeatureCache(models.Model):
469
    query = models.ForeignKey(
470
        to=Query,
471
        on_delete=models.CASCADE,
472
        related_name='features',
473
        verbose_name=_('Query'))
474
    lat = models.FloatField()
475
    lon = models.FloatField()
476
    data = JSONField()
tests/test_opengis.py
1 1
import mock
2 2
import pytest
3 3

  
4
from passerelle.apps.opengis.models import OpenGIS
4
from django.core.management import call_command
5

  
6
from passerelle.apps.opengis.models import OpenGIS, Query, FeatureCache
7
from passerelle.base.models import Job
8
from passerelle.utils import import_site
5 9

  
6 10
import utils
7 11

  
......
233 237
        wfs_service_url='http://example.net/wfs'))
234 238

  
235 239

  
240
@pytest.fixture
241
def query(connector):
242
    return Query.objects.create(
243
        resource=connector,
244
        name='Test Query',
245
        slug='test_query',
246
        description='Test query.',
247
        typename='pvo_patrimoine_voirie.pvoparking',
248
        filter_expression=('<Filter><PropertyIsEqualTo><PropertyName>typeparking'
249
                           '</PropertyName></PropertyIsEqualTo></Filter>')
250
    )
251

  
252

  
236 253
def geoserver_responses(url, **kwargs):
237 254
    if kwargs['params'].get('request') == 'GetCapabilities':
238 255
        return utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES)
......
255 272
    return utils.FakedResponse(status_code=200, content=FAKE_ERROR[:10])
256 273

  
257 274

  
275
def geoserver_geolocated_responses(url, **kwargs):
276
    if kwargs['params'].get('request') == 'GetCapabilities':
277
        return utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES)
278
    return utils.FakedResponse(status_code=200, content=FAKE_GEOLOCATED_FEATURE)
279

  
280

  
258 281
@mock.patch('passerelle.utils.Request.get')
259 282
def test_feature_info(mocked_get, app, connector):
260 283
    endpoint = utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
......
474 497
                   })
475 498
    assert resp.json['err'] == 1
476 499
    assert resp.json['err_desc'] == 'Webservice returned status code 404'
500

  
501

  
502
@mock.patch('passerelle.utils.Request.get')
503
def test_opengis_query_cache_update(mocked_get, app, connector, query):
504
    mocked_get.side_effect = geoserver_geolocated_responses
505
    query.update_cache()
506

  
507
    assert mocked_get.call_args[1]['params']['FILTER'] == query.filter_expression
508
    assert mocked_get.call_args[1]['params']['TYPENAMES'] == query.typename
509
    assert FeatureCache.objects.count() == 4
510

  
511
    feature = FeatureCache.objects.get(lon=1914059.51, lat=4224699.2)
512
    assert feature.data['properties']['code_post'] == 38000
513

  
514

  
515
@mock.patch('passerelle.utils.Request.get')
516
def test_opengis_query_q_endpoint(mocked_get, app, connector, query):
517
    endpoint = utils.generic_endpoint_url('opengis', 'q/test_query/', slug=connector.slug)
518
    assert endpoint == '/opengis/test/q/test_query/'
519
    mocked_get.side_effect = geoserver_geolocated_responses
520
    query.update_cache()
521
    feature = FeatureCache.objects.get(lon=1914059.51, lat=4224699.2)
522
    resp = app.get(endpoint)
523

  
524
    assert len(resp.json['features']) == FeatureCache.objects.count()
525

  
526
    feature_data = next(feature for feature in resp.json['features']
527
                        if feature['geometry']['coordinates'][0] == 1914059.51)
528
    assert feature_data == feature.data
529

  
530

  
531
@mock.patch('passerelle.utils.Request.get')
532
def test_opengis_query_cache_update_change(mocked_get, app, connector, query):
533
    mocked_get.side_effect = geoserver_geolocated_responses
534
    query.update_cache()
535
    assert FeatureCache.objects.count() == 4
536

  
537
    def new_response(url, **kwargs):
538
        if kwargs['params'].get('request') == 'GetCapabilities':
539
            return utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES)
540
        return utils.FakedResponse(
541
            content='{"features": [{"properties": {}, "geometry": {"coordinates": [1, 1]}}]}',
542
            status_code=200
543
        )
544
    mocked_get.side_effect = new_response
545
    query.update_cache()
546
    assert FeatureCache.objects.count() == 1
547

  
548

  
549
@mock.patch('passerelle.utils.Request.get')
550
def test_opengis_query_q_endpoint_cache_empty(mocked_get, app, connector, query):
551
    endpoint = utils.generic_endpoint_url('opengis', 'q/test_query/', slug=connector.slug)
552
    assert not FeatureCache.objects.exists()
553
    resp = app.get(endpoint)
554

  
555
    assert resp.json['err'] == 1
556
    assert 'not synchronized' in resp.json['err_desc']
557
    assert not FeatureCache.objects.exists()
558
    assert mocked_get.call_count == 0
559

  
560

  
561
@mock.patch('passerelle.utils.Request.get')
562
def test_opengis_query_cache_update_jobs(mocked_get, app, connector, query):
563
    mocked_get.side_effect = geoserver_geolocated_responses
564

  
565
    # fixtures created one query
566
    job = Job.objects.get(method_name='update_queries')
567
    assert not FeatureCache.objects.exists()
568

  
569
    connector.jobs()
570
    assert FeatureCache.objects.count() == 4
571
    job.refresh_from_db()
572
    assert job.status == 'completed'
573

  
574
    # modifying a query triggers an update
575
    query.save()
576
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 1
577
    connector.jobs()
578

  
579
    # modifying the connector triggers an update
580
    connector.save()
581
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 1
582
    connector.jobs()
583

  
584
    # two queries to update
585
    query.save()
586
    query.pk = None
587
    query.slug = query.name = 'test2'
588
    query.save()
589
    with mock.patch.object(Query, 'update_cache') as mocked:
590
        connector.jobs()
591
        assert mocked.call_count == 2
592

  
593
    # now only one
594
    query.save()
595
    with mock.patch.object(Query, 'update_cache') as mocked:
596
        connector.jobs()
597
        assert mocked.call_count == 1
598

  
599

  
600
@mock.patch('passerelle.utils.Request.get')
601
def test_opengis_query_cache_update_daily(mocked_get, app, connector, query):
602
    mocked_get.side_effect = geoserver_geolocated_responses
603
    assert not FeatureCache.objects.exists()
604
    call_command('cron', 'daily')
605
    assert FeatureCache.objects.count() == 4
606

  
607

  
608
@mock.patch('passerelle.utils.Request.get')
609
def test_opengis_query_endpoint_documentation(mocked_get, app, connector, query):
610
    resp = app.get(connector.get_absolute_url())
611
    assert query.name in resp.text
612
    assert query.description in resp.text
613
    assert '/opengis/test/q/test_query/' in resp.text
614

  
615

  
616
def test_opengis_export_import(query):
617
    assert OpenGIS.objects.count() == 1
618
    assert Query.objects.count() == 1
619
    serialization = {'resources': [query.resource.export_json()]}
620
    OpenGIS.objects.all().delete()
621
    assert OpenGIS.objects.count() == 0
622
    assert Query.objects.count() == 0
623
    import_site(serialization)
624
    assert OpenGIS.objects.count() == 1
625
    assert Query.objects.count() == 1
477
-