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. +

+ + +$ curl -H "Accept: application/json" \ + https://www.example.net/api/forms/ + + + +[ + { + url: "https://www.example.net/inscriptions/1/", + last_update_time: "2015-03-26T23:08:45", + receipt_time: "2015-03-26T23:08:44", + id: 1 + }, + { + url: "https://www.example.net/inscriptions/3/", + last_update_time: "2015-03-27T12:11:21", + receipt_time: "2015-03-27T12:45:19", + id: 3 + }, + { + url: "https://www.example.net/signalement/1/", + last_update_time: "2015-03-25T14:14:21", + receipt_time: "2015-03-25T14:48:20", + id: 1 + } +] + + +

+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 : +

+ + +$ curl -H "Accept: application/json" \ + https://www.example.net/api/forms/?status=done + + +

+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