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
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- master
- main

jobs:
coverage:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

Unreleased
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Add support for Django 5.2
- Add support for python 3.13


django-fsm-2 4.0.0 2024-09-02
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ class DbState(models.Model):
id = models.CharField(primary_key=True, max_length=50)
label = models.CharField(max_length=255)

def __unicode__(self):
def __str__(self):
return self.label


Expand Down
49 changes: 22 additions & 27 deletions django_fsm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@
from django_fsm.signals import pre_transition

__all__ = [
"TransitionNotAllowed",
"GET_STATE",
"RETURN_VALUE",
"ConcurrentTransition",
"FSMFieldMixin",
"ConcurrentTransitionMixin",
"FSMField",
"FSMFieldMixin",
"FSMIntegerField",
"FSMKeyField",
"ConcurrentTransitionMixin",
"transition",
"TransitionNotAllowed",
"can_proceed",
"has_transition_perm",
"GET_STATE",
"RETURN_VALUE",
"transition",
]


class TransitionNotAllowed(Exception):
class TransitionNotAllowed(Exception): # noqa: N818
"""Raised when a transition is not allowed"""

def __init__(self, *args, **kwargs):
Expand All @@ -42,11 +42,11 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class InvalidResultState(Exception):
class InvalidResultState(Exception): # noqa: N818
"""Raised when we got invalid result state"""


class ConcurrentTransition(Exception):
class ConcurrentTransition(Exception): # noqa: N818
"""
Raised when the transition cannot be executed because the
object has become stale (state has been changed since it
Expand Down Expand Up @@ -91,7 +91,7 @@ def __eq__(self, other):
return False


def get_available_FIELD_transitions(instance, field):
def get_available_FIELD_transitions(instance, field): # noqa: N802
"""
List of transitions available in current model state
with all conditions met
Expand All @@ -105,14 +105,14 @@ def get_available_FIELD_transitions(instance, field):
yield meta.get_transition(curr_state)


def get_all_FIELD_transitions(instance, field):
def get_all_FIELD_transitions(instance, field): # noqa: N802
"""
List of all transitions available in current model state
"""
return field.get_all_transitions(instance.__class__)


def get_available_user_FIELD_transitions(instance, user, field):
def get_available_user_FIELD_transitions(instance, user, field): # noqa: N802
"""
List of transitions available in current model state
with all conditions met and user have rights on it
Expand Down Expand Up @@ -211,7 +211,7 @@ class FSMFieldDescriptor:
def __init__(self, field):
self.field = field

def __get__(self, instance, type=None):
def __get__(self, instance, instance_type=None):
if instance is None:
return self
return self.field.get_state(instance)
Expand All @@ -234,7 +234,7 @@ def __init__(self, *args, **kwargs):
self.state_proxy = {} # state -> ProxyClsRef

state_choices = kwargs.pop("state_choices", None)
choices = kwargs.get("choices", None)
choices = kwargs.get("choices")
if state_choices is not None and choices is not None:
raise ValueError("Use one of choices or state_choices value")

Expand Down Expand Up @@ -344,8 +344,7 @@ def get_all_transitions(self, instance_cls):
for transition in transitions.values():
meta = transition._django_fsm

for transition in meta.transitions.values():
yield transition
yield from meta.transitions.values()

def contribute_to_class(self, cls, name, **kwargs):
self.base_cls = cls
Expand Down Expand Up @@ -406,8 +405,6 @@ class FSMIntegerField(FSMFieldMixin, models.IntegerField):
Same as FSMField, but stores the state value in an IntegerField.
"""

pass


class FSMKeyField(FSMFieldMixin, models.ForeignKey):
"""
Expand Down Expand Up @@ -557,7 +554,7 @@ def _change_state(instance, *args, **kwargs):
return inner_transition


def can_proceed(bound_method, check_conditions=True):
def can_proceed(bound_method, check_conditions=True): # noqa: FBT002
"""
Returns True if model in state allows to call bound_method

