Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion django_fsm/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from functools import partial
from typing import Any

from django.conf import settings
Expand All @@ -15,6 +16,13 @@

import django_fsm as fsm

try:
import django_fsm_log # noqa: F401
except ModuleNotFoundError:
FSM_LOG_ENABLED = False

Check warning on line 22 in django_fsm/admin.py

View check run for this annotation

Codecov / codecov/patch

django_fsm/admin.py#L21-L22

Added lines #L21 - L22 were not covered by tests
else:
FSM_LOG_ENABLED = True

Comment on lines +19 to +25
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small improvement to avoid TypeErrors when fsm-log is not installed at all


@dataclass
class FSMObjectTransition:
Expand Down Expand Up @@ -127,7 +135,20 @@
)

try:
transition_func()
if FSM_LOG_ENABLED:
for fn in [
partial(transition_func, request=request, by=request.user),
partial(transition_func, by=request.user),
transition_func,
]:
try:
fn()
except TypeError: # noqa: PERF203
pass
else:
break
else:
transition_func()

Check warning on line 151 in django_fsm/admin.py

View check run for this annotation

Codecov / codecov/patch

django_fsm/admin.py#L151

Added line #L151 was not covered by tests
except fsm.TransitionNotAllowed:
self.message_user(
request=request,
Expand Down
50 changes: 49 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pre-commit = "*"
pytest = "*"
pytest-cov = "^4.1.0"
pytest-django = "*"
django_fsm_log = "*"

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "tests.settings"
Expand Down
33 changes: 33 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_fsm_log",
"guardian",
*PROJECT_APPS,
]
Expand Down Expand Up @@ -135,3 +136,35 @@
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


# Django FSM-log settings
DJANGO_FSM_LOG_IGNORED_MODELS = (
# "tests.testapp.models.AdminBlogPost",
"tests.testapp.models.Application",
"tests.testapp.models.BlogPost",
"tests.testapp.models.DbState",
"tests.testapp.models.FKApplication",
"tests.testapp.tests.SimpleBlogPost",
"tests.testapp.tests.test_abstract_inheritance.BaseAbstractModel",
"tests.testapp.tests.test_abstract_inheritance.InheritedFromAbstractModel",
"tests.testapp.tests.test_access_deferred_fsm_field.DeferrableModel",
"tests.testapp.tests.test_basic_transitions.SimpleBlogPost",
"tests.testapp.tests.test_conditions.BlogPostWithConditions",
"tests.testapp.tests.test_custom_data.BlogPostWithCustomData",
"tests.testapp.tests.test_exception_transitions.ExceptionalBlogPost",
"tests.testapp.tests.test_graph_transitions.VisualBlogPost",
"tests.testapp.tests.test_integer_field.BlogPostWithIntegerField",
"tests.testapp.tests.test_lock_mixin.ExtendedBlogPost",
"tests.testapp.tests.test_lock_mixin.LockedBlogPost",
"tests.testapp.tests.test_mixin_support.MixinSupportTestModel",
"tests.testapp.tests.test_multi_resultstate.MultiResultTest",
"tests.testapp.tests.test_multidecorators.MultiDecoratedModel",
"tests.testapp.tests.test_protected_field.ProtectedAccessModel",
"tests.testapp.tests.test_protected_fields.RefreshableProtectedAccessModel",
"tests.testapp.tests.test_proxy_inheritance.InheritedModel",
"tests.testapp.tests.test_state_transitions.Caterpillar",
"tests.testapp.tests.test_string_field_parameter.BlogPostWithStringField",
"tests.testapp.tests.test_transition_all_except_target.ExceptTargetTransitionShortcut",
"tests.testapp.tests.test_key_field.FKBlogPost",
)
3 changes: 3 additions & 0 deletions tests/testapp/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from django.contrib import admin
from django_fsm_log.admin import StateLogInline

from django_fsm.admin import FSMAdminMixin

Expand All @@ -20,3 +21,5 @@ class AdminBlogPostAdmin(FSMAdminMixin, admin.ModelAdmin):
"state",
"step",
]

inlines = [StateLogInline]
30 changes: 23 additions & 7 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from django.db import models
from django_fsm_log.decorators import fsm_log_by
from django_fsm_log.decorators import fsm_log_description

from django_fsm import FSMField
from django_fsm import FSMKeyField
Expand Down Expand Up @@ -172,6 +174,8 @@ class AdminBlogPost(models.Model):

# state transitions

