Projet

Général

Profil

0001-opengis-add-custom-computed-properties-on-queries-57.patch

Lauréline Guérin, 28 septembre 2021 16:17

Télécharger (14,6 ko)

Voir les différences:

Subject: [PATCH] opengis: add custom computed properties on queries (#57295)

 passerelle/apps/opengis/forms.py              | 13 ++-
 .../migrations/0015_computed_properties.py    | 17 ++++
 passerelle/apps/opengis/models.py             | 22 +++--
 .../opengis/templates/opengis/query_form.html | 29 +++++++
 passerelle/apps/opengis/views.py              | 51 ++++++++++-
 passerelle/static/js/passerelle.js            | 16 ++++
 tests/test_opengis.py                         | 86 +++++++++++++++++++
 7 files changed, 225 insertions(+), 9 deletions(-)
 create mode 100644 passerelle/apps/opengis/migrations/0015_computed_properties.py
passerelle/apps/opengis/forms.py
17 17
from xml.etree import ElementTree as ET
18 18

  
19 19
from django import forms
20
from django.forms import formset_factory
20 21
from django.utils.translation import ugettext_lazy as _
21 22

  
22 23
from passerelle.base.forms import BaseQueryFormMixin
......
24 25
from . import models
25 26

  
26 27

  
28
class ComputedPropertyForm(forms.Form):
29
    key = forms.CharField(label=_('Property name'), required=False)
30
    value = forms.CharField(
31
        label=_('Value template'), widget=forms.TextInput(attrs={'size': 60}), required=False
32
    )
33

  
34

  
35
ComputedPropertyFormSet = formset_factory(ComputedPropertyForm)
36

  
37

  
27 38
class QueryForm(BaseQueryFormMixin, forms.ModelForm):
28 39
    class Meta:
29 40
        model = models.Query
30 41
        fields = '__all__'
31
        exclude = ['resource']
42
        exclude = ['resource', 'computed_properties']
32 43

  
33 44
    def clean_filter_expression(self):
34 45
        filter_expression = self.cleaned_data['filter_expression']
passerelle/apps/opengis/migrations/0015_computed_properties.py
1
import django.contrib.postgres.fields.jsonb
2
from django.db import migrations
3

  
4

  
5
class Migration(migrations.Migration):
6

  
7
    dependencies = [
8
        ('opengis', '0014_auto_20210927_1006'),
9
    ]
10

  
11
    operations = [
12
        migrations.AddField(
13
            model_name='query',
14
            name='computed_properties',
15
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
16
        ),
17
    ]
passerelle/apps/opengis/models.py
26 26
from django.db.models import Q
27 27
from django.http import HttpResponse
28 28
from django.shortcuts import get_object_or_404
29
from django.template import Context, Template
29
from django.template import Context, Template, TemplateSyntaxError
30 30
from django.urls import reverse
31
from django.utils import timezone
32 31
from django.utils.text import slugify
33 32
from django.utils.translation import ugettext_lazy as _
34 33

  
......
492 491
        help_text=_('Template used to generate contents used in fulltext search.'),
493 492
        blank=True,
494 493
    )
494
    computed_properties = JSONField(blank=True, default=dict)
495 495

  
496 496
    delete_view = 'opengis-query-delete'
497 497
    edit_view = 'opengis-query-edit'
......
614 614
    def update_cache(self):
615 615
        data = self.resource.features(None, None, type_names=self.typename, xml_filter=self.filter_expression)
616 616
        features = []
617
        templates = {}
617 618
        if self.indexing_template:
618
            template = Template(self.indexing_template)
619
            templates['text'] = Template(self.indexing_template)
620
        for key, tplt in (self.computed_properties or {}).items():
621
            try:
622
                templates['computed_property_%s' % key] = Template(tplt)
623
            except TemplateSyntaxError:
624
                pass
619 625
        for feature in data['data']:
620 626
            geometry = feature.get('geometry') or {}
621 627
            if not geometry:
......
655 661
                lat, lon, lat2, lon2 = min_lat, min_lon, max_lat, max_lon
656 662

  
657 663
            text = ''
664
            context = Context(feature.get('properties', {}))
658 665
            if self.indexing_template:
659
                context = Context(feature.get('properties', {}))
660
                text = simplify(template.render(context))
666
                text = simplify(templates['text'].render(context))
667
            for key in self.computed_properties or {}:
668
                if not templates.get('computed_property_%s' % key):
669
                    continue
670
                if not feature.get('properties'):
671
                    feature['properties'] = {}
672
                feature['properties'][key] = templates['computed_property_%s' % key].render(context)