Expand Down Expand Up @@ -597,25 +594,23 @@ def get_state(self, model, transition, result, args=[], kwargs={}):
raise NotImplementedError


class RETURN_VALUE(State):
class RETURN_VALUE(State): # noqa: N801
def __init__(self, *allowed_states):
self.allowed_states = allowed_states if allowed_states else None

def get_state(self, model, transition, result, args=[], kwargs={}):
if self.allowed_states is not None:
if result not in self.allowed_states:
raise InvalidResultState(f"{result} is not in list of allowed states\n{self.allowed_states}")
if self.allowed_states is not None and result not in self.allowed_states:
raise InvalidResultState(f"{result} is not in list of allowed states\n{self.allowed_states}")
return result


class GET_STATE(State):
class GET_STATE(State): # noqa: N801
def __init__(self, func, states=None):
self.func = func
self.allowed_states = states

def get_state(self, model, transition, result, args=[], kwargs={}):
result_state = self.func(model, *args, **kwargs)
if self.allowed_states is not None:
if result_state not in self.allowed_states:
raise InvalidResultState(f"{result_state} is not in list of allowed states\n{self.allowed_states}")
if self.allowed_states is not None and result_state not in self.allowed_states:
raise InvalidResultState(f"{result_state} is not in list of allowed states\n{self.allowed_states}")
return result_state
28 changes: 14 additions & 14 deletions django_fsm/management/commands/graph_transitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def node_label(field, state):
return state


def generate_dot(fields_data): # noqa: C901
def generate_dot(fields_data): # noqa: C901, PLR0912
result = graphviz.Digraph()

for field, model in fields_data:
Expand Down Expand Up @@ -88,11 +88,11 @@ def generate_dot(fields_data): # noqa: C901
subgraph.node(name, label=label, shape="doublecircle")
for name, label in (sources | targets) - final_states:
subgraph.node(name, label=label, shape="circle")
if field.default: # Adding initial state notation
if label == field.default:
initial_name = node_name(field, "_initial")
subgraph.node(name=initial_name, label="", shape="point")
subgraph.edge(initial_name, name)
# Adding initial state notation
if field.default and label == field.default:
initial_name = node_name(field, "_initial")
subgraph.node(name=initial_name, label="", shape="point")
subgraph.edge(initial_name, name)
for source_name, target_name, attrs in edges:
subgraph.edge(source_name, target_name, **dict(attrs))

Expand All @@ -111,10 +111,10 @@ def add_transition(transition_source, transition_target, transition_name, source
def get_graphviz_layouts():
try:
import graphviz

return graphviz.backend.ENGINES
except Exception:
except ModuleNotFoundError:
return {"sfdp", "circo", "twopi", "dot", "neato", "fdp", "osage", "patchwork"}
else:
return graphviz.backend.ENGINES


class Command(BaseCommand):
Expand All @@ -139,10 +139,10 @@ def add_arguments(self, parser):
parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]"))

def render_output(self, graph, **options):
filename, format = options["outputfile"].rsplit(".", 1)
filename, graph_format = options["outputfile"].rsplit(".", 1)

graph.engine = options["layout"]
graph.format = format
graph.format = graph_format
graph.render(filename)

def handle(self, *args, **options):
Expand All @@ -156,10 +156,10 @@ def handle(self, *args, **options):
models = apps.get_models(app)
for model in models:
fields_data += all_fsm_fields_data(model)
elif len(field_spec) == 2:
if len(field_spec) == 2: # noqa: PLR2004
model = apps.get_model(field_spec[0], field_spec[1])
fields_data += all_fsm_fields_data(model)
elif len(field_spec) == 3:
if len(field_spec) == 3: # noqa: PLR2004
model = apps.get_model(field_spec[0], field_spec[1])
fields_data += all_fsm_fields_data(model)
else:
Expand All @@ -170,4 +170,4 @@ def handle(self, *args, **options):
if options["outputfile"]:
self.render_output(dotdata, **options)
else:
print(dotdata)
print(dotdata) # noqa: T201
14 changes: 7 additions & 7 deletions django_fsm/tests/test_key_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ class DBState(models.Model):

