F(backend): add public media derivatives pipeline
This commit is contained in:
1
core/tests/__init__.py
Normal file
1
core/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
core/tests/unit/__init__.py
Normal file
1
core/tests/unit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
174
core/tests/unit/test_media.py
Normal file
174
core/tests/unit/test_media.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import timedelta
|
||||
|
||||
from PIL import Image
|
||||
from django.core.management import call_command
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.blog.api.schemas import AuthorSchema, PostListSchema
|
||||
from apps.blog.models import Post
|
||||
from apps.events.api.schemas import EventGallerySchema, EventListSchema
|
||||
from apps.events.models import Event
|
||||
from apps.gallery.api.schemas import GallerySchema
|
||||
from apps.gallery.models import Gallery
|
||||
from apps.users.api.schemas import UserProfileSchema
|
||||
from apps.users.models import User
|
||||
from core.media import (
|
||||
BLUR_VARIANT,
|
||||
PREVIEW_VARIANT,
|
||||
THUMBNAIL_VARIANT,
|
||||
delete_image_derivatives,
|
||||
derivative_name,
|
||||
process_public_image,
|
||||
)
|
||||
|
||||
|
||||
class MediaProcessingTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.media_root = tempfile.mkdtemp()
|
||||
self.addCleanup(lambda: shutil.rmtree(self.media_root, ignore_errors=True))
|
||||
self.override = override_settings(MEDIA_ROOT=self.media_root, MEDIA_URL="/media/")
|
||||
self.override.enable()
|
||||
self.addCleanup(self.override.disable)
|
||||
self.user = User.objects.create_user(
|
||||
username="media-user",
|
||||
email="media@example.com",
|
||||
password="TestPass123!",
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def _upload(self, name: str, *, mode: str = "RGB", fmt: str = "JPEG") -> SimpleUploadedFile:
|
||||
buffer = io.BytesIO()
|
||||
Image.new(mode, (2400, 1800), color=(20, 80, 200)).save(buffer, format=fmt)
|
||||
buffer.seek(0)
|
||||
return SimpleUploadedFile(name, buffer.read(), content_type=f"image/{fmt.lower()}")
|
||||
|
||||
def test_process_public_image_generates_derivatives_for_jpeg(self):
|
||||
gallery = Gallery.objects.create(
|
||||
title="JPEG image",
|
||||
uploaded_by=self.user,
|
||||
image=self._upload("photo.jpg", fmt="JPEG"),
|
||||
)
|
||||
|
||||
result = process_public_image(gallery.image, "gallery", force=True)
|
||||
|
||||
self.assertTrue(result.processed)
|
||||
self.assertTrue(gallery.image.storage.exists(derivative_name(gallery.image.name, THUMBNAIL_VARIANT)))
|
||||
self.assertTrue(gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT)))
|
||||
self.assertTrue(gallery.image.storage.exists(derivative_name(gallery.image.name, BLUR_VARIANT)))
|
||||
|
||||
def test_process_public_image_generates_derivatives_for_png(self):
|
||||
gallery = Gallery.objects.create(
|
||||
title="PNG image",
|
||||
uploaded_by=self.user,
|
||||
image=self._upload("photo.png", mode="RGBA", fmt="PNG"),
|
||||
)
|
||||
|
||||
result = process_public_image(gallery.image, "gallery", force=True)
|
||||
|
||||
self.assertEqual(result.width, 2400)
|
||||
self.assertEqual(result.height, 1800)
|
||||
self.assertTrue(gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT)))
|
||||
|
||||
def test_delete_image_derivatives_removes_sidecars(self):
|
||||
gallery = Gallery.objects.create(
|
||||
title="Delete derivatives",
|
||||
uploaded_by=self.user,
|
||||
image=self._upload("cleanup.jpg", fmt="JPEG"),
|
||||
)
|
||||
process_public_image(gallery.image, "gallery", force=True)
|
||||
|
||||
delete_image_derivatives(gallery.image, "gallery")
|
||||
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(gallery.image.name, THUMBNAIL_VARIANT)))
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT)))
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(gallery.image.name, BLUR_VARIANT)))
|
||||
|
||||
def test_replacing_image_removes_previous_sidecars(self):
|
||||
gallery = Gallery.objects.create(
|
||||
title="Replace derivatives",
|
||||
uploaded_by=self.user,
|
||||
image=self._upload("replace-first.jpg", fmt="JPEG"),
|
||||
)
|
||||
first_name = gallery.image.name
|
||||
|
||||
gallery.image = self._upload("replace-second.jpg", fmt="JPEG")
|
||||
gallery.save()
|
||||
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(first_name, THUMBNAIL_VARIANT)))
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(first_name, PREVIEW_VARIANT)))
|
||||
self.assertFalse(gallery.image.storage.exists(derivative_name(first_name, BLUR_VARIANT)))
|
||||
|
||||
def test_backfill_command_is_idempotent(self):
|
||||
gallery = Gallery(title="Backfill image", uploaded_by=self.user)
|
||||
gallery._defer_image_processing = True
|
||||
gallery.image = self._upload("backfill.jpg", fmt="JPEG")
|
||||
gallery.save()
|
||||
|
||||
first = io.StringIO()
|
||||
second = io.StringIO()
|
||||
call_command("backfill_media_derivatives", stdout=first)
|
||||
call_command("backfill_media_derivatives", stdout=second)
|
||||
|
||||
self.assertIn("processed=1", first.getvalue())
|
||||
self.assertIn("skipped=1", second.getvalue())
|
||||
|
||||
def test_schema_resolvers_expose_derivative_urls(self):
|
||||
self.user.profile_picture.save("profile.jpg", self._upload("profile.jpg", fmt="JPEG"))
|
||||
|
||||
gallery = Gallery.objects.create(
|
||||
title="Schema image",
|
||||
uploaded_by=self.user,
|
||||
image=self._upload("schema-gallery.jpg", fmt="JPEG"),
|
||||
)
|
||||
event = Event.objects.create(
|
||||
title="Schema event",
|
||||
description="Event description",
|
||||
start_time=timezone.now(),
|
||||
end_time=timezone.now() + timedelta(hours=2),
|
||||
featured_image=self._upload("schema-event.jpg", fmt="JPEG"),
|
||||
)
|
||||
post = Post.objects.create(
|
||||
title="Schema post",
|
||||
content="Post body",
|
||||
author=self.user,
|
||||
featured_image=self._upload("schema-post.jpg", fmt="JPEG"),
|
||||
status=Post.StatusChoices.PUBLISHED,
|
||||
)
|
||||
|
||||
context = {"request": self.factory.get("/")}
|
||||
|
||||
self.assertIn(
|
||||
derivative_name(event.featured_image.name, THUMBNAIL_VARIANT),
|
||||
EventListSchema.resolve_absolute_featured_image_thumbnail_url(event, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(event.featured_image.name, PREVIEW_VARIANT),
|
||||
EventListSchema.resolve_absolute_featured_image_preview_url(event, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(gallery.image.name, PREVIEW_VARIANT),
|
||||
EventGallerySchema.resolve_absolute_image_preview_url(gallery, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(gallery.image.name, BLUR_VARIANT),
|
||||
GallerySchema.resolve_absolute_image_blur_url(gallery, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(post.featured_image.name, THUMBNAIL_VARIANT),
|
||||
PostListSchema.resolve_absolute_featured_image_thumbnail_url(post, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(self.user.profile_picture.name, THUMBNAIL_VARIANT),
|
||||
AuthorSchema.resolve_profile_picture_thumbnail_url(self.user, context),
|
||||
)
|
||||
self.assertIn(
|
||||
derivative_name(self.user.profile_picture.name, PREVIEW_VARIANT),
|
||||
UserProfileSchema.resolve_profile_picture_preview_url(self.user, context),
|
||||
)
|
||||
Reference in New Issue
Block a user