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