0001-export-evolutions-in-form-API-10820.patch
help/fr/api-get.page | ||
---|---|---|
54 | 54 |
}, |
55 | 55 |
"workflow": { |
56 | 56 |
"status": { |
57 |
"id": "new",
|
|
57 |
"id": "1",
|
|
58 | 58 |
"name": "New" |
59 | 59 |
}, |
60 | 60 |
"data": { |
... | ... | |
110 | 110 |
"submission": { |
111 | 111 |
"backoffice": false, |
112 | 112 |
"channel": "Web" |
113 |
} |
|
113 |
}, |
|
114 |
"evolution": [ |
|
115 |
{ |
|
116 |
"status": "1", |
|
117 |
"time": "2013-01-04T13:39:49", |
|
118 |
"user": { |
|
119 |
"id": 1, |
|
120 |
"name": "Fred" |
|
121 |
"email": "fred@example.com", |
|
122 |
"NameID": ["123456"] |
|
123 |
}, |
|
124 |
"parts": [ |
|
125 |
{ |
|
126 |
"type": "wscall-error", |
|
127 |
"summary": "description de l'erreur", |
|
128 |
"label": "appel du web-service XYZ", |
|
129 |
"data": "données reçues jusqu'à 10000 octets..." |
|
130 |
}, |
|
131 |
{ |
|
132 |
"type": "workflow-comment", |
|
133 |
"content": "commentaire" |
|
134 |
} |
|
135 |
] |
|
136 |
}, |
|
137 |
] |
|
114 | 138 |
} |
115 | 139 |
</code> |
116 | 140 | |
... | ... | |
133 | 157 |
dans l'attribut <code>submission</code>. |
134 | 158 |
</p> |
135 | 159 | |
160 |
<p> |
|
161 |
L'historique du formulaire, ses transitions dans différents statuts, est disponible dans l'attribut |
|
162 |
<code>evolution</code>. Cette liste de dictionnaires contient l'instant de la transition |
|
163 |
dans l'attribut <code>time</code>, le code du statut concerné dans <code>status</code> et |
|
164 |
une description de l'utilisateur responsable de la transition dans <code>user</code>. L'attribut |
|
165 |
optionnel <code>parts</code> peut contenir une liste de dictionnaires liés aux actions de workflow, |
|
166 |
comme un commentaire ou une erreur lors de l'appel d'un <em>web service</em>. |
|
167 |
</p> |
|
168 | ||
169 | ||
136 | 170 |
<note> |
137 | 171 |
<p> |
138 | 172 |
Il est bien sûr nécessaire de disposer des autorisations nécessaires pour |
... | ... | |
302 | 336 |
paramètre supplémentaire <code>anonymise</code>. Quand celui-ci est présent des données anonymisées |
303 | 337 |
des formulaires sont renvoyées et les contrôles d'accès sont simplifiés à une signature simple, il |
304 | 338 |
n'est pas nécessaire de préciser l'identifiant d'un utilisateur. |
305 |
<p> |
|
339 |
</p>
|
|
306 | 340 | |
307 | 341 |
<screen> |
308 | 342 |
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \ |
tests/test_api.py | ||
---|---|---|
17 | 17 |
from wcs.users import User |
18 | 18 |
from wcs.roles import Role |
19 | 19 |
from wcs.formdef import FormDef |
20 |
from wcs.formdata import Evolution |
|
20 | 21 |
from wcs.categories import Category |
21 | 22 |
from wcs.data_sources import NamedDataSource |
22 | 23 |
from wcs.workflows import Workflow, EditableWorkflowStatusItem |
... | ... | |
1007 | 1008 |
if i%7 == 0: |
1008 | 1009 |
formdata.backoffice_submission = True |
1009 | 1010 |
formdata.submission_channel = 'mail' |
1010 | ||
1011 | 1011 |
formdata.store() |
1012 | 1012 | |
1013 | 1013 |
# check access is denied if the user has not the appropriate role |
... | ... | |
1030 | 1030 |
assert 'fields' in resp.json[0] |
1031 | 1031 |
assert 'file' not in resp.json[0]['fields'] # no file export in full lists |
1032 | 1032 |
assert 'user' in resp.json[0] |
1033 |
assert 'evolution' in resp.json[0] |
|
1034 |
assert len(resp.json[0]['evolution']) == 2 |
|
1035 |
assert 'status' in resp.json[0]['evolution'][0] |
|
1036 |
assert 'who' in resp.json[0]['evolution'][0] |
|
1037 |
assert 'time' in resp.json[0]['evolution'][0] |
|
1038 |
assert resp.json[0]['evolution'][0]['who']['id'] == local_user.id |
|
1033 | 1039 | |
1034 | 1040 |
assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['backoffice'] is True |
1035 | 1041 |
assert [x for x in resp.json if x['fields']['foobar'] == 'FOO BAR 0'][0]['submission']['channel'] == 'Mail' |
... | ... | |
1105 | 1111 |
assert 'file' not in resp.json[0]['fields'] # no file export in full lists |
1106 | 1112 |
assert 'foobar3' in resp.json[0]['fields'] |
1107 | 1113 |
assert 'foobar' not in resp.json[0]['fields'] |
1114 |
assert 'evolution' in resp.json[0] |
|
1115 |
assert len(resp.json[0]['evolution']) == 2 |
|
1116 |
assert 'status' in resp.json[0]['evolution'][0] |
|
1117 |
assert not 'who' in resp.json[0]['evolution'][0] |
|
1118 |
assert 'time' in resp.json[0]['evolution'][0] |
|
1108 | 1119 | |
1109 | 1120 |
# check access is granted event if there is no user |
1110 | 1121 |
resp = get_app(pub).get(sign_uri('/api/forms/test/list?anonymise&full=on')) |
... | ... | |
1115 | 1126 |
assert 'file' not in resp.json[0]['fields'] # no file export in full lists |
1116 | 1127 |
assert 'foobar3' in resp.json[0]['fields'] |
1117 | 1128 |
assert 'foobar' not in resp.json[0]['fields'] |
1129 |
assert 'evolution' in resp.json[0] |
|
1130 |
assert len(resp.json[0]['evolution']) == 2 |
|
1131 |
assert 'status' in resp.json[0]['evolution'][0] |
|
1132 |
assert not 'who' in resp.json[0]['evolution'][0] |
|
1133 |
assert 'time' in resp.json[0]['evolution'][0] |
|
1118 | 1134 |
# check anonymise is enforced on detail view |
1119 | 1135 |
resp = get_app(pub).get(sign_uri('/api/forms/%s/?anonymise&full=on' % resp.json[0]['id'])) |
1120 | 1136 |
assert 'receipt_time' in resp.json |
... | ... | |
1123 | 1139 |
assert 'file' not in resp.json['fields'] # no file export in detail |
1124 | 1140 |
assert 'foobar3' in resp.json['fields'] |
1125 | 1141 |
assert 'foobar' not in resp.json['fields'] |
1142 |
assert 'evolution' in resp.json |
|
1143 |
assert len(resp.json['evolution']) == 2 |
|
1144 |
assert 'status' in resp.json['evolution'][0] |
|
1145 |
assert not 'who' in resp.json['evolution'][0] |
|
1146 |
assert 'time' in resp.json['evolution'][0] |
|
1126 | 1147 | |
1127 | 1148 |
def test_roles(pub, local_user): |
1128 | 1149 |
Role.wipe() |
tests/test_formdata.py | ||
---|---|---|
12 | 12 |
from wcs.formdata import Evolution |
13 | 13 |
from wcs.workflows import Workflow, WorkflowCriticalityLevel |
14 | 14 |
from wcs.wf.anonymise import AnonymiseWorkflowStatusItem |
15 |
from wcs.wf.wscall import JournalWsCallErrorPart |
|
16 |
from wcs.wf.register_comment import JournalEvolutionPart |
|
15 | 17 |
from wcs.qommon.form import NoUpload |
16 | 18 |
import mock |
17 | 19 | |
18 | 20 |
from utilities import create_temporary_pub, clean_temporary_pub |
19 | 21 | |
22 |
from test_api import local_user |
|
23 | ||
20 | 24 |
def pytest_generate_tests(metafunc): |
21 | 25 |
if 'pub' in metafunc.fixturenames: |
22 | 26 |
metafunc.parametrize('pub', ['pickle', 'sql'], indirect=True) |
... | ... | |
434 | 438 |
variables = formdata.get_substitution_variables() |
435 | 439 |
assert variables.get('form_var_xxx') == 'un' |
436 | 440 |
assert variables.get('form_var_xxx_raw') == '1' |
441 | ||
442 | ||
443 |
def test_get_json_export_dict_evolution(pub, local_user): |
|
444 |
Workflow.wipe() |
|
445 |
workflow = Workflow(name='test') |
|
446 |
st_new = workflow.add_status('New') |
|
447 |
st_finished = workflow.add_status('Finished') |
|
448 |
workflow.store() |
|
449 | ||
450 |
formdef = FormDef() |
|
451 |
formdef.workflow_id = workflow.id |
|
452 |
formdef.name = 'foo' |
|
453 |
formdef.fields = [] |
|
454 |
formdef.store() |
|
455 |
formdef.data_class().wipe() |
|
456 | ||
457 |
d = formdef.data_class()() |
|
458 |
d.status = 'wf-%s' % st_new.id |
|
459 |
d.user_id = local_user.id |
|
460 |
d.receipt_time = time.localtime() |
|
461 |
evo = Evolution() |
|
462 |
evo.time = time.localtime() |
|
463 |
evo.status = 'wf-%s' % st_new.id |
|
464 |
evo.who = '_submitter' |
|
465 |
d.evolution = [evo] |
|
466 |
d.store() |
|
467 |
evo.add_part(JournalEvolutionPart(d, "ok")) |
|
468 |
evo.add_part(JournalWsCallErrorPart("summary", "label", "data")) |
|
469 |
evo = Evolution() |
|
470 |
evo.time = time.localtime() |
|
471 |
evo.status = 'wf-%s' % st_finished.id |
|
472 |
evo.who = '_submitter' |
|
473 |
d.evolution.append(evo) |
|
474 |
d.store() |
|
475 | ||
476 |
export = d.get_json_export_dict() |
|
477 |
assert 'evolution' in export |
|
478 |
assert len(export['evolution']) == 2 |
|
479 |
assert export['evolution'][0]['status'] == st_new.id |
|
480 |
assert 'time' in export['evolution'][0] |
|
481 |
assert export['evolution'][0]['who']['id'] == local_user.id |
|
482 |
assert export['evolution'][0]['who']['email'] == local_user.email |
|
483 |
assert export['evolution'][0]['who']['NameID'] == local_user.name_identifiers |
|
484 |
assert 'parts' in export['evolution'][0] |
|
485 |
assert len(export['evolution'][0]['parts']) == 2 |
|
486 |
assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment' |
|
487 |
assert export['evolution'][0]['parts'][0]['content'] == 'ok' |
|
488 |
assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error' |
|
489 |
assert export['evolution'][0]['parts'][1]['summary'] == 'summary' |
|
490 |
assert export['evolution'][0]['parts'][1]['label'] == 'label' |
|
491 |
assert export['evolution'][0]['parts'][1]['data'] == 'data' |
|
492 |
assert export['evolution'][1]['status'] == st_finished.id |
|
493 |
assert 'time' in export['evolution'][1] |
|
494 |
assert export['evolution'][1]['who']['id'] == local_user.id |
|
495 |
assert export['evolution'][1]['who']['email'] == local_user.email |
|
496 |
assert export['evolution'][1]['who']['NameID'] == local_user.name_identifiers |
|
497 |
assert 'parts' not in export['evolution'][1] |
|
498 | ||
499 |
export = d.get_json_export_dict(anonymise=True) |
|
500 |
assert 'evolution' in export |
|
501 |
assert len(export['evolution']) == 2 |
|
502 |
assert export['evolution'][0]['status'] == st_new.id |
|
503 |
assert 'time' in export['evolution'][0] |
|
504 |
assert 'who' not in export['evolution'][0] |
|
505 |
assert 'parts' in export['evolution'][0] |
|
506 |
assert len(export['evolution'][0]['parts']) == 2 |
|
507 |
assert len(export['evolution'][0]['parts'][0]) == 1 |
|
508 |
assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment' |
|
509 |
assert len(export['evolution'][0]['parts'][1]) == 1 |
|
510 |
assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error' |
|
511 |
assert export['evolution'][1]['status'] == st_finished.id |
|
512 |
assert 'time' in export['evolution'][1] |
|
513 |
assert 'who' not in export['evolution'][0] |
|
514 |
assert 'parts' not in export['evolution'][1] |
|
515 |
wcs/formdata.py | ||
---|---|---|
152 | 152 |
l.append(p.view()) |
153 | 153 |
return l |
154 | 154 | |
155 |
def get_json_export_dict(self, user, anonymise=False): |
|
156 |
data = { |
|
157 |
'status': self.status[3:], |
|
158 |
'time': self.time, |
|
159 |
} |
|
160 |
if not anonymise: |
|
161 |
try: |
|
162 |
if self.who != '_submitter': |
|
163 |
user = get_publisher().user_class.get(self.who) |
|
164 |
except KeyError: |
|
165 |
pass |
|
166 |
else: |
|
167 |
if user: |
|
168 |
data['who'] = user.get_json_export_dict() |
|
169 |
if self.comment: |
|
170 |
data['comment'] = self.comment |
|
171 |
parts = [] |
|
172 |
for part in self.parts or []: |
|
173 |
if hasattr(part, 'get_json_export_dict'): |
|
174 |
parts.append(part.get_json_export_dict(anonymise=anonymise)) |
|
175 |
if parts: |
|
176 |
data['parts'] = parts |
|
177 |
return data |
|
178 | ||
155 | 179 | |
156 | 180 |
class FormData(StorableObject): |
157 | 181 |
_names = 'XX' |
... | ... | |
703 | 727 |
user = get_publisher().user_class.get(self.user_id) |
704 | 728 |
except KeyError: |
705 | 729 |
user = None |
706 |
# this is custom code so it is possible to mark forms as anonyms, this |
|
707 |
# is done through the VoteAnonymity field, this is very specific but |
|
708 |
# isn't generalised yet into an useful extension mechanism, as it's not |
|
709 |
# clear at the moment what could be useful. |
|
710 |
for f in self.formdef.fields: |
|
711 |
if f.key == 'vote-anonymity': |
|
712 |
user = None |
|
713 |
break |
|
714 | 730 |
if user: |
715 |
data['user'] = {'id': user.id, 'name': user.display_name} |
|
716 |
if user.email: |
|
717 |
data['user']['email'] = user.email |
|
718 |
if user.name_identifiers: |
|
719 |
data['user']['NameID'] = user.name_identifiers |
|
731 |
data['user'] = user.get_json_export_dict() |
|
720 | 732 | |
721 | 733 |
data['fields'] = get_json_dict(self.formdef.fields, self.data, |
722 | 734 |
include_files=include_files, anonymise=anonymise) |
... | ... | |
756 | 768 |
'channel': self.get_submission_channel_label(), |
757 | 769 |
} |
758 | 770 | |
771 |
if self.evolution: |
|
772 |
evolution = data['evolution'] = [] |
|
773 |
for evo in self.evolution: |
|
774 |
evolution.append(evo.get_json_export_dict(None if anonymise else user, |
|
775 |
anonymise=anonymise)) |
|
776 | ||
759 | 777 |
return data |
760 | 778 | |
761 | 779 |
def export_to_json(self, include_files=True, anonymise=False): |
wcs/users.py | ||
---|---|---|
188 | 188 |
return self.__dict__['form_data'].get(attr[1:]) |
189 | 189 |
raise AttributeError() |
190 | 190 | |
191 |
def get_json_export_dict(self): |
|
192 |
data = { |
|
193 |
'id': self.id, |
|
194 |
'name': self.display_name, |
|
195 |
} |
|
196 |
if self.email: |
|
197 |
data['email'] = self.email |
|
198 |
if self.name_identifiers: |
|
199 |
data['NameID'] = self.name_identifiers |
|
200 |
return data |
|
201 | ||
191 | 202 | |
192 | 203 |
Substitutions.register('session_user', category=N_('User'), comment=N_('Session User')) |
193 | 204 |
Substitutions.register('session_user_display_name', category=N_('User'), comment=N_('Session User Display Name')) |
wcs/wf/register_comment.py | ||
---|---|---|
51 | 51 |
[(x or htmltext('</p><p>')) for x in self.content.splitlines()]) + \ |
52 | 52 |
htmltext('</p>') |
53 | 53 | |
54 |
def get_json_export_dict(self, anonymise=False): |
|
55 |
d = { |
|
56 |
'type': 'workflow-comment', |
|
57 |
} |
|
58 |
if not anonymise: |
|
59 |
d['content'] = self.content |
|
60 |
return d |
|
61 | ||
54 | 62 | |
55 | 63 |
class RegisterCommenterWorkflowStatusItem(WorkflowStatusItem): |
56 | 64 |
description = N_('Record in Log') |
wcs/wf/wscall.py | ||
---|---|---|
79 | 79 |
r += htmltext('</div>') |
80 | 80 |
return r.getvalue() |
81 | 81 | |
82 |
def get_json_export_dict(self, anonymise=False): |
|
83 |
d = { |
|
84 |
'type': 'wscall-error', |
|
85 |
} |
|
86 |
if not anonymise: |
|
87 |
d.update({ |
|
88 |
'summary': self.summary, |
|
89 |
'label': self.label, |
|
90 |
'data': self.data, |
|
91 |
}) |
|
92 |
return d |
|
93 | ||
82 | 94 | |
83 | 95 |
class WebserviceCallStatusItem(WorkflowStatusItem): |
84 | 96 |
description = N_('Webservice Call') |
85 |
- |