0001-workflows-add-actions-tracing-54497.patch
tests/api/test_workflow.py | ||
---|---|---|
11 | 11 |
from wcs.qommon.http_request import HTTPRequest |
12 | 12 |
from wcs.qommon.ident.password_accounts import PasswordAccount |
13 | 13 |
from wcs.wf.jump import JumpWorkflowStatusItem |
14 |
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem |
|
14 |
from wcs.wf.register_comment import JournalEvolutionPart, RegisterCommenterWorkflowStatusItem
|
|
15 | 15 |
from wcs.workflows import Workflow |
16 | 16 | |
17 | 17 |
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app |
... | ... | |
371 | 371 |
assert formdef.data_class().get(formdata.id).evolution[-1].who is None |
372 | 372 | |
373 | 373 | |
374 |
def get_latest_comment(formdata): |
|
375 |
for evolution in reversed(formdata.evolution): |
|
376 |
for part in reversed(evolution.parts): |
|
377 |
if isinstance(part, JournalEvolutionPart): |
|
378 |
return part.content |
|
379 | ||
380 | ||
374 | 381 |
def test_workflow_global_webservice_trigger(pub, local_user): |
375 | 382 |
workflow = Workflow(name='test') |
376 | 383 |
workflow.add_status('Status1', 'st1') |
... | ... | |
406 | 413 | |
407 | 414 |
# anonymous call |
408 | 415 |
get_app(pub).post(formdata.get_url() + 'hooks/plop/', status=200) |
409 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD'
|
|
416 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD'
|
|
410 | 417 | |
411 | 418 |
add_to_journal.comment = 'HELLO WORLD 2' |
412 | 419 |
workflow.store() |
413 | 420 |
get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=200) |
414 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 2'
|
|
421 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 2'
|
|
415 | 422 | |
416 | 423 |
# call requiring user |
417 | 424 |
add_to_journal.comment = 'HELLO WORLD 3' |
... | ... | |
419 | 426 |
workflow.store() |
420 | 427 |
get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=403) |
421 | 428 |
get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/'), status=200) |
422 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 3'
|
|
429 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 3'
|
|
423 | 430 | |
424 | 431 |
# call requiring roles |
425 | 432 |
add_to_journal.comment = 'HELLO WORLD 4' |
... | ... | |
436 | 443 |
local_user.roles = [role.id] |
437 | 444 |
local_user.store() |
438 | 445 |
get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), status=200) |
439 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 4'
|
|
446 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 4'
|
|
440 | 447 | |
441 | 448 |
# call adding data |
442 | 449 |
add_to_journal.comment = 'HELLO {{plop_test}}' |
... | ... | |
445 | 452 |
sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), {'test': 'foobar'}, status=200 |
446 | 453 |
) |
447 | 454 |
# (django templating make it turn into HTML) |
448 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == '<div>HELLO foobar</div>'
|
|
455 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == '<div>HELLO foobar</div>'
|
|
449 | 456 | |
450 | 457 |
# call adding data but with no actions |
451 | 458 |
ac1.items = [] |
... | ... | |
491 | 498 | |
492 | 499 |
# anonymous call |
493 | 500 |
get_app(pub).post(formdata.get_url() + 'hooks/plop', status=200) |
494 |
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD' |
|
501 |
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD' |
wcs/api.py | ||
---|---|---|
165 | 165 | |
166 | 166 |
if item.status: |
167 | 167 |
self.formdata.jump_status(item.status) |
168 |
self.formdata.perform_workflow() |
|
168 |
self.formdata.perform_workflow(event=('api-post-edit-action', item.id))
|
|
169 | 169 | |
170 | 170 |
return json.dumps({'err': 0, 'data': {'id': str(self.formdata.id)}}) |
171 | 171 | |
... | ... | |
324 | 324 |
formdata.store() |
325 | 325 |
formdata.just_created() |
326 | 326 |
formdata.store() |
327 |
formdata.perform_workflow() |
|
327 |
formdata.perform_workflow(event='api-created')
|
|
328 | 328 |
formdata.store() |
329 | 329 |
return json.dumps( |
330 | 330 |
{ |
... | ... | |
599 | 599 |
else: |
600 | 600 |
formdata.just_created() |
601 | 601 |
formdata.store() |
602 |
formdata.perform_workflow() |
|
602 |
formdata.perform_workflow(event='api-created')
|
|
603 | 603 |
formdata.store() |
604 | 604 |
return json.dumps( |
605 | 605 |
{ |
wcs/backoffice/data_management.py | ||
---|---|---|
370 | 370 |
data_instance.data = item |
371 | 371 |
data_instance.just_created() |
372 | 372 |
data_instance.store() |
373 |
data_instance.perform_workflow() |
|
373 |
data_instance.perform_workflow(event='csv-import-created')
|
|
374 | 374 | |
375 | 375 |
def done_action_url(self): |
376 | 376 |
carddef = self.kwargs['carddef_class'].get(self.kwargs['carddef_id']) |
wcs/backoffice/submission.py | ||
---|---|---|
344 | 344 |
return self.redirect_after_submitted(form, filled) |
345 | 345 | |
346 | 346 |
def redirect_after_submitted(self, form, filled): |
347 |
url = filled.perform_workflow() |
|
347 |
url = filled.perform_workflow(event='backoffice-created')
|
|
348 | 348 |
if url: |
349 | 349 |
pass # always redirect to an URL the workflow returned |
350 | 350 |
elif not self.formdef.is_of_concern_for_user(self.user, filled): |
wcs/formdata.py | ||
---|---|---|
516 | 516 |
current_level = current_level - 100 |
517 | 517 |
return levels[current_level] |
518 | 518 | |
519 |
def perform_workflow(self): |
|
519 |
def perform_workflow(self, event=None):
|
|
520 | 520 |
url = None |
521 | 521 |
get_publisher().substitutions.feed(self) |
522 | 522 |
wf_status = self.get_status() |
523 | 523 |
from wcs.workflows import perform_items |
524 | 524 | |
525 |
url = perform_items(wf_status.items, self) |
|
525 |
url = perform_items(wf_status.items, self, event=event)
|
|
526 | 526 |
return url |
527 | 527 | |
528 | 528 |
def perform_global_action(self, action_id, user): |
... | ... | |
531 | 531 |
for action in self.formdef.workflow.get_global_actions_for_user(formdata=self, user=user): |
532 | 532 |
if action.id != action_id: |
533 | 533 |
continue |
534 |
perform_items(action.items, self) |
|
534 |
perform_items(action.items, self, event=('global-action', action.id))
|
|
535 | 535 |
break |
536 | 536 | |
537 | 537 |
def get_workflow_messages(self, position='top'): |
... | ... | |
636 | 636 |
return None |
637 | 637 | |
638 | 638 |
def jump_status(self, status_id, user_id=None): |
639 |
from wcs.workflows import ActionsTracingEvolutionPart |
|
640 | ||
639 | 641 |
if status_id == '_previous': |
640 | 642 |
previous_status = self.pop_previous_marked_status() |
641 | 643 |
if not previous_status: |
... | ... | |
650 | 652 |
self.status == status |
651 | 653 |
and self.evolution[-1].status == status |
652 | 654 |
and not self.evolution[-1].comment |
653 |
and not self.evolution[-1].parts |
|
655 |
and not [ |
|
656 |
x for x in self.evolution[-1].parts or [] if not isinstance(x, ActionsTracingEvolutionPart) |
|
657 |
] |
|
654 | 658 |
): |
655 | 659 |
# if status do not change and last evolution is empty, |
656 | 660 |
# just update last jump time on last evolution, do not add one |
wcs/forms/root.py | ||
---|---|---|
1430 | 1430 |
url = None |
1431 | 1431 |
if existing_formdata is None: |
1432 | 1432 |
self.clean_submission_context() |
1433 |
url = filled.perform_workflow() |
|
1433 |
url = filled.perform_workflow(event='frontoffice-created')
|
|
1434 | 1434 | |
1435 | 1435 |
if not filled.user_id: |
1436 | 1436 |
get_session().mark_anonymous_formdata(filled) |
... | ... | |
1483 | 1483 |
wf_status = item.get_target_status(self.edited_data) |
1484 | 1484 |
if wf_status: |
1485 | 1485 |
self.edited_data.jump_status(wf_status[0].id, user_id=user_id) |
1486 |
url = self.edited_data.perform_workflow() |
|
1486 |
url = self.edited_data.perform_workflow(event=('edit-action', item.id))
|
|
1487 | 1487 |
else: |
1488 | 1488 |
# add history entry |
1489 | 1489 |
evo = Evolution() |
wcs/qommon/storage.py | ||
---|---|---|
865 | 865 |
def remove_self(self): |
866 | 866 |
assert not self.is_readonly() |
867 | 867 |
self.remove_object(self.id) |
868 |
self.id = None |
|
868 | 869 | |
869 | 870 |
def get_last_modification_info(self): |
870 | 871 |
if not get_publisher().snapshot_class: |
wcs/wf/create_formdata.py | ||
---|---|---|
507 | 507 |
new_formdata.store() |
508 | 508 |
if formdef.enable_tracking_codes: |
509 | 509 |
code.formdata = new_formdata # this will .store() the code |
510 |
new_formdata.perform_workflow() |
|
510 |
new_formdata.perform_workflow(event=('workflow-created', formdata.get_display_id()))
|
|
511 | 511 |
new_formdata.store() |
512 | 512 | |
513 | 513 |
# update local object as it may have been modified by new_formdata |
wcs/wf/jump.py | ||
---|---|---|
40 | 40 |
JUMP_TIMEOUT_INTERVAL = max((60 // int(os.environ.get('WCS_JUMP_TIMEOUT_CHECKS', '3')), 1)) |
41 | 41 | |
42 | 42 | |
43 |
def jump_and_perform(formdata, action, workflow_data=None): |
|
43 |
def jump_and_perform(formdata, action, workflow_data=None, event=None):
|
|
44 | 44 |
action.handle_markers_stack(formdata) |
45 | 45 |
if workflow_data: |
46 | 46 |
formdata.update_workflow_data(workflow_data) |
47 | 47 |
formdata.store() |
48 | 48 |
formdata.jump_status(action.status) |
49 |
url = formdata.perform_workflow() |
|
49 |
url = formdata.perform_workflow(event=event)
|
|
50 | 50 |
return url |
51 | 51 | |
52 | 52 | |
... | ... | |
89 | 89 |
workflow_data = None |
90 | 90 |
if hasattr(get_request(), '_json'): |
91 | 91 |
workflow_data = get_request().json |
92 |
url = jump_and_perform(self.formdata, item, workflow_data=workflow_data) |
|
92 |
url = jump_and_perform( |
|
93 |
self.formdata, item, workflow_data=workflow_data, event=('api-trigger', item.trigger) |
|
94 |
) |
|
93 | 95 |
else: |
94 | 96 |
if get_request().is_json(): |
95 | 97 |
get_response().status_code = 403 |
... | ... | |
354 | 356 |
get_publisher().substitutions.feed(formdef) |
355 | 357 |
get_publisher().substitutions.feed(formdata) |
356 | 358 |
if jump_action.must_jump(formdata): |
357 |
jump_and_perform(formdata, jump_action) |
|
359 |
jump_and_perform(formdata, jump_action, event=('timeout-jump', jump_action.id))
|
|
358 | 360 |
break |
359 | 361 | |
360 | 362 |
wcs/workflows.py | ||
---|---|---|
69 | 69 |
return -1 |
70 | 70 | |
71 | 71 | |
72 |
def perform_items(items, formdata, depth=20): |
|
72 |
def perform_items(items, formdata, depth=20, event=None):
|
|
73 | 73 |
if depth == 0: # prevents infinite loops |
74 | 74 |
return |
75 | 75 |
url = None |
76 | 76 |
old_status = formdata.status |
77 |
performed_actions = [] |
|
77 | 78 |
for item in items: |
79 |
if getattr(item.perform, 'empty', False): |
|
80 |
continue |
|
78 | 81 |
if not item.check_condition(formdata): |
79 | 82 |
continue |
83 |
performed_actions.append((datetime.datetime.now(), item.id)) |
|
80 | 84 |
try: |
81 | 85 |
url = item.perform(formdata) or url |
82 | 86 |
except AbortActionException as e: |
... | ... | |
84 | 88 |
break |
85 | 89 |
if formdata.status != old_status: |
86 | 90 |
break |
91 |
if performed_actions: |
|
92 |
latest_evolution = formdata.evolution[-1] if formdata.evolution else None |
|
93 |
if latest_evolution: |
|
94 |
latest_evolution.add_part(ActionsTracingEvolutionPart(event, performed_actions)) |
|
95 |
if formdata.id: |
|
96 |
# don't save formdata it has been removed |
|
97 |
formdata.store() |
|
87 | 98 |
if formdata.status != old_status: |
88 | 99 |
if not formdata.evolution: |
89 | 100 |
formdata.evolution = [] |
... | ... | |
94 | 105 |
formdata.store() |
95 | 106 |
# performs the items of the new status |
96 | 107 |
wf_status = formdata.get_status() |
97 |
url = perform_items(wf_status.items, formdata, depth=depth - 1) or url |
|
108 |
url = perform_items(wf_status.items, formdata, depth=depth - 1, event='continuation') or url
|
|
98 | 109 |
if url: |
99 | 110 |
# hack around webtest as it checks type(url) is str and |
100 | 111 |
# this won't work on django safe strings (isinstance would work); |
... | ... | |
319 | 330 |
) |
320 | 331 | |
321 | 332 | |
333 |
class ActionsTracingEvolutionPart(EvolutionPart): |
|
334 |
def __init__(self, event, actions): |
|
335 |
if isinstance(event, tuple): |
|
336 |
self.event = event[0] |
|
337 |
self.event_args = event[1:] |
|
338 |
else: |
|
339 |
self.event = event |
|
340 |
self.event_args = None |
|
341 |
self.actions = actions |
|
342 | ||
343 | ||
322 | 344 |
class DuplicateGlobalActionNameError(Exception): |
323 | 345 |
pass |
324 | 346 | |
... | ... | |
1458 | 1480 |
continue |
1459 | 1481 |
formdata.evolution[-1].add_part(WorkflowGlobalActionTimeoutTriggerMarker(trigger.id)) |
1460 | 1482 |
formdata.store() |
1461 |
perform_items(action.items, formdata) |
|
1483 |
perform_items( |
|
1484 |
action.items, |
|
1485 |
formdata, |
|
1486 |
event=('global-action-timeout', (action.id, trigger.id)), |
|
1487 |
) |
|
1462 | 1488 |
break |
1463 | 1489 | |
1464 | 1490 | |
... | ... | |
1705 | 1731 |
# check for global actions |
1706 | 1732 |
for action in filled.formdef.workflow.get_global_actions_for_user(filled, user): |
1707 | 1733 |
if 'button-action-%s' % action.id in get_request().form: |
1708 |
url = perform_items(action.items, filled) |
|
1734 |
url = perform_items(action.items, filled, event=('global-action-button', action.id))
|
|
1709 | 1735 |
if url: |
1710 | 1736 |
return url |
1711 | 1737 |
return |
... | ... | |
1745 | 1771 |
if evo.status: |
1746 | 1772 |
filled.status = evo.status |
1747 | 1773 |
filled.store() |
1748 |
url = filled.perform_workflow() |
|
1774 |
url = filled.perform_workflow(event='workflow-form-submit')
|
|
1749 | 1775 |
if url: |
1750 | 1776 |
return url |
1751 | 1777 | |
... | ... | |
1907 | 1933 |
return '<%s %s %r>' % (self.__class__.__name__, self.id, self.name) |
1908 | 1934 | |
1909 | 1935 | |
1936 |
def empty_mark(func): |
|
1937 |
# mark method as not executing anything |
|
1938 |
func.empty = True |
|
1939 |
return func |
|
1940 | ||
1941 | ||
1910 | 1942 |
class WorkflowStatusItem(XmlSerialisable): |
1911 | 1943 |
node_name = 'item' |
1912 | 1944 |
description = 'XX' |
... | ... | |
1959 | 1991 |
def get_add_role_label(self): |
1960 | 1992 |
return self.parent.parent.get_add_role_label() |
1961 | 1993 | |
1994 |
@empty_mark |
|
1962 | 1995 |
def perform(self, formdata): |
1963 | 1996 |
pass |
1964 | 1997 | |
1965 |
- |