diff --git a/.checkignore b/.checkignore
deleted file mode 100644
index e93d446..0000000
--- a/.checkignore
+++ /dev/null
@@ -1,2 +0,0 @@
-tests/*
-django_fsm/tests/*
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 515a86c..9bd7701 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -23,4 +23,4 @@ jobs:
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions
- name: Test with tox
- run: tox
\ No newline at end of file
+ run: tox
diff --git a/.gitignore b/.gitignore
index a9a6d20..b2d05d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,132 @@
-*.pyc
-dist/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
build/
-django_fsm.egg-info/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# sqlite
+test.db
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0812619..a5d2dd2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ default_language_version:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-added-large-files
args: ["--maxkb=700"]
@@ -31,8 +31,16 @@ repos:
- id: django-upgrade
args: [--target-version, "3.2"]
- # - repo: https://github.com/astral-sh/ruff-pre-commit
- # rev: v0.0.291
- # hooks:
- # - id: ruff-format
- # - id: ruff
+
+ - repo: https://github.com/python-poetry/poetry
+ rev: 1.6.1
+ hooks:
+ - id: poetry-check
+ - id: poetry-lock
+ - id: poetry-export
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.291
+ hooks:
+ - id: ruff-format
+ - id: ruff
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e326809..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-dist: xenial
-language: python
-sudo: false
-cache: pip
-
-python:
- - 2.7
- - 3.6
- - 3.7
- - 3.8
-
-install:
- - pip install tox tox-travis
-
-script:
- - tox --skip-missing-interpreters
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3cbde9d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,132 @@
+# Changelog
+
+## django-fsm unreleased
+
+- Enable Github actions for testing
+- Remove support for django deprecated versions
+- add support for django 4.2
+- add support for django 5.0
+- Remove support for python deprecated versions
+- add support for python 3.11
+- add support for python 3.12
+
+## django-fsm 2.8.1 2022-08-15
+
+- Improve fix for get_available_FIELD_transition
+
+## django-fsm 2.8.0 2021-11-05
+
+- Fix get_available_FIELD_transition on django\>=3.2
+- Fix refresh_from_db for ConcurrentTransitionMixin
+
+## django-fsm 2.7.1 2020-10-13
+
+- Fix warnings on Django 3.1+
+
+## django-fsm 2.7.0 2019-12-03
+
+- Django 3.0 support
+- Test on Python 3.8
+
+## django-fsm 2.6.1 2019-04-19
+
+- Update pypi classifiers to latest django/python supported versions
+- Several fixes for graph_transition command
+
+## django-fsm 2.6.0 2017-06-08
+
+- Fix django 1.11 compatibility
+- Fix TypeError in [graph_transitions]{.title-ref} command when using
+ django\'s lazy translations
+
+## django-fsm 2.5.0 2017-03-04
+
+- graph_transition command fix for django 1.10
+- graph_transition command supports GET_STATE targets
+- signal data extended with method args/kwargs and field
+- sets allowed to be passed to the transition decorator
+
+## django-fsm 2.4.0 2016-05-14
+
+- graph_transition commnad now works with multiple FSM\'s per model
+- Add ability to set target state from transition return value or
+ callable
+
+## django-fsm 2.3.0 2015-10-15
+
+- Add source state shortcut \'+\' to specify transitions from all
+ states except the target
+- Add object-level permission checks
+- Fix translated labels for graph of FSMIntegerField
+- Fix multiple signals for several transition decorators
+
+## django-fsm 2.2.1 2015-04-27
+
+- Improved exception message for unmet transition conditions.
+- Don\'t send post transition signal in case of no state changes on
+ exception
+- Allow empty string as correct state value
+- Improved graphviz fsm visualisation
+- Clean django 1.8 warnings
+
+## django-fsm 2.2.0 2014-09-03
+
+- Support for [class
+ substitution](http://schinckel.net/2013/06/13/django-proxy-model-state-machine/)
+ to proxy classes depending on the state
+- Added ConcurrentTransitionMixin with optimistic locking support
+- Default db_index=True for FSMIntegerField removed
+- Graph transition code migrated to new graphviz library with python 3
+ support
+- Ability to change state on transition exception
+
+## django-fsm 2.1.0 2014-05-15
+
+- Support for attaching permission checks on model transitions
+
+## django-fsm 2.0.0 2014-03-15
+
+- Backward incompatible release
+- All public code import moved directly to django_fsm package
+- Correct support for several \@transitions decorator with different
+ source states and conditions on same method
+- save parameter from transition decorator removed
+- get_available_FIELD_transitions return Transition data object
+ instead of tuple
+- Models got get_available_FIELD_transitions, even if field specified
+ as string reference
+- New get_all_FIELD_transitions method contributed to class
+
+## django-fsm 1.6.0 2014-03-15
+
+- FSMIntegerField and FSMKeyField support
+
+## django-fsm 1.5.1 2014-01-04
+
+- Ad-hoc support for state fields from proxy and inherited models
+
+## django-fsm 1.5.0 2013-09-17
+
+- Python 3 compatibility
+
+## django-fsm 1.4.0 2011-12-21
+
+- Add graph_transition command for drawing state transition picture
+
+## django-fsm 1.3.0 2011-07-28
+
+- Add direct field modification protection
+
+## django-fsm 1.2.0 2011-03-23
+
+- Add pre_transition and post_transition signals
+
+## django-fsm 1.1.0 2011-02-22
+
+- Add support for transition conditions
+- Allow multiple FSMField in one model
+- Contribute get_available_FIELD_transitions for model class
+
+## django-fsm 1.0.0 2010-10-12
+
+- Initial public release
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
deleted file mode 100644
index b90ce3b..0000000
--- a/CHANGELOG.rst
+++ /dev/null
@@ -1,161 +0,0 @@
-Changelog
-=========
-
-django-fsm unreleased
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Enable Github actions for testing
-- Remove support for django deprecated versions
-- add support for django 4.2
-- add support for django 5.0
-- Remove support for python deprecated versions
-- add support for python 3.11
-- add support for python 3.12
-
-django-fsm 2.8.1 2022-08-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Improve fix for get_available_FIELD_transition
-
-
-django-fsm 2.8.0 2021-11-05
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Fix get_available_FIELD_transition on django>=3.2
-- Fix refresh_from_db for ConcurrentTransitionMixin
-
-
-django-fsm 2.7.1 2020-10-13
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Fix warnings on Django 3.1+
-
-
-django-fsm 2.7.0 2019-12-03
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Django 3.0 support
-- Test on Python 3.8
-
-
-django-fsm 2.6.1 2019-04-19
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Update pypi classifiers to latest django/python supported versions
-- Several fixes for graph_transition command
-
-
-django-fsm 2.6.0 2017-06-08
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Fix django 1.11 compatibility
-- Fix TypeError in `graph_transitions` command when using django's lazy translations
-
-
-django-fsm 2.5.0 2017-03-04
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- graph_transition command fix for django 1.10
-- graph_transition command supports GET_STATE targets
-- signal data extended with method args/kwargs and field
-- sets allowed to be passed to the transition decorator
-
-
-django-fsm 2.4.0 2016-05-14
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- graph_transition commnad now works with multiple FSM's per model
-- Add ability to set target state from transition return value or callable
-
-
-django-fsm 2.3.0 2015-10-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Add source state shortcut '+' to specify transitions from all states except the target
-- Add object-level permission checks
-- Fix translated labels for graph of FSMIntegerField
-- Fix multiple signals for several transition decorators
-
-
-django-fsm 2.2.1 2015-04-27
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Improved exception message for unmet transition conditions.
-- Don't send post transition signal in case of no state changes on
- exception
-- Allow empty string as correct state value
-- Improved graphviz fsm visualisation
-- Clean django 1.8 warnings
-
-django-fsm 2.2.0 2014-09-03
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Support for `class
- substitution `__
- to proxy classes depending on the state
-- Added ConcurrentTransitionMixin with optimistic locking support
-- Default db\_index=True for FSMIntegerField removed
-- Graph transition code migrated to new graphviz library with python 3
- support
-- Ability to change state on transition exception
-
-django-fsm 2.1.0 2014-05-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Support for attaching permission checks on model transitions
-
-django-fsm 2.0.0 2014-03-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Backward incompatible release
-- All public code import moved directly to django\_fsm package
-- Correct support for several @transitions decorator with different
- source states and conditions on same method
-- save parameter from transition decorator removed
-- get\_available\_FIELD\_transitions return Transition data object
- instead of tuple
-- Models got get\_available\_FIELD\_transitions, even if field
- specified as string reference
-- New get\_all\_FIELD\_transitions method contributed to class
-
-django-fsm 1.6.0 2014-03-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- FSMIntegerField and FSMKeyField support
-
-django-fsm 1.5.1 2014-01-04
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Ad-hoc support for state fields from proxy and inherited models
-
-django-fsm 1.5.0 2013-09-17
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Python 3 compatibility
-
-django-fsm 1.4.0 2011-12-21
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Add graph\_transition command for drawing state transition picture
-
-django-fsm 1.3.0 2011-07-28
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Add direct field modification protection
-
-django-fsm 1.2.0 2011-03-23
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Add pre\_transition and post\_transition signals
-
-django-fsm 1.1.0 2011-02-22
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Add support for transition conditions
-- Allow multiple FSMField in one model
-- Contribute get\_available\_FIELD\_transitions for model class
-
-django-fsm 1.0.0 2010-10-12
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Initial public release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9336c76
--- /dev/null
+++ b/README.md
@@ -0,0 +1,419 @@
+# Django friendly finite state machine support
+
+[![Build
+Status](https://travis-ci.org/viewflow/django-fsm.svg?branch=master)](https://travis-ci.org/viewflow/django-fsm)
+
+django-fsm adds simple declarative state management for django models.
+
+If you need parallel task execution, view and background task code reuse
+over different flows - check my new project django-viewflow:
+
+
+
+Instead of adding a state field to a django model and managing its
+values by hand, you use `FSMField` and mark model methods with the
+`transition` decorator. These methods could contain side-effects of the
+state change.
+
+Nice introduction is available here:
+
+
+You may also take a look at django-fsm-admin project containing a mixin
+and template tags to integrate django-fsm state transitions into the
+django admin.
+
+
+
+Transition logging support could be achieved with help of django-fsm-log
+package
+
+
+
+FSM really helps to structure the code, especially when a new developer
+comes to the project. FSM is most effective when you use it for some
+sequential steps.
+
+## Installation
+
+``` bash
+$ pip install django-fsm
+```
+
+Or, for the latest git version
+
+``` bash
+$ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm
+```
+
+## Usage
+
+Add FSMState field to your model
+
+``` python
+from django_fsm import FSMField, transition
+
+class BlogPost(models.Model):
+ state = FSMField(default='new')
+```
+
+Use the `transition` decorator to annotate model methods
+
+``` python
+@transition(field=state, source='new', target='published')
+def publish(self):
+ """
+ This function may contain side-effects,
+ like updating caches, notifying users, etc.
+ The return value will be discarded.
+ """
+```
+
+The `field` parameter accepts both a string attribute name or an actual
+field instance.
+
+If calling publish() succeeds without raising an exception, the state
+field will be changed, but not written to the database.
+
+``` python
+from django_fsm import can_proceed
+
+def publish_view(request, post_id):
+ post = get_object_or_404(BlogPost, pk=post_id)
+ if not can_proceed(post.publish):
+ raise PermissionDenied
+
+ post.publish()
+ post.save()
+ return redirect('/')
+```
+
+If some conditions are required to be met before changing the state, use
+the `conditions` argument to `transition`. `conditions` must be a list
+of functions taking one argument, the model instance. The function must
+return either `True` or `False` or a value that evaluates to `True` or
+`False`. If all functions return `True`, all conditions are considered
+to be met and the transition is allowed to happen. If one of the
+functions returns `False`, the transition will not happen. These
+functions should not have any side effects.
+
+You can use ordinary functions
+
+``` python
+def can_publish(instance):
+ # No publishing after 17 hours
+ if datetime.datetime.now().hour > 17:
+ return False
+ return True
+```
+
+Or model methods
+
+``` python
+def can_destroy(self):
+ return self.is_under_investigation()
+```
+
+Use the conditions like this:
+
+``` python
+@transition(field=state, source='new', target='published', conditions=[can_publish])
+def publish(self):
+ """
+ Side effects galore
+ """
+
+@transition(field=state, source='*', target='destroyed', conditions=[can_destroy])
+def destroy(self):
+ """
+ Side effects galore
+ """
+```
+
+You can instantiate a field with `protected=True` option to prevent
+direct state field modification.
+
+``` python
+class BlogPost(models.Model):
+ state = FSMField(default='new', protected=True)
+
+model = BlogPost()
+model.state = 'invalid' # Raises AttributeError
+```
+
+Note that calling
+[refresh_from_db](https://docs.djangoproject.com/en/1.8/ref/models/instances/#django.db.models.Model.refresh_from_db)
+on a model instance with a protected FSMField will cause an exception.
+
+### `source` state
+
+`source` parameter accepts a list of states, or an individual state or
+`django_fsm.State` implementation.
+
+You can use `*` for `source` to allow switching to `target` from any
+state.
+
+You can use `+` for `source` to allow switching to `target` from any
+state excluding `target` state.
+
+### `target` state
+
+`target` state parameter could point to a specific state or
+`django_fsm.State` implementation
+
+``` python
+from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE
+@transition(field=state,
+ source='*',
+ target=RETURN_VALUE('for_moderators', 'published'))
+def publish(self, is_public=False):
+ return 'for_moderators' if is_public else 'published'
+
+@transition(
+ field=state,
+ source='for_moderators',
+ target=GET_STATE(
+ lambda self, allowed: 'published' if allowed else 'rejected',
+ states=['published', 'rejected']))
+def moderate(self, allowed):
+ pass
+
+@transition(
+ field=state,
+ source='for_moderators',
+ target=GET_STATE(
+ lambda self, **kwargs: 'published' if kwargs.get("allowed", True) else 'rejected',
+ states=['published', 'rejected']))
+def moderate(self, allowed=True):
+ pass
+```
+
+### `custom` properties
+
+Custom properties can be added by providing a dictionary to the `custom`
+keyword on the `transition` decorator.
+
+``` python
+@transition(field=state,
+ source='*',
+ target='onhold',
+ custom=dict(verbose='Hold for legal reasons'))
+def legal_hold(self):
+ """
+ Side effects galore
+ """
+```
+
+### `on_error` state
+
+If the transition method raises an exception, you can provide a specific
+target state
+
+``` python
+@transition(field=state, source='new', target='published', on_error='failed')
+def publish(self):
+ """
+ Some exception could happen here
+ """
+```
+
+### `state_choices`
+
+Instead of passing a two-item iterable `choices` you can instead use the
+three-element `state_choices`, the last element being a string reference
+to a model proxy class.
+
+The base class instance would be dynamically changed to the
+corresponding Proxy class instance, depending on the state. Even for
+queryset results, you will get Proxy class instances, even if the
+QuerySet is executed on the base class.
+
+Check the [test
+case](https://github.com/kmmbvnr/django-fsm/blob/master/tests/testapp/tests/test_state_transitions.py)
+for example usage. Or read about [implementation
+internals](http://schinckel.net/2013/06/13/django-proxy-model-state-machine/)
+
+### Permissions
+
+It is common to have permissions attached to each model transition.
+`django-fsm` handles this with `permission` keyword on the `transition`
+decorator. `permission` accepts a permission string, or callable that
+expects `instance` and `user` arguments and returns True if the user can
+perform the transition.
+
+``` python
+@transition(field=state, source='*', target='published',
+ permission=lambda instance, user: not user.has_perm('myapp.can_make_mistakes'))
+def publish(self):
+ pass
+
+@transition(field=state, source='*', target='removed',
+ permission='myapp.can_remove_post')
+def remove(self):
+ pass
+```
+
+You can check permission with `has_transition_permission` method
+
+``` python
+from django_fsm import has_transition_perm
+def publish_view(request, post_id):
+ post = get_object_or_404(BlogPost, pk=post_id)
+ if not has_transition_perm(post.publish, request.user):
+ raise PermissionDenied
+
+ post.publish()
+ post.save()
+ return redirect('/')
+```
+
+### Model methods
+
+`get_all_FIELD_transitions` Enumerates all declared transitions
+
+`get_available_FIELD_transitions` Returns all transitions data available
+in current state
+
+`get_available_user_FIELD_transitions` Enumerates all transitions data
+available in current state for provided user
+
+### Foreign Key constraints support
+
+If you store the states in the db table you could use FSMKeyField to
+ensure Foreign Key database integrity.
+
+In your model :
+
+``` python
+class DbState(models.Model):
+ id = models.CharField(primary_key=True, max_length=50)
+ label = models.CharField(max_length=255)
+
+ def __unicode__(self):
+ return self.label
+
+
+class BlogPost(models.Model):
+ state = FSMKeyField(DbState, default='new')
+
+ @transition(field=state, source='new', target='published')
+ def publish(self):
+ pass
+```
+
+In your fixtures/initial_data.json :
+
+``` json
+[
+ {
+ "pk": "new",
+ "model": "myapp.dbstate",
+ "fields": {
+ "label": "_NEW_"
+ }
+ },
+ {
+ "pk": "published",
+ "model": "myapp.dbstate",
+ "fields": {
+ "label": "_PUBLISHED_"
+ }
+ }
+]
+```
+
+Note : source and target parameters in \@transition decorator use pk
+values of DBState model as names, even if field \"real\" name is used,
+without \_id postfix, as field parameter.
+
+### Integer Field support
+
+You can also use `FSMIntegerField`. This is handy when you want to use
+enum style constants.
+
+``` python
+class BlogPostStateEnum(object):
+ NEW = 10
+ PUBLISHED = 20
+ HIDDEN = 30
+
+class BlogPostWithIntegerField(models.Model):
+ state = FSMIntegerField(default=BlogPostStateEnum.NEW)
+
+ @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED)
+ def publish(self):
+ pass
+```
+
+### Signals
+
+`django_fsm.signals.pre_transition` and
+`django_fsm.signals.post_transition` are called before and after allowed
+transition. No signals on invalid transition are called.
+
+Arguments sent with these signals:
+
+**sender** The model class.
+
+**instance** The actual instance being processed
+
+**name** Transition name
+
+**source** Source model state
+
+**target** Target model state
+
+## Optimistic locking
+
+`django-fsm` provides optimistic locking mixin, to avoid concurrent
+model state changes. If model state was changed in database
+`django_fsm.ConcurrentTransition` exception would be raised on
+model.save()
+
+``` python
+from django_fsm import FSMField, ConcurrentTransitionMixin
+
+class BlogPost(ConcurrentTransitionMixin, models.Model):
+ state = FSMField(default='new')
+```
+
+For guaranteed protection against race conditions caused by concurrently
+executed transitions, make sure:
+
+- Your transitions do not have any side effects except for changes in
+ the database,
+- You always run the save() method on the object within
+ `django.db.transaction.atomic()` block.
+
+Following these recommendations, you can rely on
+ConcurrentTransitionMixin to cause a rollback of all the changes that
+have been executed in an inconsistent (out of sync) state, thus
+practically negating their effect.
+
+## Drawing transitions
+
+Renders a graphical overview of your models states transitions
+
+You need `pip install "graphviz>=0.4"` library and add `django_fsm` to
+your `INSTALLED_APPS`:
+
+``` python
+INSTALLED_APPS = (
+ ...
+ 'django_fsm',
+ ...
+)
+```
+
+``` bash
+# Create a dot file
+$ ./manage.py graph_transitions > transitions.dot
+
+# Create a PNG image file only for specific model
+$ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog
+```
+
+## Changelog
+
+### django-fsm 2.8.1 2022-08-15
+
+- Improve fix for get_available_FIELD_transition
diff --git a/README.rst b/README.rst
deleted file mode 100644
index f5b80fb..0000000
--- a/README.rst
+++ /dev/null
@@ -1,435 +0,0 @@
-Django friendly finite state machine support
-============================================
-
-|Build Status|
-
-django-fsm adds simple declarative state management for django models.
-
-If you need parallel task execution, view and background task code reuse
-over different flows - check my new project django-viewflow:
-
-https://github.com/viewflow/viewflow
-
-
-Instead of adding a state field to a django model and managing its
-values by hand, you use ``FSMField`` and mark model methods with
-the ``transition`` decorator. These methods could contain side-effects
-of the state change.
-
-Nice introduction is available here:
-https://gist.github.com/Nagyman/9502133
-
-You may also take a look at django-fsm-admin project containing a mixin
-and template tags to integrate django-fsm state transitions into the
-django admin.
-
-https://github.com/gadventures/django-fsm-admin
-
-Transition logging support could be achieved with help of django-fsm-log
-package
-
-https://github.com/gizmag/django-fsm-log
-
-FSM really helps to structure the code, especially when a new developer
-comes to the project. FSM is most effective when you use it for some
-sequential steps.
-
-
-Installation
-------------
-
-.. code:: bash
-
- $ pip install django-fsm
-
-Or, for the latest git version
-
-.. code:: bash
-
- $ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm
-
-Usage
------
-
-Add FSMState field to your model
-
-.. code:: python
-
- from django_fsm import FSMField, transition
-
- class BlogPost(models.Model):
- state = FSMField(default='new')
-
-Use the ``transition`` decorator to annotate model methods
-
-.. code:: python
-
- @transition(field=state, source='new', target='published')
- def publish(self):
- """
- This function may contain side-effects,
- like updating caches, notifying users, etc.
- The return value will be discarded.
- """
-
-The ``field`` parameter accepts both a string attribute name or an
-actual field instance.
-
-If calling publish() succeeds without raising an exception, the state
-field will be changed, but not written to the database.
-
-.. code:: python
-
- from django_fsm import can_proceed
-
- def publish_view(request, post_id):
- post = get_object_or_404(BlogPost, pk=post_id)
- if not can_proceed(post.publish):
- raise PermissionDenied
-
- post.publish()
- post.save()
- return redirect('/')
-
-If some conditions are required to be met before changing the state, use
-the ``conditions`` argument to ``transition``. ``conditions`` must be a
-list of functions taking one argument, the model instance. The function
-must return either ``True`` or ``False`` or a value that evaluates to
-``True`` or ``False``. If all functions return ``True``, all conditions
-are considered to be met and the transition is allowed to happen. If one
-of the functions returns ``False``, the transition will not happen.
-These functions should not have any side effects.
-
-You can use ordinary functions
-
-.. code:: python
-
- def can_publish(instance):
- # No publishing after 17 hours
- if datetime.datetime.now().hour > 17:
- return False
- return True
-
-Or model methods
-
-.. code:: python
-
- def can_destroy(self):
- return self.is_under_investigation()
-
-Use the conditions like this:
-
-.. code:: python
-
- @transition(field=state, source='new', target='published', conditions=[can_publish])
- def publish(self):
- """
- Side effects galore
- """
-
- @transition(field=state, source='*', target='destroyed', conditions=[can_destroy])
- def destroy(self):
- """
- Side effects galore
- """
-
-You can instantiate a field with ``protected=True`` option to prevent
-direct state field modification.
-
-.. code:: python
-
- class BlogPost(models.Model):
- state = FSMField(default='new', protected=True)
-
- model = BlogPost()
- model.state = 'invalid' # Raises AttributeError
-
-Note that calling
-`refresh_from_db `_
-on a model instance with a protected FSMField will cause an exception.
-
-``source`` state
-~~~~~~~~~~~~~~~~
-
-``source`` parameter accepts a list of states, or an individual state or ``django_fsm.State`` implementation.
-
-You can use ``*`` for ``source`` to allow switching to ``target`` from any state.
-
-You can use ``+`` for ``source`` to allow switching to ``target`` from any state excluding ``target`` state.
-
-``target`` state
-~~~~~~~~~~~~~~~~
-
-``target`` state parameter could point to a specific state or ``django_fsm.State`` implementation
-
-.. code:: python
-
- from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE
- @transition(field=state,
- source='*',
- target=RETURN_VALUE('for_moderators', 'published'))
- def publish(self, is_public=False):
- return 'for_moderators' if is_public else 'published'
-
- @transition(
- field=state,
- source='for_moderators',
- target=GET_STATE(
- lambda self, allowed: 'published' if allowed else 'rejected',
- states=['published', 'rejected']))
- def moderate(self, allowed):
- pass
-
- @transition(
- field=state,
- source='for_moderators',
- target=GET_STATE(
- lambda self, **kwargs: 'published' if kwargs.get("allowed", True) else 'rejected',
- states=['published', 'rejected']))
- def moderate(self, allowed=True):
- pass
-
-
-``custom`` properties
-~~~~~~~~~~~~~~~~~~~~~
-
-Custom properties can be added by providing a dictionary to the
-``custom`` keyword on the ``transition`` decorator.
-
-.. code:: python
-
- @transition(field=state,
- source='*',
- target='onhold',
- custom=dict(verbose='Hold for legal reasons'))
- def legal_hold(self):
- """
- Side effects galore
- """
-
-``on_error`` state
-~~~~~~~~~~~~~~~~~~
-
-If the transition method raises an exception, you can provide a
-specific target state
-
-.. code:: python
-
- @transition(field=state, source='new', target='published', on_error='failed')
- def publish(self):
- """
- Some exception could happen here
- """
-
-``state_choices``
-~~~~~~~~~~~~~~~~~
-
-Instead of passing a two-item iterable ``choices`` you can instead use the
-three-element ``state_choices``, the last element being a string reference
-to a model proxy class.
-
-The base class instance would be dynamically changed to the corresponding Proxy
-class instance, depending on the state. Even for queryset results, you
-will get Proxy class instances, even if the QuerySet is executed on the base class.
-
-Check the `test
-case `__
-for example usage. Or read about `implementation
-internals `__
-
-Permissions
-~~~~~~~~~~~
-
-It is common to have permissions attached to each model transition.
-``django-fsm`` handles this with ``permission`` keyword on the
-``transition`` decorator. ``permission`` accepts a permission string, or
-callable that expects ``instance`` and ``user`` arguments and returns
-True if the user can perform the transition.
-
-.. code:: python
-
- @transition(field=state, source='*', target='published',
- permission=lambda instance, user: not user.has_perm('myapp.can_make_mistakes'))
- def publish(self):
- pass
-
- @transition(field=state, source='*', target='removed',
- permission='myapp.can_remove_post')
- def remove(self):
- pass
-
-You can check permission with ``has_transition_permission`` method
-
-.. code:: python
-
- from django_fsm import has_transition_perm
- def publish_view(request, post_id):
- post = get_object_or_404(BlogPost, pk=post_id)
- if not has_transition_perm(post.publish, request.user):
- raise PermissionDenied
-
- post.publish()
- post.save()
- return redirect('/')
-
-Model methods
-~~~~~~~~~~~~~
-
-``get_all_FIELD_transitions`` Enumerates all declared transitions
-
-``get_available_FIELD_transitions`` Returns all transitions data
-available in current state
-
-``get_available_user_FIELD_transitions`` Enumerates all transitions data
-available in current state for provided user
-
-Foreign Key constraints support
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you store the states in the db table you could use FSMKeyField to
-ensure Foreign Key database integrity.
-
-In your model :
-
-.. code:: python
-
- class DbState(models.Model):
- id = models.CharField(primary_key=True, max_length=50)
- label = models.CharField(max_length=255)
-
- def __unicode__(self):
- return self.label
-
-
- class BlogPost(models.Model):
- state = FSMKeyField(DbState, default='new')
-
- @transition(field=state, source='new', target='published')
- def publish(self):
- pass
-
-In your fixtures/initial\_data.json :
-
-.. code:: json
-
- [
- {
- "pk": "new",
- "model": "myapp.dbstate",
- "fields": {
- "label": "_NEW_"
- }
- },
- {
- "pk": "published",
- "model": "myapp.dbstate",
- "fields": {
- "label": "_PUBLISHED_"
- }
- }
- ]
-
-Note : source and target parameters in @transition decorator use pk
-values of DBState model as names, even if field "real" name is used,
-without \_id postfix, as field parameter.
-
-Integer Field support
-~~~~~~~~~~~~~~~~~~~~~
-
-You can also use ``FSMIntegerField``. This is handy when you want to use
-enum style constants.
-
-.. code:: python
-
- class BlogPostStateEnum(object):
- NEW = 10
- PUBLISHED = 20
- HIDDEN = 30
-
- class BlogPostWithIntegerField(models.Model):
- state = FSMIntegerField(default=BlogPostStateEnum.NEW)
-
- @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED)
- def publish(self):
- pass
-
-Signals
-~~~~~~~
-
-``django_fsm.signals.pre_transition`` and
-``django_fsm.signals.post_transition`` are called before and after
-allowed transition. No signals on invalid transition are called.
-
-Arguments sent with these signals:
-
-**sender** The model class.
-
-**instance** The actual instance being processed
-
-**name** Transition name
-
-**source** Source model state
-
-**target** Target model state
-
-Optimistic locking
-------------------
-
-``django-fsm`` provides optimistic locking mixin, to avoid concurrent
-model state changes. If model state was changed in database
-``django_fsm.ConcurrentTransition`` exception would be raised on
-model.save()
-
-.. code:: python
-
- from django_fsm import FSMField, ConcurrentTransitionMixin
-
- class BlogPost(ConcurrentTransitionMixin, models.Model):
- state = FSMField(default='new')
-
-For guaranteed protection against race conditions caused by concurrently
-executed transitions, make sure:
-
-- Your transitions do not have any side effects except for changes in the database,
-- You always run the save() method on the object within ``django.db.transaction.atomic()`` block.
-
-Following these recommendations, you can rely on
-ConcurrentTransitionMixin to cause a rollback of all the changes that
-have been executed in an inconsistent (out of sync) state, thus
-practically negating their effect.
-
-Drawing transitions
--------------------
-
-Renders a graphical overview of your models states transitions
-
-You need ``pip install "graphviz>=0.4"`` library and add ``django_fsm`` to
-your ``INSTALLED_APPS``:
-
-.. code:: python
-
- INSTALLED_APPS = (
- ...
- 'django_fsm',
- ...
- )
-
-.. code:: bash
-
- # Create a dot file
- $ ./manage.py graph_transitions > transitions.dot
-
- # Create a PNG image file only for specific model
- $ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog
-
-Changelog
----------
-
-
-django-fsm 2.8.1 2022-08-15
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- Improve fix for get_available_FIELD_transition
-
-.. |Build Status| image:: https://travis-ci.org/viewflow/django-fsm.svg?branch=master
- :target: https://travis-ci.org/viewflow/django-fsm
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..8113b66
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,388 @@
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+
+[[package]]
+name = "asgiref"
+version = "3.7.2"
+description = "ASGI specs, helper code, and adapters"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
+ {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
+
+[package.extras]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
+
+[[package]]
+name = "backports-zoneinfo"
+version = "0.2.1"
+description = "Backport of the standard library zoneinfo module"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
+ {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
+]
+
+[package.extras]
+tzdata = ["tzdata"]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.3.2"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"},
+ {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"},
+ {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"},
+ {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
+ {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
+ {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
+ {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
+ {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"},
+ {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"},
+ {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"},
+ {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"},
+ {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"},
+ {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
+ {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
+]
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "distlib"
+version = "0.3.7"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+]
+
+[[package]]
+name = "django"
+version = "4.2.6"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Django-4.2.6-py3-none-any.whl", hash = "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215"},
+ {file = "Django-4.2.6.tar.gz", hash = "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f"},
+]
+
+[package.dependencies]
+asgiref = ">=3.6.0,<4"
+"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
+sqlparse = ">=0.3.1"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+argon2 = ["argon2-cffi (>=19.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "django-guardian"
+version = "2.4.0"
+description = "Implementation of per object permissions for Django."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"},
+ {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"},
+]
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "filelock"
+version = "3.12.4"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
+ {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
+typing = ["typing-extensions (>=4.7.1)"]
+
+[[package]]
+name = "graphviz"
+version = "0.20.1"
+description = "Simple Python interface for Graphviz"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "graphviz-0.20.1-py3-none-any.whl", hash = "sha256:587c58a223b51611c0cf461132da386edd896a029524ca61a1462b880bf97977"},
+ {file = "graphviz-0.20.1.zip", hash = "sha256:8c58f14adaa3b947daf26c19bc1e98c4e0702cdc31cf99153e6f06904d492bf8"},
+]
+
+[package.extras]
+dev = ["flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"]
+docs = ["sphinx (>=5)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"]
+test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>=3)"]
+
+[[package]]
+name = "identify"
+version = "2.5.30"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"},
+ {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+ {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "platformdirs"
+version = "3.11.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
+ {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
+
+[[package]]
+name = "pre-commit"
+version = "3.5.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
+ {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "setuptools"
+version = "68.2.2"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
+ {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "sqlparse"
+version = "0.4.4"
+description = "A non-validating SQL parser."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
+ {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
+]
+
+[package.extras]
+dev = ["build", "flake8"]
+doc = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2023.3"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+ {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
+ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.24.6"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"},
+ {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<4"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.8"
+content-hash = "a86614edcf82151f75bb613525e9ebb46b64d0cdf738dedb3bfb119eff020c94"
diff --git a/poetry.toml b/poetry.toml
new file mode 100644
index 0000000..53b35d3
--- /dev/null
+++ b/poetry.toml
@@ -0,0 +1,3 @@
+[virtualenvs]
+create = true
+in-project = true
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b087299
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,48 @@
+[tool.poetry]
+name = "django-fsm"
+version = "2.8.1"
+description = "Django friendly finite state machine support."
+authors = ["Mikhail Podgurskiy "]
+license = "MIT License"
+readme = "README.md"
+homepage = "http://github.com/kmmbvnr/django-fsm"
+repository = "http://github.com/kmmbvnr/django-fsm"
+documentation = "http://github.com/kmmbvnr/django-fsm"
+classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.1",
+ "Framework :: Django :: 4.2",
+ "Framework :: Django :: 5.0",
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
+ 'Framework :: Django',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+]
+packages = [{ include = "django_fsm" }]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+django = ">=3.2"
+
+[tool.poetry.group.graphviz.dependencies]
+graphviz = "*"
+
+[tool.poetry.group.dev.dependencies]
+coverage = "*"
+django-guardian = "*"
+graphviz = "*"
+pre-commit = "*"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
index 562fc68..b0b7553 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,32 @@
-django>=1.6
+asgiref==3.7.2 ; python_version >= "3.8" and python_version < "4.0" \
+ --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \
+ --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed
+backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" \
+ --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \
+ --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \
+ --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \
+ --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \
+ --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \
+ --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \
+ --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \
+ --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \
+ --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \
+ --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \
+ --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \
+ --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \
+ --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \
+ --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \
+ --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \
+ --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2
+django==4.2.6 ; python_version >= "3.8" and python_version < "4.0" \
+ --hash=sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f \
+ --hash=sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215
+sqlparse==0.4.4 ; python_version >= "3.8" and python_version < "4.0" \
+ --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \
+ --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c
+typing-extensions==4.8.0 ; python_version >= "3.8" and python_version < "3.11" \
+ --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
+ --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
+tzdata==2023.3 ; python_version >= "3.8" and python_version < "4.0" and sys_platform == "win32" \
+ --hash=sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a \
+ --hash=sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda
diff --git a/tox.ini b/tox.ini
index c373e9c..7eb24f4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,6 +18,7 @@ deps =
graphviz==0.20.1
pep8==1.7.1
pyflakes==3.1.0
+
commands = {posargs:python ./tests/manage.py test}
@@ -30,4 +31,4 @@ python =
3.9: py39
3.10: py310
3.11: py311
- 3.12: py312
\ No newline at end of file
+ 3.12: py312