Skip to content

Commit

Permalink
Add docs and support django5 and py312 (#9)
Browse files Browse the repository at this point in the history
* Add docs and support django5 and py312

* Add py312 to the test matrix
  • Loading branch information
hassaanalansary authored Jun 1, 2024
1 parent 9b5d99a commit aeb4d88
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
161 changes: 161 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,167 @@ Run tests
pip install -r requirements_dev.txt
pytest
```
Here's the provided text translated from reStructuredText (.rst) format to Markdown (.md):

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
==========
Expand Down
2 changes: 1 addition & 1 deletion bulk_tracker/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.8"
__version__ = "0.1.0"
2 changes: 1 addition & 1 deletion docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------------------------------------
Expand Down
14 changes: 13 additions & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down Expand Up @@ -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
================

Expand Down
10 changes: 4 additions & 6 deletions tests/test_create_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,22 @@ 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,
objects: list[ModifiedObject[Post]],
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)

# Act
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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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/
Expand All @@ -11,6 +11,7 @@ python =
3.9: py39, flake8, isort
3.10: py310
3.11: py311
3.12: py312

[testenv]
deps =
Expand All @@ -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 =
Expand Down

0 comments on commit aeb4d88

Please sign in to comment.