Skip to content

Commit 9ea11ac

Browse files
author
Test User
committed
Remove models.Model inheritance from ChangesMixin
ChangesMixin is now a plain Python mixin class that works when combined with models.Model in user code. This fixes AppRegistryNotReady errors when packages that import ChangesMixin are in INSTALLED_APPS. - ChangesMixin no longer inherits from models.Model - Signals connect once per class on first instantiation - Users must now explicitly inherit from both: class MyModel(ChangesMixin, models.Model)
1 parent 1abbefa commit 9ea11ac

3 files changed

Lines changed: 30 additions & 19 deletions

File tree

django_model_changes/changes.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
SAVE = 0
1414
DELETE = 1
1515

16+
# Track which classes have had signals connected
17+
_connected_classes: set[type] = set()
1618

17-
class ChangesMixin(models.Model):
19+
20+
class ChangesMixin:
1821
r"""
1922
ChangesMixin keeps track of changes for model instances.
2023
24+
Use with models.Model: ``class MyModel(ChangesMixin, models.Model)``
25+
2126
It allows you to retrieve the following states from an instance:
2227
2328
1. current_state()
@@ -70,27 +75,33 @@ class ChangesMixin(models.Model):
7075
"""
7176

7277
_states: list[dict[str, Any]]
73-
74-
class Meta:
75-
abstract = True
78+
_meta: Options
79+
pk: Any
7680

7781
def __init__(self, *args: Any, **kwargs: Any) -> None:
7882
super().__init__(*args, **kwargs)
7983

84+
# Connect signals once per class (on first instantiation)
85+
cls = self.__class__
86+
if cls not in _connected_classes:
87+
_connected_classes.add(cls)
88+
# cls is guaranteed to be a Model subclass at runtime since ChangesMixin
89+
# must be used with models.Model
90+
sender: type[models.Model] = cls # type: ignore[assignment]
91+
signals.post_save.connect(
92+
_post_save,
93+
sender=sender,
94+
dispatch_uid=f"django-changes-{cls.__name__}",
95+
)
96+
signals.post_delete.connect(
97+
_post_delete,
98+
sender=sender,
99+
dispatch_uid=f"django-changes-{cls.__name__}",
100+
)
101+
80102
self._states = []
81103
self._save_state(new_instance=True)
82104

83-
signals.post_save.connect(
84-
_post_save,
85-
sender=self.__class__,
86-
dispatch_uid=f"django-changes-{self.__class__.__name__}",
87-
)
88-
signals.post_delete.connect(
89-
_post_delete,
90-
sender=self.__class__,
91-
dispatch_uid=f"django-changes-{self.__class__.__name__}",
92-
)
93-
94105
def _save_state(self, new_instance: bool = False, event_type: str | int = "save") -> None:
95106
# Pipe the pk on deletes so that a correct snapshot of the current
96107
# state can be taken.

tests/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from django.db import models
22

3-
from django_model_changes.changes import ChangesMixin
3+
from django_model_changes import ChangesMixin
44

55

6-
class User(ChangesMixin):
6+
class User(ChangesMixin, models.Model):
77
name = models.CharField(max_length=100)
88
flag = models.BooleanField(default=False)
99

1010

11-
class Article(ChangesMixin):
11+
class Article(ChangesMixin, models.Model):
1212
title = models.CharField(max_length=20)
1313
user = models.ForeignKey(User, on_delete=models.CASCADE)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)