Skip to content

Commit 7c372e1

Browse files
committed
Make admin compatible with FSMIntegerField and FSMKeyField
1 parent 252ea44 commit 7c372e1

6 files changed

Lines changed: 34 additions & 20 deletions

File tree

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Changelog
44
Unreleased
55
~~~~~~~~~~
66

7+
django-fsm-2 4.2.3 2026-03-15
8+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9+
10+
- Make admin compatible with FSMIntegerField and FSMKeyField
11+
712

813
django-fsm-2 4.2.2 2026-03-14
914
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

django_fsm/admin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ class FSMAdminMixin(_ModelAdmin):
7171

7272
@override
7373
def __init__(self, model: type[fsm._FSMModel], admin_site: admin.AdminSite) -> None:
74-
if not self.fsm_fields: # pragma: no cover
74+
if not self.fsm_fields:
7575
# django-fsm-admin retro compatibility
76-
if hasattr(self, "fsm_field"):
76+
if hasattr(self, "fsm_field"): # pragma: no cover
7777
logger.warning(
7878
"'fsm_field' declaration is deprecated, please update to 'fsm_fields'"
7979
)
@@ -92,14 +92,14 @@ def get_readonly_fields(
9292
read_only_fields = list(super().get_readonly_fields(request, obj))
9393

9494
for fsm_field_name in self.fsm_fields:
95-
if fsm_field_name in read_only_fields: # pragma: no cover
96-
continue
97-
9895
field = self.model._meta.get_field(fsm_field_name)
9996

100-
if not isinstance(field, fsm.FSMField): # pragma: no cover
97+
if not isinstance(field, fsm.FSMFieldMixin):
10198
raise ImproperlyConfigured(f"'{fsm_field_name}' is not an FSMField")
10299

100+
if fsm_field_name in read_only_fields: # pragma: no cover
101+
continue
102+
103103
if getattr(field, "protected", False):
104104
read_only_fields.append(fsm_field_name)
105105

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-fsm-2"
3-
version = "4.2.2"
3+
version = "4.2.3"
44
description = "Django friendly finite state machine support."
55
authors = [{ name = "Mikhail Podgurskiy", email = "kmmbvnr@gmail.com" }]
66
requires-python = ">=3.10"

tests/testapp/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class AdminBlogPostAdmin(FSMAdminMixin, admin.ModelAdmin[AdminBlogPost]):
3131
fsm_fields = [
3232
"state",
3333
"step",
34+
"key_state",
3435
]
3536

3637
fsm_forms = {

tests/testapp/models.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import django_fsm as fsm
99
from django_fsm import GET_STATE
1010
from django_fsm import RETURN_VALUE
11-
from django_fsm import FSMField
12-
from django_fsm import FSMKeyField
1311
from django_fsm import transition
1412

1513

@@ -19,7 +17,7 @@ class Application(models.Model):
1917
Test workflow
2018
"""
2119

22-
state = FSMField(default="new")
20+
state = fsm.FSMField(default="new")
2321

2422
@transition(field=state, source="new", target="published", on_error="failed")
2523
def standard(self) -> None:
@@ -106,7 +104,7 @@ class FKApplication(models.Model):
106104
Test workflow for FSMKeyField
107105
"""
108106

109-
state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
107+
state = fsm.FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
110108

111109
@transition(field=state, source="new", target="published")
112110
def standard(self) -> None:
@@ -175,7 +173,7 @@ def on_error(self) -> None:
175173

176174

177175
class MultiStateApplication(Application):
178-
another_state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
176+
another_state = fsm.FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
179177

180178
@transition(field=another_state, source="new", target="published")
181179
def another_state_standard(self) -> None:
@@ -198,7 +196,7 @@ class BlogPost(models.Model):
198196
Test workflow
199197
"""
200198

201-
state = FSMField(choices=BlogPostState.choices, default=BlogPostState.NEW, protected=True)
199+
state = fsm.FSMField(choices=BlogPostState.choices, default=BlogPostState.NEW, protected=True)
202200

203201
class Meta:
204202
permissions = [
@@ -274,27 +272,37 @@ class AdminBlogPostState(models.TextChoices):
274272
HIDDEN = "hidden", "Hidden"
275273

276274

277-
class AdminBlogPostStep(models.TextChoices):
278-
STEP_1 = "step1", "Step one"
279-
STEP_2 = "step2", "Step two"
280-
STEP_3 = "step3", "Step three"
275+
class AdminBlogPostStep(models.IntegerChoices):
276+
STEP_1 = 1, "Step one"
277+
STEP_2 = 2, "Step two"
278+
STEP_3 = 3, "Step three"
279+
280+
281+
class AdminBlogPostDbState(fsm.FSMModelMixin, models.Model):
282+
id = models.CharField(primary_key=True)
283+
label = models.CharField()
284+
285+
def __str__(self):
286+
return self.label
281287

282288

283289
class AdminBlogPost(fsm.FSMModelMixin, models.Model):
284290
title = models.CharField(max_length=50)
285291

286-
state = FSMField(
292+
state = fsm.FSMField(
287293
choices=AdminBlogPostState.choices,
288294
default=AdminBlogPostState.CREATED,
289295
protected=True,
290296
)
291297

292-
step = FSMField(
298+
step = fsm.FSMIntegerField(
293299
choices=AdminBlogPostStep.choices,
294300
default=AdminBlogPostStep.STEP_1,
295301
protected=False,
296302
)
297303

304+
key_state = fsm.FSMKeyField(DbState, on_delete=models.CASCADE, null=True)
305+
298306
# state transitions
299307
def __str__(self) -> str:
300308
return f"{self.title} ({self.state})"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)