Skip to content

Commit 290bc49

Browse files
committed
Test and fix graph_transition command
1 parent 987a11e commit 290bc49

File tree

4 files changed

+144
-86
lines changed

4 files changed

+144
-86
lines changed

django_fsm/management/commands/graph_transitions.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,28 @@ def generate_dot(fields_data): # noqa: C901, PLR0912
3535

3636
# dump nodes and edges
3737
for transition in field.get_all_transitions(model):
38-
if transition.source == "*":
39-
any_targets.add((transition.target, transition.name))
40-
elif transition.source == "+":
41-
any_except_targets.add((transition.target, transition.name))
42-
else:
43-
_targets = (
44-
(state for state in transition.target.allowed_states)
45-
if isinstance(transition.target, (GET_STATE, RETURN_VALUE))
46-
else (transition.target,)
47-
)
48-
source_name_pair = (
49-
((state, node_name(field, state)) for state in transition.source.allowed_states)
50-
if isinstance(transition.source, (GET_STATE, RETURN_VALUE))
51-
else ((transition.source, node_name(field, transition.source)),)
52-
)
53-
for source, source_name in source_name_pair:
54-
if transition.on_error:
55-
on_error_name = node_name(field, transition.on_error)
56-
targets.add((on_error_name, node_label(field, transition.on_error)))
57-
edges.add((source_name, on_error_name, (("style", "dotted"),)))
58-
for target in _targets:
38+
_targets = list(
39+
(state for state in transition.target.allowed_states)
40+
if isinstance(transition.target, (GET_STATE, RETURN_VALUE))
41+
else (transition.target,)
42+
)
43+
source_name_pair = (
44+
((state, node_name(field, state)) for state in transition.source.allowed_states)
45+
if isinstance(transition.source, (GET_STATE, RETURN_VALUE))
46+
else ((transition.source, node_name(field, transition.source)),)
47+
)
48+
for source, source_name in source_name_pair:
49+
if transition.on_error:
50+
on_error_name = node_name(field, transition.on_error)
51+
targets.add((on_error_name, node_label(field, transition.on_error)))
52+
edges.add((source_name, on_error_name, (("style", "dotted"),)))
53+
54+
for target in _targets:
55+
if transition.source == "*":
56+
any_targets.add((target, transition.name))
57+
elif transition.source == "+":
58+
any_except_targets.add((target, transition.name))
59+
else:
5960
add_transition(source, target, transition.name, source_name, field, sources, targets, edges)
6061

