Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pictures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions pictures/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import django.dispatch

process_picture_done = django.dispatch.Signal()
35 changes: 31 additions & 4 deletions pictures/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

from typing import Protocol

from django.apps import apps
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:
Expand All @@ -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 = "",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that matters for async task runners, is that the signature is JSON-serializable.
You don't need to concatenate the strings; just pass a triple:

Suggested change
field: str = "",
sender: tuple[str, str, str],

You can drop the default, since this will be required. And I'd prefer to keep the naming somewhat consistent. Thus, this would be the sender (sending the task).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making it required means that it needs to be placed before new and old, which is more of a breaking change than making it optional. It does make upgrading more difficult as existing celery tasks without the kwarg being set could be on users queues, so they would end up being rejected.

Now that we're sending along the field as the sender the storage could also be dropped as that could be found in the task from the field, but that would cause issues on upgrading again due to it being set as a kwarg to the celery tasks.

) -> None: ...


Expand All @@ -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 []
Expand All @@ -41,6 +44,21 @@ 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=sender,
storage=storage.deconstruct(),
file_name=file_name,
new=new,
old=old,
field=field,
)


process_picture: PictureProcessor = _process_picture

Expand All @@ -57,21 +75,24 @@ 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(
storage=storage,
file_name=file_name,
new=new,
old=old,
field=field,
)
)

Expand All @@ -91,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(
Expand All @@ -107,6 +130,7 @@ def process_picture( # noqa: F811
file_name=file_name,
new=new,
old=old,
field=field,
),
queue=conf.get_settings().QUEUE_NAME,
)
Expand All @@ -125,20 +149,23 @@ 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(
storage=storage,
file_name=file_name,
new=new,
old=old,
field=field,
)
)
71 changes: 71 additions & 0 deletions tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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


@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=[],
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