Projet

Général

Profil

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

Valentin Deniaud, 06 avril 2020 18:28

Télécharger (11,5 ko)

Voir les différences:

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

 .../opengis/migrations/0008_featurecache.py   |  27 ++++
 passerelle/apps/opengis/models.py             |  61 ++++++-
 tests/test_opengis.py                         | 151 +++++++++++++++++-
 3 files changed, 235 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-04-01 09:21
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_20200401_1032'),
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
21 21
import six
22 22

  
23 23
import pyproj
24
from jsonfield import JSONField
24 25

  
25 26
from django.core.cache import cache
26
from django.db import models
27
from django.db import models, transaction
27 28
from django.http import HttpResponse
28 29
from django.shortcuts import get_object_or_404
29 30
from django.urls import reverse
31
from django.utils import timezone
30 32
from django.utils.six.moves.html_parser import HTMLParser
31 33
from django.utils.text import slugify
32 34
from django.utils.translation import ugettext_lazy as _
......
398 400
        Query.objects.bulk_create(new)
399 401
        return instance
400 402

  
403
    def daily(self):
404
        super(OpenGIS, self).daily()
405
        self.update_queries()
406

  
407
    def update_queries(self, query_id=None):
408
        queries = self.queries.all()
409
        if query_id:
410
            queries = queries.filter(pk=query_id)
411
        for query in queries:
412
            query.update_cache()
413

  
401 414
    def create_query_url(self):
402 415
        return reverse('opengis-query-new', kwargs={'slug': self.slug})
403 416

  
417
    def save(self, *args, **kwargs):
418
        super(OpenGIS, self).save(*args, **kwargs)
419
        if self.queries.exists():
420
            self.add_job('update_queries')
421

  
404 422

  
405 423
class Query(BaseQuery):
406 424
    resource = models.ForeignKey(
......
431 449
        return endpoint
432 450

  
433 451
    def q(self, request):
434
        return self.resource.features(request, self.typename, property_name=None,
435
                             xml_filter=self.filter_expression)
452
        features = self.features.all()
453
        if not features.exists():
454
            raise APIError('Data is not synchronized yet. Retry in a few minutes.')
455
        data = {
456
            'type': 'FeatureCollection',
457
            'name': self.typename
458
        }
459
        data['features'] = list(features.values_list('data', flat=True))
460
        return data
461

  
462
    def update_cache(self):
463
        data = self.resource.features(None, self.typename, None, xml_filter=self.filter_expression)
464
        features = []
465
        for feature in data['data']:
466
            geometry = feature.get('geometry') or {}
467
            try:
468
                lon, lat = geometry['coordinates']
469
            except (KeyError, TypeError):
470
                self.resource.logger.warning('invalid coordinates in geometry: %s', geometry)
471
                continue
472
            features.append(FeatureCache(query=self, lat=lat, lon=lon, data=feature))
473
        with transaction.atomic():
474
            self.features.all().delete()
475
            FeatureCache.objects.bulk_create(features)
476

  
477
    def save(self, *args, **kwargs):
478
        super(Query, self).save(*args, **kwargs)
479
        self.resource.add_job('update_queries', query_id=self.pk)
480

  
481

  
482
class FeatureCache(models.Model):
483
    query = models.ForeignKey(
484
        to=Query,
485
        on_delete=models.CASCADE,
486
        related_name='features',
487
        verbose_name=_('Query'))
488
    lat = models.FloatField()
489
    lon = models.FloatField()
490
    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

  
......
253 257
        wfs_service_url='http://example.net/wfs'))
254 258

  
255 259

  
260
@pytest.fixture
261
def query(connector):
262
    return Query.objects.create(
263
        resource=connector,
264
        name='Test Query',
265
        slug='test_query',
266
        description='Test query.',
267
        typename='pvo_patrimoine_voirie.pvoparking',
268
        filter_expression=('<Filter><PropertyIsEqualTo><PropertyName>typeparking'
269
                           '</PropertyName></PropertyIsEqualTo></Filter>')
270
    )
271

  
272

  
256 273
def geoserver_responses(url, **kwargs):
257 274
    if kwargs['params'].get('request') == 'GetCapabilities':
258 275
        assert kwargs['params'].get('service')
......
277 294
    return utils.FakedResponse(status_code=200, content=FAKE_ERROR[:10])
278 295

  
279 296

  
297
def geoserver_geolocated_responses(url, **kwargs):
298
    if kwargs['params'].get('request') == 'GetCapabilities':
299
        return utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES)
300
    return utils.FakedResponse(status_code=200, content=FAKE_GEOLOCATED_FEATURE)
301

  
302

  
280 303
@mock.patch('passerelle.utils.Request.get')
281 304
def test_feature_info(mocked_get, app, connector):
282 305
    endpoint = utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
......
499 522
                   })
500 523
    assert resp.json['err'] == 1
501 524
    assert resp.json['err_desc'] == 'Webservice returned status code 404'
525

  
526

  
527
@mock.patch('passerelle.utils.Request.get')
528
def test_opengis_query_cache_update(mocked_get, app, connector, query):
529
    mocked_get.side_effect = geoserver_geolocated_responses