661 673
            features.append(
662 674
                FeatureCache(
663 675
                    query=self,
passerelle/apps/opengis/templates/opengis/query_form.html
1 1
{% extends "passerelle/manage/resource_child_form.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block form %}
5
    {{ block.super }}
6
    <h3>{% trans "Computed properties" %}</h3>
7
    {{ formset.management_form }}
8
    <table id="computed-property-forms">
9
        <thead>
10
            <tr>
11
                {% for field in formset.0 %}
12
                <th class="column-{{ field.name }}{% if field.required %} required{% endif %}">{{ field.label }}</th>
13
                {% endfor %}
14
            </tr>
15
        </thead>
16
        <tbody>
17
        {% for sub_form in formset %}
18
        <tr class='computed-property-form'>
19
            {% for field in sub_form %}
20
            <td class="field-{{ field.name }}">
21
                {{ field.errors.as_ul }}
22
                {{ field }}
23
            </td>
24
            {% endfor %}
25
        </tr>
26
        {% endfor %}
27
        </tbody>
28
    </table>
29
    <button id="add-computed-property-form" type="button">{% trans "Add another computed property" %}</button>
30
{% endblock %}
passerelle/apps/opengis/views.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
from django.http import HttpResponseRedirect
17 18
from django.views.generic import CreateView, DeleteView, UpdateView
18 19

  
19 20
from passerelle.base.mixins import ResourceChildViewMixin
20 21

  
21 22
from . import models
22
from .forms import QueryForm
23
from .forms import ComputedPropertyFormSet, QueryForm
23 24

  
24 25

  
25
class QueryNew(ResourceChildViewMixin, CreateView):
26
class QueryMixin:
27
    def form_valid(self, form):
28
        self.object = form.save(commit=False)  # save object and update cache only once
29
        return HttpResponseRedirect(self.get_success_url())
30

  
31
    def post(self, *args, **kwargs):
32
        self.object = self.get_object()
33
        form = self.get_form()
34
        formset = ComputedPropertyFormSet(data=self.request.POST)
35
        if form.is_valid() and formset.is_valid():
36
            response = self.form_valid(form)
37
            self.object.computed_properties = {}
38
            for sub_data in formset.cleaned_data:
39
                if not sub_data.get('key'):
40
                    continue
41
                self.object.computed_properties[sub_data['key']] = sub_data['value']
42
            self.object.save()
43
            return response
44
        else:
45
            return self.form_invalid(form)
46

  
47

  
48
class QueryNew(QueryMixin, ResourceChildViewMixin, CreateView):
26 49
    model = models.Query
27 50
    form_class = QueryForm
28 51

  
52
    def get_object(self):
53
        return None
54

  
55
    def get_context_data(self, **kwargs):
56
        context = super().get_context_data(**kwargs)
57
        data = None
58
        if self.request.method == 'POST':
59
            data = self.request.POST
60
        context['formset'] = ComputedPropertyFormSet(data=data)
61
        return context
62

  
29 63
    def get_form_kwargs(self):
30 64
        kwargs = super(QueryNew, self).get_form_kwargs()
31 65
        kwargs['instance'] = self.model(resource=self.resource)
......
35 69
        return self.object.get_absolute_url()
36 70

  
37 71

  
38
class QueryEdit(ResourceChildViewMixin, UpdateView):
72
class QueryEdit(QueryMixin, ResourceChildViewMixin, UpdateView):
39 73
    model = models.Query
40 74
    form_class = QueryForm
41 75

  
76
    def get_context_data(self, **kwargs):
77
        context = super().get_context_data(**kwargs)
78
        data = None
79
        if self.request.method == 'POST':
80
            data = self.request.POST
81
        context['formset'] = ComputedPropertyFormSet(
82
            data=data,
83
            initial=[{'key': k, 'value': v} for k, v in self.get_object().computed_properties.items()],
84
        )
85
        return context
86

  
42 87

  
43 88
class QueryDelete(ResourceChildViewMixin, DeleteView):
44 89
    model = models.Query
passerelle/static/js/passerelle.js
48 48
          var $slug_field = $(this).parents('form').find('[name=' + $(this).data('slug-sync') + ']');
49 49
          $slug_field.val($.slugify($(this).val()));
50 50
  });
51

  
52
  if ($('#add-computed-property-form').length) {
53
    var property_forms = $('.computed-property-form');
54
    var total_form = $('#id_form-TOTAL_FORMS');
55
    var form_num = property_forms.length - 1;
56
    $('#add-computed-property-form').on('click', function() {
57
      var new_form = $(property_forms[0]).clone();
58
      var form_regex = RegExp(`form-(\\d){1}-`,'g');
59
      form_num++;
60
      new_form.html(new_form.html().replace(form_regex, `form-${form_num}-`));
61
      new_form.appendTo('#computed-property-forms tbody');
62
      $('#id_form-' + form_num + '-key').val('');
63
      $('#id_form-' + form_num + '-value').val('');
64
      total_form.val(form_num + 1);
65
    })
66
  }
51 67
});
tests/test_opengis.py
668 668
    assert feature.data['properties']['code_post'] == 38000
