Repository: hassaanalansary/django-bulk-tracker (language: python)

Issue:
Support for django 4.1 and 4.2
Hi @hassaanalansary 
Can you please confirm support for django 4.1 and 4.2? 


Discussion / hints:
Yes both are supported

this is the matrix of the supported platforms
    py{39,310,311}-dj{32,40,41}
    py{310,311}-dj{42,main}
    
you can find it `tox.ini`
It would be helpful if you can add it to the trove classifiers.Sent from my iPhoneOn Jul 23, 2023, at 7:20 PM, hassaanalansary ***@***.***> wrote:﻿
Yes both are supported
this is the matrix of the supported platforms
py{39,310,311}-dj{32,40,41}
py{310,311}-dj{42,main}
you can find it tox.ini

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>
I will try

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9dfc09..1a64a05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 All notable changes to this project will be documented in this file.
 
-## 0.0.8 (2024-05-31)
+## 0.1.0 (2024-05-31)
 - `post_create_signal()`, `post_update_signal()`, and `post_delete_signal()` were not waiting for transaction to commit before sending signals. 
 This was causing the signals to be sent before the object was actually created/updated/deleted, Causing a race condition.
 This has been fixed by using `transaction.on_commit()`. 
diff --git a/README.md b/README.md
index 307345b..0fc01a4 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,10 @@ django-bulk-tracker
 ===============
 
 [![Build Status](https://github.com/hassaanalansary/django-bulk-tracker/actions/workflows/tests.yml/badge.svg)](https://github.com/hassaanalansary/django-bulk-tracker/actions)
-
+[![Build Status](https://github.com/hassaanalansary/django-bulk-tracker/workflows/Test/badge.svg)](https://github.com/hassaanalansary/django-bulk-tracker/actions)
+[![codecov](https://codecov.io/gh/hassaanalansary/django-bulk-tracker/branch/master/graph/badge.svg)](https://codecov.io/gh/hassaanalansary/django-bulk-tracker)
+[![Supported Python versions](https://img.shields.io/pypi/pyversions/django-bulk-tracker.svg)](https://pypi.python.org/pypi/django-bulk-tracker)
+[![Supported Django versions](https://img.shields.io/pypi/djversions/django-bulk-tracker.svg)](https://pypi.python.org/pypi/django-bulk-tracker)
 
 Documentation
 =============
@@ -15,6 +18,166 @@ pip install -r requirements_dev.txt
 pytest
 ```
 
+Usage
+=====
+
+`django-bulk-tracker` will emit a signal whenever you update, create, or delete a record in the database.
+
+`django-bulk-tracker` supports bulk operations:
+
+- `queryset.update()`
+- `queryset.bulk_update()`
+- `queryset.bulk_create()`
+- `queryset.delete()`
+
+and single operations:
+
+- `create()`
+- `save() # update and create`
+- `delete()`
+
+All you need to do is define your Model and inherit from:
+
+```python
+from bulk_tracker.models import BulkTrackerModel
+
+class Author(BulkTrackerModel):
+    first_name = models.CharField(max_length=200)
+    last_name = models.CharField(max_length=200)
+```
+
+OR if you have a custom queryset inherit from or Don't want to support single-operation:
+
+```python
+from bulk_tracker.managers import BulkTrackerQuerySet
+
+class MyModelQuerySet(BulkTrackerQuerySet):
+    def do_something_custom(self):
+        pass
+```
+
+Now you can listen to the signals `post_update_signal`, `post_create_signal`, `post_delete_signal`:
+
+```python
+@receiver(post_update_signal, sender=MyModel)
+def i_am_a_receiver_function(
+    sender,
+    objects: list[ModifiedObject[MyModel]],
+    tracking_info_: TrackingInfo | None = None,
+    **kwargs,
+):
+    do_stuff()
+```
+
+**Hint:** All signals have the same signature for consistency and also in case you want to assign one function to listen to multiple signals.
+
+`ModifiedObject` is a very simple object, it contains 2 attributes:
+1. `instance` this is your model instance after it has been updated, or created
+2. `changed_values` is a dict[str, Any] which contains the changed fields only in the case of `post_update_signal`, in the case of `post_create_signal` and `post_delete_signal`, `changed_values` will be an empty dict `{}`.
+
+**Optionally** you can pass `tracking_info_` to your functions, as in:
+
+```python
+from bulk_tracker.helper_objects import TrackingInfo
+
+def a_function_that_updates_records():
+    user = self.request.user
+    MyModel.objects.filter(name='john').update(
+        name='jack',
+        tracking_info_=TrackingInfo(user=user, comment="Updated from a function", kwargs={'app-build':'1.1.8'}, is_robust=True),
+        )
+```
+
+**Hint:** `tracking_info_` has a trailing underscore to avoid collision with your actual fields. You can use `TrackingInfo` to implement any kind of behavior like logging in your signal handlers and you need to capture more info about the operation that is happening.
+
+For single operations as well
+-----------------------------
+
+- `create()`
+- `save() # update and create`
+- `delete()`
+
+To support, we rely on the amazing library `django-model-utils` to track the model instances:
+
+1. Do the above
+2. You need to inherit your model from `BulkTrackerModel`
+3. Add `tracker = FieldTracker()` to your model
+
+As in:
+
+```python
+from bulk_tracker.models import BulkTrackerModel
+from model_utils import FieldTracker
+
+class MyModel(BulkTrackerModel):
+    objects = MyModelQuerySet.as_manager() # MyModelManager() if you have
+    tracker = FieldTracker()
+```
+
+Robust Send
+----------
+
+`robust_send` if you have multiple receivers for the same signal, and you want to make sure that all of them are executed, even if one of them raises an exception. You can add `TrackingInfo(is_robust=True)` in your operation. You can read more about `robust_send` in the [official documentation](https://docs.djangoproject.com/en/5.0/topics/signals/#sending-signals).
+
+As in:
+
+```python
+MyModel.objects.filter(name='john').update(
+    name='jack',
+    tracking_info_=TrackingInfo(is_robust=True),
+)
+```
+
+Complete Example
+================
+
+```python
+# models.py
+from bulk_tracker.models import BulkTrackerModel
+from model_utils import FieldTracker
+
+from myapp.managers import MyModelManager
+
+class MyModel(BulkTrackerModel):
+    first_field = models.CharField()
+    second_field = models.CharField()
+
+    objects = MyModelManager()
+    tracker = FieldTracker()
+```
+
+```python
+# managers.py
+from bulk_tracker.managers import BulkTrackerQuerySet # optional
+
+class MyModelQuerySet(BulkTrackerQuerySet):
+    pass
+
+class MyModelManager(BulkTrackerManager.from_queryset(MyModelQuerySet)): # optional
+    pass
+```
+
+```python
+# signal_handlers.py
+from bulk_tracker.signals import post_update_signal
+from bulk_tracker.helper_objects import ModifiedObject, TrackingInfo
+
+@receiver(post_update_signal, sender=MyModel)
+def i_am_a_receiver_function(
+    sender,
+    objects: list[ModifiedObject[MyModel]],
+    tracking_info_: TrackingInfo | None = None,
+    **kwargs,
+):
+    user = tracking_info_.user if tracking_info_ else None
+    for modified_object in modified_objects:
+        if 'name' in modified_object.changed_values:
+            log(f"field 'name' has changed by {user or ''}")
+            notify_user()
+```
+
+
+
 Contribute
 ==========
 
diff --git a/bulk_tracker/__init__.py b/bulk_tracker/__init__.py
index a73339b..3dc1f76 100644
--- a/bulk_tracker/__init__.py
+++ b/bulk_tracker/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "0.0.8"
+__version__ = "0.1.0"
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 14addd9..84a1484 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -14,7 +14,7 @@ The repository can be found at https://github.com/hassaanalansary/django-bulk-tr
 - Python 3.9 or higher
 - Django 3.2 or higher
 
-``django-bulk-tracker`` is currently tested with Python 3.9+ and Django 3.2 and 4.0+.
+``django-bulk-tracker`` is currently tested with Python 3.9+ and Django 3.2, 4.0+ and 5.0+.
 
 Adding django-bulk-tracker to your Django application
 -----------------------------------------------------
diff --git a/docs/source/usage.rst b/docs/source/usage.rst
index 442ed3e..6fd8edf 100644
--- a/docs/source/usage.rst
+++ b/docs/source/usage.rst
@@ -63,7 +63,7 @@ in case of ``post_create_signal`` and ``post_delete_signal``, ``changed_values``
         user = self.request.user
         MyModel.objects.filter(name='john').update(
             name='jack',
-            tracking_info_=TrackingInfo(user=user, comment="Updated from a function", kwargs={'app-build':1.1.8}),
+            tracking_info_=TrackingInfo(user=user, comment="Updated from a function", kwargs={'app-build':'1.1.8'}, is_robust=True),
             )
 
 .. hint::
@@ -99,7 +99,19 @@ as in ::
         tracker = FieldTracker()
 
 
+robust_send
+----------
 
+``robust_send`` if you have multiple receivers for the same signal, and you want to make sure that all of them are executed, even if one of them raise an exception.
+you can add ``TrackingInfo(is_robust=True)`` in your operation.
+you can read more about robust_send in the [official documentation](https://docs.djangoproject.com/en/5.0/topics/signals/#sending-signals)
+
+as in::
+
+    MyModel.objects.filter(name='john').update(
+        name='jack',
+        tracking_info_=TrackingInfo(is_robust=True),
+        )
 Complete Example
 ================
 
diff --git a/setup.cfg b/setup.cfg
index eed0007..132cd6d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,6 +19,9 @@ classifiers =
     Framework :: Django
     Framework :: Django :: 3.0
     Framework :: Django :: 4.0
+    Framework :: Django :: 4.1
+    Framework :: Django :: 4.2
+    Framework :: Django :: 5.0
     Intended Audience :: Developers
     License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
     Operating System :: OS Independent
@@ -26,6 +29,7 @@ classifiers =
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
     Programming Language :: Python :: 3.11
+    Programming Language :: Python :: 3.12
 
 
 [options]
diff --git a/tox.ini b/tox.ini
index 8f5c0e2..31aaf0e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 envlist =
     py{39,310,311}-dj{32,40,41}
-    py{310,311}-dj{42,main}
+    py{310,311,312}-dj{42,50,main}
     isort
     black
 toxworkdir = /tmp/tox/
@@ -11,6 +11,7 @@ python =
     3.9: py39, flake8, isort
     3.10: py310
     3.11: py311
+    3.12: py312
 
 [testenv]
 deps =
@@ -19,6 +20,8 @@ deps =
     dj32: Django==3.2.*
     dj40: Django==4.0.*
     dj41: Django==4.1.*
+    dj42: Django==4.2.*
+    dj50: Django==5.0.*
     djmain: https://github.com/django/django/archive/main.tar.gz
 
 passenv =


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 8a77050..02aceb7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,7 +9,7 @@ jobs:
       fail-fast: false
       max-parallel: 5
       matrix:
-        python-version: ['3.9', '3.10', '3.11']
+        python-version: ['3.9', '3.10', '3.11', '3.12']
 
     services:
       postgres:
diff --git a/tests/test_create_signal.py b/tests/test_create_signal.py
index 5c3cf33..a12fc10 100644
--- a/tests/test_create_signal.py
+++ b/tests/test_create_signal.py
@@ -116,7 +116,6 @@ def post_create_receiver(
     @patch("bulk_tracker.signals.post_create_signal.send")
     def test_model_save_should_only_emit_post_create_signal_once(self, mocked_signal):
         # Arrange
-        signal_called_with = {}
 
         def post_create_receiver(
             sender,
@@ -124,8 +123,7 @@ def post_create_receiver(
             tracking_info_: TrackingInfo | None = None,
             **kwargs,
         ):
-            signal_called_with["objects"] = objects
-            signal_called_with["tracking_info_"] = tracking_info_
+            pass
 
         post_create_signal.connect(post_create_receiver, sender=Post)
 
@@ -133,7 +131,7 @@ def post_create_receiver(
         Post(title="Sound of Winter", publish_date="1998-01-08", author=self.author_john).save()
 
         # Assert
-        self.assertEqual(mocked_signal.call_count, 1, msg="The signal wasn't called once")
+        mocked_signal.assert_called_once()
 
     @patch("bulk_tracker.signals.post_create_signal.send")
     def test_model_create_should_only_emit_post_create_signal_once(self, mocked_signal):
@@ -155,7 +153,7 @@ def post_create_receiver(
         Post.objects.create(title="Sound of Winter", publish_date="1998-03-08", author=self.author_john)
 
         # Assert
-        self.assertEqual(mocked_signal.call_count, 1, msg="The signal wasn't called once")
+        mocked_signal.assert_called_once()
 
     @patch("bulk_tracker.signals.post_create_signal.send")
     def test_model_bulk_create_should_only_emit_post_create_signal_once(self, mocked_signal):
@@ -182,7 +180,7 @@ def post_create_receiver(
         Post.objects.bulk_create(posts)
 
         # Assert
-        self.assertEqual(mocked_signal.call_count, 1, msg="The signal wasn't called once")
+        mocked_signal.assert_called_once()
 
     @patch("bulk_tracker.signals.post_create_signal.send")
     def test_create_signal_should_be_only_emitted_after_transaction_commit(self, mocked_signal):