530
    query.update_cache()
531

  
532
    assert mocked_get.call_args[1]['params']['filter'] == query.filter_expression
533
    assert mocked_get.call_args[1]['params']['typenames'] == query.typename
534
    assert FeatureCache.objects.count() == 4
535

  
536
    feature = FeatureCache.objects.get(lon=1914059.51, lat=4224699.2)
537
    assert feature.data['properties']['code_post'] == 38000
538

  
539

  
540
@mock.patch('passerelle.utils.Request.get')
541
def test_opengis_query_q_endpoint(mocked_get, app, connector, query):
542
    endpoint = utils.generic_endpoint_url('opengis', 'query/test_query/', slug=connector.slug)
543
    assert endpoint == '/opengis/test/query/test_query/'
544
    mocked_get.side_effect = geoserver_geolocated_responses
545
    query.update_cache()
546
    feature = FeatureCache.objects.get(lon=1914059.51, lat=4224699.2)
547
    resp = app.get(endpoint)
548

  
549
    assert len(resp.json['features']) == FeatureCache.objects.count()
550

  
551
    feature_data = next(feature for feature in resp.json['features']
552
                        if feature['geometry']['coordinates'][0] == 1914059.51)
553
    assert feature_data == feature.data
554

  
555

  
556
@mock.patch('passerelle.utils.Request.get')
557
def test_opengis_query_cache_update_change(mocked_get, app, connector, query):
558
    mocked_get.side_effect = geoserver_geolocated_responses
559
    query.update_cache()
560
    assert FeatureCache.objects.count() == 4
561

  
562
    def new_response(url, **kwargs):
563
        if kwargs['params'].get('request') == 'GetCapabilities':
564
            return utils.FakedResponse(status_code=200, content=FAKE_SERVICE_CAPABILITIES)
565
        return utils.FakedResponse(
566
            content='{"features": [{"properties": {}, "geometry": {"coordinates": [1, 1]}}]}',
567
            status_code=200
568
        )
569
    mocked_get.side_effect = new_response
570
    query.update_cache()
571
    assert FeatureCache.objects.count() == 1
572

  
573

  
574
@mock.patch('passerelle.utils.Request.get')
575
def test_opengis_query_q_endpoint_cache_empty(mocked_get, app, connector, query):
576
    endpoint = utils.generic_endpoint_url('opengis', 'query/test_query/', slug=connector.slug)
577
    assert not FeatureCache.objects.exists()
578
    resp = app.get(endpoint)
579

  
580
    assert resp.json['err'] == 1
581
    assert 'not synchronized' in resp.json['err_desc']
582
    assert not FeatureCache.objects.exists()
583
    assert mocked_get.call_count == 0
584

  
585

  
586
@mock.patch('passerelle.utils.Request.get')
587
def test_opengis_query_cache_update_jobs(mocked_get, app, connector, query):
588
    mocked_get.side_effect = geoserver_geolocated_responses
589

  
590
    # fixtures created one query
591
    job = Job.objects.get(method_name='update_queries')
592
    assert not FeatureCache.objects.exists()
593

  
594
    connector.jobs()
595
    assert FeatureCache.objects.count() == 4
596
    job.refresh_from_db()
597
    assert job.status == 'completed'
598

  
599
    # modifying a query triggers an update
600
    query.save()
601
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 1
602
    connector.jobs()
603

  
604
    # modifying the connector triggers an update
605
    connector.save()
606
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 1
607
    connector.jobs()
608

  
609
    # two queries to update
610
    query.save()
611
    query.pk = None
612
    query.slug = query.name = 'test2'
613
    query.save()
614
    with mock.patch.object(Query, 'update_cache') as mocked:
615
        connector.jobs()
616
        assert mocked.call_count == 2
617

  
618
    # now only one
619
    query.save()
620
    with mock.patch.object(Query, 'update_cache') as mocked:
621
        connector.jobs()
622
        assert mocked.call_count == 1
623

  
624

  
625
@mock.patch('passerelle.utils.Request.get')
626
def test_opengis_query_cache_update_daily(mocked_get, app, connector, query):
627
    mocked_get.side_effect = geoserver_geolocated_responses
628
    assert not FeatureCache.objects.exists()
629
    call_command('cron', 'daily')
630
    assert FeatureCache.objects.count() == 4
631

  
632

  
633
@mock.patch('passerelle.utils.Request.get')
634
def test_opengis_query_endpoint_documentation(mocked_get, app, connector, query):
635
    resp = app.get(connector.get_absolute_url())
636
    assert query.name in resp.text
637
    assert query.description in resp.text
638
    assert '/opengis/test/q/test_query/' in resp.text
639

  
640

  
641
def test_opengis_export_import(query):
642
    assert OpenGIS.objects.count() == 1
643
    assert Query.objects.count() == 1
644
    serialization = {'resources': [query.resource.export_json()]}
645
    OpenGIS.objects.all().delete()
646
    assert OpenGIS.objects.count() == 0
647
    assert Query.objects.count() == 0
648
    import_site(serialization)
649
    assert OpenGIS.objects.count() == 1
650
    assert Query.objects.count() == 1
502
-