pax_global_header 0000666 0000000 0000000 00000000064 15102141373 0014507 g ustar 00root root 0000000 0000000 52 comment=3e82f03bf2540636f01988663f590c270a93ba80
django-commons-django-fsm-2-f5829bc/ 0000775 0000000 0000000 00000000000 15102141373 0017230 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/.github/ 0000775 0000000 0000000 00000000000 15102141373 0020570 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/.github/dependabot.yml 0000664 0000000 0000000 00000000162 15102141373 0023417 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
django-commons-django-fsm-2-f5829bc/.github/workflows/ 0000775 0000000 0000000 00000000000 15102141373 0022625 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/.github/workflows/coverage.yml 0000664 0000000 0000000 00000001471 15102141373 0025146 0 ustar 00root root 0000000 0000000 name: Coverage
on:
pull_request:
push:
branches:
- main
jobs:
coverage:
name: Check coverage
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v5
- uses: snok/install-poetry@v1
with:
version: 1.3.2
virtualenvs-create: true
virtualenvs-in-project: true
- name: Set up Python 3.13
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: poetry
- name: Install requirements
run: poetry install
- name: Run tests
run: poetry run coverage run -m pytest --cov=django_fsm --cov-report=xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
django-commons-django-fsm-2-f5829bc/.github/workflows/lint.yml 0000664 0000000 0000000 00000000434 15102141373 0024317 0 ustar 00root root 0000000 0000000 name: django-fsm linting
on:
pull_request:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- uses: pre-commit/action@v3.0.1
django-commons-django-fsm-2-f5829bc/.github/workflows/release.yml 0000664 0000000 0000000 00000006376 15102141373 0025004 0 ustar 00root root 0000000 0000000 name: Release
on:
push:
tags:
- '*.*.*'
env:
# Change these for your project's URLs
PYPI_URL: https://pypi.org/p/django-fsm-2
PYPI_TEST_URL: https://test.pypi.org/p/django-fsm-2
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install pypa/build
run:
python3 -m pip install build --user
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v5
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
name: >-
Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: ${{ env.PYPI_URL }}
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github-release:
name: >-
Sign the Python 🐍 distribution 📦 with Sigstore
and upload them to GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.1.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
'${{ github.ref_name }}'
--repo '${{ github.repository }}'
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
publish-to-testpypi:
name: Publish Python 🐍 distribution 📦 to TestPyPI
needs:
- build
runs-on: ubuntu-latest
environment:
name: testpypi
url: ${{ env.PYPI_TEST_URL }}
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
django-commons-django-fsm-2-f5829bc/.github/workflows/test.yml 0000664 0000000 0000000 00000001225 15102141373 0024327 0 ustar 00root root 0000000 0000000 name: django-fsm testing
on:
pull_request:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions
- name: Test with tox
run: tox
django-commons-django-fsm-2-f5829bc/.gitignore 0000664 0000000 0000000 00000003431 15102141373 0021221 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
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
django-commons-django-fsm-2-f5829bc/.pre-commit-config.yaml 0000664 0000000 0000000 00000002344 15102141373 0023514 0 ustar 00root root 0000000 0000000 default_language_version:
python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
args: ["--maxkb=700"]
- id: check-case-conflict
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
#- id: no-commit-to-branch
- id: trailing-whitespace
- repo: https://github.com/crate-ci/typos
rev: v1.37.2
hooks:
- id: typos
args: []
types_or:
- python
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args:
- "--py38-plus"
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.28.0
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
- repo: https://github.com/python-poetry/poetry
rev: 2.2.1
hooks:
- id: poetry-check
additional_dependencies:
- poetry-plugin-sort
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
hooks:
- id: ruff-format
- id: ruff-check
django-commons-django-fsm-2-f5829bc/CHANGELOG.rst 0000664 0000000 0000000 00000011217 15102141373 0021253 0 ustar 00root root 0000000 0000000 Changelog
=========
django-fsm-2 4.1.0 2025-11-03
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Add support for Django 6.0
- Add support for Django 5.2
- Add support for python 3.14
- Add support for python 3.13
django-fsm-2 4.0.0 2024-09-02
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Add support for Django 5.1
- Remove support for Django 3.2
- Remove support for Django 4.0
- Remove support for Django 4.1
- Move the project to ``django-commons``
django-fsm-2 3.0.0 2024-03-26
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
First release of the forked version of django-fsm
- Drop support for Python < 3.8.
- Add support for python 3.11
- Add support for python 3.12
- Drop support for django < 3.2
- Add support for django 4.2
- Add support for django 5.0
- Enable Github actions for testing
- Remove South support...if exists
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 command 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
django-commons-django-fsm-2-f5829bc/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000000263 15102141373 0022030 0 ustar 00root root 0000000 0000000 # Django FSM 2 Code of Conduct
The django-fsm-2 project utilizes the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md).
django-commons-django-fsm-2-f5829bc/LICENSE 0000664 0000000 0000000 00000002075 15102141373 0020241 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2010 Mikhail Podgurskiy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
django-commons-django-fsm-2-f5829bc/README.md 0000664 0000000 0000000 00000030514 15102141373 0020512 0 ustar 00root root 0000000 0000000 # Django friendly finite state machine support
[](https://github.com/django-commons/django-fsm-2/actions/workflows/test.yml)
[](https://codecov.io/github/django-commons/django-fsm-2)
[](https://github.com/django-commons/django-fsm-2#settings)
[](https://github.com/django-commons/anymail-history/LICENSE)
django-fsm adds simple declarative state management for django models.
> [!IMPORTANT]
> Django FSM-2 is a maintained fork of [Django FSM](https://github.com/viewflow/django-fsm).
>
> Big thanks to Mikhail Podgurskiy for starting this awesome project and maintaining it for so many years.
>
> Unfortunately, after 2 years without any releases, the project was brutally archived. [Viewflow](https://github.com/viewflow/viewflow) is presented as an alternative but the transition is not that easy.
>
> If what you need is just a simple state machine, tailor-made for Django, Django FSM-2 is the successor of Django FSM, with dependencies updates, typing (planned)
## Introduction
**FSM really helps to structure the code, and centralize the lifecycle of your Models.**
Instead of adding a CharField field to a django model and manage its
values by hand everywhere, `FSMFields` offer the ability to declare your
`transitions` once with the decorator. These methods could contain side-effects, permissions, or logic to make the lifecycle management easier.
Nice introduction is available here:
## Installation
First, install the package with pip.
``` bash
$ pip install django-fsm-2
```
Or, for the latest git version
``` bash
$ pip install -e git://github.com/django-commons/django-fsm-2.git#egg=django-fsm
```
Register django_fsm in your list of Django applications
```python
INSTALLED_APPS = (
...,
'django_fsm',
...,
)
```
## Migration from django-fsm
django-fsm-2 is a drop-in replacement, it's actually the same project but from a different source.
So all you need to do is to replace `django-fsm` dependency with `django-fsm-2`. And voila!
``` bash
$ pip install django-fsm-2
```
## 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)
label = models.CharField()
def __str__(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
1. You need `pip install "graphviz>=0.4"` library
2. Make sure `django_fsm` is in your `INSTALLED_APPS` settings:
``` python
INSTALLED_APPS = (
...
'django_fsm',
...
)
```
3. Then you can use `graph_transitions` command:
``` 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
# Exclude some transitions
$ ./manage.py graph_transitions -e transition_1,transition_2 myapp.Blog
```
## Extensions
You may also take a look at django-fsm-2-admin project containing a mixin
and template tags to integrate django-fsm-2 state transitions into the
django admin.
Transition logging support could be achieved with help of django-fsm-log
package
django-commons-django-fsm-2-f5829bc/django_fsm/ 0000775 0000000 0000000 00000000000 15102141373 0021337 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/django_fsm/__init__.py 0000664 0000000 0000000 00000053077 15102141373 0023464 0 ustar 00root root 0000000 0000000 """
State tracking functionality for django models
"""
from __future__ import annotations
import inspect
from functools import partialmethod
from functools import wraps
from django import VERSION as DJANGO_VERSION
from django.apps import apps as django_apps
from django.db import models
from django.db.models import Field
from django.db.models.query_utils import DeferredAttribute
from django.db.models.signals import class_prepared
from django_fsm.signals import post_transition
from django_fsm.signals import pre_transition
__all__ = [
"GET_STATE",
"RETURN_VALUE",
"ConcurrentTransition",
"ConcurrentTransitionMixin",
"FSMField",
"FSMFieldMixin",
"FSMIntegerField",
"FSMKeyField",
"TransitionNotAllowed",
"can_proceed",
"has_transition_perm",
"transition",
]
class TransitionNotAllowed(Exception): # noqa: N818
"""Raised when a transition is not allowed"""
def __init__(self, *args, **kwargs):
self.object = kwargs.pop("object", None)
self.method = kwargs.pop("method", None)
super().__init__(*args, **kwargs)
class InvalidResultState(Exception): # noqa: N818
"""Raised when we got invalid result state"""
class ConcurrentTransition(Exception): # noqa: N818
"""
Raised when the transition cannot be executed because the
object has become stale (state has been changed since it
was fetched from the database).
"""
class Transition:
def __init__(self, method, source, target, on_error, conditions, permission, custom):
self.method = method
self.source = source
self.target = target
self.on_error = on_error
self.conditions = conditions
self.permission = permission
self.custom = custom
@property
def name(self):
return self.method.__name__
def has_perm(self, instance, user):
if not self.permission:
return True
if callable(self.permission):
return bool(self.permission(instance, user))
if user.has_perm(self.permission, instance):
return True
if user.has_perm(self.permission):
return True
return False
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
if isinstance(other, str):
return other == self.name
if isinstance(other, Transition):
return other.name == self.name
return False
def get_available_FIELD_transitions(instance, field): # noqa: N802
"""
List of transitions available in current model state
with all conditions met
"""
curr_state = field.get_state(instance)
transitions = field.transitions[instance.__class__]
for transition in transitions.values():
meta = transition._django_fsm
if meta.has_transition(curr_state) and meta.conditions_met(instance, curr_state):
yield meta.get_transition(curr_state)
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): # noqa: N802
"""
List of transitions available in current model state
with all conditions met and user have rights on it
"""
for transition in get_available_FIELD_transitions(instance, field):
if transition.has_perm(instance, user):
yield transition
class FSMMeta:
"""
Models methods transitions meta information
"""
def __init__(self, field, method):
self.field = field
self.transitions = {} # source -> Transition
def get_transition(self, source):
transition = self.transitions.get(source, None)
if transition is None:
transition = self.transitions.get("*", None)
if transition is None:
transition = self.transitions.get("+", None)
return transition
def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):
if source in self.transitions:
raise AssertionError(f"Duplicate transition for {source} state")
self.transitions[source] = Transition(
method=method,
source=source,
target=target,
on_error=on_error,
conditions=conditions,
permission=permission,
custom=custom,
)
def has_transition(self, state):
"""
Lookup if any transition exists from current model state using current method
"""
if state in self.transitions:
return True
if "*" in self.transitions:
return True
if "+" in self.transitions and self.transitions["+"].target != state:
return True
return False
def conditions_met(self, instance, state):
"""
Check if all conditions have been met
"""
transition = self.get_transition(state)
if transition is None:
return False
if transition.conditions is None:
return True
return all(condition(instance) for condition in transition.conditions)
def has_transition_perm(self, instance, state, user):
transition = self.get_transition(state)
if not transition:
return False
return transition.has_perm(instance, user)
def next_state(self, current_state):
transition = self.get_transition(current_state)
if transition is None:
raise TransitionNotAllowed(f"No transition from {current_state}")
return transition.target
def exception_state(self, current_state):
transition = self.get_transition(current_state)
if transition is None:
raise TransitionNotAllowed(f"No transition from {current_state}")
return transition.on_error
class FSMFieldDescriptor:
def __init__(self, field):
self.field = field
def __get__(self, instance, instance_type=None):
if instance is None:
return self
return self.field.get_state(instance)
def __set__(self, instance, value):
if self.field.protected and self.field.name in instance.__dict__:
raise AttributeError(f"Direct {self.field.name} modification is not allowed")
# Update state
self.field.set_proxy(instance, value)
self.field.set_state(instance, value)
class FSMFieldMixin:
descriptor_class = FSMFieldDescriptor
def __init__(self, *args, **kwargs):
self.protected = kwargs.pop("protected", False)
self.transitions = {} # cls -> (transitions name -> method)
self.state_proxy = {} # state -> ProxyClsRef
state_choices = kwargs.pop("state_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")
if state_choices is not None:
choices = []
for state, title, proxy_cls_ref in state_choices:
choices.append((state, title))
self.state_proxy[state] = proxy_cls_ref
kwargs["choices"] = choices
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.protected:
kwargs["protected"] = self.protected
return name, path, args, kwargs
def get_state(self, instance):
# The state field may be deferred. We delegate the logic of figuring this out
# and loading the deferred field on-demand to Django's built-in DeferredAttribute class.
return DeferredAttribute(self).__get__(instance)
def set_state(self, instance, state):
instance.__dict__[self.name] = state
def set_proxy(self, instance, state):
"""
Change class
"""
if state in self.state_proxy:
state_proxy = self.state_proxy[state]
try:
app_label, model_name = state_proxy.split(".")
except ValueError:
# If we can't split, assume a model in current app
app_label = instance._meta.app_label
model_name = state_proxy
model = django_apps.get_app_config(app_label).get_model(model_name)
if model is None:
raise ValueError(f"No model found {state_proxy}")
instance.__class__ = model
def change_state(self, instance, method, *args, **kwargs):
meta = method._django_fsm
method_name = method.__name__
current_state = self.get_state(instance)
if not meta.has_transition(current_state):
raise TransitionNotAllowed(
f"Can't switch from state '{current_state}' using method '{method_name}'",
object=instance,
method=method,
)
if not meta.conditions_met(instance, current_state):
raise TransitionNotAllowed(
f"Transition conditions have not been met for method '{method_name}'", object=instance, method=method
)
next_state = meta.next_state(current_state)
signal_kwargs = {
"sender": instance.__class__,
"instance": instance,
"name": method_name,
"field": meta.field,
"source": current_state,
"target": next_state,
"method_args": args,
"method_kwargs": kwargs,
}
pre_transition.send(**signal_kwargs)
try:
result = method(instance, *args, **kwargs)
if next_state is not None:
if hasattr(next_state, "get_state"):
next_state = next_state.get_state(instance, transition, result, args=args, kwargs=kwargs)
signal_kwargs["target"] = next_state
self.set_proxy(instance, next_state)
self.set_state(instance, next_state)
except Exception as exc:
exception_state = meta.exception_state(current_state)
if exception_state:
self.set_proxy(instance, exception_state)
self.set_state(instance, exception_state)
signal_kwargs["target"] = exception_state
signal_kwargs["exception"] = exc
post_transition.send(**signal_kwargs)
raise
else:
post_transition.send(**signal_kwargs)
return result
def get_all_transitions(self, instance_cls):
"""
Returns [(source, target, name, method)] for all field transitions
"""
transitions = self.transitions[instance_cls]
for transition in transitions.values():
meta = transition._django_fsm
yield from meta.transitions.values()
def contribute_to_class(self, cls, name, **kwargs):
self.base_cls = cls
super().contribute_to_class(cls, name, **kwargs)
setattr(cls, self.name, self.descriptor_class(self))
setattr(cls, f"get_all_{self.name}_transitions", partialmethod(get_all_FIELD_transitions, field=self))
setattr(cls, f"get_available_{self.name}_transitions", partialmethod(get_available_FIELD_transitions, field=self))
setattr(
cls,
f"get_available_user_{self.name}_transitions",
partialmethod(get_available_user_FIELD_transitions, field=self),
)
class_prepared.connect(self._collect_transitions)
def _collect_transitions(self, *args, **kwargs):
sender = kwargs["sender"]
if not issubclass(sender, self.base_cls):
return
def is_field_transition_method(attr):
return (
(inspect.ismethod(attr) or inspect.isfunction(attr))
and hasattr(attr, "_django_fsm")
and (
attr._django_fsm.field in [self, self.name]
or (
isinstance(attr._django_fsm.field, Field)
and attr._django_fsm.field.name == self.name
and attr._django_fsm.field.creation_counter == self.creation_counter
)
)
)
sender_transitions = {}
transitions = inspect.getmembers(sender, predicate=is_field_transition_method)
for method_name, method in transitions:
method._django_fsm.field = self
sender_transitions[method_name] = method
self.transitions[sender] = sender_transitions
class FSMField(FSMFieldMixin, models.CharField):
"""
State Machine support for Django model as CharField
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("max_length", 50)
super().__init__(*args, **kwargs)
class FSMIntegerField(FSMFieldMixin, models.IntegerField):
"""
Same as FSMField, but stores the state value in an IntegerField.
"""
class FSMKeyField(FSMFieldMixin, models.ForeignKey):
"""
State Machine support for Django model
"""
def get_state(self, instance):
return instance.__dict__[self.attname]
def set_state(self, instance, state):
instance.__dict__[self.attname] = self.to_python(state)
class FSMModelMixin:
"""
Mixin that allows refresh_from_db for models with fsm protected fields
"""
def _get_protected_fsm_fields(self):
def is_fsm_and_protected(f):
return isinstance(f, FSMFieldMixin) and f.protected
protected_fields = filter(is_fsm_and_protected, self._meta.concrete_fields)
return {f.attname for f in protected_fields}
def refresh_from_db(self, *args, **kwargs):
fields = kwargs.pop("fields", None)
# Use provided fields, if not set then reload all non-deferred fields.0
if not fields:
deferred_fields = self.get_deferred_fields()
protected_fields = self._get_protected_fsm_fields()
skipped_fields = deferred_fields.union(protected_fields)
fields = [f.attname for f in self._meta.concrete_fields if f.attname not in skipped_fields]
kwargs["fields"] = fields
super().refresh_from_db(*args, **kwargs)
class ConcurrentTransitionMixin:
"""
Protects a Model from undesirable effects caused by concurrently executed transitions,
e.g. running the same transition multiple times at the same time, or running different
transitions with the same SOURCE state at the same time.
This behavior is achieved using an idea based on optimistic locking. No additional
version field is required though; only the state field(s) is/are used for the tracking.
This scheme is not that strict as true *optimistic locking* mechanism, it is however
more lightweight - leveraging the specifics of FSM models.
Instance of a model based on this Mixin will be prevented from saving into DB if any
of its state fields (instances of FSMFieldMixin) has been changed since the object
was fetched from the database. *ConcurrentTransition* exception will be raised in such
cases.
For guaranteed protection against such race conditions, 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.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._update_initial_state()
@property
def state_fields(self):
return filter(lambda field: isinstance(field, FSMFieldMixin), self._meta.fields)
def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update, returning_fields=None):
# _do_update is called once for each model class in the inheritance hierarchy.
# We can only filter the base_qs on state fields (can be more than one!) present in this particular model.
# Select state fields to filter on
filter_on = filter(lambda field: field.model == base_qs.model, self.state_fields)
# state filter will be used to narrow down the standard filter checking only PK
state_filter = {field.attname: self.__initial_states[field.attname] for field in filter_on}
# Django 6.0+ added returning_fields parameter to _do_update
if DJANGO_VERSION >= (6, 0):
updated = super()._do_update(
base_qs=base_qs.filter(**state_filter),
using=using,
pk_val=pk_val,
values=values,
update_fields=update_fields,
forced_update=forced_update,
returning_fields=returning_fields,
)
else:
updated = super()._do_update(
base_qs=base_qs.filter(**state_filter),
using=using,
pk_val=pk_val,
values=values,
update_fields=update_fields,
forced_update=forced_update,
)
# It may happen that nothing was updated in the original _do_update method not because of unmatching state,
# but because of missing PK. This codepath is possible when saving a new model instance with *preset PK*.
# In this case Django does not know it has to do INSERT operation, so it tries UPDATE first and falls back to
# INSERT if UPDATE fails.
# Thus, we need to make sure we only catch the case when the object *is* in the DB, but with changed state; and
# mimic standard _do_update behavior otherwise. Django will pick it up and execute _do_insert.
if not updated and base_qs.filter(pk=pk_val).using(using).exists():
raise ConcurrentTransition("Cannot save object! The state has been changed since fetched from the database!")
return updated
def _update_initial_state(self):
self.__initial_states = {field.attname: field.value_from_object(self) for field in self.state_fields}
def refresh_from_db(self, *args, **kwargs):
super().refresh_from_db(*args, **kwargs)
self._update_initial_state()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self._update_initial_state()
def transition(field, source="*", target=None, on_error=None, conditions=[], permission=None, custom={}):
"""
Method decorator to mark allowed transitions.
Set target to None if current state needs to be validated and
has not changed after the function call.
"""
def inner_transition(func):
wrapper_installed, fsm_meta = True, getattr(func, "_django_fsm", None)
if not fsm_meta:
wrapper_installed = False
fsm_meta = FSMMeta(field=field, method=func)
setattr(func, "_django_fsm", fsm_meta)
if isinstance(source, (list, tuple, set)):
for state in source:
func._django_fsm.add_transition(func, state, target, on_error, conditions, permission, custom)
else:
func._django_fsm.add_transition(func, source, target, on_error, conditions, permission, custom)
@wraps(func)
def _change_state(instance, *args, **kwargs):
return fsm_meta.field.change_state(instance, func, *args, **kwargs)
if not wrapper_installed:
return _change_state
return func
return inner_transition
def can_proceed(bound_method, check_conditions=True): # noqa: FBT002
"""
Returns True if model in state allows to call bound_method
Set ``check_conditions`` argument to ``False`` to skip checking
conditions.
"""
if not hasattr(bound_method, "_django_fsm"):
raise TypeError(f"{bound_method.__func__.__name__} method is not transition")
meta = bound_method._django_fsm
self = bound_method.__self__
current_state = meta.field.get_state(self)
return meta.has_transition(current_state) and (not check_conditions or meta.conditions_met(self, current_state))
def has_transition_perm(bound_method, user):
"""
Returns True if model in state allows to call bound_method and user have rights on it
"""
if not hasattr(bound_method, "_django_fsm"):
raise TypeError(f"{bound_method.__func__.__name__} method is not transition")
meta = bound_method._django_fsm
self = bound_method.__self__
current_state = meta.field.get_state(self)
return (
meta.has_transition(current_state)
and meta.conditions_met(self, current_state)
and meta.has_transition_perm(self, current_state, user)
)
class State:
def get_state(self, model, transition, result, args=[], kwargs={}):
raise NotImplementedError
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 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): # 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 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
django-commons-django-fsm-2-f5829bc/django_fsm/management/ 0000775 0000000 0000000 00000000000 15102141373 0023453 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/django_fsm/management/__init__.py 0000664 0000000 0000000 00000000000 15102141373 0025552 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/django_fsm/management/commands/ 0000775 0000000 0000000 00000000000 15102141373 0025254 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/django_fsm/management/commands/__init__.py 0000664 0000000 0000000 00000000000 15102141373 0027353 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/django_fsm/management/commands/graph_transitions.py 0000664 0000000 0000000 00000016413 15102141373 0031371 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from itertools import chain
import graphviz
from django.apps import apps
from django.core.management.base import BaseCommand
from django.utils.encoding import force_str
from django_fsm import GET_STATE
from django_fsm import RETURN_VALUE
from django_fsm import FSMFieldMixin
def all_fsm_fields_data(model):
return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)]
def node_name(field, state) -> str:
opts = field.model._meta
return "{}.{}.{}.{}".format(opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state)
def node_label(field, state: str | None) -> str:
if isinstance(state, (int, bool)) and hasattr(field, "choices") and field.choices:
state = dict(field.choices).get(state)
return force_str(state)
def generate_dot(fields_data, ignore_transitions: list[str] | None = None): # noqa: C901, PLR0912
ignore_transitions = ignore_transitions or []
result = graphviz.Digraph()
for field, model in fields_data:
sources, targets, edges, any_targets, any_except_targets = set(), set(), set(), set(), set()
# dump nodes and edges
for transition in field.get_all_transitions(model):
if transition.name in ignore_transitions:
continue
_targets = list(
(state for state in transition.target.allowed_states)
if isinstance(transition.target, (GET_STATE, RETURN_VALUE))
else (transition.target,)
)
source_name_pair = (
((state, node_name(field, state)) for state in transition.source.allowed_states)
if isinstance(transition.source, (GET_STATE, RETURN_VALUE))
else ((transition.source, node_name(field, transition.source)),)
)
for source, source_name in source_name_pair:
if transition.on_error:
on_error_name = node_name(field, transition.on_error)
targets.add((on_error_name, node_label(field, transition.on_error)))
edges.add((source_name, on_error_name, (("style", "dotted"),)))
for target in _targets:
if transition.source == "*":
any_targets.add((target, transition.name))
elif transition.source == "+":
any_except_targets.add((target, transition.name))
else:
add_transition(source, target, transition.name, source_name, field, sources, targets, edges)
targets.update(
{(node_name(field, target), node_label(field, target)) for target, _ in chain(any_targets, any_except_targets)}
)
for target, name in any_targets:
target_name = node_name(field, target)
all_nodes = sources | targets
for source_name, label in all_nodes:
sources.add((source_name, label))
edges.add((source_name, target_name, (("label", name),)))
for target, name in any_except_targets:
target_name = node_name(field, target)
all_nodes = sources | targets
all_nodes.remove((target_name, node_label(field, target)))
for source_name, label in all_nodes:
sources.add((source_name, label))
edges.add((source_name, target_name, (("label", name),)))
# construct subgraph
opts = field.model._meta
subgraph = graphviz.Digraph(
name=f"cluster_{opts.app_label}_{opts.object_name}_{field.name}",
graph_attr={"label": f"{opts.app_label}.{opts.object_name}.{field.name}"},
)
final_states = targets - sources
for name, label in final_states:
subgraph.node(name, label=label, shape="doublecircle")
for name, label in (sources | targets) - final_states:
subgraph.node(name, label=label, shape="circle")
# 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))
result.subgraph(subgraph)
return result
def add_transition(transition_source, transition_target, transition_name, source_name, field, sources, targets, edges):
target_name = node_name(field, transition_target)
sources.add((source_name, node_label(field, transition_source)))
targets.add((target_name, node_label(field, transition_target)))
edges.add((source_name, target_name, (("label", transition_name),)))
def get_graphviz_layouts():
try:
import graphviz
except ModuleNotFoundError:
return {"sfdp", "circo", "twopi", "dot", "neato", "fdp", "osage", "patchwork"}
else:
return graphviz.ENGINES
class Command(BaseCommand):
help = "Creates a GraphViz dot file with transitions for selected fields"
def add_arguments(self, parser):
parser.add_argument(
"--output",
"-o",
action="store",
dest="outputfile",
help="Render output file. Type of output dependent on file extensions. Use png or jpg to render graph to image.",
)
parser.add_argument(
"--layout",
"-l",
action="store",
dest="layout",
default="dot",
help=f"Layout to be used by GraphViz for visualization. Layouts: {get_graphviz_layouts()}.",
)
parser.add_argument(
"--exclude",
"-e",
action="store",
dest="exclude",
default="",
help="Ignore transitions with this name.",
)
parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]"))
def render_output(self, graph, **options):
filename, graph_format = options["outputfile"].rsplit(".", 1)
graph.engine = options["layout"]
graph.format = graph_format
graph.render(filename)
def handle(self, *args, **options):
fields_data = []
if len(args) != 0:
for arg in args:
field_spec = arg.split(".")
if len(field_spec) == 1:
app = apps.get_app_config(field_spec[0])
for model in apps.get_models(app):
fields_data += all_fsm_fields_data(model)
if len(field_spec) == 2: # noqa: PLR2004
model = apps.get_model(field_spec[0], field_spec[1])
fields_data += all_fsm_fields_data(model)
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:
for model in apps.get_models():
fields_data += all_fsm_fields_data(model)
dotdata = generate_dot(fields_data, ignore_transitions=options["exclude"].split(","))
if options["outputfile"]:
self.render_output(dotdata, **options)
else:
print(dotdata) # noqa: T201
django-commons-django-fsm-2-f5829bc/django_fsm/signals.py 0000664 0000000 0000000 00000000225 15102141373 0023350 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db.models.signals import ModelSignal
pre_transition = ModelSignal()
post_transition = ModelSignal()
django-commons-django-fsm-2-f5829bc/poetry.lock 0000664 0000000 0000000 00000076343 15102141373 0021441 0 ustar 00root root 0000000 0000000 # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[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"
groups = ["main", "dev"]
markers = "python_version < \"3.9\""
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"
groups = ["dev"]
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 = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.5.4"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"},
{file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"},
{file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"},
{file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"},
{file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"},
{file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"},
{file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"},
{file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"},
{file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"},
{file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"},
{file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"},
{file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"},
{file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"},
{file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"},
{file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"},
{file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"},
{file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"},
{file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"},
{file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"},
{file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"},
{file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"},
{file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"},
{file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"},
{file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"},
{file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"},
{file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"},
{file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"},
{file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"},
{file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"},
{file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"},
{file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"},
{file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"},
{file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"},
{file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"},
{file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"},
{file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"},
{file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"},
{file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"},
{file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"},
{file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"},
{file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"},
{file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"},
{file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"},
{file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"},
{file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"},
{file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"},
{file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"},
{file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"},
{file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"},
{file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"},
{file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"},
{file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "django"
version = "4.2.16"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"},
{file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"},
]
[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"
groups = ["dev"]
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 = "exceptiongroup"
version = "1.2.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""]
[[package]]
name = "graphviz"
version = "0.20.3"
description = "Simple Python interface for Graphviz"
optional = false
python-versions = ">=3.8"
groups = ["dev", "graphviz"]
files = [
{file = "graphviz-0.20.3-py3-none-any.whl", hash = "sha256:81f848f2904515d8cd359cc611faba817598d2feaac4027b266aa3eda7b3dde5"},
{file = "graphviz-0.20.3.zip", hash = "sha256:09d6bc81e6a9fa392e7ba52135a9d49f1ed62526f96499325930e87ca1b5925d"},
]
[package.extras]
dev = ["flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"]
docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"]
test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"]
[[package]]
name = "identify"
version = "2.5.36"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[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"
groups = ["dev"]
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 = "pytest"
version = "8.2.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2.0"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
]
[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "pytest-django"
version = "4.8.0"
description = "A Django plugin for pytest."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"},
{file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"},
]
[package.dependencies]
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["Django", "django-configurations (>=2.0)"]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
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 = "sqlparse"
version = "0.5.0"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
{file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
]
[package.extras]
dev = ["build", "hatch"]
doc = ["sphinx"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_full_version <= \"3.11.0a6\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "tzdata"
version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main", "dev"]
markers = "sys_platform == \"win32\""
files = [
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[[package]]
name = "virtualenv"
version = "20.26.3"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "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) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""]
[metadata]
lock-version = "2.1"
python-versions = "^3.8"
content-hash = "092f3ae7c4a31bb2f69f516ae5a635f67668e6f29e4c35257422bdaf2c9615ce"
django-commons-django-fsm-2-f5829bc/poetry.toml 0000664 0000000 0000000 00000000056 15102141373 0021450 0 ustar 00root root 0000000 0000000 [virtualenvs]
create = true
in-project = true
django-commons-django-fsm-2-f5829bc/pyproject.toml 0000664 0000000 0000000 00000006003 15102141373 0022143 0 ustar 00root root 0000000 0000000 [tool.poetry]
name = "django-fsm-2"
version = "4.1.0"
description = "Django friendly finite state machine support."
authors = [
"Mikhail Podgurskiy ",
]
license = "MIT License"
readme = "README.md"
homepage = "http://github.com/django-commons/django-fsm-2"
repository = "http://github.com/django-commons/django-fsm-2"
documentation = "http://github.com/django-commons/django-fsm-2"
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 :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'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',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Topic :: Software Development :: Libraries :: Python Modules',
]
packages = [{ include = "django_fsm" }]
[tool.poetry.dependencies]
python = "^3.8"
django = ">=4.2"
[tool.poetry.group.graphviz.dependencies]
graphviz = "*"
[tool.poetry.group.dev.dependencies]
coverage = "*"
django-guardian = "*"
graphviz = "*"
pre-commit = "*"
pytest = "*"
pytest-cov = "*"
pytest-django = "*"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "tests.settings"
[tool.ruff]
line-length = 130
target-version = "py38"
fix = true
[tool.ruff.lint]
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
"S101", # Use of `assert` detected
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
"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
"PLC0415", # `import` should be at the top-level of a file
"PLR0913", # Too many arguments in function definition
]
fixable = [
"I", # isort
"RUF100", # Unused `noqa` directive
]
[tool.ruff.lint.extend-per-file-ignores]
"tests/*" = [
"DJ008", # Model does not define `__str__` method
]
[tool.ruff.lint.isort]
force-single-line = true
required-imports = ["from __future__ import annotations"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
django-commons-django-fsm-2-f5829bc/tests/ 0000775 0000000 0000000 00000000000 15102141373 0020372 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/__init__.py 0000664 0000000 0000000 00000000000 15102141373 0022471 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/manage.py 0000775 0000000 0000000 00000001272 15102141373 0022201 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
from __future__ import annotations
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
django-commons-django-fsm-2-f5829bc/tests/settings.py 0000664 0000000 0000000 00000006600 15102141373 0022606 0 ustar 00root root 0000000 0000000 """
Django settings for tests project.
Generated by 'django-admin startproject' using Django 4.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from __future__ import annotations
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "nokey" # noqa: S105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
PROJECT_APPS = (
"django_fsm",
"tests.testapp",
)
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"guardian",
*PROJECT_APPS,
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "tests.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "tests.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Authentication
# https://docs.djangoproject.com/en/4.2/topics/auth/
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend", # this is default
"guardian.backends.ObjectPermissionBackend",
)
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
django-commons-django-fsm-2-f5829bc/tests/testapp/ 0000775 0000000 0000000 00000000000 15102141373 0022052 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/testapp/__init__.py 0000664 0000000 0000000 00000000000 15102141373 0024151 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/testapp/apps.py 0000664 0000000 0000000 00000000203 15102141373 0023362 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.apps import AppConfig
class TestAppConfig(AppConfig):
name = "tests.testapp"
django-commons-django-fsm-2-f5829bc/tests/testapp/fixtures/ 0000775 0000000 0000000 00000000000 15102141373 0023723 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/testapp/fixtures/test_states_data.json 0000664 0000000 0000000 00000001056 15102141373 0030153 0 ustar 00root root 0000000 0000000 [
{
"model": "testapp.dbstate",
"pk": "new",
"fields": { "label": "_New"}
},
{
"model": "testapp.dbstate",
"pk": "draft",
"fields": { "label": "_Draft"}
},
{
"model": "testapp.dbstate",
"pk": "dept",
"fields": { "label": "_Dept"}
},
{
"model": "testapp.dbstate",
"pk": "dean",
"fields": { "label": "_Dean"}
},
{
"model": "testapp.dbstate",
"pk": "done",
"fields": { "label": "_Done"}
}
]
django-commons-django-fsm-2-f5829bc/tests/testapp/models.py 0000664 0000000 0000000 00000014614 15102141373 0023715 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django_fsm import GET_STATE
from django_fsm import RETURN_VALUE
from django_fsm import FSMField
from django_fsm import FSMKeyField
from django_fsm import transition
class Application(models.Model):
"""
Student application need to be approved by dept chair and dean.
Test workflow
"""
state = FSMField(default="new")
@transition(field=state, source="new", target="published")
def standard(self):
pass
@transition(field=state, source="published")
def no_target(self):
pass
@transition(field=state, source="*", target="blocked")
def any_source(self):
pass
@transition(field=state, source="+", target="hidden")
def any_source_except_target(self):
pass
@transition(
field=state,
source="new",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state(self, *, allowed: bool):
pass
@transition(
field=state,
source="*",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state_any_source(self, *, allowed: bool):
pass
@transition(
field=state,
source="+",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state_any_source_except_target(self, *, allowed: bool):
pass
@transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked"))
def return_value(self):
return "published"
@transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked"))
def return_value_any_source(self):
return "published"
@transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked"))
def return_value_any_source_except_target(self):
return "published"
@transition(field=state, source="new", target="published", on_error="failed")
def on_error(self):
pass
class DbState(models.Model):
"""
States in DB
"""
id = models.CharField(primary_key=True, max_length=50)
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class FKApplication(models.Model):
"""
Student application need to be approved by dept chair and dean.
Test workflow for FSMKeyField
"""
state = FSMKeyField(DbState, default="new", on_delete=models.CASCADE)
@transition(field=state, source="new", target="published")
def standard(self):
pass
@transition(field=state, source="published")
def no_target(self):
pass
@transition(field=state, source="*", target="blocked")
def any_source(self):
pass
@transition(field=state, source="+", target="hidden")
def any_source_except_target(self):
pass
@transition(
field=state,
source="new",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state(self, *, allowed: bool):
pass
@transition(
field=state,
source="*",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state_any_source(self, *, allowed: bool):
pass
@transition(
field=state,
source="+",
target=GET_STATE(
lambda _, allowed: "published" if allowed else "rejected",
states=["published", "rejected"],
),
)
def get_state_any_source_except_target(self, *, allowed: bool):
pass
@transition(field=state, source="new", target=RETURN_VALUE("moderated", "blocked"))
def return_value(self):
return "published"
@transition(field=state, source="*", target=RETURN_VALUE("moderated", "blocked"))
def return_value_any_source(self):
return "published"
@transition(field=state, source="+", target=RETURN_VALUE("moderated", "blocked"))
def return_value_any_source_except_target(self):
return "published"
@transition(field=state, source="new", target="published", on_error="failed")
def on_error(self):
pass
class BlogPostState(models.IntegerChoices):
NEW = 0, "New"
PUBLISHED = 1, "Published"
HIDDEN = 2, "Hidden"
REMOVED = 3, "Removed"
RESTORED = 4, "Restored"
MODERATED = 5, "Moderated"
STOLEN = 6, "Stolen"
FAILED = 7, "Failed"
class BlogPost(models.Model):
"""
Test workflow
"""
state = FSMField(choices=BlogPostState.choices, default=BlogPostState.NEW, protected=True)
class Meta:
permissions = [
("can_publish_post", "Can publish post"),
("can_remove_post", "Can remove post"),
]
def can_restore(self, user):
return user.is_superuser or user.is_staff
@transition(
field=state,
source=BlogPostState.NEW,
target=BlogPostState.PUBLISHED,
on_error=BlogPostState.FAILED,
permission="testapp.can_publish_post",
)
def publish(self):
pass
@transition(field=state, source=BlogPostState.PUBLISHED)
def notify_all(self):
pass
@transition(
field=state,
source=BlogPostState.PUBLISHED,
target=BlogPostState.HIDDEN,
on_error=BlogPostState.FAILED,
)
def hide(self):
pass
@transition(
field=state,
source=BlogPostState.NEW,
target=BlogPostState.REMOVED,
on_error=BlogPostState.FAILED,
permission=lambda _, u: u.has_perm("testapp.can_remove_post"),
)
def remove(self):
raise Exception(f"No rights to delete {self}")
@transition(
field=state,
source=BlogPostState.NEW,
target=BlogPostState.RESTORED,
on_error=BlogPostState.FAILED,
permission=can_restore,
)
def restore(self):
pass
@transition(field=state, source=[BlogPostState.PUBLISHED, BlogPostState.HIDDEN], target=BlogPostState.STOLEN)
def steal(self):
pass
@transition(field=state, source="*", target=BlogPostState.MODERATED)
def moderate(self):
pass
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/ 0000775 0000000 0000000 00000000000 15102141373 0023214 5 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/testapp/tests/__init__.py 0000664 0000000 0000000 00000000000 15102141373 0025313 0 ustar 00root root 0000000 0000000 django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_abstract_inheritance.py 0000664 0000000 0000000 00000003440 15102141373 0031002 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import can_proceed
from django_fsm import transition
class BaseAbstractModel(models.Model):
state = FSMField(default="new")
class Meta:
abstract = True
@transition(field=state, source="new", target="published")
def publish(self):
pass
class AnotherFromAbstractModel(BaseAbstractModel):
"""
This class exists to trigger a regression when multiple concrete classes
inherit from a shared abstract class (example: BaseAbstractModel).
Don't try to remove it.
"""
@transition(field="state", source="published", target="sticked")
def stick(self):
pass
class InheritedFromAbstractModel(BaseAbstractModel):
@transition(field="state", source="published", target="sticked")
def stick(self):
pass
class TestinheritedModel(TestCase):
def setUp(self):
self.model = InheritedFromAbstractModel()
def test_known_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
assert self.model.state == "published"
assert can_proceed(self.model.stick)
self.model.stick()
assert self.model.state == "sticked"
def test_field_available_transitions_works(self):
self.model.publish()
assert self.model.state == "published"
transitions = self.model.get_available_state_transitions()
assert [data.target for data in transitions] == ["sticked"]
def test_field_all_transitions_works(self):
transitions = self.model.get_all_state_transitions()
assert {("new", "published"), ("published", "sticked")} == {(data.source, data.target) for data in transitions}
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_access_deferred_fsm_field.py 0000664 0000000 0000000 00000001532 15102141373 0031737 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import can_proceed
from django_fsm import transition
class DeferrableModel(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published")
def publish(self):
pass
@transition(field=state, source="+", target="removed")
def remove(self):
pass
class Test(TestCase):
def setUp(self):
DeferrableModel.objects.create()
self.model = DeferrableModel.objects.only("id").get()
def test_usecase(self):
assert self.model.state == "new"
assert can_proceed(self.model.remove)
self.model.remove()
assert self.model.state == "removed"
assert not can_proceed(self.model.remove)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_basic_transitions.py 0000664 0000000 0000000 00000021004 15102141373 0030340 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import Transition
from django_fsm import TransitionNotAllowed
from django_fsm import can_proceed
from django_fsm import transition
from django_fsm.signals import post_transition
from django_fsm.signals import pre_transition
class SimpleBlogPost(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published")
def publish(self):
pass
@transition(source="published", field=state)
def notify_all(self):
pass
@transition(source="published", target="hidden", field=state)
def hide(self):
pass
@transition(source="new", target="removed", field=state)
def remove(self):
raise Exception("Upss")
@transition(source=["published", "hidden"], target="stolen", field=state)
def steal(self):
pass
@transition(source="*", target="moderated", field=state)
def moderate(self):
pass
@transition(source="+", target="blocked", field=state)
def block(self):
pass
@transition(source="*", target="", field=state)
def empty(self):
pass
class FSMFieldTest(TestCase):
def setUp(self):
self.model = SimpleBlogPost()
def test_initial_state_instantiated(self):
assert self.model.state == "new"
def test_known_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
assert self.model.state == "published"
assert can_proceed(self.model.hide)
self.model.hide()
assert self.model.state == "hidden"
def test_unknown_transition_fails(self):
assert not can_proceed(self.model.hide)
with pytest.raises(TransitionNotAllowed):
self.model.hide()
def test_state_non_changed_after_fail(self):
assert can_proceed(self.model.remove)
with pytest.raises(Exception, match="Upss"):
self.model.remove()
assert self.model.state == "new"
def test_allowed_null_transition_should_succeed(self):
self.model.publish()
self.model.notify_all()
assert self.model.state == "published"
def test_unknown_null_transition_should_fail(self):
with pytest.raises(TransitionNotAllowed):
self.model.notify_all()
assert self.model.state == "new"
def test_multiple_source_support_path_1_works(self):
self.model.publish()
self.model.steal()
assert self.model.state == "stolen"
def test_multiple_source_support_path_2_works(self):
self.model.publish()
self.model.hide()
self.model.steal()
assert self.model.state == "stolen"
def test_star_shortcut_succeed(self):
assert can_proceed(self.model.moderate)
self.model.moderate()
assert self.model.state == "moderated"
def test_plus_shortcut_succeeds_for_other_source(self):
"""Tests that the '+' shortcut succeeds for a source
other than the target.
"""
assert can_proceed(self.model.block)
self.model.block()
assert self.model.state == "blocked"
def test_plus_shortcut_fails_for_same_source(self):
"""Tests that the '+' shortcut fails if the source
equals the target.
"""
self.model.block()
assert not can_proceed(self.model.block)
with pytest.raises(TransitionNotAllowed):
self.model.block()
def test_empty_string_target(self):
self.model.empty()
assert self.model.state == ""
class StateSignalsTests(TestCase):
def setUp(self):
self.model = SimpleBlogPost()
self.pre_transition_called = False
self.post_transition_called = False
pre_transition.connect(self.on_pre_transition, sender=SimpleBlogPost)
post_transition.connect(self.on_post_transition, sender=SimpleBlogPost)
def on_pre_transition(self, sender, instance, name, source, target, **kwargs):
assert instance.state == source
self.pre_transition_called = True
def on_post_transition(self, sender, instance, name, source, target, **kwargs):
assert instance.state == target
self.post_transition_called = True
def test_signals_called_on_valid_transition(self):
self.model.publish()
assert self.pre_transition_called
assert self.post_transition_called
def test_signals_not_called_on_invalid_transition(self):
with pytest.raises(TransitionNotAllowed):
self.model.hide()
assert not self.pre_transition_called
assert not self.post_transition_called
class LazySenderTests(StateSignalsTests):
def setUp(self):
self.model = SimpleBlogPost()
self.pre_transition_called = False
self.post_transition_called = False
pre_transition.connect(self.on_pre_transition, sender="testapp.SimpleBlogPost")
post_transition.connect(self.on_post_transition, sender="testapp.SimpleBlogPost")
class TestFieldTransitionsInspect(TestCase):
def setUp(self):
self.model = SimpleBlogPost()
def test_in_operator_for_available_transitions(self):
# store the generator in a list, so we can reuse the generator and do multiple asserts
transitions = list(self.model.get_available_state_transitions())
assert "publish" in transitions
assert "xyz" not in transitions
# inline method for faking the name of the transition
def publish():
pass
obj = Transition(
method=publish,
source="",
target="",
on_error="",
conditions="",
permission="",
custom="",
)
assert obj in transitions
def test_available_conditions_from_new(self):
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {("*", "moderated"), ("new", "published"), ("new", "removed"), ("*", ""), ("+", "blocked")}
assert actual == expected
def test_available_conditions_from_published(self):
self.model.publish()
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {
("*", "moderated"),
("published", None),
("published", "hidden"),
("published", "stolen"),
("*", ""),
("+", "blocked"),
}
assert actual == expected
def test_available_conditions_from_hidden(self):
self.model.publish()
self.model.hide()
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {("*", "moderated"), ("hidden", "stolen"), ("*", ""), ("+", "blocked")}
assert actual == expected
def test_available_conditions_from_stolen(self):
self.model.publish()
self.model.steal()
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {("*", "moderated"), ("*", ""), ("+", "blocked")}
assert actual == expected
def test_available_conditions_from_blocked(self):
self.model.block()
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {("*", "moderated"), ("*", "")}
assert actual == expected
def test_available_conditions_from_empty(self):
self.model.empty()
transitions = self.model.get_available_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {("*", "moderated"), ("*", ""), ("+", "blocked")}
assert actual == expected
def test_all_conditions(self):
transitions = self.model.get_all_state_transitions()
actual = {(transition.source, transition.target) for transition in transitions}
expected = {
("*", "moderated"),
("new", "published"),
("new", "removed"),
("published", None),
("published", "hidden"),
("published", "stolen"),
("hidden", "stolen"),
("*", ""),
("+", "blocked"),
}
assert actual == expected
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_conditions.py 0000664 0000000 0000000 00000002670 15102141373 0027003 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import TransitionNotAllowed
from django_fsm import can_proceed
from django_fsm import transition
def condition_func(instance):
return True
class BlogPostWithConditions(models.Model):
state = FSMField(default="new")
def model_condition(self):
return True
def unmet_condition(self):
return False
@transition(field=state, source="new", target="published", conditions=[condition_func, model_condition])
def publish(self):
pass
@transition(field=state, source="published", target="destroyed", conditions=[condition_func, unmet_condition])
def destroy(self):
pass
class ConditionalTest(TestCase):
def setUp(self):
self.model = BlogPostWithConditions()
def test_initial_staet(self):
assert self.model.state == "new"
def test_known_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
assert self.model.state == "published"
def test_unmet_condition(self):
self.model.publish()
assert self.model.state == "published"
assert not can_proceed(self.model.destroy)
with pytest.raises(TransitionNotAllowed):
self.model.destroy()
assert can_proceed(self.model.destroy, check_conditions=False)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_custom_data.py 0000664 0000000 0000000 00000002546 15102141373 0027137 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class BlogPostWithCustomData(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published", conditions=[], custom={"label": "Publish", "type": "*"})
def publish(self):
pass
@transition(field=state, source="published", target="destroyed", custom={"label": "Destroy", "type": "manual"})
def destroy(self):
pass
@transition(field=state, source="published", target="review", custom={"label": "Periodic review", "type": "automated"})
def review(self):
pass
class CustomTransitionDataTest(TestCase):
def setUp(self):
self.model = BlogPostWithCustomData()
def test_initial_state(self):
assert self.model.state == "new"
transitions = list(self.model.get_available_state_transitions())
assert len(transitions) == 1
assert transitions[0].target == "published"
assert transitions[0].custom == {"label": "Publish", "type": "*"}
def test_all_transitions_have_custom_data(self):
transitions = self.model.get_all_state_transitions()
for t in transitions:
assert t.custom["label"] is not None
assert t.custom["type"] is not None
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_exception_transitions.py 0000664 0000000 0000000 00000002771 15102141373 0031267 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import can_proceed
from django_fsm import transition
from django_fsm.signals import post_transition
class ExceptionalBlogPost(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published", on_error="crashed")
def publish(self):
raise Exception("Upss")
@transition(field=state, source="new", target="deleted")
def delete(self):
raise Exception("Upss")
class FSMFieldExceptionTest(TestCase):
def setUp(self):
self.model = ExceptionalBlogPost()
post_transition.connect(self.on_post_transition, sender=ExceptionalBlogPost)
self.post_transition_data = None
def on_post_transition(self, **kwargs):
self.post_transition_data = kwargs
def test_state_changed_after_fail(self):
assert can_proceed(self.model.publish)
with pytest.raises(Exception, match="Upss"):
self.model.publish()
assert self.model.state == "crashed"
assert self.post_transition_data["target"] == "crashed"
assert "exception" in self.post_transition_data
def test_state_not_changed_after_fail(self):
assert can_proceed(self.model.delete)
with pytest.raises(Exception, match="Upss"):
self.model.delete()
assert self.model.state == "new"
assert self.post_transition_data is None
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_graph_transitions.py 0000664 0000000 0000000 00000002276 15102141373 0030372 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.core.management import call_command
from django.test import TestCase
from django_fsm.management.commands.graph_transitions import get_graphviz_layouts
from django_fsm.management.commands.graph_transitions import node_label
from tests.testapp.models import BlogPost
from tests.testapp.models import BlogPostState
class GraphTransitionsCommandTest(TestCase):
MODELS_TO_TEST = [
"testapp.Application",
"testapp.FKApplication",
]
def test_node_label(self):
assert node_label(BlogPost.state.field, BlogPostState.PUBLISHED.value) == BlogPostState.PUBLISHED.label
def test_app(self):
call_command("graph_transitions", "testapp")
def test_single_model(self):
for model in self.MODELS_TO_TEST:
call_command("graph_transitions", model)
def test_single_model_with_layouts(self):
for model in self.MODELS_TO_TEST:
for layout in get_graphviz_layouts():
call_command("graph_transitions", "-l", layout, model)
def test_exclude(self):
for model in self.MODELS_TO_TEST:
call_command("graph_transitions", "-e", "standard,no_target", model)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_integer_field.py 0000664 0000000 0000000 00000002177 15102141373 0027434 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMIntegerField
from django_fsm import TransitionNotAllowed
from django_fsm import transition
class BlogPostStateEnum:
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
@transition(field=state, source=BlogPostStateEnum.PUBLISHED, target=BlogPostStateEnum.HIDDEN)
def hide(self):
pass
class BlogPostWithIntegerFieldTest(TestCase):
def setUp(self):
self.model = BlogPostWithIntegerField()
def test_known_transition_should_succeed(self):
self.model.publish()
assert self.model.state == BlogPostStateEnum.PUBLISHED
self.model.hide()
assert self.model.state == BlogPostStateEnum.HIDDEN
def test_unknown_transition_fails(self):
with pytest.raises(TransitionNotAllowed):
self.model.hide()
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_key_field.py 0000664 0000000 0000000 00000010230 15102141373 0026554 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMKeyField
from django_fsm import TransitionNotAllowed
from django_fsm import can_proceed
from django_fsm import transition
from tests.testapp.models import DbState
FK_AVAILABLE_STATES = (
("New", "_NEW_"),
("Published", "_PUBLISHED_"),
("Hidden", "_HIDDEN_"),
("Removed", "_REMOVED_"),
("Stolen", "_STOLEN_"),
("Moderated", "_MODERATED_"),
)
class FKBlogPost(models.Model):
state = FSMKeyField(DbState, default="new", protected=True, on_delete=models.CASCADE)
@transition(field=state, source="new", target="published")
def publish(self):
pass
@transition(field=state, source="published")
def notify_all(self):
pass
@transition(field=state, source="published", target="hidden")
def hide(self):
pass
@transition(field=state, source="new", target="removed")
def remove(self):
raise Exception("Upss")
@transition(field=state, source=["published", "hidden"], target="stolen")
def steal(self):
pass
@transition(field=state, source="*", target="moderated")
def moderate(self):
pass
class FSMKeyFieldTest(TestCase):
def setUp(self):
DbState.objects.bulk_create(DbState(pk=item[0], label=item[1]) for item in FK_AVAILABLE_STATES)
self.model = FKBlogPost()
def test_initial_state_instantiated(self):
assert self.model.state == "new"
def test_known_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
assert self.model.state == "published"
assert can_proceed(self.model.hide)
self.model.hide()
assert self.model.state == "hidden"
def test_unknown_transition_fails(self):
assert not can_proceed(self.model.hide)
with pytest.raises(TransitionNotAllowed):
self.model.hide()
def test_state_non_changed_after_fail(self):
assert can_proceed(self.model.remove)
with pytest.raises(Exception, match="Upss"):
self.model.remove()
assert self.model.state == "new"
def test_allowed_null_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
self.model.notify_all()
assert self.model.state == "published"
def test_unknown_null_transition_should_fail(self):
with pytest.raises(TransitionNotAllowed):
self.model.notify_all()
assert self.model.state == "new"
def test_multiple_source_support_path_1_works(self):
self.model.publish()
self.model.steal()
assert self.model.state == "stolen"
def test_multiple_source_support_path_2_works(self):
self.model.publish()
self.model.hide()
self.model.steal()
assert self.model.state == "stolen"
def test_star_shortcut_succeed(self):
assert can_proceed(self.model.moderate)
self.model.moderate()
assert self.model.state == "moderated"
"""
# TODO: FIX it
class BlogPostStatus(models.Model):
name = models.CharField(unique=True, max_length=10)
objects = models.Manager()
class BlogPostWithFKState(models.Model):
status = FSMKeyField(BlogPostStatus, default=lambda: BlogPostStatus.objects.get(name="new"))
@transition(field=status, source='new', target='published')
def publish(self):
pass
@transition(field=status, source='published', target='hidden')
def hide(self):
pass
class BlogPostWithFKStateTest(TestCase):
def setUp(self):
BlogPostStatus.objects.bulk_create([
BlogPostStatus(name="new")
BlogPostStatus(name="published")
BlogPostStatus(name="hidden")
])
self.model = BlogPostWithFKState()
def test_known_transition_should_succeed(self):
self.model.publish()
self.assertEqual(self.model.state, 'published')
self.model.hide()
self.assertEqual(self.model.state, 'hidden')
def test_unknown_transition_fails(self):
with pytest.raises(TransitionNotAllowed):
self.model.hide()
"""
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_lock_mixin.py 0000664 0000000 0000000 00000005406 15102141373 0026766 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import ConcurrentTransition
from django_fsm import ConcurrentTransitionMixin
from django_fsm import FSMField
from django_fsm import transition
class LockedBlogPost(ConcurrentTransitionMixin, models.Model):
state = FSMField(default="new")
text = models.CharField(max_length=50)
@transition(field=state, source="new", target="published")
def publish(self):
pass
@transition(field=state, source="published", target="removed")
def remove(self):
pass
class ExtendedBlogPost(LockedBlogPost):
review_state = FSMField(default="waiting", protected=True)
notes = models.CharField(max_length=50)
@transition(field=review_state, source="waiting", target="rejected")
def reject(self):
pass
class TestLockMixin(TestCase):
def test_create_succeed(self):
LockedBlogPost.objects.create(text="test_create_succeed")
def test_crud_succeed(self):
post = LockedBlogPost(text="test_crud_succeed")
post.publish()
post.save()
post = LockedBlogPost.objects.get(pk=post.pk)
assert post.state == "published"
post.text = "test_crud_succeed2"
post.save()
post = LockedBlogPost.objects.get(pk=post.pk)
assert post.text == "test_crud_succeed2"
post.delete()
def test_save_and_change_succeed(self):
post = LockedBlogPost(text="test_crud_succeed")
post.publish()
post.save()
post.remove()
post.save()
post.delete()
def test_concurrent_modifications_raise_exception(self):
post1 = LockedBlogPost.objects.create()
post2 = LockedBlogPost.objects.get(pk=post1.pk)
post1.publish()
post1.save()
post2.text = "aaa"
post2.publish()
with pytest.raises(ConcurrentTransition):
post2.save()
def test_inheritance_crud_succeed(self):
post = ExtendedBlogPost(text="test_inheritance_crud_succeed", notes="reject me")
post.publish()
post.save()
post = ExtendedBlogPost.objects.get(pk=post.pk)
assert post.state == "published"
post.text = "test_inheritance_crud_succeed2"
post.reject()
post.save()
post = ExtendedBlogPost.objects.get(pk=post.pk)
assert post.review_state == "rejected"
assert post.text == "test_inheritance_crud_succeed2"
def test_concurrent_modifications_after_refresh_db_succeed(self): # bug 255
post1 = LockedBlogPost.objects.create()
post2 = LockedBlogPost.objects.get(pk=post1.pk)
post1.publish()
post1.save()
post2.refresh_from_db()
post2.remove()
post2.save()
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_mixin_support.py 0000664 0000000 0000000 00000001275 15102141373 0027552 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class WorkflowMixin:
@transition(field="state", source="*", target="draft")
def draft(self):
pass
@transition(field="state", source="draft", target="published")
def publish(self):
pass
class MixinSupportTestModel(WorkflowMixin, models.Model):
state = FSMField(default="new")
class Test(TestCase):
def test_usecase(self):
model = MixinSupportTestModel()
model.draft()
assert model.state == "draft"
model.publish()
assert model.state == "published"
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_model_create_with_generic.py 0000664 0000000 0000000 00000002110 15102141373 0031771 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class Ticket(models.Model): ...
class TaskState(models.TextChoices):
NEW = "new", "New"
DONE = "done", "Done"
class Task(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
causality = GenericForeignKey("content_type", "object_id")
state = FSMField(default=TaskState.NEW)
@transition(field=state, source=TaskState.NEW, target=TaskState.DONE)
def do(self):
pass
class Test(TestCase):
def setUp(self):
self.ticket = Ticket.objects.create()
def test_model_objects_create(self):
"""Check a model with state field can be created
if one of the other fields is a property or a virtual field.
"""
Task.objects.create(causality=self.ticket)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_multi_resultstate.py 0000664 0000000 0000000 00000004377 15102141373 0030431 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import GET_STATE
from django_fsm import RETURN_VALUE
from django_fsm import FSMField
from django_fsm import transition
from django_fsm.signals import post_transition
from django_fsm.signals import pre_transition
class MultiResultTest(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target=RETURN_VALUE("for_moderators", "published"))
def publish(self, *, is_public=False):
return "published" if is_public else "for_moderators"
@transition(
field=state,
source="for_moderators",
target=GET_STATE(lambda _, allowed: "published" if allowed else "rejected", states=["published", "rejected"]),
)
def moderate(self, allowed):
pass
class Test(TestCase):
def test_return_state_succeed(self):
instance = MultiResultTest()
instance.publish(is_public=True)
assert instance.state == "published"
def test_get_state_succeed(self):
instance = MultiResultTest(state="for_moderators")
instance.moderate(allowed=False)
assert instance.state == "rejected"
class TestSignals(TestCase):
def setUp(self):
self.pre_transition_called = False
self.post_transition_called = False
pre_transition.connect(self.on_pre_transition, sender=MultiResultTest)
post_transition.connect(self.on_post_transition, sender=MultiResultTest)
def on_pre_transition(self, sender, instance, name, source, target, **kwargs):
assert instance.state == source
self.pre_transition_called = True
def on_post_transition(self, sender, instance, name, source, target, **kwargs):
assert instance.state == target
self.post_transition_called = True
def test_signals_called_with_get_state(self):
instance = MultiResultTest(state="for_moderators")
instance.moderate(allowed=False)
assert self.pre_transition_called
assert self.post_transition_called
def test_signals_called_with_return_value(self):
instance = MultiResultTest()
instance.publish(is_public=True)
assert self.pre_transition_called
assert self.post_transition_called
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_multidecorators.py 0000664 0000000 0000000 00000002101 15102141373 0030037 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
from django_fsm.signals import post_transition
class MultiDecoratedModel(models.Model):
counter = models.IntegerField(default=0)
signal_counter = models.IntegerField(default=0)
state = FSMField(default="SUBMITTED_BY_USER")
@transition(field=state, source="SUBMITTED_BY_USER", target="REVIEW_USER")
@transition(field=state, source="SUBMITTED_BY_ADMIN", target="REVIEW_ADMIN")
@transition(field=state, source="SUBMITTED_BY_ANONYMOUS", target="REVIEW_ANONYMOUS")
def review(self):
self.counter += 1
def count_calls(sender, instance, name, source, target, **kwargs):
instance.signal_counter += 1
post_transition.connect(count_calls, sender=MultiDecoratedModel)
class TestStateProxy(TestCase):
def test_transition_method_called_once(self):
model = MultiDecoratedModel()
model.review()
assert model.counter == 1
assert model.signal_counter == 1
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_object_permissions.py 0000664 0000000 0000000 00000003134 15102141373 0030527 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.contrib.auth.models import User
from django.db import models
from django.test import TestCase
from django.test.utils import override_settings
from guardian.shortcuts import assign_perm
from django_fsm import FSMField
from django_fsm import has_transition_perm
from django_fsm import transition
class ObjectPermissionTestModel(models.Model):
state = FSMField(default="new")
class Meta:
permissions = [
("can_publish_objectpermissiontestmodel", "Can publish ObjectPermissionTestModel"),
]
@transition(
field=state,
source="new",
target="published",
on_error="failed",
permission="testapp.can_publish_objectpermissiontestmodel",
)
def publish(self):
pass
@override_settings(
AUTHENTICATION_BACKENDS=("django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend")
)
class ObjectPermissionFSMFieldTest(TestCase):
def setUp(self):
super().setUp()
self.model = ObjectPermissionTestModel.objects.create()
self.unprivileged = User.objects.create(username="unprivileged")
self.privileged = User.objects.create(username="object_only_privileged")
assign_perm("can_publish_objectpermissiontestmodel", self.privileged, self.model)
def test_object_only_access_success(self):
assert has_transition_perm(self.model.publish, self.privileged)
self.model.publish()
def test_object_only_other_access_prohibited(self):
assert not has_transition_perm(self.model.publish, self.unprivileged)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_permissions.py 0000664 0000000 0000000 00000003341 15102141373 0027201 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import TestCase
from django_fsm import has_transition_perm
from tests.testapp.models import BlogPost
class PermissionFSMFieldTest(TestCase):
def setUp(self):
self.model = BlogPost()
self.unprivileged = User.objects.create(username="unprivileged")
self.privileged = User.objects.create(username="privileged")
self.staff = User.objects.create(username="staff", is_staff=True)
self.privileged.user_permissions.add(Permission.objects.get_by_natural_key("can_publish_post", "testapp", "blogpost"))
self.privileged.user_permissions.add(Permission.objects.get_by_natural_key("can_remove_post", "testapp", "blogpost"))
def test_privileged_access_succeed(self):
assert has_transition_perm(self.model.publish, self.privileged)
assert has_transition_perm(self.model.remove, self.privileged)
transitions = self.model.get_available_user_state_transitions(self.privileged)
assert {"publish", "remove", "moderate"} == {transition.name for transition in transitions}
def test_unprivileged_access_prohibited(self):
assert not has_transition_perm(self.model.publish, self.unprivileged)
assert not has_transition_perm(self.model.remove, self.unprivileged)
transitions = self.model.get_available_user_state_transitions(self.unprivileged)
assert {"moderate"} == {transition.name for transition in transitions}
def test_permission_instance_method(self):
assert not has_transition_perm(self.model.restore, self.unprivileged)
assert has_transition_perm(self.model.restore, self.staff)
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_protected_field.py 0000664 0000000 0000000 00000002124 15102141373 0027760 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class ProtectedAccessModel(models.Model):
status = FSMField(default="new", protected=True)
@transition(field=status, source="new", target="published")
def publish(self):
pass
class MultiProtectedAccessModel(models.Model):
status1 = FSMField(default="new", protected=True)
status2 = FSMField(default="new", protected=True)
class TestDirectAccessModels(TestCase):
def test_multi_protected_field_create(self):
obj = MultiProtectedAccessModel.objects.create()
assert obj.status1 == "new"
assert obj.status2 == "new"
def test_no_direct_access(self):
instance = ProtectedAccessModel()
assert instance.status == "new"
def try_change():
instance.status = "change"
with pytest.raises(AttributeError):
try_change()
instance.publish()
instance.save()
assert instance.status == "published"
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_protected_fields.py 0000664 0000000 0000000 00000002031 15102141373 0030140 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import FSMModelMixin
from django_fsm import transition
class RefreshableProtectedAccessModel(models.Model):
status = FSMField(default="new", protected=True)
@transition(field=status, source="new", target="published")
def publish(self):
pass
class RefreshableModel(FSMModelMixin, RefreshableProtectedAccessModel):
pass
class TestDirectAccessModels(TestCase):
def test_no_direct_access(self):
instance = RefreshableProtectedAccessModel()
assert instance.status == "new"
def try_change():
instance.status = "change"
with pytest.raises(AttributeError):
try_change()
instance.publish()
instance.save()
assert instance.status == "published"
def test_refresh_from_db(self):
instance = RefreshableModel()
instance.save()
instance.refresh_from_db()
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_proxy_inheritance.py 0000664 0000000 0000000 00000003152 15102141373 0030360 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import can_proceed
from django_fsm import transition
class BaseModel(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published")
def publish(self):
pass
class InheritedModel(BaseModel):
class Meta:
proxy = True
@transition(field="state", source="published", target="sticked")
def stick(self):
pass
class TestinheritedModel(TestCase):
def setUp(self):
self.model = InheritedModel()
def test_known_transition_should_succeed(self):
assert can_proceed(self.model.publish)
self.model.publish()
assert self.model.state == "published"
assert can_proceed(self.model.stick)
self.model.stick()
assert self.model.state == "sticked"
def test_field_available_transitions_works(self):
self.model.publish()
assert self.model.state == "published"
transitions = self.model.get_available_state_transitions()
assert [data.target for data in transitions] == ["sticked"]
def test_field_all_transitions_base_model(self):
transitions = BaseModel().get_all_state_transitions()
assert {("new", "published")} == {(data.source, data.target) for data in transitions}
def test_field_all_transitions_works(self):
transitions = self.model.get_all_state_transitions()
assert {("new", "published"), ("published", "sticked")} == {(data.source, data.target) for data in transitions}
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_state_transitions.py 0000664 0000000 0000000 00000003055 15102141373 0030405 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class Insect(models.Model):
class STATE:
CATERPILLAR = "CTR"
BUTTERFLY = "BTF"
STATE_CHOICES = ((STATE.CATERPILLAR, "Caterpillar", "Caterpillar"), (STATE.BUTTERFLY, "Butterfly", "Butterfly"))
state = FSMField(default=STATE.CATERPILLAR, state_choices=STATE_CHOICES)
@transition(field=state, source=STATE.CATERPILLAR, target=STATE.BUTTERFLY)
def cocoon(self):
pass
def fly(self):
raise NotImplementedError
def crawl(self):
raise NotImplementedError
class Caterpillar(Insect):
class Meta:
proxy = True
def crawl(self):
"""
Do crawl
"""
class Butterfly(Insect):
class Meta:
proxy = True
def fly(self):
"""
Do fly
"""
class TestStateProxy(TestCase):
def test_initial_proxy_set_succeed(self):
insect = Insect()
assert isinstance(insect, Caterpillar)
def test_transition_proxy_set_succeed(self):
insect = Insect()
insect.cocoon()
assert isinstance(insect, Butterfly)
def test_load_proxy_set(self):
Insect.objects.bulk_create(
[
Insect(state=Insect.STATE.CATERPILLAR),
Insect(state=Insect.STATE.BUTTERFLY),
]
)
insects = Insect.objects.all()
assert {Caterpillar, Butterfly} == {insect.__class__ for insect in insects}
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_string_field_parameter.py 0000664 0000000 0000000 00000001504 15102141373 0031336 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import transition
class BlogPostWithStringField(models.Model):
state = FSMField(default="new")
@transition(field="state", source="new", target="published", conditions=[])
def publish(self):
pass
@transition(field="state", source="published", target="destroyed")
def destroy(self):
pass
@transition(field="state", source="published", target="review")
def review(self):
pass
class StringFieldTestCase(TestCase):
def setUp(self):
self.model = BlogPostWithStringField()
def test_initial_state(self):
assert self.model.state == "new"
self.model.publish()
assert self.model.state == "published"
django-commons-django-fsm-2-f5829bc/tests/testapp/tests/test_transition_all_except_target.py 0000664 0000000 0000000 00000001450 15102141373 0032565 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.db import models
from django.test import TestCase
from django_fsm import FSMField
from django_fsm import can_proceed
from django_fsm import transition
class ExceptTargetTransition(models.Model):
state = FSMField(default="new")
@transition(field=state, source="new", target="published")
def publish(self):
pass
@transition(field=state, source="+", target="removed")
def remove(self):
pass
class Test(TestCase):
def setUp(self):
self.model = ExceptTargetTransition()
def test_usecase(self):
assert self.model.state == "new"
assert can_proceed(self.model.remove)
self.model.remove()
assert self.model.state == "removed"
assert not can_proceed(self.model.remove)
django-commons-django-fsm-2-f5829bc/tests/urls.py 0000664 0000000 0000000 00000000250 15102141373 0021726 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from django.contrib import admin
from django.urls import path
urlpatterns = [
path("admin/", admin.site.urls, name="admin"),
]
django-commons-django-fsm-2-f5829bc/tests/wsgi.py 0000664 0000000 0000000 00000000646 15102141373 0021723 0 ustar 00root root 0000000 0000000 """WSGI config for silvr project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""
from __future__ import annotations
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
application = get_wsgi_application()
django-commons-django-fsm-2-f5829bc/tox.ini 0000664 0000000 0000000 00000001223 15102141373 0020541 0 ustar 00root root 0000000 0000000 [tox]
envlist =
py{38,39,310,311}-dj42
py{310,311,312}-dj50
py{310,311,312}-dj51
py{310,311,312,313,314}-dj52
py{312,313,314}-dj60
py{312,313,314}-djmain
skipsdist = True
[testenv]
deps =
dj42: Django==4.2
dj50: Django==5.0
dj51: Django==5.1
dj52: Django==5.2
dj60: Django>=6.0a1,<6.1
djmain: https://github.com/django/django/tarball/main
django-guardian
graphviz
pep8
pyflakes
pytest
pytest-django
pytest-cov
commands = {posargs:python -m pytest}
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
3.14: py314