Skip to content

Commit 9508263

Browse files
committed
Make it compatible with fsm-log
1 parent 58095a0 commit 9508263

File tree

8 files changed

+113
-33
lines changed

8 files changed

+113
-33
lines changed

django_fsm/admin.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from functools import partial
45
from typing import Any
56

67
from django.conf import settings
@@ -15,6 +16,13 @@
1516

1617
import django_fsm as fsm
1718

19+
try:
20+
import django_fsm_log # noqa: F401
21+
except ModuleNotFoundError:
22+
FSM_LOG_ENABLED = False
23+
else:
24+
FSM_LOG_ENABLED = True
25+
1826

1927
@dataclass
2028
class FSMObjectTransition:
@@ -127,7 +135,20 @@ def response_change(self, request: HttpRequest, obj: Any) -> HttpResponse:
127135
)
128136

129137
try:
130-
transition_func()
138+
if FSM_LOG_ENABLED:
139+
for fn in [
140+
partial(transition_func, request=request, by=request.user),
141+
partial(transition_func, by=request.user),
142+
transition_func,
143+
]:
144+
try:
145+
fn()
146+
except TypeError: # noqa: PERF203
147+
pass
148+
else:
149+
break
150+
else:
151+
transition_func()
131152
except fsm.TransitionNotAllowed:
132153
self.message_user(
133154
request=request,

tests/settings.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"django.contrib.sessions",
4444
"django.contrib.messages",
4545
"django.contrib.staticfiles",
46+
"django_fsm_log",
4647
"guardian",
4748
*PROJECT_APPS,
4849
]
@@ -135,3 +136,35 @@
135136
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
136137

137138
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
139+
140+
141+
# Django FSM-log settings
142+
DJANGO_FSM_LOG_IGNORED_MODELS = (
143+
# "tests.testapp.models.AdminBlogPost",
144+
"tests.testapp.models.Application",
145+
"tests.testapp.models.BlogPost",
146+
"tests.testapp.models.DbState",
147+
"tests.testapp.models.FKApplication",
148+
"tests.testapp.tests.SimpleBlogPost",
149+
"tests.testapp.tests.test_abstract_inheritance.BaseAbstractModel",
150+
"tests.testapp.tests.test_abstract_inheritance.InheritedFromAbstractModel",
151+
"tests.testapp.tests.test_access_deferred_fsm_field.DeferrableModel",
152+
"tests.testapp.tests.test_basic_transitions.SimpleBlogPost",
153+
"tests.testapp.tests.test_conditions.BlogPostWithConditions",
154+
"tests.testapp.tests.test_custom_data.BlogPostWithCustomData",
155+
"tests.testapp.tests.test_exception_transitions.ExceptionalBlogPost",
156+
"tests.testapp.tests.test_graph_transitions.VisualBlogPost",
157+
"tests.testapp.tests.test_integer_field.BlogPostWithIntegerField",
158+
"tests.testapp.tests.test_lock_mixin.ExtendedBlogPost",
159+
"tests.testapp.tests.test_lock_mixin.LockedBlogPost",
160+
"tests.testapp.tests.test_mixin_support.MixinSupportTestModel",
161+
"tests.testapp.tests.test_multi_resultstate.MultiResultTest",
162+
"tests.testapp.tests.test_multidecorators.TestModel",
163+
"tests.testapp.tests.test_protected_field.ProtectedAccessModel",
164+
"tests.testapp.tests.test_protected_fields.RefreshableProtectedAccessModel",
165+
"tests.testapp.tests.test_proxy_inheritance.InheritedModel",
166+
"tests.testapp.tests.test_state_transitions.Caterpillar",
167+
"tests.testapp.tests.test_string_field_parameter.BlogPostWithStringField",
168+
"tests.testapp.tests.test_transition_all_except_target.TestExceptTargetTransitionShortcut",
169+
"tests.testapp.tests.test_key_field.FKBlogPost",
170+
)

tests/testapp/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from django.contrib import admin
4+
from django_fsm_log.admin import StateLogInline
45

56
from django_fsm.admin import FSMAdminMixin
67

@@ -20,3 +21,5 @@ class AdminBlogPostAdmin(FSMAdminMixin, admin.ModelAdmin):
2021
"state",
2122
"step",
2223
]
24+
25+
inlines = [StateLogInline]

tests/testapp/models.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
from django.db import models
4+
from django_fsm_log.decorators import fsm_log_by
5+
from django_fsm_log.decorators import fsm_log_description
46

57
from django_fsm import FSMField
68
from django_fsm import FSMKeyField
@@ -172,6 +174,8 @@ class AdminBlogPost(models.Model):
172174

173175
# state transitions
174176

177+
@fsm_log_by
178+
@fsm_log_description
175179
@transition(
176180
field=state,
177181
source="*",
@@ -180,17 +184,21 @@ class AdminBlogPost(models.Model):
180184
"admin": False,
181185
},
182186
)
183-
def secret_transition(self):
187+
def secret_transition(self, by=None, description=None):
184188
pass
185189

