0002-base-add-generic-BaseQuery-20535.patch
passerelle/apps/arcgis/models.py | ||
---|---|---|
31 | 31 |
from passerelle.utils.api import endpoint |
32 | 32 |
from passerelle.utils.conversion import num2deg |
33 | 33 |
from passerelle.utils.templates import render_to_string, validate_template |
34 |
from passerelle.base.models import BaseResource, HTTPResource |
|
34 |
from passerelle.base.models import BaseResource, HTTPResource, BaseQuery
|
|
35 | 35 | |
36 | 36 | |
37 | 37 |
class ArcGISError(APIError): |
... | ... | |
224 | 224 |
description=_('Query'), |
225 | 225 |
pattern=r'^(?P<query_slug>[\w:_-]+)/$', |
226 | 226 |
perm='can_access', |
227 |
parameters={ |
|
228 |
'q': {'description': _('Search text in display field')}, |
|
229 |
'full': { |
|
230 |
'description': _('Returns all ArcGIS informations (geometry, metadata)'), |
|
231 |
'type': 'bool', |
|
232 |
}, |
|
233 |
}, |
|
234 | 227 |
show=False) |
235 | 228 |
def q(self, request, query_slug, q=None, full=False, **kwargs): |
236 | 229 |
query = get_object_or_404(Query, resource=self, slug=query_slug) |
... | ... | |
240 | 233 |
for key in request.GET: |
241 | 234 |
if key not in refs: |
242 | 235 |
kwargs[key] = request.GET[key] |
243 |
return query.q(request, q=None, full=full, **kwargs) |
|
236 |
return query.query(request, q=None, full=full, **kwargs)
|
|
244 | 237 | |
245 | 238 |
def export_json(self): |
246 | 239 |
d = super(ArcGIS, self).export_json() |
... | ... | |
286 | 279 |
raise ValidationError(_('invalid reference')) |
287 | 280 | |
288 | 281 | |
289 |
@six.python_2_unicode_compatible |
|
290 |
class Query(models.Model): |
|
282 |
class Query(BaseQuery): |
|
291 | 283 |
resource = models.ForeignKey( |
292 | 284 |
to=ArcGIS, |
293 | 285 |
related_name='queries', |
294 | 286 |
verbose_name=_('Resource')) |
295 | 287 | |
296 |
name = models.CharField( |
|
297 |
verbose_name=_('Name'), |
|
298 |
max_length=128) |
|
299 |
slug = models.SlugField( |
|
300 |
verbose_name=_('Slug'), |
|
301 |
max_length=128) |
|
302 |
description = models.TextField( |
|
303 |
verbose_name=_('Description'), |
|
304 |
blank=True) |
|
305 | ||
306 | 288 |
folder = models.CharField( |
307 | 289 |
verbose_name=_('ArcGis Folder'), |
308 | 290 |
max_length=64, |
... | ... | |
338 | 320 |
validators=[validate_template], |
339 | 321 |
blank=True) |
340 | 322 | |
341 |
class Meta: |
|
342 |
unique_together = [ |
|
343 |
('resource', 'name'), |
|
344 |
('resource', 'slug'), |
|
345 |
] |
|
346 |
ordering = ['name'] |
|
323 |
delete_view = 'arcgis-query-delete' |
|
324 |
edit_view = 'arcgis-query-edit' |
|
347 | 325 | |
348 | 326 |
@property |
349 | 327 |
def where_references(self): |
... | ... | |
353 | 331 |
else: |
354 | 332 |
return [] |
355 | 333 | |
356 |
def q(self, request, q=None, full=False, **kwargs): |
|
334 |
def query(self, request, q=None, full=False, **kwargs):
|
|
357 | 335 |
kwargs.update({ |
358 | 336 |
'service': self.service, |
359 | 337 |
}) |
... | ... | |
371 | 349 |
kwargs['where'] = formatter.format(self.where, **format_kwargs) |
372 | 350 |
return self.resource.mapservice_query(request, q=q, full=full, **kwargs) |
373 | 351 | |
374 |
class QueryEndpoint: |
|
375 |
http_method = 'get' |
|
376 | ||
377 |
def __init__(self, query): |
|
378 |
self.object = query.resource |
|
379 |
self.query = query |
|
380 |
self.name = 'q/%s/' % query.slug |
|
381 | ||
382 |
@property |
|
383 |
def description(self): |
|
384 |
return self.query.name |
|
385 | ||
386 |
@property |
|
387 |
def long_description(self): |
|
388 |
return self.query.description |
|
389 | ||
390 |
def example_url(self): |
|
391 |
kwargs = { |
|
392 |
'connector': self.object.get_connector_slug(), |
|
393 |
'slug': self.object.slug, |
|
394 |
'endpoint': self.name, |
|
395 |
} |
|
396 |
query_string = '' |
|
397 |
if self.query.where_references: |
|
398 |
query_string = '?' + '&'.join( |
|
399 |
['%s=%s' % (ref, '') for ref, _ in self.query.where_references + [('q', ''), ('full', '')]]) |
|
400 |
return reverse('generic-endpoint', kwargs=kwargs) + query_string |
|
401 | ||
402 |
def example_url_as_html(self): |
|
403 |
kwargs = { |
|
404 |
'connector': self.object.get_connector_slug(), |
|
405 |
'slug': self.object.slug, |
|
406 |
'endpoint': self.name, |
|
407 |
} |
|
408 |
query_string = '' |
|
409 |
if self.query.where_references: |
|
410 |
query_string = '?' + '&'.join( |
|
411 |
[format_html('{0}=<i class="varname">{1}</i>', ref, ref) |
|
412 |
for ref, klass in self.query.where_references + [('q', ''), ('full', '')]]) |
|
413 |
return mark_safe(reverse('generic-endpoint', kwargs=kwargs) + query_string) |
|
414 | ||
415 |
def get_params(self): |
|
416 |
params = [] |
|
417 |
for ref, klass in self.query.where_references: |
|
418 |
params.append({ |
|
419 |
'name': ref, |
|
420 |
'type': 'integer' if klass is int else 'string', |
|
421 |
}) |
|
422 | ||
423 |
# Copy generic params descriptions from mapservice_query if they |
|
424 |
# are not overloaded by the query |
|
425 |
for param in self.object.mapservice_query.endpoint_info.get_params(): |
|
426 |
if (param['name'] in ('folder', 'service', 'layer', 'id_template') |
|
427 |
and getattr(self.query, param['name'])): |
|
428 |
continue |
|
429 |
if param['name'] == 'template' and self.query.text_template: |
|
430 |
continue |
|
431 |
params.append(param) |
|
432 | ||
433 |
return params |
|
434 | ||
435 |
@property |
|
436 |
def endpoint(self): |
|
437 |
return self.QueryEndpoint(self) |
|
438 | ||
439 |
def __str__(self): |
|
440 |
return self.name |
|
441 | ||
442 |
def export_json(self): |
|
443 |
d = {} |
|
444 |
fields = [ |
|
445 |
f for f in self.__class__._meta.get_fields() |
|
446 |
if f.concrete and ( |
|
447 |
not f.is_relation |
|
448 |
or f.one_to_one |
|
449 |
or (f.many_to_one and f.related_model) |
|
450 |
) and f.name not in ['id', 'resource'] |
|
451 |
] |
|
452 |
for field in fields: |
|
453 |
d[field.name] = getattr(self, field.name) |
|
454 |
return d |
|
455 | ||
456 |
@classmethod |
|
457 |
def import_json(cls, d): |
|
458 |
return cls(**d) |
|
459 | ||
460 |
def delete_url(self): |
|
461 |
return reverse('arcgis-query-delete', |
|
462 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
|
463 | ||
464 |
def edit_url(self): |
|
465 |
return reverse('arcgis-query-edit', |
|
466 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
|
352 |
def as_endpoint(self): |
|
353 |
endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name) |
|
354 | ||
355 |
# build parameters list from original mapservice endpoint |
|
356 |
mapservice_endpoint = self.resource.mapservice_query.endpoint_info |
|
357 |
endpoint.func = mapservice_endpoint.func |
|
358 |
endpoint.show_undocumented_params = False |
|
359 |
keep_params = ['folder', 'service', 'layer', 'id_template', 'q', 'full'] |
|
360 |
if not self.text_template: |
|
361 |
keep_params.append('template') |
|
362 |
endpoint.parameters = {name: mapservice_endpoint.parameters[name] |
|
363 |
for name in keep_params if not getattr(self, name, None)} |
|
364 | ||
365 |
if not self.where_references: |
|
366 |
return endpoint |
|
367 | ||
368 |
for ref, klass in self.where_references: |
|
369 |
endpoint.parameters[ref] = {'type': 'integer' if klass is int else 'string'} |
|
370 |
return endpoint |
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 | ||
---|---|---|
257 | 257 |
endpoint_info.http_method = http_method |
258 | 258 |
endpoints.append(endpoint_info) |
259 | 259 |
endpoints.sort(key=lambda x: (x.name or '', x.pattern or '')) |
260 |
if hasattr(self, 'queries'): |
|
261 |
self.append_custom_queries(endpoints) |
|
260 | 262 |
return endpoints |
261 | 263 | |
264 |
def append_custom_queries(self, endpoints): |
|
265 |
for query in self.queries.all(): |
|
266 |
if hasattr(query, 'as_endpoint'): |
|
267 |
endpoints.append(query.as_endpoint()) |
|
268 | ||
262 | 269 |
def get_connector_permissions(self): |
263 | 270 |
perms = {} |
264 | 271 |
for endpoint_info in self.get_endpoints_infos(): |
... | ... | |
971 | 978 | |
972 | 979 |
def __str__(self): |
973 | 980 |
return '%s %s %s' % (self.timestamp, self.appname, self.slug) |
981 | ||
982 | ||
983 |
@six.python_2_unicode_compatible |
|
984 |
class BaseQuery(models.Model): |
|
985 |
'''Base for building custom queries. |
|
986 | ||
987 |
It must define "resource" attribute as a ForeignKey to a BaseResource subclass, |
|
988 |
and probably extend its "as_endpoint" method to document its parameters. |
|
989 |
''' |
|
990 | ||
991 |
name = models.CharField( |
|
992 |
verbose_name=_('Name'), |
|
993 |
max_length=128) |
|
994 |
slug = models.SlugField( |
|
995 |
verbose_name=_('Slug'), |
|
996 |
max_length=128) |
|
997 |
description = models.TextField( |
|
998 |
verbose_name=_('Description'), |
|
999 |
blank=True) |
|
1000 | ||
1001 |
http_method = 'get' |
|
1002 | ||
1003 |
class Meta: |
|
1004 |
abstract = True |
|
1005 |
unique_together = [ |
|
1006 |
('resource', 'name'), |
|
1007 |
('resource', 'slug'), |
|
1008 |
] |
|
1009 |
ordering = ['name'] |
|
1010 | ||
1011 |
def as_endpoint(self, path=''): |
|
1012 |
name = '%s/%s/' % (path, self.slug) if path else self.slug + '/' |
|
1013 |
e = endpoint(name=name, description=self.name, long_description=self.description) |
|
1014 |
e.http_method = self.http_method |
|
1015 |
e.object = self.resource |
|
1016 |
return e |
|
1017 | ||
1018 |
def __str__(self): |
|
1019 |
return self.name |
|
1020 | ||
1021 |
def export_json(self): |
|
1022 |
d = {} |
|
1023 |
fields = [ |
|
1024 |
f for f in self.__class__._meta.get_fields() |
|
1025 |
if f.concrete and ( |
|
1026 |
not f.is_relation |
|
1027 |
or f.one_to_one |
|
1028 |
or (f.many_to_one and f.related_model) |
|
1029 |
) and f.name not in ['id', 'resource'] |
|
1030 |
] |
|
1031 |
for field in fields: |
|
1032 |
d[field.name] = getattr(self, field.name) |
|
1033 |
return d |
|
1034 | ||
1035 |
@classmethod |
|
1036 |
def import_json(cls, d): |
|
1037 |
return cls(**d) |
|
1038 | ||
1039 |
def delete_url(self): |
|
1040 |
return reverse(self.delete_view, |
|
1041 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
|
1042 | ||
1043 |
def edit_url(self): |
|
1044 |
return reverse(self.edit_view, |
|
1045 |
kwargs={'slug': self.resource.slug, 'pk': self.pk}) |
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', |
... | ... | |
290 | 290 |
where='adress LIKE {adress:s}') |
291 | 291 | |
292 | 292 | |
293 |
def test_query_q_method(arcgis, query, rf): |
|
293 |
def test_query_query_method(arcgis, query, rf):
|
|
294 | 294 |
with mock.patch('passerelle.utils.Request.get') as requests_get: |
295 | 295 |
requests_get.return_value = utils.FakedResponse(content=STATES, |
296 | 296 |
status_code=200) |
297 |
query.q(rf.get('/', data={'adress': "AVENUE D'ANNAM"}), full=True) == [] |
|
297 |
query.query(rf.get('/', data={'adress': "AVENUE D'ANNAM"}), full=True) == []
|
|
298 | 298 |
assert requests_get.call_count == 1 |
299 | 299 |
assert requests_get.call_args[0][0] == 'https://arcgis.example.net/services/fold/serv/MapServer/1/query' |
300 | 300 |
args = requests_get.call_args[1]['params'] |
... | ... | |
379 | 379 |
assert resp.content_type == 'image/png' |
380 | 380 | |
381 | 381 | |
382 |
def test_query_documentation(arcgis, query, app): |
|
383 |
resp = app.get(arcgis.get_absolute_url()) |
|
384 |
assert query.name in resp.text |
|
385 |
assert query.description in resp.text |
|
386 |
# additional parameter appears in endpoint documentation |
|
387 |
assert '<span class="param-name">adress</span>' in resp.text |
|
388 | ||
389 | ||
382 | 390 |
def test_export_import(query): |
383 | 391 |
assert ArcGIS.objects.count() == 1 |
384 | 392 |
assert Query.objects.count() == 1 |
385 |
- |