From 2808cdb31b5206c855d13bfdfca3e2a966a5b08c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?=
Date: Sun, 28 May 2017 00:17:55 +0200
Subject: [PATCH] api: add data and geojson views covering all formdatas
(#14260)
---
help/fr/api-get.page | 64 +++++++++++++++++++++++++++++++
tests/test_api.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++
wcs/api.py | 100 +++++++++++++++++++++++++++++++++++-------------
wcs/formdata.py | 6 ++-
4 files changed, 248 insertions(+), 28 deletions(-)
diff --git a/help/fr/api-get.page b/help/fr/api-get.page
index 2d35b383..d251e654 100644
--- a/help/fr/api-get.page
+++ b/help/fr/api-get.page
@@ -358,6 +358,60 @@ n'est pas nécessaire de préciser l'identifiant d'un utilisateur.
+
+ Données de l'ensemble des formulaires
+
+
+ De manière similaire à l'API de récupération de la liste des demandes d'un
+ formulaire, il est possible de récupérer l'ensemble des demandes de la
+ plateforme, peu importe leurs types.
+
+Des paramètres peuvent être envoyés dans la requête pour filtrer les résultats.
+Il s'agit des mêmes paramètres que ceux du tableau global en backoffice.
+Par exemple, pour avoir une liste limitée aux demandes terminées :
+
+Le paramètre full n'est pas pris en charge dans cette API; le
+paramètre anonymise non plus, les données l'étant déjà.
+
+
+Données géolocalisées
@@ -400,6 +454,16 @@ De manière identique aux appels précédents, des filtres peuvent être passés
Les URL retournées pour les demandes pointent vers l'interface de gestion de celles-ci.
+
+Il est également possible d'obtenir les informations géographiques de
+l'ensemble des demandes :
+
+
+
+curl -H "Accept: application/json" \
+ https://www.example.net/api/forms/geojson
+
+
diff --git a/tests/test_api.py b/tests/test_api.py
index 231783c0..8f17bd03 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1446,6 +1446,112 @@ def test_api_geojson_formdata(pub, local_user):
formdef.store()
resp = get_app(pub).get(sign_uri('/api/forms/test/geojson', user=local_user), status=404)
+def test_api_global_geojson(pub, local_user):
+ Role.wipe()
+ role = Role(name='test')
+ role.store()
+
+ FormDef.wipe()
+ formdef = FormDef()
+ formdef.name = 'test'
+ formdef.workflow_roles = {'_receiver': role.id}
+ formdef.fields = []
+ formdef.store()
+
+ data_class = formdef.data_class()
+ data_class.wipe()
+
+ formdef.geolocations = {'base': 'Location'}
+ formdef.store()
+
+ for i in range(30):
+ formdata = data_class()
+ date = time.strptime('2014-01-20', '%Y-%m-%d')
+ formdata.geolocations = {'base': {'lat': 48, 'lon': 2}}
+ formdata.user_id = local_user.id
+ formdata.just_created()
+ if i%3 == 0:
+ formdata.jump_status('new')
+ else:
+ formdata.jump_status('finished')
+ formdata.store()
+
+ if not pub.is_using_postgresql():
+ resp = get_app(pub).get(sign_uri('/api/forms/geojson', user=local_user), status=404)
+ pytest.skip('this requires SQL')
+ return
+
+ # check empty content if user doesn't have the appropriate role
+ resp = get_app(pub).get(sign_uri('/api/forms/geojson', user=local_user))
+ assert 'features' in resp.json
+ assert len(resp.json['features']) == 0
+
+ # add proper role to user
+ local_user.roles = [role.id]
+ local_user.store()
+
+ # check it gets the data
+ resp = get_app(pub).get(sign_uri('/api/forms/geojson', user=local_user))
+ assert 'features' in resp.json
+ assert len(resp.json['features']) == 10
+
+ # check with a filter
+ resp = get_app(pub).get(sign_uri('/api/forms/geojson?status=done', user=local_user))
+ assert 'features' in resp.json
+ assert len(resp.json['features']) == 20
+
+def test_api_global_listing(pub, local_user):
+ Role.wipe()
+ role = Role(name='test')
+ role.store()
+
+ FormDef.wipe()
+ formdef = FormDef()
+ formdef.name = 'test'
+ formdef.workflow_roles = {'_receiver': role.id}
+ formdef.fields = [
+ fields.StringField(id='0', label='foobar', varname='foobar'),
+ ]
+ formdef.store()
+
+ data_class = formdef.data_class()
+ data_class.wipe()
+
+ formdef.store()
+
+ for i in range(30):
+ formdata = data_class()
+ date = time.strptime('2014-01-20', '%Y-%m-%d')
+ formdata.data = {'0': 'FOO BAR'}
+ formdata.user_id = local_user.id
+ formdata.just_created()
+ if i%3 == 0:
+ formdata.jump_status('new')
+ else:
+ formdata.jump_status('finished')
+ formdata.store()
+
+ if not pub.is_using_postgresql():
+ resp = get_app(pub).get(sign_uri('/api/forms/geojson', user=local_user), status=404)
+ pytest.skip('this requires SQL')
+ return
+
+ # check empty content if user doesn't have the appropriate role
+ resp = get_app(pub).get(sign_uri('/api/forms/', user=local_user))
+ assert len(resp.json['data']) == 0
+
+ # add proper role to user
+ local_user.roles = [role.id]
+ local_user.store()
+
+ # check it gets the data
+ resp = get_app(pub).get(sign_uri('/api/forms/', user=local_user))
+ assert len(resp.json['data']) == 10
+
+ # check with a filter
+ resp = get_app(pub).get(sign_uri('/api/forms/?status=done', user=local_user))
+ assert len(resp.json['data']) == 20
+
def test_roles(pub, local_user):
Role.wipe()
role = Role(name='Hello World')
diff --git a/wcs/api.py b/wcs/api.py
index e71551a1..7da57984 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -22,7 +22,7 @@ import urllib2
import sys
from quixote import get_request, get_publisher, get_response, get_session, redirect
-from quixote.directory import Directory
+from quixote.directory import Directory, AccessControlled
from qommon import _
from qommon import misc
@@ -39,6 +39,7 @@ import wcs.qommon.storage as st
from wcs.api_utils import is_url_signed, get_user_from_api_query_string
from backoffice.management import FormPage as BackofficeFormPage
+from backoffice.management import ManagementDirectory
def posted_json_data_to_formdata_data(formdef, data):
# remap fields from varname to field id
@@ -68,6 +69,34 @@ def posted_json_data_to_formdata_data(formdef, data):
return data
+def get_formdata_dict(formdata, user, consider_status_visibility=True):
+ if consider_status_visibility:
+ status = formdata.get_visible_status(user=user)
+ if not status:
+ # skip hidden forms
+ return None
+ else:
+ status = formdata.get_status()
+
+ title = _('%(name)s #%(id)s (%(status)s)') % {
+ 'name': formdata.formdef.name,
+ 'id': formdata.get_display_id(),
+ 'status': status.name,
+ }
+ d = {'title': title,
+ 'name': formdata.formdef.name,
+ 'url': formdata.get_url(),
+ 'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', formdata.receipt_time),
+ 'status': status.name,
+ 'status_css_class': status.extra_css_class,
+ 'keywords': formdata.formdef.keywords_list,
+ }
+ d.update(formdata.get_substitution_variables(minimal=True))
+ if get_request().form.get('full') == 'on':
+ d.update(formdata.get_json_export_dict(include_files=False))
+ return d
+
+
class ApiFormdataPage(FormStatusPage):
_q_exports_orig = ['', 'download']
@@ -145,12 +174,49 @@ class ApiFormPage(BackofficeFormPage):
return ApiFormdataPage(self.formdef, formdata)
-class ApiFormsDirectory(Directory):
- def _q_lookup(self, component):
+class ApiFormsDirectory(AccessControlled, Directory):
+ _q_exports = ['', 'geojson']
+
+ def _q_access(self):
if not is_url_signed():
# grant access to admins, to ease debug
if not (get_request().user and get_request().user.is_admin):
raise AccessForbiddenError('user not authenticated')
+
+ def _q_index(self):
+ if not get_publisher().is_using_postgresql():
+ raise TraversalError()
+
+ get_request().user = get_user_from_api_query_string() or get_request().user
+
+ from wcs import sql
+
+ management_directory = ManagementDirectory()
+ criterias = management_directory.get_global_listing_criterias()
+
+ limit = int(get_request().form.get('limit',
+ get_publisher().get_site_option('default-page-size') or 20))
+ offset = int(get_request().form.get('offset', 0))
+ order_by = get_request().form.get('order_by',
+ get_publisher().get_site_option('default-sort-order') or '-receipt_time')
+
+ output = [get_formdata_dict(x, user=get_request().user, consider_status_visibility=False)
+ for x in sql.AnyFormData.select(
+ criterias, order_by=order_by, limit=limit, offset=offset)]
+
+ get_response().set_content_type('application/json')
+ return json.dumps({'data': output},
+ cls=misc.JSONEncoder,
+ encoding=get_publisher().site_charset)
+
+
+ def geojson(self):
+ if not get_publisher().is_using_postgresql():
+ raise TraversalError()
+ get_request().user = get_user_from_api_query_string() or get_request().user
+ return ManagementDirectory().geojson()
+
+ def _q_lookup(self, component):
return ApiFormPage(component)
@@ -485,31 +551,11 @@ class ApiUserDirectory(Directory):
for form in self.get_user_forms(user):
if form.is_draft():
continue
- visible_status = form.get_visible_status(user=user)
- # skip hidden forms
- if not visible_status:
+ formdata_dict = get_formdata_dict(form, user)
+ if not formdata_dict:
+ # skip hidden forms
continue
- name = form.formdef.name
- id = form.get_display_id()
- status = visible_status.name
- title = _('%(name)s #%(id)s (%(status)s)') % {
- 'name': name,
- 'id': id,
- 'status': status
- }
- url = form.get_url()
- d = {'title': title,
- 'name': form.formdef.name,
- 'url': url,
- 'datetime': misc.strftime.strftime('%Y-%m-%d %H:%M:%S', form.receipt_time),
- 'status': status,
- 'status_css_class': visible_status.extra_css_class,
- 'keywords': form.formdef.keywords_list,
- }
- d.update(form.get_substitution_variables(minimal=True))
- if get_request().form.get('full') == 'on':
- d.update(form.get_json_export_dict(include_files=False))
- forms.append(d)
+ forms.append(formdata_dict)
return json.dumps(forms,
cls=misc.JSONEncoder,
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 7b6266be..c04a6bce 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -30,6 +30,7 @@ from qommon import _
from qommon.storage import StorableObject, Intersects, Contains
import qommon.misc
from qommon import ezt
+from qommon.evalutils import make_datetime
from qommon.substitution import Substitutions
from roles import Role
@@ -555,7 +556,10 @@ class FormData(StorableObject):
'form_criticality_level': self.criticality_level,
})
if self.receipt_time:
- d['form_receipt_datetime'] = datetime.datetime(*self.receipt_time[:6])
+ # always get receipt time as a datetime object, this handles
+ # both normal formdata (where receipt_time is a time.struct_time)
+ # and sql.AnyFormData where it's already a datetime object.
+ d['form_receipt_datetime'] = make_datetime(self.receipt_time)
d['form_status'] = self.get_status_label()
--
2.11.0