label = models.CharField(max_length=255)

def __unicode__(self):
return self.label

class Meta:
app_label = "django_fsm"

def __str__(self):
return self.label


class FKBlogPost(models.Model):
state = FSMKeyField(DBState, default="new", protected=True, on_delete=models.CASCADE)

class Meta:
app_label = "django_fsm"

@transition(field=state, source="new", target="published")
def publish(self):
pass
Expand All @@ -57,9 +60,6 @@ def steal(self):
def moderate(self):
pass

class Meta:
app_label = "django_fsm"


class FSMKeyFieldTest(TestCase):
def setUp(self):
Expand Down Expand Up @@ -119,7 +119,7 @@ def test_star_shortcut_succeed(self):


"""
TODO FIX it
# TODO: FIX it
class BlogPostStatus(models.Model):
name = models.CharField(max_length=10, unique=True)
objects = models.Manager()
Expand Down
6 changes: 3 additions & 3 deletions django_fsm/tests/test_protected_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
class ProtectedAccessModel(models.Model):
status = FSMField(default="new", protected=True)

class Meta:
app_label = "django_fsm"

@transition(field=status, source="new", target="published")
def publish(self):
pass

class Meta:
app_label = "django_fsm"


class MultiProtectedAccessModel(models.Model):
status1 = FSMField(default="new", protected=True)
Expand Down
6 changes: 3 additions & 3 deletions django_fsm/tests/test_protected_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
class RefreshableProtectedAccessModel(models.Model):
status = FSMField(default="new", protected=True)

class Meta:
app_label = "django_fsm"

@transition(field=status, source="new", target="published")
def publish(self):
pass

class Meta:
app_label = "django_fsm"


class RefreshableModel(FSMModelMixin, RefreshableProtectedAccessModel):
pass
Expand Down
6 changes: 3 additions & 3 deletions django_fsm/tests/test_proxy_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ def publish(self):


class InheritedModel(BaseModel):
class Meta:
proxy = True

@transition(field="state", source="published", target="sticked")
def stick(self):
pass

class Meta:
proxy = True


class TestinheritedModel(TestCase):
def setUp(self):
Expand Down
34 changes: 23 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,31 @@ target-version = "py38"
fix = true

[tool.ruff.lint]
# select = ["ALL"]
extend-select = [
"F", # Pyflakes
"E", # pycodestyle
"W", # pycodestyle
"UP", # pyupgrade
select = ["ALL"]
extend-ignore = [
"COM812", # This rule may cause conflicts when used with the formatter
"D", # pydocstyle
"DOC", # pydoclint
"B",
"PTH",
"ANN", # Missing type annotation
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
"PT", # Use a regular `assert` instead of unittest-style
"DJ008", # Model does not define `__str__` method
"ARG001", # Unused function argument
"ARG002", # Unused method argument
"TRY002", # Create your own exception
"TRY003", # Avoid specifying long messages outside the exception class
"EM101", # Exception must not use a string literal, assign to variable first
"EM102", # Exception must not use an f-string literal, assign to variable first
"SLF001", # Private member accessed
"SIM103", # Return the condition directly
"PLR0913", # Too many arguments in function definition
]
fixable = [
"I", # isort
"PERF",
"RET",
"C",
# "B",
"RUF100", # Unused `noqa` directive
]
fixable = ["I"]


[tool.ruff.lint.isort]
Expand Down
2 changes: 1 addition & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)

DATABASE_ENGINE = "sqlite3"
SECRET_KEY = "nokey"
SECRET_KEY = "nokey" # noqa: S105
MIDDLEWARE_CLASSES = ()
DATABASES = {
"default": {
Expand Down
Loading