0003-base-add-generic-BaseQuery-20535.patch
passerelle/apps/arcgis/models.py | ||
---|---|---|
29 | 29 |
from passerelle.utils.jsonresponse import APIError |
30 | 30 |
from passerelle.utils.api import endpoint |
31 | 31 |
from passerelle.utils.templates import render_to_string, validate_template |
32 |
from passerelle.base.models import BaseResource, HTTPResource |
|
32 |
from passerelle.base.models import BaseResource, HTTPResource, BaseQuery
|
|
33 | 33 | |
34 | 34 | |
35 | 35 |
class ArcGISError(APIError): |
... | ... | |
195 | 195 |
description=_('Query'), |
196 | 196 |
pattern=r'^(?P<query_slug>[\w:_-]+)/$', |
197 | 197 |
perm='can_access', |
198 |
parameters={ |
|
199 |
'q': {'description': _('Search text in display field')}, |
|
200 |
'full': { |
|
201 |
'description': _('Returns all ArcGIS informations (geometry, metadata)'), |
|
202 |
'type': 'bool', |
|
203 |
}, |
|
204 |
}, |
|
205 | 198 |
show=False) |
206 | 199 |
def q(self, request, query_slug, q=None, full=False, **kwargs): |
207 | 200 |
query = get_object_or_404(Query, resource=self, slug=query_slug) |
... | ... | |
226 | 219 |
if instance and overwrite: |
227 | 220 |
Query.objects.filter(resource=instance).delete() |
228 | 221 |
for query in queries: |
222 |
query['resource'] = instance |
|
229 | 223 |
q = Query.import_json(query) |
230 |
q.resource = instance |
|
231 | 224 |
new.append(q) |
232 | 225 |
Query.objects.bulk_create(new) |
233 | 226 |
return instance |
... | ... | |
257 | 250 |
raise ValidationError(_('invalid reference')) |
258 | 251 | |
259 | 252 | |
260 |
@six.python_2_unicode_compatible |
|
261 |
class Query(models.Model): |
|
253 |
class Query(BaseQuery): |
|
262 | 254 |
resource = models.ForeignKey( |
263 | 255 |
to=ArcGIS, |
264 | 256 |
verbose_name=_('Resource')) |
265 | 257 | |
266 |
name = models.CharField( |
|
267 |
verbose_name=_('Name'), |
|
268 |
max_length=128) |
|
269 |
slug = models.SlugField( |
|
270 |
verbose_name=_('Slug'), |
|
271 |
max_length=128) |
|
272 |
description = models.TextField( |
|
273 |
verbose_name=_('Description'), |
|
274 |
blank=True) |
|
275 | ||
276 | 258 |
folder = models.CharField( |
277 | 259 |
verbose_name=_('ArcGis Folder'), |
278 | 260 |
max_length=64, |
... | ... | |
308 | 290 |
validators=[validate_template], |
309 | 291 |
blank=True) |
310 | 292 | |
311 |
class Meta: |
|
312 |
unique_together = [ |
|
313 |
('resource', 'name'), |
|
314 |
('resource', 'slug'), |
|
315 |
] |
|
316 |
ordering = ['name'] |
|
293 |
def __init__(self, *args, **kwargs): |
|
294 |
super(Query, self).__init__(*args, **kwargs) |
|
295 |
self.endpoint = self.resource.mapservice_query |
|
296 |
self.called_from = self.resource.q |
|
317 | 297 | |
318 | 298 |
@property |
319 | 299 |
def where_references(self): |
... | ... | |
339 | 319 |
format_kwargs = {key: request.GET.get(key, '') for key, klass in self.where_references} |
340 | 320 |
formatter = SqlFormatter() |
341 | 321 |
kwargs['where'] = formatter.format(self.where, **format_kwargs) |
342 |
return self.resource.mapservice_query(request, q=q, full=full, **kwargs) |
|
343 | ||
344 |
class QueryEndpoint: |
|
345 |
http_method = 'get' |
|
346 | ||
347 |
def __init__(self, query): |
|
348 |
self.object = query.resource |
|
349 |
self.query = query |
|
350 |
self.name = 'q/%s/' % query.slug |
|
351 | ||
352 |
@property |
|
353 |
def description(self): |
|
354 |
return self.query.name |
|
355 | ||
356 |
@property |
|
357 |
def long_description(self): |
|
358 |
return self.query.description |
|
359 | ||
360 |
def example_url(self): |
|
361 |
kwargs = { |
|
362 |
'connector': self.object.get_connector_slug(), |
|
363 |
'slug': self.object.slug, |
|
364 |
'endpoint': self.name, |
|
365 |
} |
|
366 |
query_string = '' |
|
367 |
if self.query.where_references: |
|
368 |
query_string = '?' + '&'.join( |
|
369 |
['%s=%s' % (ref, '') for ref, _ in self.query.where_references + [('q', ''), ('full', '')]]) |
|
370 |
return reverse('generic-endpoint', kwargs=kwargs) + query_string |
|
371 | ||
372 |
def example_url_as_html(self): |
|
373 |
kwargs = { |
|
374 |
'connector': self.object.get_connector_slug(), |
|
375 |
'slug': self.object.slug, |
|
376 |
'endpoint': self.name, |
|
377 |
} |
|
378 |
query_string = '' |
|
379 |
if self.query.where_references: |
|
380 |
query_string = '?' + '&'.join( |
|
381 |
[format_html('{0}=<i class="varname">{1}</i>', ref, ref) |
|
382 |
for ref, klass in self.query.where_references + [('q', ''), ('full', '')]]) |
|
383 |
return mark_safe(reverse('generic-endpoint', kwargs=kwargs) + query_string) |
|
384 | ||
385 |
def get_params(self): |
|
386 |
params = [] |
|
387 |
for ref, klass in self.query.where_references: |
|
388 |
params.append({ |
|
389 |
'name': ref, |
|
390 |
'type': 'integer' if klass is int else 'string', |
|
391 |
}) |
|
392 | ||
393 |
# Copy generic params descriptions from mapservice_query if they |
|
394 |
# are not overloaded by the query |
|
395 |
for param in self.object.mapservice_query.endpoint_info.get_params(): |
|
396 |
if (param['name'] in ('folder', 'service', 'layer', 'id_template') |
|
397 |
and getattr(self.query, param['name'])): |
|
398 |
continue |
|
399 |
if param['name'] == 'template' and self.query.text_template: |
|
400 |
continue |
|
401 |
params.append(param) |
|
402 | ||
403 |
return params |
|
322 |
return self.endpoint(request, q=q, full=full, **kwargs) |
|
404 | 323 | |
405 |
@property |
|
406 |
def endpoint(self): |
|
407 |
return self.QueryEndpoint(self) |
|
324 |
def as_endpoint(self): |
|
325 |
endpoint = super(Query, self).as_endpoint() |
|
326 |
endpoint.exclude_params = [param for param in ('folder', 'service', 'layer', 'id_template') |
|
327 |
if getattr(self, param)] |
|
328 |
endpoint.exclude_params.extend(['lat', 'lon', 'latmin', 'lonmin', 'latmax', 'lonmax']) |
|
329 |
if self.text_template: |
|
330 |
endpoint.exclude_params.append('template') |
|
408 | 331 | |
409 |
def __str__(self):
|
|
410 |
return self.name
|
|
332 |
if not self.where_references:
|
|
333 |
return endpoint
|
|
411 | 334 | |
412 |
def export_json(self): |
|
413 |
d = {} |
|
414 |
fields = [ |
|
415 |
f for f in self.__class__._meta.get_fields() |
|
416 |
if f.concrete and ( |
|
417 |
not f.is_relation |
|
418 |
or f.one_to_one |
|
419 |
or (f.many_to_one and f.related_model) |
|
420 |
) and f.name not in ['id', 'resource'] |
|
421 |
] |
|
422 |
for field in fields: |
|
423 |
d[field.name] = getattr(self, field.name) |
|
424 |
return d |
|
335 |
for ref, klass in self.where_references: |
|
336 |
endpoint.extra_params.append({ |
|
337 |
'name': ref, |
|
338 |
'type': 'integer' if klass is int else 'string', |
|
339 |
}) |
|
425 | 340 | |
426 |
@classmethod |
|
427 |
def import_json(cls, d): |
|
428 |
return cls(**d) |
|
341 |
params = [ref for ref, _ in self.where_references] |
|
342 |
params = {param: {'example_value': ''} for param in params} |
|
343 |
endpoint.parameters.update(params) |
|
344 |
return endpoint |
|
429 | 345 | |
430 | 346 |
def delete_url(self): |
431 | 347 |
return reverse('arcgis-query-delete', |
passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
|
3 | ||
4 |
{% block endpoints %} |
|
5 |
{{ block.super }} |
|
6 | ||
7 | ||
8 |
{% if object.query_set.exists %} |
|
9 |
<h2>{% trans "Custom queries" %}</h2> |
|
10 |
<ul class="endpoints"> |
|
11 |
{% for query in object.query_set.all %} |
|
12 |
{% include "passerelle/manage/endpoint.html" with endpoint=query.endpoint %} |
|
13 |
{% endfor %} |
|
14 |
</ul> |
|
15 |
{% endif %} |
|
16 |
{% endblock %} |
passerelle/base/models.py | ||
---|---|---|
256 | 256 |
endpoint_info.http_method = http_method |
257 | 257 |
endpoints.append(endpoint_info) |
258 | 258 |
endpoints.sort(key=lambda x: (x.name or '', x.pattern or '')) |
259 |
if hasattr(self, 'query_set'): |
|
260 |
self.append_custom_queries(endpoints) |
|
259 | 261 |
return endpoints |
260 | 262 | |
263 |
def append_custom_queries(self, endpoints): |
|
264 |
for query in self.query_set.all(): |
|
265 |
if hasattr(query, 'as_endpoint'): |
|
266 |
endpoints.append(query.as_endpoint()) |
|
267 | ||
261 | 268 |
def get_connector_permissions(self): |
262 | 269 |
perms = {} |
263 | 270 |
for endpoint_info in self.get_endpoints_infos(): |
... | ... | |
955 | 962 | |
956 | 963 |
class Meta: |
957 | 964 |
abstract = True |
965 | ||
966 | ||
967 |
@six.python_2_unicode_compatible |
|
968 |
class BaseQuery(models.Model): |
|
969 |
'''Base for building custom queries. |
|
970 | ||
971 |
A query stores parameters, which are then used to call an endpoint. |
|
972 |
It must define "resource" attribute as a ForeignKey to a BaseResource subclass, |
|
973 |
as well as "endpoint" which should be the endpoint to call, and "called_from" |
|
974 |
which should be the endpoint that triggers the query. |
|
975 |
''' |
|
976 | ||
977 |
name = models.CharField( |
|
978 |
verbose_name=_('Name'), |
|
979 |
max_length=128) |
|
980 |
slug = models.SlugField( |
|
981 |
verbose_name=_('Slug'), |
|
982 |
max_length=128) |
|
983 |
description = models.TextField( |
|
984 |
verbose_name=_('Description'), |
|
985 |
blank=True) |
|
986 | ||
987 |
http_method = 'get' |
|
988 | ||
989 |
class Meta: |
|
990 |
abstract = True |
|
991 |
unique_together = [ |
|
992 |
('resource', 'name'), |
|
993 |
('resource', 'slug'), |
|
994 |
] |
|
995 |
ordering = ['name'] |
|
996 | ||
997 |
def as_endpoint(self): |
|
998 |
# deepcopying is too dangerous, instead copy then deepcopy only relevant attributes |
|
999 |
endpoint = copy.copy(self.endpoint.endpoint_info) |
|
1000 |
attr_to_change = ['descriptions', 'long_descriptions', 'parameters', 'extra_params'] |
|
1001 |
for attr in attr_to_change: |
|
1002 |
setattr(endpoint, attr, copy.deepcopy(getattr(endpoint, attr))) |
|
1003 |
endpoint.name = '%s/%s/' % (self.called_from.endpoint_info.name, self.slug) |
|
1004 |
endpoint.http_method = self.http_method |
|
1005 |
endpoint.descriptions[self.http_method] = self.name |
|
1006 |
endpoint.long_descriptions[self.http_method] = self.description |
|
1007 |
new_params = self.called_from.endpoint_info.parameters |
|
1008 |
endpoint.parameters.update(new_params) |
|
1009 |
for param, info in new_params.items(): |
|
1010 |
d = {'name': param} |
|
1011 |
d.update(info) |
|
1012 |
endpoint.extra_params.append(d) |
|
1013 |
return endpoint |
|
1014 | ||
1015 |
def __str__(self): |
|
1016 |
return self.name |
|
1017 | ||
1018 |
def export_json(self): |
|
1019 |
d = {} |
|
1020 |
fields = [ |
|
1021 |
f for f in self.__class__._meta.get_fields() |
|
1022 |
if f.concrete and ( |
|
1023 |
not f.is_relation |
|
1024 |
or f.one_to_one |
|
1025 |
or (f.many_to_one and f.related_model) |
|
1026 |
) and f.name not in ['id', 'resource'] |
|
1027 |
] |
|
1028 |
for field in fields: |
|
1029 |
d[field.name] = getattr(self, field.name) |
|
1030 |
return d |
|
1031 | ||
1032 |
@classmethod |
|
1033 |
def import_json(cls, d): |
|
1034 |
return cls(**d) |
|
1035 | ||
1036 |
def delete_url(self): |
|
1037 |
return reverse(self.delete_view, |
|
1038 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
|
1039 | ||
1040 |
def edit_url(self): |
|
1041 |
return reverse(self.edit_view, |
|
1042 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
passerelle/utils/api.py | ||
---|---|---|
43 | 43 |
parameters=None, |
44 | 44 |
cache_duration=None, |
45 | 45 |
post=None, |
46 |
show=True): |
|
46 |
show=True, |
|
47 |
extra_params=None, |
|
48 |
exclude_params=None): |
|
47 | 49 |
self.perm = perm |
48 | 50 |
self.methods = methods |
49 | 51 |
self.serializer_type = serializer_type |
... | ... | |
71 | 73 |
if post.get('long_description'): |
72 | 74 |
self.long_descriptions['post'] = post.get('long_description') |
73 | 75 |
self.show = show |
76 |
self.extra_params = extra_params or [] |
|
77 |
self.exclude_params = exclude_params or set() |
|
74 | 78 | |
75 | 79 |
def __call__(self, func): |
76 | 80 |
func.endpoint_info = self |
... | ... | |
81 | 85 | |
82 | 86 |
def get_example_params(self): |
83 | 87 |
return {param: info['example_value'] for param, info in self.parameters.items() |
84 |
if 'example_value' in info} |
|
88 |
if not param in self.exclude_params and 'example_value' in info}
|
|
85 | 89 | |
86 | 90 |
def get_query_parameters(self): |
87 | 91 |
query_parameters = [] |
... | ... | |
190 | 194 |
typ = type_to_str(defaults[param]) |
191 | 195 |
if typ: |
192 | 196 |
param_info['type'] = typ |
193 |
params.append(param_info) |
|
197 |
if param not in self.exclude_params: |
|
198 |
params.append(param_info) |
|
199 |
params.extend(self.extra_params) |
|
194 | 200 |
return params |
tests/test_arcgis.py | ||
---|---|---|
281 | 281 |
resource=arcgis, |
282 | 282 |
name='Adresses', |
283 | 283 |
slug='adresses', |
284 |
description='Recherche d\'une adresse',
|
|
284 |
description='Rechercher une adresse',
|
|
285 | 285 |
id_template='{{ attributes.ident }}', |
286 | 286 |
text_template='{{ attributes.address }} - {{ attributes.codepost }}', |
287 | 287 |
folder='fold', |
... | ... | |
338 | 338 |
} |
339 | 339 | |
340 | 340 | |
341 |
def test_query_documentation(arcgis, query, app): |
|
342 |
resp = app.get(arcgis.get_absolute_url()) |
|
343 |
assert query.name in resp.text |
|
344 |
assert query.description in resp.text |
|
345 |
# additional parameter appears in endpoint documentation |
|
346 |
assert '<span class="param-name">adress</span>' in resp.text |
|
347 | ||
348 | ||
341 | 349 |
def test_export_import(query): |
342 | 350 |
assert ArcGIS.objects.count() == 1 |
343 | 351 |
assert Query.objects.count() == 1 |
344 |
- |