190+
@fsm_log_by
191+
@fsm_log_description
186192
@transition(
187193
field=state,
188194
source=[AdminBlogPostState.CREATED],
189195
target=AdminBlogPostState.REVIEWED,
190196
)
191-
def moderate(self):
197+
def moderate(self, by=None, description=None):
192198
pass
193199

200+
@fsm_log_by
201+
@fsm_log_description
194202
@transition(
195203
field=state,
196204
source=[
@@ -199,9 +207,11 @@ def moderate(self):
199207
],
200208
target=AdminBlogPostState.PUBLISHED,
201209
)
202-
def publish(self):
210+
def publish(self, by=None, description=None):
203211
pass
204212

213+
@fsm_log_by
214+
@fsm_log_description
205215
@transition(
206216
field=state,
207217
source=[
@@ -210,11 +220,13 @@ def publish(self):
210220
],
211221
target=AdminBlogPostState.HIDDEN,
212222
)
213-
def hide(self):
223+
def hide(self, by=None, description=None):
214224
pass
215225

216226
# step transitions
217227

228+
@fsm_log_by
229+
@fsm_log_description
218230
@transition(
219231
field=step,
220232
source=[AdminBlogPostStep.STEP_1],
@@ -223,17 +235,21 @@ def hide(self):
223235
"label": "Go to Step 2",
224236
},
225237
)
226-
def step_two(self):
238+
def step_two(self, by=None, description=None):
227239
pass
228240

241+
@fsm_log_by
242+
@fsm_log_description
229243
@transition(
230244
field=step,
231245
source=[AdminBlogPostStep.STEP_2],
232246
target=AdminBlogPostStep.STEP_3,
233247
)
234-
def step_three(self):
248+
def step_three(self, by=None, description=None):
235249
pass
236250

251+
@fsm_log_by
252+
@fsm_log_description
237253
@transition(
238254
field=step,
239255
source=[
@@ -242,5 +258,5 @@ def step_three(self):
242258
],
243259
target=AdminBlogPostStep.STEP_1,
244260
)
245-
def step_reset(self):
261+
def step_reset(self, by=None, description=None):
246262
pass

tests/testapp/tests/test_admin.py

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

3-
from typing import Any
43
from unittest.mock import patch
54

65
from django.contrib import messages
76
from django.contrib.admin.sites import AdminSite
7+
from django.contrib.auth import get_user_model
88
from django.test import TestCase
99
from django.test.client import RequestFactory
10+
from django_fsm_log.models import StateLog
1011

1112
from django_fsm import ConcurrentTransition
1213
from django_fsm import FSMField
@@ -15,20 +16,6 @@
1516
from tests.testapp.models import AdminBlogPostState
1617

1718

18-
class MockRequest:
19-
path = "/path"
20-
user: Any
21-
22-
23-
class MockSuperUser:
24-
def has_perm(self, perm, obj=None):
25-
return True
26-
27-
28-
request = MockRequest()
29-
request.user = MockSuperUser()
30-
31-
3219
class ModelAdminTest(TestCase):
3320
@classmethod
3421
def setUpTestData(cls):
@@ -37,6 +24,9 @@ def setUpTestData(cls):
3724
blog_post.save()
3825
cls.blog_post = blog_post
3926

27+
cls.request = RequestFactory().get(path="/path")
28+
cls.request.user = get_user_model().objects.create_user(username="jacob", password="password", is_staff=True) # noqa: S106
29+
4030
def setUp(self):
4131
self.model_admin = AdminBlogPostAdmin(AdminBlogPost, AdminSite())
4232

@@ -47,13 +37,13 @@ def test_get_fsm_field_instance(self):
4737
assert isinstance(fsm_field, FSMField)
4838

4939
def test_readonly_fields(self):
50-
assert self.model_admin.get_readonly_fields(request=request) == ("state",)
40+
assert self.model_admin.get_readonly_fields(request=self.request) == ("state",)
5141

5242
def test_get_fsm_block_label(self):
5343
assert self.model_admin.get_fsm_block_label(fsm_field_name="MyField") == "Transition (MyField)"
5444

5545
def test_get_fsm_object_transitions(self):
56-
fsm_object_transitions = self.model_admin.get_fsm_object_transitions(request=request, obj=self.blog_post)
46+
fsm_object_transitions = self.model_admin.get_fsm_object_transitions(request=self.request, obj=self.blog_post)
5747

5848
assert len(fsm_object_transitions) == 2 # noqa: PLR2004
5949
state_transition, step_transition = fsm_object_transitions
@@ -70,7 +60,7 @@ def test_get_fsm_object_transitions(self):
7060
assert sorted([t.name for t in step_transition.available_transitions]) == ["step_two"]
7161

7262
def test_get_fsm_redirect_url(self):
73-
assert self.model_admin.get_fsm_redirect_url(request=request, obj=None) == "/path"
63+
assert self.model_admin.get_fsm_redirect_url(request=self.request, obj=None) == "/path"
7464

