export evolutions in form API (#10820)
This commit is contained in:
parent
0b343f025b
commit
885456f9a3
|
@ -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" \
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
11
wcs/users.py
11
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'))
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue