From c4d9d3678e93db15a0e234c31effa655e2be2030 Mon Sep 17 00:00:00 2001
From: Benjamin Dauvergne
Date: Tue, 3 May 2016 23:54:11 +0200
Subject: [PATCH] export evolutions in form API (#10820)
---
help/fr/api-get.page | 40 +++++++++++++++++++++--
tests/test_api.py | 23 +++++++++++++-
tests/test_formdata.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++
wcs/formdata.py | 44 ++++++++++++++++++--------
wcs/users.py | 11 +++++++
wcs/wf/register_comment.py | 8 +++++
wcs/wf/wscall.py | 12 +++++++
7 files changed, 200 insertions(+), 17 deletions(-)
diff --git a/help/fr/api-get.page b/help/fr/api-get.page
index cd663ea..2f89c18 100644
--- a/help/fr/api-get.page
+++ b/help/fr/api-get.page
@@ -54,7 +54,7 @@ Le contenu ainsi obtenu est le suivant :
},
"workflow": {
"status": {
- "id": "new",
+ "id": "1",
"name": "New"
},
"data": {
@@ -110,7 +110,31 @@ Le contenu ainsi obtenu est le suivant :
"submission": {
"backoffice": false,
"channel": "Web"
- }
+ },
+ "evolution": [
+ {
+ "status": "1",
+ "time": "2013-01-04T13:39:49",
+ "user": {
+ "id": 1,
+ "name": "Fred"
+ "email": "fred@example.com",
+ "NameID": ["123456"]
+ },
+ "parts": [
+ {
+ "type": "wscall-error",
+ "summary": "description de l'erreur",
+ "label": "appel du web-service XYZ",
+ "data": "données reçues jusqu'à 10000 octets..."
+ },
+ {
+ "type": "workflow-comment",
+ "content": "commentaire"
+ }
+ ]
+ },
+ ]
}
@@ -133,6 +157,16 @@ backoffice et quel était le canal d'origine de la demande, est disponible
dans l'attribut submission.
+
+L'historique du formulaire, ses transitions dans différents statuts, est disponible dans l'attribut
+evolution. Cette liste de dictionnaires contient l'instant de la transition
+dans l'attribut time, le code du statut concerné dans status et
+une description de l'utilisateur responsable de la transition dans user. L'attribut
+optionnel parts peut contenir une liste de dictionnaires liés aux actions de workflow,
+comme un commentaire ou une erreur lors de l'appel d'un web service.
+
+
+
Il est bien sûr nécessaire de disposer des autorisations nécessaires pour
@@ -302,7 +336,7 @@ Les API « Liste de formulaires » et le mode Pull de récupération d'un formul
paramètre supplémentaire anonymise. Quand celui-ci est présent des données anonymisées
des formulaires sont renvoyées et les contrôles d'accès sont simplifiés à une signature simple, il
n'est pas nécessaire de préciser l'identifiant d'un utilisateur.
-
+
curl -H "Accept: application/json" \
diff --git a/tests/test_api.py b/tests/test_api.py
index 6fe7b35..f11fc6a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -17,6 +17,7 @@ from wcs.qommon.form import PicklableUpload
from wcs.users import User
from wcs.roles import Role
from wcs.formdef import FormDef
+from wcs.formdata import Evolution
from wcs.categories import Category
from wcs.data_sources import NamedDataSource
from wcs.workflows import Workflow, EditableWorkflowStatusItem
@@ -1007,7 +1008,6 @@ def test_api_list_formdata(pub, local_user):
if i%7 == 0:
formdata.backoffice_submission = True
formdata.submission_channel = 'mail'
-
formdata.store()
# check access is denied if the user has not the appropriate role
@@ -1030,6 +1030,12 @@ def test_api_list_formdata(pub, local_user):
assert 'fields' in resp.json[0]
assert 'file' not in resp.json[0]['fields'] # no file export in full lists
assert 'user' in resp.json[0]
+ assert 'evolution' in resp.json[0]
+ assert len(resp.json[0]['evolution']) == 2
+ assert 'status' in resp.json[0]['evolution'][0]
+ assert 'who' in resp.json[0]['evolution'][0]
+ assert 'time' in resp.json[0]['evolution'][0]
+ assert resp.json[0]['evolution'][0]['who']['id'] == local_user.id
assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['backoffice'] is True
assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['channel'] == 'Mail'
@@ -1105,6 +1111,11 @@ def test_api_anonymized_formdata(pub, local_user):
assert 'file' not in resp.json[0]['fields'] # no file export in full lists
assert 'foobar3' in resp.json[0]['fields']
assert 'foobar' not in resp.json[0]['fields']
+ assert 'evolution' in resp.json[0]
+ assert len(resp.json[0]['evolution']) == 2
+ assert 'status' in resp.json[0]['evolution'][0]
+ assert not 'who' in resp.json[0]['evolution'][0]
+ assert 'time' in resp.json[0]['evolution'][0]
# check access is granted event if there is no user
resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on'))
@@ -1115,6 +1126,11 @@ def test_api_anonymized_formdata(pub, local_user):
assert 'file' not in resp.json[0]['fields'] # no file export in full lists
assert 'foobar3' in resp.json[0]['fields']
assert 'foobar' not in resp.json[0]['fields']
+ assert 'evolution' in resp.json[0]
+ assert len(resp.json[0]['evolution']) == 2
+ assert 'status' in resp.json[0]['evolution'][0]
+ assert not 'who' in resp.json[0]['evolution'][0]
+ assert 'time' in resp.json[0]['evolution'][0]
# check anonymise is enforced on detail view
resp = get_app(pub).get(sign_uri('/api/forms/%s/?anonymise&full=on' % resp.json[0]['id']))
assert 'receipt_time' in resp.json
@@ -1123,6 +1139,11 @@ def test_api_anonymized_formdata(pub, local_user):
assert 'file' not in resp.json['fields'] # no file export in detail
assert 'foobar3' in resp.json['fields']
assert 'foobar' not in resp.json['fields']
+ assert 'evolution' in resp.json
+ assert len(resp.json['evolution']) == 2
+ assert 'status' in resp.json['evolution'][0]
+ assert not 'who' in resp.json['evolution'][0]
+ assert 'time' in resp.json['evolution'][0]
def test_roles(pub, local_user):
Role.wipe()
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index e50416d..da6dcbb 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -12,11 +12,15 @@ from wcs.formdef import FormDef
from wcs.formdata import Evolution
from wcs.workflows import Workflow, WorkflowCriticalityLevel
from wcs.wf.anonymise import AnonymiseWorkflowStatusItem
+from wcs.wf.wscall import JournalWsCallErrorPart
+from wcs.wf.register_comment import JournalEvolutionPart
from wcs.qommon.form import NoUpload
import mock
from utilities import create_temporary_pub, clean_temporary_pub
+from test_api import local_user
+
def pytest_generate_tests(metafunc):
if 'pub' in metafunc.fixturenames:
metafunc.parametrize('pub', ['pickle', 'sql'], indirect=True)
@@ -434,3 +438,78 @@ def test_field_item_substvars(pub):
variables = formdata.get_substitution_variables()
assert variables.get('form_var_xxx') == 'un'
assert variables.get('form_var_xxx_raw') == '1'
+
+
+def test_get_json_export_dict_evolution(pub, local_user):
+ Workflow.wipe()
+ workflow = Workflow(name='test')
+ st_new = workflow.add_status('New')
+ st_finished = workflow.add_status('Finished')
+ workflow.store()
+
+ formdef = FormDef()
+ formdef.workflow_id = workflow.id
+ formdef.name = 'foo'
+ formdef.fields = []
+ formdef.store()
+ formdef.data_class().wipe()
+
+ d = formdef.data_class()()
+ d.status = 'wf-%s' % st_new.id
+ d.user_id = local_user.id
+ d.receipt_time = time.localtime()
+ evo = Evolution()
+ evo.time = time.localtime()
+ evo.status = 'wf-%s' % st_new.id
+ evo.who = '_submitter'
+ d.evolution = [evo]
+ d.store()
+ evo.add_part(JournalEvolutionPart(d, "ok"))
+ evo.add_part(JournalWsCallErrorPart("summary", "label", "data"))
+ evo = Evolution()
+ evo.time = time.localtime()
+ evo.status = 'wf-%s' % st_finished.id
+ evo.who = '_submitter'
+ d.evolution.append(evo)
+ d.store()
+
+ export = d.get_json_export_dict()
+ assert 'evolution' in export
+ assert len(export['evolution']) == 2
+ assert export['evolution'][0]['status'] == st_new.id
+ assert 'time' in export['evolution'][0]
+ assert export['evolution'][0]['who']['id'] == local_user.id
+ assert export['evolution'][0]['who']['email'] == local_user.email
+ assert export['evolution'][0]['who']['NameID'] == local_user.name_identifiers
+ assert 'parts' in export['evolution'][0]
+ assert len(export['evolution'][0]['parts']) == 2
+ assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment'
+ assert export['evolution'][0]['parts'][0]['content'] == 'ok'
+ assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error'
+ assert export['evolution'][0]['parts'][1]['summary'] == 'summary'
+ assert export['evolution'][0]['parts'][1]['label'] == 'label'
+ assert export['evolution'][0]['parts'][1]['data'] == 'data'
+ assert export['evolution'][1]['status'] == st_finished.id
+ assert 'time' in export['evolution'][1]
+ assert export['evolution'][1]['who']['id'] == local_user.id
+ assert export['evolution'][1]['who']['email'] == local_user.email
+ assert export['evolution'][1]['who']['NameID'] == local_user.name_identifiers
+ assert 'parts' not in export['evolution'][1]
+
+ export = d.get_json_export_dict(anonymise=True)
+ assert 'evolution' in export
+ assert len(export['evolution']) == 2
+ assert export['evolution'][0]['status'] == st_new.id
+ assert 'time' in export['evolution'][0]
+ assert 'who' not in export['evolution'][0]
+ assert 'parts' in export['evolution'][0]
+ assert len(export['evolution'][0]['parts']) == 2
+ assert len(export['evolution'][0]['parts'][0]) == 1
+ assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment'
+ assert len(export['evolution'][0]['parts'][1]) == 1
+ assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error'
+ assert export['evolution'][1]['status'] == st_finished.id
+ assert 'time' in export['evolution'][1]
+ assert 'who' not in export['evolution'][0]
+ assert 'parts' not in export['evolution'][1]
+
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 512c34f..712a35b 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -152,6 +152,30 @@ class Evolution(object):
l.append(p.view())
return l
+ def get_json_export_dict(self, user, anonymise=False):
+ data = {
+ 'status': self.status[3:],
+ 'time': self.time,
+ }
+ if not anonymise:
+ try:
+ if self.who != '_submitter':
+ user = get_publisher().user_class.get(self.who)
+ except KeyError:
+ pass
+ else:
+ if user:
+ data['who'] = user.get_json_export_dict()
+ if self.comment:
+ data['comment'] = self.comment
+ parts = []
+ for part in self.parts or []:
+ if hasattr(part, 'get_json_export_dict'):
+ parts.append(part.get_json_export_dict(anonymise=anonymise))
+ if parts:
+ data['parts'] = parts
+ return data
+
class FormData(StorableObject):
_names = 'XX'
@@ -703,20 +727,8 @@ class FormData(StorableObject):
user = get_publisher().user_class.get(self.user_id)
except KeyError:
user = None
- # this is custom code so it is possible to mark forms as anonyms, this
- # is done through the VoteAnonymity field, this is very specific but
- # isn't generalised yet into an useful extension mechanism, as it's not
- # clear at the moment what could be useful.
- for f in self.formdef.fields:
- if f.key == 'vote-anonymity':
- user = None
- break
if user:
- data['user'] = {'id': user.id, 'name': user.display_name}
- if user.email:
- data['user']['email'] = user.email
- if user.name_identifiers:
- data['user']['NameID'] = user.name_identifiers
+ data['user'] = user.get_json_export_dict()
data['fields'] = get_json_dict(self.formdef.fields, self.data,
include_files=include_files, anonymise=anonymise)
@@ -756,6 +768,12 @@ class FormData(StorableObject):
'channel': self.get_submission_channel_label(),
}
+ if self.evolution:
+ evolution = data['evolution'] = []
+ for evo in self.evolution:
+ evolution.append(evo.get_json_export_dict(None if anonymise else user,
+ anonymise=anonymise))
+
return data
def export_to_json(self, include_files=True, anonymise=False):
diff --git a/wcs/users.py b/wcs/users.py
index 8bb602d..9a14f60 100644
--- a/wcs/users.py
+++ b/wcs/users.py
@@ -188,6 +188,17 @@ class User(StorableObject):
return self.__dict__['form_data'].get(attr[1:])
raise AttributeError()
+ def get_json_export_dict(self):
+ data = {
+ 'id': self.id,
+ 'name': self.display_name,
+ }
+ if self.email:
+ data['email'] = self.email
+ if self.name_identifiers:
+ data['NameID'] = self.name_identifiers
+ return data
+
Substitutions.register('session_user', category=N_('User'), comment=N_('Session User'))
Substitutions.register('session_user_display_name', category=N_('User'), comment=N_('Session User Display Name'))
diff --git a/wcs/wf/register_comment.py b/wcs/wf/register_comment.py
index a7dd418..40af647 100644
--- a/wcs/wf/register_comment.py
+++ b/wcs/wf/register_comment.py
@@ -51,6 +51,14 @@ class JournalEvolutionPart: #pylint: disable=C1001
[(x or htmltext('
')) for x in self.content.splitlines()]) + \
htmltext('
')
+ def get_json_export_dict(self, anonymise=False):
+ d = {
+ 'type': 'workflow-comment',
+ }
+ if not anonymise:
+ d['content'] = self.content
+ return d
+
class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem):
description = N_('Record in Log')
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index a35e53b..ddcadb8 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -79,6 +79,18 @@ class JournalWsCallErrorPart: #pylint: disable=C1001
r += htmltext('')
return r.getvalue()
+ def get_json_export_dict(self, anonymise=False):
+ d = {
+ 'type': 'wscall-error',
+ }
+ if not anonymise:
+ d.update({
+ 'summary': self.summary,
+ 'label': self.label,
+ 'data': self.data,
+ })
+ return d
+
class WebserviceCallStatusItem(WorkflowStatusItem):
description = N_('Webservice Call')
--
2.1.4