0001-opengis-add-custom-computed-properties-on-queries-57.patch
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 |
- |