From 78e1b614f7e901462f12c3a5e696a223217a383e Mon Sep 17 00:00:00 2001 From: Pascal F Date: Wed, 25 Jun 2025 14:04:00 +0200 Subject: [PATCH] Add 'ignore' to graph_transition command --- README.md | 3 ++ .../management/commands/graph_transitions.py | 32 +++++++++++++------ tests/testapp/tests/test_graph_transitions.py | 27 ++++++++++------ 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 882ea49..507f926 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,9 @@ $ ./manage.py graph_transitions > transitions.dot # Create a PNG image file only for specific model $ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog + +# Exclude some transitions +$ ./manage.py graph_transitions -e transition_1,transition_2 myapp.Blog ``` ## Extensions diff --git a/django_fsm/management/commands/graph_transitions.py b/django_fsm/management/commands/graph_transitions.py index d1cd6e6..ef8a5e0 100644 --- a/django_fsm/management/commands/graph_transitions.py +++ b/django_fsm/management/commands/graph_transitions.py @@ -16,18 +16,21 @@ def all_fsm_fields_data(model): return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)] -def node_name(field, state): +def node_name(field, state) -> str: opts = field.model._meta return "{}.{}.{}.{}".format(opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state) -def node_label(field, state): - if isinstance(state, int) or (isinstance(state, bool) and hasattr(field, "choices")): +def node_label(field, state) -> str: + if isinstance(state, int): + return str(state) + if isinstance(state, bool) and hasattr(field, "choices"): return force_str(dict(field.choices).get(state)) return state -def generate_dot(fields_data): # noqa: C901, PLR0912 +def generate_dot(fields_data, ignore_transitions: list[str] | None = None): # noqa: C901, PLR0912 + ignore_transitions = ignore_transitions or [] result = graphviz.Digraph() for field, model in fields_data: @@ -35,6 +38,9 @@ def generate_dot(fields_data): # noqa: C901, PLR0912 # dump nodes and edges for transition in field.get_all_transitions(model): + if transition.name in ignore_transitions: + continue + _targets = list( (state for state in transition.target.allowed_states) if isinstance(transition.target, (GET_STATE, RETURN_VALUE)) @@ -127,7 +133,7 @@ def add_arguments(self, parser): "-o", action="store", dest="outputfile", - help=("Render output file. Type of output dependent on file extensions. Use png or jpg to render graph to image."), + help="Render output file. Type of output dependent on file extensions. Use png or jpg to render graph to image.", ) parser.add_argument( "--layout", @@ -137,6 +143,14 @@ def add_arguments(self, parser): default="dot", help=f"Layout to be used by GraphViz for visualization. Layouts: {get_graphviz_layouts()}.", ) + parser.add_argument( + "--exclude", + "-e", + action="store", + dest="exclude", + default="", + help="Ignore transitions with this name.", + ) parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]")) def render_output(self, graph, **options): @@ -153,9 +167,8 @@ def handle(self, *args, **options): field_spec = arg.split(".") if len(field_spec) == 1: - app = apps.get_app(field_spec[0]) - models = apps.get_models(app) - for model in models: + app = apps.get_app_config(field_spec[0]) + for model in apps.get_models(app): fields_data += all_fsm_fields_data(model) if len(field_spec) == 2: # noqa: PLR2004 model = apps.get_model(field_spec[0], field_spec[1]) @@ -166,7 +179,8 @@ def handle(self, *args, **options): else: for model in apps.get_models(): fields_data += all_fsm_fields_data(model) - dotdata = generate_dot(fields_data) + + dotdata = generate_dot(fields_data, ignore_transitions=options["exclude"].split(",")) if options["outputfile"]: self.render_output(dotdata, **options) diff --git a/tests/testapp/tests/test_graph_transitions.py b/tests/testapp/tests/test_graph_transitions.py index d8f27ad..9c47b12 100644 --- a/tests/testapp/tests/test_graph_transitions.py +++ b/tests/testapp/tests/test_graph_transitions.py @@ -7,16 +7,23 @@ class GraphTransitionsCommandTest(TestCase): - def test_dummy(self): - call_command("graph_transitions", "testapp.Application") + MODELS_TO_TEST = [ + "testapp.Application", + "testapp.FKApplication", + ] - def test_layouts(self): - for layout in get_graphviz_layouts(): - call_command("graph_transitions", "-l", layout, "testapp.Application") + def test_app(self): + call_command("graph_transitions", "testapp") - def test_fk_dummy(self): - call_command("graph_transitions", "testapp.FKApplication") + def test_single_model(self): + for model in self.MODELS_TO_TEST: + call_command("graph_transitions", model) - def test_fk_layouts(self): - for layout in get_graphviz_layouts(): - call_command("graph_transitions", "-l", layout, "testapp.FKApplication") + def test_single_model_with_layouts(self): + for model in self.MODELS_TO_TEST: + for layout in get_graphviz_layouts(): + call_command("graph_transitions", "-l", layout, model) + + def test_exclude(self): + for model in self.MODELS_TO_TEST: + call_command("graph_transitions", "-e", "standard,no_target", model)