export evolutions in form API (#10820)

This commit is contained in:
Benjamin Dauvergne 2016-05-03 23:54:11 +02:00
parent 0b343f025b
commit 885456f9a3
7 changed files with 200 additions and 17 deletions

View File

@ -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"
}
]
},
]
}
</code>
@ -133,6 +157,16 @@ backoffice et quel était le canal d'origine de la demande, est disponible
dans l'attribut <code>submission</code>.
</p>
<p>
L'historique du formulaire, ses transitions dans différents statuts, est disponible dans l'attribut
<code>evolution</code>. Cette liste de dictionnaires contient l'instant de la transition
dans l'attribut <code>time</code>, le code du statut concerné dans <code>status</code> et
une description de l'utilisateur responsable de la transition dans <code>user</code>. L'attribut
optionnel <code>parts</code> peut contenir une liste de dictionnaires liés aux actions de workflow,
comme un commentaire ou une erreur lors de l'appel d'un <em>web service</em>.
</p>
<note>
<p>
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 <code>anonymise</code>. 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.
<p>
</p>
<screen>
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \

View File

@ -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()

View File

@ -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,77 @@ 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]

View File

@ -152,6 +152,31 @@ class Evolution(object):
l.append(p.view())
return l
def get_json_export_dict(self, user, anonymise=False):
data = {
'time': self.time,
}
if self.status:
data['status'] = self.status[3:]
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'
@ -704,20 +729,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)
@ -757,6 +770,12 @@ class FormData(StorableObject):
'channel': self.submission_channel or 'web',
}
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):

View File

@ -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'))

View File

@ -51,6 +51,14 @@ class JournalEvolutionPart: #pylint: disable=C1001
[(x or htmltext('</p><p>')) for x in self.content.splitlines()]) + \
htmltext('</p>')
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')

View File

@ -79,6 +79,18 @@ class JournalWsCallErrorPart: #pylint: disable=C1001
r += htmltext('</div>')
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')