From 789b7735d7e408a00aa3399073ce2bfedbf70620 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sun, 15 Dec 2024 17:04:37 +0100 Subject: [PATCH] Resolve #194 -- Add support to run Pictures for a image CDN --- .github/workflows/ci.yml | 1 + README.md | 41 +++++++ pictures/conf.py | 1 + pictures/contrib/rest_framework.py | 4 +- pictures/models.py | 60 ++++++---- pictures/tasks.py | 5 + tests/contrib/test_rest_framework.py | 12 +- tests/test_models.py | 172 +++++++++++++-------------- tests/test_utils.py | 15 ++- 9 files changed, 191 insertions(+), 120 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 733e89d..c41005e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,7 @@ jobs: strategy: matrix: extras: + - "nopillow" - "celery" - "dramatiq" - "django-rq" diff --git a/README.md b/README.md index 469801e..8cc0d95 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,47 @@ Note that the `media` keys are only included, if you have specified breakpoints. `PictureField` is compatible with [Django Cleanup](https://github.com/un1t/django-cleanup), which automatically deletes its file and corresponding `SimplePicture` files. +### external image processing (via CDNs) + +This package is built with growth in mind. You can start small and grow big. +Should you use a CDN, or some other external image processing service, you can +you set up can be complete in two simple steps: + +1. Override `PICTURES["PROCESSOR"]` to disable the default processing. +2. Override `PICTURES["PICTURE_CLASS"]` implement any custom behavior. + +```python +# settings.py +PICTURES = { + "PROCESSOR": "pictures.tasks.noop", # disable default processing and do nothing + "PICTURE_CLASS": "path.to.MyPicture", # override the default picture class +} +``` + +The `MyPicture` class should implement the `url` property, that returns the +URL of the image. You can use the `Picture` class as a base class. + +Available attributes are: +* `parent_name` - name of the source file uploaded to the `PictureField` +* `aspect_ratio` - aspect ratio of the output image +* `width` - width of the output image +* `file_type` - file type of the output image + +```python +# path/to.py +from pathlib import Path +from pictures.models import Picture + + +class MyPicture(Picture): + @property + def url(self): + return ( + f"https://cdn.example.com/{self.aspect_ratio}/" + f"{Path(self.parent_name).stem}_{self.width}w.{self.file_type.lower()}" + ) +``` + [drf]: https://www.django-rest-framework.org/ [celery]: https://docs.celeryproject.org/en/stable/ [dramatiq]: https://dramatiq.io/ diff --git a/pictures/conf.py b/pictures/conf.py index 51662a9..88f8a8b 100644 --- a/pictures/conf.py +++ b/pictures/conf.py @@ -21,6 +21,7 @@ def get_settings(): "PIXEL_DENSITIES": [1, 2], "USE_PLACEHOLDERS": settings.DEBUG, "QUEUE_NAME": "pictures", + "PICTURE_CLASS": "pictures.models.PillowPicture", "PROCESSOR": "pictures.tasks.process_picture", **getattr(settings, "PICTURES", {}), }, diff --git a/pictures/contrib/rest_framework.py b/pictures/contrib/rest_framework.py index d9abbd3..c0d6e4a 100644 --- a/pictures/contrib/rest_framework.py +++ b/pictures/contrib/rest_framework.py @@ -6,11 +6,11 @@ __all__ = ["PictureField"] from pictures import utils -from pictures.models import PictureFieldFile, SimplePicture +from pictures.models import Picture, PictureFieldFile def default(obj): - if isinstance(obj, SimplePicture): + if isinstance(obj, Picture): return obj.url raise TypeError(f"Type '{type(obj).__name__}' not serializable") diff --git a/pictures/models.py b/pictures/models.py index 0a706e1..070feae 100644 --- a/pictures/models.py +++ b/pictures/models.py @@ -1,5 +1,6 @@ from __future__ import annotations +import abc import dataclasses import io import math @@ -12,19 +13,18 @@ from django.db.models import ImageField from django.db.models.fields.files import ImageFieldFile from django.urls import reverse -from PIL import Image, ImageOps - -__all__ = ["PictureField", "PictureFieldFile"] - from django.utils.module_loading import import_string +from PIL import Image, ImageOps from pictures import conf, utils +__all__ = ["PictureField", "PictureFieldFile", "Picture"] + RGB_FORMATS = ["JPEG"] @dataclasses.dataclass -class SimplePicture: +class Picture(abc.ABC): """A simple picture class similar to Django's image class.""" parent_name: str @@ -37,13 +37,35 @@ def __post_init__(self): self.aspect_ratio = Fraction(self.aspect_ratio) if self.aspect_ratio else None def __hash__(self): - return hash(self.name) + return hash(self.url) def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented return self.deconstruct() == other.deconstruct() + def deconstruct(self): + return ( + f"{self.__class__.__module__}.{self.__class__.__qualname__}", + ( + self.parent_name, + self.file_type, + str(self.aspect_ratio) if self.aspect_ratio else None, + self.storage.deconstruct(), + self.width, + ), + {}, + ) + + @property + @abc.abstractmethod + def url(self) -> str: + return NotImplemented + + +class PillowPicture(Picture): + """A simple picture class similar to Django's image class.""" + @property def url(self) -> str: if conf.get_settings().USE_PLACEHOLDERS: @@ -78,7 +100,7 @@ def name(self) -> str: def path(self) -> Path: return Path(self.storage.path(self.name)) - def process(self, image) -> Image: + def process(self, image) -> "Image": image = ImageOps.exif_transpose(image) # crates a copy height = self.height or self.width / Fraction(*image.size) size = math.floor(self.width), math.floor(height) @@ -101,23 +123,10 @@ def save(self, image): def delete(self): self.storage.delete(self.name) - def deconstruct(self): - return ( - f"{self.__class__.__module__}.{self.__class__.__qualname__}", - ( - self.parent_name, - self.file_type, - str(self.aspect_ratio) if self.aspect_ratio else None, - self.storage.deconstruct(), - self.width, - ), - {}, - ) - class PictureFieldFile(ImageFieldFile): - def __xor__(self, other) -> tuple[set[SimplePicture], set[SimplePicture]]: + def __xor__(self, other) -> tuple[set[Picture], set[Picture]]: """Return the new and obsolete :class:`SimpleFile` instances.""" if not isinstance(other, PictureFieldFile): return NotImplemented @@ -179,7 +188,7 @@ def height(self): return self._get_image_dimensions()[1] @property - def aspect_ratios(self) -> {Fraction | None: {str: {int: SimplePicture}}}: + def aspect_ratios(self) -> {Fraction | None: {str: {int: Picture}}}: self._require_file() return self.get_picture_files( file_name=self.name, @@ -197,11 +206,12 @@ def get_picture_files( img_height: int, storage: Storage, field: PictureField, - ) -> {Fraction | None: {str: {int: SimplePicture}}}: + ) -> {Fraction | None: {str: {int: Picture}}}: + PictureClass = import_string(conf.get_settings().PICTURE_CLASS) return { ratio: { file_type: { - width: SimplePicture(file_name, file_type, ratio, storage, width) + width: PictureClass(file_name, file_type, ratio, storage, width) for width in utils.source_set( (img_width, img_height), ratio=ratio, @@ -214,7 +224,7 @@ def get_picture_files( for ratio in field.aspect_ratios } - def get_picture_files_list(self) -> set[SimplePicture]: + def get_picture_files_list(self) -> set[Picture]: return { picture for sources in self.aspect_ratios.values() diff --git a/pictures/tasks.py b/pictures/tasks.py index aa9e89f..5be5ddf 100644 --- a/pictures/tasks.py +++ b/pictures/tasks.py @@ -8,6 +8,11 @@ from pictures import conf, utils +def noop(*args, **kwargs) -> None: + """Do not process the picture, but rely on some other service to do so.""" + pass + + class PictureProcessor(Protocol): def __call__( diff --git a/tests/contrib/test_rest_framework.py b/tests/contrib/test_rest_framework.py index 2d5d865..f0f178a 100644 --- a/tests/contrib/test_rest_framework.py +++ b/tests/contrib/test_rest_framework.py @@ -3,7 +3,7 @@ import pytest from django.core.files.storage import default_storage -from pictures.models import SimplePicture +from pictures.models import Picture from tests.testapp import models serializers = pytest.importorskip("rest_framework.serializers") @@ -31,11 +31,17 @@ class Meta: fields = ["image_invalid"] +class TestPicture(Picture): + @property + def url(self): + return f"/media/{self.parent_name}" + + def test_default(settings): settings.PICTURES["USE_PLACEHOLDERS"] = False assert ( rest_framework.default( - obj=SimplePicture( + obj=TestPicture( parent_name="testapp/simplemodel/image.jpg", file_type="WEBP", aspect_ratio=Fraction("4/3"), @@ -43,7 +49,7 @@ def test_default(settings): width=800, ) ) - == "/media/testapp/simplemodel/image/4_3/800w.webp" + == "/media/testapp/simplemodel/image.jpg" ) diff --git a/tests/test_models.py b/tests/test_models.py index d8b1d0e..2aa329a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -10,7 +10,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from PIL import Image, ImageDraw -from pictures.models import PictureField, SimplePicture +from pictures.models import PictureField, PillowPicture from tests.testapp.models import JPEGModel, Profile, SimpleModel @@ -24,8 +24,8 @@ def override_field_aspect_ratios(field, aspect_ratios): field.aspect_ratios = old_ratios -class TestSimplePicture: - picture_with_ratio = SimplePicture( +class TestPillowPicture: + picture_with_ratio = PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction("4/3"), @@ -33,7 +33,7 @@ class TestSimplePicture: width=800, ) - picture_without_ratio = SimplePicture( + picture_without_ratio = PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, @@ -90,7 +90,7 @@ def test_delete(self): def test_process__copy(self): """Do not mutate input image.""" image = Image.new("RGB", (800, 800), (255, 55, 255)) - assert SimplePicture( + assert PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, @@ -100,7 +100,7 @@ def test_process__copy(self): assert image.size == (800, 800), "Image was mutated." - assert SimplePicture( + assert PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio="4/3", @@ -122,168 +122,168 @@ def test_symmetric_difference(self, image_upload_file): obj.picture ^ "not a picture" assert obj.picture ^ obj2.picture == ( { - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, @@ -292,224 +292,224 @@ def test_symmetric_difference(self, image_upload_file): ), }, { - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=500, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=800, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=700, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=300, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=600, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=100, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=200, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=400, ), - SimplePicture( + PillowPicture( parent_name="testapp/profile/image.png", file_type="WEBP", aspect_ratio=Fraction(1, 1), @@ -640,56 +640,56 @@ def test_integration(self, image_upload_file): assert obj.picture.aspect_ratios == { None: { "WEBP": { - 800: SimplePicture( + 800: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=800, ), - 100: SimplePicture( + 100: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=100, ), - 200: SimplePicture( + 200: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=200, ), - 300: SimplePicture( + 300: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=300, ), - 400: SimplePicture( + 400: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=400, ), - 500: SimplePicture( + 500: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=500, ), - 600: SimplePicture( + 600: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, storage=default_storage, width=600, ), - 700: SimplePicture( + 700: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=None, @@ -700,56 +700,56 @@ def test_integration(self, image_upload_file): }, "3/2": { "WEBP": { - 800: SimplePicture( + 800: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=800, ), - 100: SimplePicture( + 100: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=100, ), - 200: SimplePicture( + 200: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=200, ), - 300: SimplePicture( + 300: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=300, ), - 400: SimplePicture( + 400: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=400, ), - 500: SimplePicture( + 500: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=500, ), - 600: SimplePicture( + 600: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), storage=default_storage, width=600, ), - 700: SimplePicture( + 700: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(3, 2), @@ -760,56 +760,56 @@ def test_integration(self, image_upload_file): }, "16/9": { "WEBP": { - 800: SimplePicture( + 800: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=800, ), - 100: SimplePicture( + 100: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=100, ), - 200: SimplePicture( + 200: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=200, ), - 300: SimplePicture( + 300: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=300, ), - 400: SimplePicture( + 400: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=400, ), - 500: SimplePicture( + 500: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=500, ), - 600: SimplePicture( + 600: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), storage=default_storage, width=600, ), - 700: SimplePicture( + 700: PillowPicture( parent_name="testapp/simplemodel/image.png", file_type="WEBP", aspect_ratio=Fraction(16, 9), diff --git a/tests/test_utils.py b/tests/test_utils.py index b0947f1..a569026 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ from django.core.files.storage import Storage, default_storage from pictures import utils -from pictures.models import SimplePicture +from pictures.models import Picture from tests.testapp.models import SimpleModel @@ -145,8 +145,15 @@ def test_placeholder(): assert img.height == 1200 +class TestPicture(Picture): + @property + def url(self): + return f"/media/{self.parent_name}" + + def test_reconstruct(image_upload_file): - picture = SimplePicture( + + picture = TestPicture( image_upload_file.name, "WEBP", "16/9", @@ -158,7 +165,7 @@ def test_reconstruct(image_upload_file): assert isinstance(reconstructed, Storage) assert utils.reconstruct( - "pictures.models.SimplePicture", + "tests.test_utils.TestPicture", [], { "parent_name": "test.jpg", @@ -167,7 +174,7 @@ def test_reconstruct(image_upload_file): "storage": default_storage, "width": 100, }, - ) == SimplePicture( + ) == TestPicture( "test.jpg", "JPEG", "16/9",