Projet

Général

Profil

0001-workflows-add-actions-tracing-54497.patch

Frédéric Péters, 14 juin 2021 16:01

Télécharger (16,9 ko)

Voir les différences:

Subject: [PATCH 1/2] workflows: add actions tracing (#54497)

 tests/api/test_workflow.py        | 21 ++++++++++-----
 wcs/api.py                        |  6 ++---
 wcs/backoffice/data_management.py |  2 +-
 wcs/backoffice/submission.py      |  2 +-
 wcs/formdata.py                   | 12 ++++++---
 wcs/forms/root.py                 |  4 +--
 wcs/qommon/storage.py             |  1 +
 wcs/wf/create_formdata.py         |  2 +-
 wcs/wf/jump.py                    | 10 ++++---
 wcs/workflows.py                  | 43 +++++++++++++++++++++++++++----
 10 files changed, 75 insertions(+), 28 deletions(-)
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
-