Skip to content

Commit ac57d78

Browse files
committed
choices everywhere
1 parent 48cdaa2 commit ac57d78

9 files changed

Lines changed: 170 additions & 87 deletions

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ Use `custom` to attach arbitrary data to a transition.
240240

241241
```python
242242
import django_fsm as fsm
243+
243244
@fsm.transition(
244245
field=state,
245246
source='*',
@@ -257,6 +258,7 @@ state.
257258

258259
```python
259260
import django_fsm as fsm
261+
260262
@fsm.transition(
261263
field=state,
262264
source='new',
@@ -276,6 +278,7 @@ accepts a permission string or a callable that receives `(instance, user)`.
276278

277279
```python
278280
import django_fsm as fsm
281+
279282
@fsm.transition(
280283
field=state,
281284
source='*',
@@ -461,6 +464,7 @@ class MyAdmin(fsm.admin.FSMAdminMixin, admin.ModelAdmin):
461464

462465
``` python
463466
import django_fsm as fsm
467+
464468
@fsm.transition(
465469
field='state',
466470
source=['startstate'],
@@ -478,9 +482,9 @@ or by overriding some methods in FSMAdminMixin
478482

479483
``` python
480484
import django_fsm as fsm
481-
import django_fsm.admin
485+
from django_fsm.admin import FSMAdminMixin
482486
@admin.register(AdminBlogPost)
483-
class MyAdmin(fsm.admin.FSMAdminMixin, admin.ModelAdmin):
487+
class MyAdmin(FSMAdminMixin, admin.ModelAdmin):
484488
...
485489

486490
def get_fsm_label(self, transition): # this method
@@ -499,7 +503,6 @@ class MyAdmin(fsm.admin.FSMAdminMixin, admin.ModelAdmin):
499503
4. Hiding a transition is possible by adding ``custom={"admin": False}`` to the transition decorator:
500504

501505
``` python
502-
import django_fsm as fsm
503506
@fsm.transition(
504507
field='state',
505508
source=['startstate'],
@@ -532,9 +535,10 @@ Then one must explicitly allow that a transition method shows up in the admin in
532535

533536
``` python
534537
import django_fsm as fsm
535-
import django_fsm.admin
538+
from django_fsm.admin import FSMAdminMixin
539+
536540
@admin.register(AdminBlogPost)
537-
class MyAdmin(fsm.admin.FSMAdminMixin, admin.ModelAdmin):
541+
class MyAdmin(FSMAdminMixin, admin.ModelAdmin):
538542
fsm_default_disallow_transition = False
539543
...
540544
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ DJANGO_SETTINGS_MODULE = "tests.settings"
7979

8080
[tool.ruff]
8181
line-length = 100
82-
target-version = "py310"
8382
fix = true
8483

8584
[tool.ruff.lint]
@@ -113,6 +112,7 @@ fixable = [
113112
[tool.ruff.lint.extend-per-file-ignores]
114113
"tests/*" = [
115114
"DJ008", # Model does not define `__str__` method
115+
"TID252", # Prefer absolute imports over relative imports from parent modules
116116
]
117117

118118
[tool.ruff.lint.isort]

tests/testapp/choices.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
from django.db import models
4+
5+
6+
class ApplicationState(models.TextChoices):
7+
NEW = "new", "New"
8+
FAILED = "failed", "Failed"
9+
PUBLISHED = "published", "Published"
10+
BLOCKED = "blocked", "Blocked"
11+
HIDDEN = "hidden", "Hidden"
12+
REJECTED = "rejected", "Rejected"
13+
MODERATED = "moderated", "Moderated"
14+
15+
REMOVED = "removed", "Removed"
16+
STOLEN = "stolen", "Stolen"

tests/testapp/models.py

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,44 @@
77

88
import django_fsm as fsm
99

10+
from .choices import ApplicationState
11+
1012

1113
class Application(models.Model):
1214
"""
1315
Student application need to be approved by dept chair and dean.
1416
Test workflow
1517
"""
1618

17-
state = fsm.FSMField(default="new")
19+
state = fsm.FSMField(default=ApplicationState.NEW)
1820

19-
@fsm.transition(field=state, source="new", target="published", on_error="failed")
21+
@fsm.transition(
22+
field=state,
23+
source=ApplicationState.NEW,
24+
target=ApplicationState.PUBLISHED,
25+
on_error=ApplicationState.FAILED,
26+
)
2027
def standard(self) -> None:
2128
pass
2229

23-
@fsm.transition(field=state, source="published")
30+
@fsm.transition(field=state, source=ApplicationState.PUBLISHED)
2431
def no_target(self) -> None:
2532
pass
2633

27-
@fsm.transition(field=state, source=fsm.ANY_STATE, target="blocked")
34+
@fsm.transition(field=state, source=fsm.ANY_STATE, target=ApplicationState.BLOCKED)
2835
def any_source(self) -> None:
2936
pass
3037

31-
@fsm.transition(field=state, source=fsm.ANY_OTHER_STATE, target="hidden")
38+
@fsm.transition(field=state, source=fsm.ANY_OTHER_STATE, target=ApplicationState.HIDDEN)
3239
def any_source_except_target(self) -> None:
3340
pass
3441

3542
@fsm.transition(
3643
field=state,
37-
source="new",
44+
source=ApplicationState.NEW,
3845
target=fsm.GET_STATE(
39-
lambda _, allowed: "published" if allowed else "rejected",
40-
states=["published", "rejected"],
46+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
47+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
4148
),
4249
)
4350
def get_state(self, *, allowed: bool) -> None:
@@ -47,8 +54,8 @@ def get_state(self, *, allowed: bool) -> None:
4754
field=state,
4855
source=fsm.ANY_STATE,
4956
target=fsm.GET_STATE(
50-
lambda _, allowed: "published" if allowed else "rejected",
51-
states=["published", "rejected"],
57+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
58+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
5259
),
5360
)
5461
def get_state_any_source(self, *, allowed: bool) -> None:
@@ -58,30 +65,43 @@ def get_state_any_source(self, *, allowed: bool) -> None:
5865
field=state,
5966
source=fsm.ANY_OTHER_STATE,
6067
target=fsm.GET_STATE(
61-
lambda _, allowed: "published" if allowed else "rejected",
62-
states=["published", "rejected"],
68+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
69+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
6370
),
6471
)
6572
def get_state_any_source_except_target(self, *, allowed: bool) -> None:
6673
pass
6774

68-
@fsm.transition(field=state, source="new", target=fsm.RETURN_VALUE("moderated", "blocked"))
75+
@fsm.transition(
76+
field=state,
77+
source=ApplicationState.NEW,
78+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
79+
)
6980
def return_value(self) -> str:
70-
return "published"
81+
return ApplicationState.PUBLISHED
7182

7283
@fsm.transition(
73-
field=state, source=fsm.ANY_STATE, target=fsm.RETURN_VALUE("moderated", "blocked")
84+
field=state,
85+
source=fsm.ANY_STATE,
86+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
7487
)
7588
def return_value_any_source(self) -> str:
76-
return "published"
89+
return ApplicationState.PUBLISHED
7790

7891
@fsm.transition(
79-
field=state, source=fsm.ANY_OTHER_STATE, target=fsm.RETURN_VALUE("moderated", "blocked")
92+
field=state,
93+
source=fsm.ANY_OTHER_STATE,
94+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
8095
)
8196
def return_value_any_source_except_target(self) -> str:
82-
return "published"
97+
return ApplicationState.PUBLISHED
8398

84-
@fsm.transition(field=state, source="new", target="published", on_error="failed")
99+
@fsm.transition(
100+
field=state,
101+
source=ApplicationState.NEW,
102+
target=ApplicationState.PUBLISHED,
103+
on_error=ApplicationState.FAILED,
104+
)
85105
def on_error(self) -> None:
86106
pass
87107

@@ -105,30 +125,30 @@ class FKApplication(models.Model):
105125
Test workflow for FSMKeyField
106126
"""
107127

108-
state = fsm.FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
128+
state = fsm.FSMKeyField(DbState, default=ApplicationState.NEW, on_delete=models.CASCADE)
109129

110-
@fsm.transition(field=state, source="new", target="published")
130+
@fsm.transition(field=state, source=ApplicationState.NEW, target=ApplicationState.PUBLISHED)
111131
def standard(self) -> None:
112132
pass
113133

114-
@fsm.transition(field=state, source="published")
134+
@fsm.transition(field=state, source=ApplicationState.PUBLISHED)
115135
def no_target(self) -> None:
116136
pass
117137

118-
@fsm.transition(field=state, source=fsm.ANY_STATE, target="blocked")
138+
@fsm.transition(field=state, source=fsm.ANY_STATE, target=ApplicationState.BLOCKED)
119139
def any_source(self) -> None:
120140
pass
121141

122-
@fsm.transition(field=state, source=fsm.ANY_OTHER_STATE, target="hidden")
142+
@fsm.transition(field=state, source=fsm.ANY_OTHER_STATE, target=ApplicationState.HIDDEN)
123143
def any_source_except_target(self) -> None:
124144
pass
125145

126146
@fsm.transition(
127147
field=state,
128-
source="new",
148+
source=ApplicationState.NEW,
129149
target=fsm.GET_STATE(
130-
lambda _, allowed: "published" if allowed else "rejected",
131-
states=["published", "rejected"],
150+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
151+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
132152
),
133153
)
134154
def get_state(self, *, allowed: bool) -> None:
@@ -138,8 +158,8 @@ def get_state(self, *, allowed: bool) -> None:
138158
field=state,
139159
source=fsm.ANY_STATE,
140160
target=fsm.GET_STATE(
141-
lambda _, allowed: "published" if allowed else "rejected",
142-
states=["published", "rejected"],
161+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
162+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
143163
),
144164
)
145165
def get_state_any_source(self, *, allowed: bool) -> None:
@@ -149,38 +169,53 @@ def get_state_any_source(self, *, allowed: bool) -> None:
149169
field=state,
150170
source=fsm.ANY_OTHER_STATE,
151171
target=fsm.GET_STATE(
152-
lambda _, allowed: "published" if allowed else "rejected",
153-
states=["published", "rejected"],
172+
lambda _, allowed: ApplicationState.PUBLISHED if allowed else ApplicationState.REJECTED,
173+
states=[ApplicationState.PUBLISHED, ApplicationState.REJECTED],
154174
),
155175
)
156176
def get_state_any_source_except_target(self, *, allowed: bool) -> None:
157177
pass
158178

159-
@fsm.transition(field=state, source="new", target=fsm.RETURN_VALUE("moderated", "blocked"))
179+
@fsm.transition(
180+
field=state,
181+
source=ApplicationState.NEW,
182+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
183+
)
160184
def return_value(self) -> str:
161185
return "published"
162186

163187
@fsm.transition(
164-
field=state, source=fsm.ANY_STATE, target=fsm.RETURN_VALUE("moderated", "blocked")
188+
field=state,
189+
source=fsm.ANY_STATE,
190+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
165191
)
166192
def return_value_any_source(self) -> str:
167193
return "published"
168194

169195
@fsm.transition(
170-
field=state, source=fsm.ANY_OTHER_STATE, target=fsm.RETURN_VALUE("moderated", "blocked")
196+
field=state,
197+
source=fsm.ANY_OTHER_STATE,
198+
target=fsm.RETURN_VALUE(ApplicationState.MODERATED, ApplicationState.BLOCKED),
171199
)
172200
def return_value_any_source_except_target(self) -> str:
173201
return "published"
174202

175-
@fsm.transition(field=state, source="new", target="published", on_error="failed")
203+
@fsm.transition(
204+
field=state,
205+
source=ApplicationState.NEW,
206+
target=ApplicationState.PUBLISHED,
207+
on_error=ApplicationState.FAILED,
208+
)
176209
def on_error(self) -> None:
177210
pass
178211

179212

180213
class MultiStateApplication(Application):
181-
another_state = fsm.FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
214+
another_state = fsm.FSMKeyField(DbState, default=ApplicationState.NEW, on_delete=models.CASCADE)
182215

183-
@fsm.transition(field=another_state, source="new", target="published")
216+
@fsm.transition(
217+
field=another_state, source=ApplicationState.NEW, target=ApplicationState.PUBLISHED
218+
)
184219
def another_state_standard(self) -> None:
185220
pass
186221

tests/testapp/tests/test_abstract_inheritance.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
import django_fsm as fsm
77

88

9+
class StateChoice(models.TextChoices):
10+
NEW = "NEW", "new"
11+
PUBLISHED = "PUBLISHED", "published"
12+
STICKED = "STICKED", "sticked"
13+
14+
915
class BaseAbstractModel(models.Model):
10-
state = fsm.FSMField(default="new")
16+
state = fsm.FSMField(choices=StateChoice.choices, default=StateChoice.NEW)
1117

1218
class Meta:
1319
abstract = True
1420

15-
@fsm.transition(field=state, source="new", target="published")
21+
@fsm.transition(field=state, source=StateChoice.NEW, target=StateChoice.PUBLISHED)
1622
def publish(self):
1723
pass
1824

@@ -24,13 +30,13 @@ class AnotherFromAbstractModel(BaseAbstractModel):
2430
Don't try to remove it.
2531
"""
2632

27-
@fsm.transition(field="state", source="published", target="sticked")
33+
@fsm.transition(field="state", source=StateChoice.PUBLISHED, target=StateChoice.STICKED)
2834
def stick(self):
2935
pass
3036

3137

3238
class InheritedFromAbstractModel(BaseAbstractModel):
33-
@fsm.transition(field="state", source="published", target="sticked")
39+
@fsm.transition(field="state", source=StateChoice.PUBLISHED, target=StateChoice.STICKED)
3440
def stick(self):
3541
pass
3642

@@ -42,20 +48,21 @@ def setUp(self):
4248
def test_known_transition_should_succeed(self):
4349
assert fsm.can_proceed(self.model.publish)
4450
self.model.publish()
45-
assert self.model.state == "published"
51+
assert self.model.state == StateChoice.PUBLISHED
4652

4753
assert fsm.can_proceed(self.model.stick)
4854
self.model.stick()
49-
assert self.model.state == "sticked"
55+
assert self.model.state == StateChoice.STICKED
5056

5157
def test_field_available_transitions_works(self):
5258
self.model.publish()
53-
assert self.model.state == "published"
59+
assert self.model.state == StateChoice.PUBLISHED
5460
transitions = self.model.get_available_state_transitions() # type: ignore[attr-defined]
55-
assert [data.target for data in transitions] == ["sticked"]
61+
assert [data.target for data in transitions] == [StateChoice.STICKED]
5662

5763
def test_field_all_transitions_works(self):
5864
transitions = self.model.get_all_state_transitions() # type: ignore[attr-defined]
59-
assert {("new", "published"), ("published", "sticked")} == {
60-
(data.source, data.target) for data in transitions
61-
}
65+
assert {
66+
(StateChoice.NEW, StateChoice.PUBLISHED),
67+
(StateChoice.PUBLISHED, StateChoice.STICKED),
68+
} == {(data.source, data.target) for data in transitions}

0 commit comments

Comments
 (0)