Skip to content

Commit

Permalink
Deprecate StateLog model, to instead bring your own model.
Browse files Browse the repository at this point in the history
This is an implementation that support the idea exposed in
#133

It's still a Draft, I'm open for any feedback more specifically around the names.
Not tested yet with a real project.

It's meant to be completely backward compatible.
  • Loading branch information
ticosax committed Apr 4, 2023
1 parent da125e2 commit 3da4756
Show file tree
Hide file tree
Showing 17 changed files with 194 additions and 91 deletions.
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 relase, django-fsm-log is deprecating StateLog and instead encourages you
to define for your own application the concrete model 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 model PersistedTransitionMixin

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.

### 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)
28 changes: 20 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() -> 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,22 @@ 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

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

klass.pending_objects.commit_for_object(instance)


class SimpleBackend(BaseBackend):
Expand All @@ -77,9 +89,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
7 changes: 6 additions & 1 deletion django_fsm_log/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
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"
25 changes: 21 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 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 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,8 @@ 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 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
28 changes: 24 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,14 @@ 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_subclass__(cls):
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.",
DeprecationWarning,
stacklevel=2,
)
4 changes: 2 additions & 2 deletions tests/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.contrib import admin

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

from .models import Article


class ArticleAdmin(admin.ModelAdmin):
inlines = [StateLogInline]
inlines = [PersistedTransitionInline]


admin.site.register(Article, ArticleAdmin)
Loading

0 comments on commit 3da4756

Please sign in to comment.