Projet

Général

Profil

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

Lauréline Guérin, 04 octobre 2021 14:39

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             | 21 ++++-
 .../opengis/templates/opengis/query_form.html | 29 ++++++
 passerelle/apps/opengis/views.py              | 51 +++++++++-
 passerelle/static/js/passerelle.js            | 16 ++++
 tests/test_opengis.py                         | 92 +++++++++++++++++++
 7 files changed, 231 insertions(+), 8 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:
......
657 663
            text = ''
658 664
            if self.indexing_template:
659 665
                context = Context(feature.get('properties', {}))
660
                text = simplify(template.render(context))
666
                text = simplify(templates['text'].render(context))
667
            context = Context(feature)
668
            for key in self.computed_properties or {}:
669
                if not templates.get('computed_property_%s' % key):
670
                    continue
671
                if not feature.get('properties'):
672
                    feature['properties'] = {}
673
                feature['properties'][key] = templates['computed_property_%s' % key].render(context)
661 674
            features.append(
662 675
                FeatureCache(
663 676
                    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 = {
682
        'foobar': 'foo',
683
        'color': '{% if properties.numero %}green{% else %}red{% endif %}',
684
    }
685
    query.save()
686
    query.update_cache()
687

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

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

  
708

  
671 709
@mock.patch('passerelle.utils.Request.get')
672 710
def test_opengis_query_q_endpoint(mocked_get, app, connector, query):
673 711
    endpoint = utils.generic_endpoint_url('opengis', 'query/test_query/', slug=connector.slug)
......
911 949
    assert resp.status_code == 302
912 950

  
913 951

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

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

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

  
1005

  
914 1006
def test_opengis_export_import(query):
915 1007
    assert OpenGIS.objects.count() == 1
916 1008
    assert Query.objects.count() == 1
917
-