From 81c616318b76b852928dce79b9d4511ee4b53dec Mon Sep 17 00:00:00 2001 From: James Meakin <12661555+jmsmkn@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:47:32 +0100 Subject: [PATCH 1/2] Add process_picture_done signal --- pictures/signals.py | 3 +++ pictures/tasks.py | 10 +++++++++- tests/test_signals.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 pictures/signals.py create mode 100644 tests/test_signals.py diff --git a/pictures/signals.py b/pictures/signals.py new file mode 100644 index 0000000..a9ea577 --- /dev/null +++ b/pictures/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +process_picture_done = django.dispatch.Signal() diff --git a/pictures/tasks.py b/pictures/tasks.py index a2bf1ba..4e70aa0 100644 --- a/pictures/tasks.py +++ b/pictures/tasks.py @@ -5,7 +5,7 @@ from django.db import transaction from PIL import Image -from pictures import conf, utils +from pictures import conf, signals, utils def noop(*args, **kwargs) -> None: @@ -41,6 +41,14 @@ def _process_picture( picture = utils.reconstruct(*picture) picture.delete() + signals.process_picture_done.send( + sender=_process_picture, + storage=storage.deconstruct(), + file_name=file_name, + new=new, + old=old, + ) + process_picture: PictureProcessor = _process_picture diff --git a/tests/test_signals.py b/tests/test_signals.py new file mode 100644 index 0000000..3a45dbf --- /dev/null +++ b/tests/test_signals.py @@ -0,0 +1,29 @@ +from unittest.mock import Mock + +import pytest + +from pictures import signals, tasks +from tests.testapp.models import SimpleModel + + +@pytest.mark.django_db +def test_process_picture_sends_process_picture_done(image_upload_file): + obj = SimpleModel.objects.create(picture=image_upload_file) + + handler = Mock() + signals.process_picture_done.connect(handler) + + tasks._process_picture( + obj.picture.storage.deconstruct(), + obj.picture.name, + new=[i.deconstruct() for i in obj.picture.get_picture_files_list()], + ) + + handler.assert_called_once_with( + signal=signals.process_picture_done, + sender=tasks._process_picture, + storage=obj.picture.storage.deconstruct(), + file_name=obj.picture.name, + new=[i.deconstruct() for i in obj.picture.get_picture_files_list()], + old=[], + ) From 2621125ca886ae72462164a920b1895c79e698c9 Mon Sep 17 00:00:00 2001 From: James Meakin <12661555+jmsmkn@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:25:50 +0100 Subject: [PATCH 2/2] Add field serialization --- pictures/models.py | 6 ++++++ pictures/tasks.py | 27 +++++++++++++++++++++++---- tests/test_signals.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pictures/models.py b/pictures/models.py index 4b3f326..0d5fec3 100644 --- a/pictures/models.py +++ b/pictures/models.py @@ -157,6 +157,7 @@ def delete_all(self): self.name, [], [i.deconstruct() for i in self.get_picture_files_list()], + self.instance_name, ) def update_all(self, other: PictureFieldFile | None = None): @@ -171,8 +172,13 @@ def update_all(self, other: PictureFieldFile | None = None): self.name, [i.deconstruct() for i in new], [i.deconstruct() for i in old], + self.instance_name, ) + @property + def instance_name(self): + return f"{self.instance._meta.app_label}.{self.instance._meta.model_name}.{self.field.name}" + @property def width(self): self._require_file() diff --git a/pictures/tasks.py b/pictures/tasks.py index 4e70aa0..2693e3d 100644 --- a/pictures/tasks.py +++ b/pictures/tasks.py @@ -2,6 +2,7 @@ from typing import Protocol +from django.apps import apps from django.db import transaction from PIL import Image @@ -19,6 +20,7 @@ def __call__( file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: ... @@ -27,6 +29,7 @@ def _process_picture( file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: new = new or [] old = old or [] @@ -41,12 +44,19 @@ def _process_picture( picture = utils.reconstruct(*picture) picture.delete() + if field: + app_label, model_name, _ = field.split(".") + sender = apps.get_model(app_label=app_label, model_name=model_name) + else: + sender = _process_picture + signals.process_picture_done.send( - sender=_process_picture, + sender=sender, storage=storage.deconstruct(), file_name=file_name, new=new, old=old, + field=field, ) @@ -65,14 +75,16 @@ def process_picture_with_dramatiq( file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: - _process_picture(storage, file_name, new, old) + _process_picture(storage, file_name, new, old, field) def process_picture( # noqa: F811 storage: tuple[str, list, dict], file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: transaction.on_commit( lambda: process_picture_with_dramatiq.send( @@ -80,6 +92,7 @@ def process_picture( # noqa: F811 file_name=file_name, new=new, old=old, + field=field, ) ) @@ -99,14 +112,16 @@ def process_picture_with_celery( file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: - _process_picture(storage, file_name, new, old) + _process_picture(storage, file_name, new, old, field) def process_picture( # noqa: F811 storage: tuple[str, list, dict], file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: transaction.on_commit( lambda: process_picture_with_celery.apply_async( @@ -115,6 +130,7 @@ def process_picture( # noqa: F811 file_name=file_name, new=new, old=old, + field=field, ), queue=conf.get_settings().QUEUE_NAME, ) @@ -133,14 +149,16 @@ def process_picture_with_django_rq( file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: - _process_picture(storage, file_name, new, old) + _process_picture(storage, file_name, new, old, field) def process_picture( # noqa: F811 storage: tuple[str, list, dict], file_name: str, new: list[tuple[str, list, dict]] | None = None, old: list[tuple[str, list, dict]] | None = None, + field: str = "", ) -> None: transaction.on_commit( lambda: process_picture_with_django_rq.delay( @@ -148,5 +166,6 @@ def process_picture( # noqa: F811 file_name=file_name, new=new, old=old, + field=field, ) ) diff --git a/tests/test_signals.py b/tests/test_signals.py index 3a45dbf..93abef2 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,6 +1,8 @@ from unittest.mock import Mock import pytest +from django.apps import apps +from django.dispatch import receiver from pictures import signals, tasks from tests.testapp.models import SimpleModel @@ -26,4 +28,44 @@ def test_process_picture_sends_process_picture_done(image_upload_file): file_name=obj.picture.name, new=[i.deconstruct() for i in obj.picture.get_picture_files_list()], old=[], + field="", ) + + +@pytest.mark.django_db +def test_process_picture_sends_process_picture_done_on_create(image_upload_file): + handler = Mock() + signals.process_picture_done.connect(handler) + + obj = SimpleModel.objects.create(picture=image_upload_file) + + handler.assert_called_once_with( + signal=signals.process_picture_done, + sender=SimpleModel, + storage=obj.picture.storage.deconstruct(), + file_name=obj.picture.name, + new=[i.deconstruct() for i in obj.picture.get_picture_files_list()], + old=[], + field="testapp.simplemodel.picture", + ) + + +@pytest.mark.django_db +def test_processed_object_found(image_upload_file): + obj = SimpleModel.objects.create() + + found_object = None + + @receiver(signals.process_picture_done, sender=SimpleModel) + def handler(*, file_name, field, **__): + nonlocal found_object + app_label, model_name, field_name = field.split(".") + model = apps.get_model(app_label=app_label, model_name=model_name) + + # Users can now modify the object that process_picture_done + # corresponds to + found_object = model.objects.get(**{field_name: file_name}) + + obj.picture.save("image.png", image_upload_file) + + assert obj == found_object