@fsm_log_by
@fsm_log_description
@transition(
field=state,
source="*",
Expand All @@ -180,17 +184,21 @@ class AdminBlogPost(models.Model):
"admin": False,
},
)
def secret_transition(self):
def secret_transition(self, by=None, description=None):
pass

@fsm_log_by
@fsm_log_description
@transition(
field=state,
source=[AdminBlogPostState.CREATED],
target=AdminBlogPostState.REVIEWED,
)
def moderate(self):
def moderate(self, by=None, description=None):
pass

@fsm_log_by
@fsm_log_description
@transition(
field=state,
source=[
Expand All @@ -199,9 +207,11 @@ def moderate(self):
],
target=AdminBlogPostState.PUBLISHED,
)
def publish(self):
def publish(self, by=None, description=None):
pass

@fsm_log_by
@fsm_log_description
@transition(
field=state,
source=[
Expand All @@ -210,11 +220,13 @@ def publish(self):
],
target=AdminBlogPostState.HIDDEN,
)
def hide(self):
def hide(self, by=None, description=None):
pass

# step transitions

@fsm_log_by
@fsm_log_description
@transition(
field=step,
source=[AdminBlogPostStep.STEP_1],
Expand All @@ -223,17 +235,21 @@ def hide(self):
"label": "Go to Step 2",
},
)
def step_two(self):
def step_two(self, by=None, description=None):
pass

@fsm_log_by
@fsm_log_description
@transition(
field=step,
source=[AdminBlogPostStep.STEP_2],
target=AdminBlogPostStep.STEP_3,
)
def step_three(self):
def step_three(self, by=None, description=None):
pass

@fsm_log_by
@fsm_log_description
@transition(
field=step,
source=[
Expand All @@ -242,5 +258,5 @@ def step_three(self):
],
target=AdminBlogPostStep.STEP_1,
)
def step_reset(self):
def step_reset(self, by=None, description=None):
pass
10 changes: 10 additions & 0 deletions tests/testapp/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.test.client import RequestFactory
from django_fsm_log.models import StateLog

from django_fsm import ConcurrentTransition
from django_fsm import FSMField
Expand Down Expand Up @@ -105,6 +106,7 @@ def setUpTestData(cls):
cls.user = get_user_model().objects.create_user(username="jacob", password="password", is_staff=True) # noqa: S106

def test_unknown_transition(self, mock_message_user):
assert StateLog.objects.count() == 0
request = RequestFactory().post(
path="/",
data={"_fsm_transition_to": "unknown_transition"},
Expand All @@ -126,8 +128,10 @@ def test_unknown_transition(self, mock_message_user):

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

def test_transition_applied(self, mock_message_user):
assert StateLog.objects.count() == 0
request = RequestFactory().post(
path="/",
data={"_fsm_transition_to": "moderate"},
Expand All @@ -150,8 +154,11 @@ def test_transition_applied(self, mock_message_user):

updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
assert updated_blog_post.state == AdminBlogPostState.REVIEWED
assert StateLog.objects.count() == 1
assert StateLog.objects.get().by == self.user

def test_transition_not_allowed_exception(self, mock_message_user):
assert StateLog.objects.count() == 0
request = RequestFactory().post(
path="/",
data={"_fsm_transition_to": "publish"},
Expand All @@ -174,8 +181,10 @@ def test_transition_not_allowed_exception(self, mock_message_user):

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

def test_concurrent_transition_exception(self, mock_message_user):
assert StateLog.objects.count() == 0
request = RequestFactory().post(
path="/",
data={"_fsm_transition_to": "moderate"},
Expand All @@ -202,3 +211,4 @@ def test_concurrent_transition_exception(self, mock_message_user):

updated_blog_post = AdminBlogPost.objects.get(pk=blog_post.pk)
assert updated_blog_post.state == AdminBlogPostState.CREATED
assert StateLog.objects.count() == 0
6 changes: 3 additions & 3 deletions tests/testapp/tests/test_transition_all_except_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django_fsm import transition


class TestExceptTargetTransitionShortcut(models.Model):
class ExceptTargetTransitionShortcut(models.Model):
state = FSMField(default="new")

@transition(field=state, source="new", target="published")
Expand All @@ -20,9 +20,9 @@ def remove(self):
pass


class Test(TestCase):
class TestExceptTargetTransitionShortcut(TestCase):
def setUp(self):
self.model = TestExceptTargetTransitionShortcut()
self.model = ExceptTargetTransitionShortcut()

def test_usecase(self):
assert self.model.state == "new"
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ deps =
dj51: Django==5.1
dj52: Django==5.2

django-fsm-log
django-guardian
graphviz
pep8
Expand Down