669 669

  
670 670

  
671
@mock.patch('passerelle.utils.Request.get')
672
def test_opengis_query_computed_properties(mocked_get, app, connector, query):
673
    mocked_get.side_effect = geoserver_geolocated_responses
674
    query.update_cache()
675

  
676
    assert FeatureCache.objects.count() == 6
677
    for feature in FeatureCache.objects.all():
678
        assert 'foobar' not in feature.data['properties']
679
        assert 'color' not in feature.data['properties']
680

  
681
    query.computed_properties = {'foobar': 'foo', 'color': '{% if numero %}green{% else %}red{% endif %}'}
682
    query.save()
683
    query.update_cache()
684

  
685
    assert FeatureCache.objects.count() == 6
686
    for feature in FeatureCache.objects.all():
687
        assert feature.data['properties']['foobar'] == 'foo'
688
        assert (
689
            feature.data['properties']['color'] == 'green'
690
            if feature.data['properties'].get('numero')
691
            else 'red'
692
        )
693

  
694
    query.computed_properties = {'foobar': '', 'color': '{% if numero %}green{% else %}red{% endif }'}
695
    query.save()
696
    query.update_cache()
697
    assert FeatureCache.objects.count() == 6
698
    for feature in FeatureCache.objects.all():
699
        assert feature.data['properties']['foobar'] == ''
700
        assert 'color' not in feature.data['properties']  # syntax error
701

  
702

  
671 703
@mock.patch('passerelle.utils.Request.get')
672 704
def test_opengis_query_q_endpoint(mocked_get, app, connector, query):
673 705
    endpoint = utils.generic_endpoint_url('opengis', 'query/test_query/', slug=connector.slug)
......
911 943
    assert resp.status_code == 302
912 944

  
913 945

  
946
def test_opengis_query_computed_properties_form(admin_user, app, connector):
947
    app = login(app)
948
    resp = app.get('/manage/opengis/%s/query/new/' % connector.slug)
949
    resp.form['slug'] = 'foo-bar'
950
    resp.form['name'] = 'Foo Bar'
951
    resp.form['typename'] = 'foo'
952
    resp.form['form-0-key'] = 'foo'
953
    resp.form['form-0-value'] = 'bar'
954
    resp = resp.form.submit()
955
    assert resp.status_code == 302
956
    assert Query.objects.filter(resource=connector).count() == 1
957
    query = Query.objects.latest('pk')
958
    assert query.computed_properties == {
959
        'foo': 'bar',
960
    }
961
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 1
962

  
963
    resp = app.get('/manage/opengis/%s/query/%s/' % (connector.slug, query.pk))
964
    assert resp.form['form-TOTAL_FORMS'].value == '2'
965
    assert resp.form['form-0-key'].value == 'foo'
966
    assert resp.form['form-0-value'].value == 'bar'
967
    assert resp.form['form-1-key'].value == ''
968
    assert resp.form['form-1-value'].value == ''
969
    resp.form['form-0-value'] = 'bar-bis'
970
    resp.form['form-1-key'] = 'blah'
971
    resp.form['form-1-value'] = 'baz'
972
    resp = resp.form.submit()
973
    assert resp.status_code == 302
974
    query.refresh_from_db()
975
    assert query.computed_properties == {
976
        'foo': 'bar-bis',
977
        'blah': 'baz',
978
    }
979
    assert Job.objects.filter(method_name='update_queries', status='registered').count() == 2
980

  
981
    resp = app.get('/manage/opengis/%s/query/%s/' % (connector.slug, query.pk))
982
    assert resp.form['form-TOTAL_FORMS'].value == '3'
983
    assert resp.form['form-0-key'].value == 'foo'
984
    assert resp.form['form-0-value'].value == 'bar-bis'
985
    assert resp.form['form-1-key'].value == 'blah'
986
    assert resp.form['form-1-value'].value == 'baz'
987
    assert resp.form['form-2-key'].value == ''
988
    assert resp.form['form-2-value'].value == ''
989
    resp.form['form-0-key'] = 'foo'
990
    resp.form['form-0-value'] = 'bar'
991
    resp.form['form-1-key'] = ''
992
    resp = resp.form.submit()
993
    assert resp.status_code == 302
994
    query.refresh_from_db()
995
    assert query.computed_properties == {
996
        'foo': 'bar',
997
    }
998

  
999

  
914 1000
def test_opengis_export_import(query):
915 1001
    assert OpenGIS.objects.count() == 1
916 1002
    assert Query.objects.count() == 1
917
-