Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate StateLog model, to instead, bring your own model. #176

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
58 changes: 43 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ by enabling a cached backend. See [Advanced Usage](#advanced-usage)
## 4.0.0 (not released)

- remove support for django 2.2 & 4.0
- Bring your own PersistedTransition model:
From this release, django-fsm-log is deprecating StateLog and instead encourages you
to define the concrete model for your own application that will persist the transition.

## 3.1.0 (2023-03-23)

Expand Down Expand Up @@ -99,14 +102,29 @@ python manage.py migrate django_fsm_log

## Usage

### Define you own model

```python
from django_fsm_log.models import PersistedTransitionMixin


class TransitionLog(PersistedTransitionMixin):
pass
```

### Register the model

```python
DJANGO_FSM_LOG_CONCRETE_MODEL = 'poll.models.TransitionLog' # This model must inherit from django_fsm_log.models.PersistedTransition
```

The app listens for the `django_fsm.signals.post_transition` signal and
creates a new record for each transition.

To query the log:

```python
from django_fsm_log.models import StateLog
StateLog.objects.all()
TransitionLog.objects.all()
# ...all recorded logs...
```

Expand All @@ -125,11 +143,10 @@ For convenience there is a custom `for_` manager method to easily filter on the

```python
from my_app.models import Article
from django_fsm_log.models import StateLog

article = Article.objects.all()[0]

StateLog.objects.for_(article)
TransitionLog.objects.for_(article)
# ...logs for article...
```

Expand Down Expand Up @@ -157,7 +174,7 @@ With this the transition gets logged when the `by` kwarg is present.

```python
article = Article.objects.create()
article.submit(by=some_user) # StateLog.by will be some_user
article.submit(by=some_user) # TransitionLog.by will be some_user
```

### `description` Decorator
Expand Down Expand Up @@ -210,21 +227,31 @@ article.submit() # logged with "Article submitted" description

There is an InlineForm available that can be used to display the history of changes.

To use it expand your own `AdminModel` by adding `StateLogInline` to its inlines:
To use it expand your own `AdminModel` by adding `PersistedTransitionInline` to its inlines:

```python
from django.contrib import admin
from django_fsm_log.admin import StateLogInline
from django_fsm_log.admin import PersistedTransitionInline


@admin.register(FSMModel)
class FSMModelAdmin(admin.ModelAdmin):
inlines = [StateLogInline]
inlines = [PersistedTransitionInline]
```

### Migration to abstract PersistedTransitionMixin model

Once you defined your own model, you'll have to create the relevant migration to create the table.

```sh
python manage.py makemigrations
```

Additionally you'd want to migrate the data from django_fsm_log.models.StateLog to your new table.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to have an example/snippet here that can be used as a base.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, it could be a nice addition, If I have to go through that path. I'll add it.
As of today I don't have incentive to migrate to an internal concrete model within the projects I maintain. It will come at some point, probably not as part of this PR though.


### Advanced Usage

You can change the behaviour of this app by turning on caching for StateLog records.
You can change the behaviour of this app by turning on caching for PersistedTransition records.
Simply add `DJANGO_FSM_LOG_STORAGE_METHOD = 'django_fsm_log.backends.CachedBackend'` to your project's settings file.
It will use your project's default cache backend by default. If you wish to use a specific cache backend, you can add to
your project's settings:
Expand All @@ -233,22 +260,23 @@ your project's settings:
DJANGO_FSM_LOG_CACHE_BACKEND = 'some_other_cache_backend'
```

The StateLog object is now available after the `django_fsm.signals.pre_transition`
The PersistedTransition object is now available after the `django_fsm.signals.pre_transition`
signal is fired, but is deleted from the cache and persisted to the database after `django_fsm.signals.post_transition`
is fired.

This is useful if:

- you need immediate access to StateLog details, and cannot wait until `django_fsm.signals.post_transition`
- you need immediate access to PersistedTransition details, and cannot wait until `django_fsm.signals.post_transition`
has been fired
- at any stage, you need to verify whether or not the StateLog has been written to the database
- at any stage, you need to verify whether or not the PersistedTransition has been written to the database

Access to the pending StateLog record is available via the `pending_objects` manager
Access to the pending PersistedTransition record is available via the `pending_objects` manager

```python
from django_fsm_log.models import StateLog
from my_app.models import TransitionLog

article = Article.objects.get(...)
pending_state_log = StateLog.pending_objects.get_for_object(article)
pending_transition_logs = TransitionLog.pending_objects.get_for_object(article)
```

## Contributing
Expand Down
19 changes: 15 additions & 4 deletions django_fsm_log/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from warnings import warn

from django.contrib.contenttypes.admin import GenericTabularInline
from django.db.models import F

from .models import StateLog
from .backends import _get_concrete_model

__all__ = ("StateLogInline",)
__all__ = ("PersistedTransitionInline",)


class StateLogInline(GenericTabularInline):
model = StateLog
class PersistedTransitionInline(GenericTabularInline):
model = _get_concrete_model()
can_delete = False

def has_add_permission(self, request, obj=None):
Expand All @@ -30,3 +32,12 @@ def get_readonly_fields(self, request, obj=None):

def get_queryset(self, request):
return super().get_queryset(request).order_by(F("timestamp").desc())


def StateLogInline(*args, **kwargs):
warn(
"StateLogInline has been deprecated by PersistedTransitionInline.",
DeprecationWarning,
stacklevel=2,
)
return PersistedTransitionInline(*args, **kwargs)
4 changes: 2 additions & 2 deletions django_fsm_log/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ class DjangoFSMLogAppConfig(AppConfig):

def ready(self):
backend = import_string(settings.DJANGO_FSM_LOG_STORAGE_METHOD)
StateLog = self.get_model("StateLog")
ConcreteModel = import_string(settings.DJANGO_FSM_LOG_CONCRETE_MODEL)

backend.setup_model(StateLog)
backend.setup_model(ConcreteModel)

pre_transition.connect(backend.pre_transition_callback)
post_transition.connect(backend.post_transition_callback)
27 changes: 19 additions & 8 deletions django_fsm_log/backends.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import typing

from django.utils.module_loading import import_string

from django_fsm_log.conf import settings

from .helpers import FSMLogDescriptor

if typing.TYPE_CHECKING:
import django_fsm_log.models


def _get_concrete_model() -> typing.Type["django_fsm_log.models.PersistedTransitionMixin"]:
return import_string(settings.DJANGO_FSM_LOG_CONCRETE_MODEL)


def _pre_transition_callback(sender, instance, name, source, target, manager, **kwargs):
if BaseBackend._get_model_qualified_name__(sender) in settings.DJANGO_FSM_LOG_IGNORED_MODELS:
Expand Down Expand Up @@ -49,21 +60,21 @@ def _get_model_qualified_name__(sender):
class CachedBackend(BaseBackend):
@staticmethod
def setup_model(model):
from .managers import PendingStateLogManager
from .managers import PendingPersistedTransitionManager

model.add_to_class("pending_objects", PendingStateLogManager())
model.add_to_class("pending_objects", PendingPersistedTransitionManager())

@staticmethod
def pre_transition_callback(sender, instance, name, source, target, **kwargs):
from .models import StateLog
klass = _get_concrete_model()

return _pre_transition_callback(sender, instance, name, source, target, StateLog.pending_objects, **kwargs)
return _pre_transition_callback(sender, instance, name, source, target, klass.pending_objects, **kwargs)

@staticmethod
def post_transition_callback(sender, instance, name, source, target, **kwargs):
from .models import StateLog
klass = _get_concrete_model()

StateLog.pending_objects.commit_for_object(instance)
klass.pending_objects.commit_for_object(instance)


class SimpleBackend(BaseBackend):
Expand All @@ -77,9 +88,9 @@ def pre_transition_callback(sender, **kwargs):

@staticmethod
def post_transition_callback(sender, instance, name, source, target, **kwargs):
from .models import StateLog
klass = _get_concrete_model()

return _pre_transition_callback(sender, instance, name, source, target, StateLog.objects, **kwargs)
return _pre_transition_callback(sender, instance, name, source, target, klass.objects, **kwargs)


if settings.DJANGO_FSM_LOG_STORAGE_METHOD == "django_fsm_log.backends.CachedBackend":
Expand Down
9 changes: 8 additions & 1 deletion django_fsm_log/conf.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from typing import List

from appconf import AppConf
from django.conf import settings # noqa: F401


class DjangoFSMLogConf(AppConf):
STORAGE_METHOD = "django_fsm_log.backends.SimpleBackend"
CACHE_BACKEND = "default"
IGNORED_MODELS = []
IGNORED_MODELS: List[str] = []
CONCRETE_MODEL: str = "django_fsm_log.models.StateLog"

class Meta:
prefix = "django_fsm_log"
holder = "django_fsm_log.conf.settings"
29 changes: 25 additions & 4 deletions django_fsm_log/managers.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
from warnings import warn

from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.query import QuerySet

from django_fsm_log.backends import cache


class StateLogQuerySet(QuerySet):
class PersistedTransitionQuerySet(QuerySet):
def _get_content_type(self, obj):
return ContentType.objects.get_for_model(obj)

def for_(self, obj):
return self.filter(content_type=self._get_content_type(obj), object_id=obj.pk)


class StateLogManager(models.Manager):
def StateLogQuerySet(*args, **kwargs):
warn("StateLogQuerySet has been renamed to PersistedTransitionQuerySet", DeprecationWarning, stacklevel=2)
return PersistedTransitionQuerySet(*args, **kwargs)


class PersistedTransitionManager(models.Manager):
use_in_migrations = True

def get_queryset(self):
return StateLogQuerySet(self.model)
return PersistedTransitionQuerySet(self.model)

def __getattr__(self, attr, *args):
# see https://code.djangoproject.com/ticket/15062 for details
Expand All @@ -26,7 +33,12 @@ def __getattr__(self, attr, *args):
return getattr(self.get_queryset(), attr, *args)


class PendingStateLogManager(models.Manager):
def StateLogManager(*args, **kwargs):
warn("StateLogManager has been renamed to PersistedTransitionManager", DeprecationWarning, stacklevel=2)
return PersistedTransitionManager(*args, **kwargs)


class PendingPersistedTransitionManager(models.Manager):
def _get_cache_key_for_object(self, obj):
return f"StateLog:{obj.__class__.__name__}:{obj.pk}"

Expand All @@ -46,3 +58,12 @@ def commit_for_object(self, obj):
def get_for_object(self, obj):
key = self._get_cache_key_for_object(obj)
return cache.get(key)


def PendingStateLogManager(*args, **kwargs):
warn(
"PendingStateLogManager has been renamed to PendingPersistedTransitionManager",
DeprecationWarning,
stacklevel=2,
)
return PendingPersistedTransitionManager(*args, **kwargs)
2 changes: 1 addition & 1 deletion django_fsm_log/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db import models, migrations
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
Expand Down
1 change: 1 addition & 0 deletions django_fsm_log/migrations/0004_auto_20190131_0341.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Generated by Django 2.1.5 on 2019-01-31 03:41

from django.db import migrations

import django_fsm_log.managers


Expand Down
29 changes: 25 additions & 4 deletions django_fsm_log/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
from warnings import warn

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.timezone import now
from django_fsm import FSMFieldMixin, FSMIntegerField

from .conf import settings
from .managers import StateLogManager
from .managers import PersistedTransitionManager


class PersistedTransitionMixin(models.Model):
"""
Abstract class that should be subclassed by the host application.
Host projects should own the migrations and
can decide on their own primary key type.
"""

class StateLog(models.Model):
timestamp = models.DateTimeField(default=now)
by = models.ForeignKey(
getattr(settings, "AUTH_USER_MODEL", "auth.User"),
Expand All @@ -26,9 +34,10 @@ class StateLog(models.Model):

description = models.TextField(blank=True, null=True)

objects = StateLogManager()
objects = PersistedTransitionManager()

class Meta:
abstract = True
get_latest_by = "timestamp"

def __str__(self):
Expand All @@ -47,3 +56,15 @@ def get_state_display(self, field_name="state"):

def get_source_state_display(self):
return self.get_state_display("source_state")


class StateLog(PersistedTransitionMixin):
def __init__(self, *args, **kwargs):
warn(
"StateLog model has been deprecated, you should now bring your own model."
"\nPlease check the documentation at https://django-fsm-log.readthedocs.io/en/latest/"
"\nto know how to.",
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"\nPlease check the documentation at https://django-fsm-log.readthedocs.io/en/latest/"
"\nto know how to.",
"\nSee the documentation at https://django-fsm-log.readthedocs.io/en/latest/.",

Copy link
Contributor

Choose a reason for hiding this comment

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

TODO: use more specific link.

DeprecationWarning,
stacklevel=2,
)
return super().__init__(*args, **kwargs)
4 changes: 2 additions & 2 deletions tests/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.contrib import admin

from django_fsm_log.admin import StateLogInline
from django_fsm_log.admin import PersistedTransitionInline

from .models import Article


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
inlines = [StateLogInline]
inlines = [PersistedTransitionInline]
Loading