6162
targets.update(

tests/testapp/models.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from django.db import models
44

5+
from django_fsm import GET_STATE
6+
from django_fsm import RETURN_VALUE
57
from django_fsm import FSMField
68
from django_fsm import FSMKeyField
79
from django_fsm import transition
@@ -15,28 +17,69 @@ class Application(models.Model):
1517

1618
state = FSMField(default="new")
1719

18-
@transition(field=state, source="new", target="draft")
19-
def draft(self):
20+
@transition(field=state, source="new", target="published")
21+
def standard(self):
2022
pass
2123

22-
@transition(field=state, source=["new", "draft"], target="dept")
23-
def submitted(self):
24+
@transition(field=state, source="published")
25+
def no_target(self):
26+
pass
27+
28+
@transition(field=state, source="*", target="blocked")
29+
def any_source(self):
30+
pass
31+
32+
@transition(field=state, source="+", target="hidden")
33+
def any_source_except_target(self):
2434
pass
2535

26-
@transition(field=state, source="dept", target="dean")
27-
def dept_approved(self):
36+
@transition(
37+
field=state,
38+
source="new",
39+
target=GET_STATE(
40+
lambda _, allowed: "published" if allowed else "rejected",
41+
states=["published", "rejected"],
42+
),
43+
)
44+
def get_state(self, *, allowed: bool):
2845
pass
2946

30-
@transition(field=state, source="dept", target="new")
31-
def dept_rejected(self):
47+
@transition(
48+
field=state,
49+
source="*",
50+
target=GET_STATE(
51+
lambda _, allowed: "published" if allowed else "rejected",
52+
states=["published", "rejected"],
53+
),
54+
)
55+
def get_state_any_source(self, *, allowed: bool):
3256
pass
3357

34-
@transition(field=state, source="dean", target="done")
35-
def dean_approved(self):
58+
@transition(
59+
field=state,
60+
source="+",
61+
target=GET_STATE(
62+
lambda _, allowed: "published" if allowed else "rejected",
63+
states=["published", "rejected"],
64+
),
65+
)
66+
def get_state_any_source_except_target(self, *, allowed: bool):
3667
pass
3768

38-
@transition(field=state, source="dean", target="dept")
39-
def dean_rejected(self):
69+
@transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked"))
70+
def return_value(self):
71+
return "published"
72+
73+
@transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked"))
74+
def return_value_any_source(self):
75+
return "published"
76+
77+
@transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked"))
78+
def return_value_any_source_except_target(self):
79+
return "published"
80+
81+
@transition(field=state, source="new", target="published", on_error="failed")
82+
def on_error(self):
4083
pass
4184

4285

@@ -61,28 +104,69 @@ class FKApplication(models.Model):
61104

62105
state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
63106

64-
@transition(field=state, source="new", target="draft")
65-
def draft(self):
107+
@transition(field=state, source="new", target="published")
108+
def standard(self):
66109
pass
67110

68-
@transition(field=state, source=["new", "draft"], target="dept")
69-
def submitted(self):
111+
@transition(field=state, source="published")
112+
def no_target(self):
113+
pass
114+
115+
@transition(field=state, source="*", target="blocked")
116+
def any_source(self):
117+
pass
118+
119+
@transition(field=state, source="+", target="hidden")
120+
def any_source_except_target(self):
70121
pass
71122

72-
@transition(field=state, source="dept", target="dean")
73-
def dept_approved(self):
123+
@transition(
124+
field=state,
125+
source="new",
126+
target=GET_STATE(
127+
lambda _, allowed: "published" if allowed else "rejected",
128+
states=["published", "rejected"],
129+
),
130+
)
131+
def get_state(self, *, allowed: bool):
74132
pass
75133

76-
@transition(field=state, source="dept", target="new")
77-
def dept_rejected(self):
134+
@transition(
135+
field=state,
136+
source="*",
137+
target=GET_STATE(
138+
lambda _, allowed: "published" if allowed else "rejected",
139+
states=["published", "rejected"],
140+
),
141+
)
142+
def get_state_any_source(self, *, allowed: bool):
78143
pass
79144

80-
@transition(field=state, source="dean", target="done")
81-
def dean_approved(self):
145+
@transition(
146+
field=state,
147+
source="+",
148+
target=GET_STATE(
149+
lambda _, allowed: "published" if allowed else "rejected",
150+
states=["published", "rejected"],
151+
),
152+
)
153+
def get_state_any_source_except_target(self, *, allowed: bool):
82154
pass
83155

84-
@transition(field=state, source="dean", target="dept")
85-
def dean_rejected(self):
156+
@transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked"))
157+
def return_value(self):
158+
return "published"
159+
160+
@transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked"))
161+
def return_value_any_source(self):
162+
return "published"
163+
164+
@transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked"))
165+
def return_value_any_source_except_target(self):
166+
return "published"
167+
168+
@transition(field=state, source="new", target="published", on_error="failed")
169+
def on_error(self):
86170
pass
87171

88172

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,22 @@
11
from __future__ import annotations
22

33
from django.core.management import call_command
4-
from django.db import models
54
from django.test import TestCase
65

7-
from django_fsm import FSMField
8-
from django_fsm import transition
6+
from django_fsm.management.commands.graph_transitions import get_graphviz_layouts
97

108

11-
class VisualBlogPost(models.Model):
12-
state = FSMField(default="new")
13-
14-
@transition(field=state, source="new", target="published")
15-
def publish(self):
16-
pass
17-
18-
@transition(source="published", field=state)
19-
def notify_all(self):
20-
pass
21-
22-
@transition(source="published", target="hidden", field=state)
23-
def hide(self):
24-
pass
25-
26-
@transition(source="new", target="removed", field=state)
27-
def remove(self):
28-
raise Exception("Upss")
29-
30-
@transition(source=["published", "hidden"], target="stolen", field=state)
31-
def steal(self):
32-
pass
33-
34-
@transition(source="*", target="moderated", field=state)
35-
def moderate(self):
36-
pass
37-
38-
@transition(source="+", target="blocked", field=state)
39-
def block(self):
40-
pass
9+
class GraphTransitionsCommandTest(TestCase):
10+
def test_dummy(self):
11+
call_command("graph_transitions", "testapp.Application")
4112

42-
@transition(source="*", target="", field=state)
43-
def empty(self):
44-
pass
13+
def test_layouts(self):
14+
for layout in get_graphviz_layouts():
15+
call_command("graph_transitions", "-l", layout, "testapp.Application")
4516

17+
def test_fk_dummy(self):
18+
call_command("graph_transitions", "testapp.FKApplication")
4619

47-
class GraphTransitionsCommandTest(TestCase):
48-
def test_dummy(self):
49-
call_command("graph_transitions", "testapp.VisualBlogPost")
20+
def test_fk_layouts(self):
21+
for layout in get_graphviz_layouts():
22+
call_command("graph_transitions", "-l", layout, "testapp.FKApplication")

tests/testapp/tests/test_transition_all_except_target.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django_fsm import transition
99

1010

11-
class TestExceptTargetTransitionShortcut(models.Model):
11+
class ExceptTargetTransition(models.Model):
1212
state = FSMField(default="new")
1313

1414
@transition(field=state, source="new", target="published")
@@ -22,7 +22,7 @@ def remove(self):
2222

2323
class Test(TestCase):
2424
def setUp(self):
25-
self.model = TestExceptTargetTransitionShortcut()
25+
self.model = ExceptTargetTransition()
2626

2727
def test_usecase(self):
2828
assert self.model.state == "new"

0 commit comments

Comments
 (0)