7565
@patch("django.contrib.admin.ModelAdmin.change_view")
7666
@patch("django_fsm.admin.FSMAdminMixin.get_fsm_object_transitions")
@@ -82,7 +72,7 @@ def test_change_view_context(
8272
mock_get_fsm_object_transitions.return_value = "object transitions"
8373

8474
self.model_admin.change_view(
85-
request=request,
75+
request=self.request,
8676
form_url="/test",
8777
object_id=self.blog_post.pk,
8878
extra_context={
@@ -91,12 +81,12 @@ def test_change_view_context(
9181
)
9282

9383
mock_get_fsm_object_transitions.assert_called_once_with(
94-
request=request,
84+
request=self.request,
9585
obj=self.blog_post,
9686
)
9787

9888
mock_super_change_view.assert_called_once_with(
99-
request=request,
89+
request=self.request,
10090
object_id=self.blog_post.pk,
10191
form_url="/test",
10292
extra_context={
@@ -111,7 +101,12 @@ class ResponseChangeTest(TestCase):
111101
def setUp(self):
112102
self.model_admin = AdminBlogPostAdmin(AdminBlogPost, AdminSite())
113103

104+
@classmethod
105+
def setUpTestData(cls):
106+
cls.user = get_user_model().objects.create_user(username="jacob", password="password", is_staff=True) # noqa: S106
107+
114108
def test_unknown_transition(self, mock_message_user):
109+
assert StateLog.objects.count() == 0
115110
request = RequestFactory().post(
116111
path="/",
117112
data={"_fsm_transition_to": "unknown_transition"},
@@ -133,12 +128,15 @@ def test_unknown_transition(self, mock_message_user):
133128

134129
updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
135130
assert updated_blog_post.state == AdminBlogPostState.CREATED
131+
assert StateLog.objects.count() == 0
136132

137133
def test_transition_applied(self, mock_message_user):
134+
assert StateLog.objects.count() == 0
138135
request = RequestFactory().post(
139136
path="/",
140137
data={"_fsm_transition_to": "moderate"},
141138
)
139+
request.user = self.user
142140

143141
blog_post = AdminBlogPost.objects.create(title="Article name")
144142
assert blog_post.state == AdminBlogPostState.CREATED
@@ -156,12 +154,16 @@ def test_transition_applied(self, mock_message_user):
156154

157155
updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
158156
assert updated_blog_post.state == AdminBlogPostState.REVIEWED
157+
assert StateLog.objects.count() == 1
158+
assert StateLog.objects.get().by == self.user
159159

160160
def test_transition_not_allowed_exception(self, mock_message_user):
161+
assert StateLog.objects.count() == 0
161162
request = RequestFactory().post(
162163
path="/",
163164
data={"_fsm_transition_to": "publish"},
164165
)
166+
request.user = self.user
165167

166168
blog_post = AdminBlogPost.objects.create(title="Article name")
167169
assert blog_post.state == AdminBlogPostState.CREATED
@@ -179,12 +181,15 @@ def test_transition_not_allowed_exception(self, mock_message_user):
179181

180182
updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
181183
assert updated_blog_post.state == AdminBlogPostState.CREATED
184+
assert StateLog.objects.count() == 0
182185

183186
def test_concurrent_transition_exception(self, mock_message_user):
187+
assert StateLog.objects.count() == 0
184188
request = RequestFactory().post(
185189
path="/",
186190
data={"_fsm_transition_to": "moderate"},
187191
)
192+
request.user = self.user
188193

189194
blog_post = AdminBlogPost.objects.create(title="Article name")
190195
assert blog_post.state == AdminBlogPostState.CREATED
@@ -206,3 +211,4 @@ def test_concurrent_transition_exception(self, mock_message_user):
206211

207212
updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
208213
assert updated_blog_post.state == AdminBlogPostState.CREATED
214+
assert StateLog.objects.count() == 0

tests/testapp/tests/test_multidecorators.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django_fsm.signals import post_transition
99

1010

11-
class TestModel(models.Model):
11+
class MultiDecoratedModel(models.Model):
1212
counter = models.IntegerField(default=0)
1313
signal_counter = models.IntegerField(default=0)
1414
state = FSMField(default="SUBMITTED_BY_USER")
@@ -24,12 +24,12 @@ def count_calls(sender, instance, name, source, target, **kwargs):
2424
instance.signal_counter += 1
2525

2626

27-
post_transition.connect(count_calls, sender=TestModel)
27+
post_transition.connect(count_calls, sender=MultiDecoratedModel)
2828

2929

3030
class TestStateProxy(TestCase):
3131
def test_transition_method_called_once(self):
32-
model = TestModel()
32+
model = MultiDecoratedModel.objects.create()
3333
model.review()
3434
assert model.counter == 1
3535
assert model.signal_counter == 1

tests/wsgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212

1313
from django.core.wsgi import get_wsgi_application
1414

15-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "silvr.settings")
15+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
1616

1717
application = get_wsgi_application()

0 commit comments

Comments
 (0)