Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
from django.test import TestCase
from wagtail.images.api.fields import ImageRenditionField
from .utils import Image, get_test_image_file
class TestImageRenditionField(TestCase):
def setUp(self):
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def test_api_representation(self):
rendition = self.image.get_rendition("width-400")
representation = ImageRenditionField("width-400").to_representation(self.image)
self.assertEqual(
set(representation.keys()), {"url", "full_url", "width", "height", "alt"}
)
self.assertEqual(representation["url"], rendition.url)
self.assertEqual(representation["full_url"], rendition.full_url)
self.assertEqual(representation["width"], rendition.width)
self.assertEqual(representation["height"], rendition.height)
self.assertEqual(representation["alt"], rendition.alt)

View File

@@ -0,0 +1,75 @@
import unittest.mock
from django.apps import apps
from django.test import TestCase
from wagtail.images.blocks import ImageChooserBlock
from .utils import (
Image,
get_test_bad_image,
get_test_image_file,
get_test_image_filename,
)
class TestImageChooserBlock(TestCase):
def setUp(self):
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
self.bad_image = get_test_bad_image()
self.bad_image.save()
def test_render(self):
block = ImageChooserBlock()
html = block.render(self.image)
expected_html = (
'<img alt="Test image" src="{}" width="640" height="480">'.format(
get_test_image_filename(self.image, "original")
)
)
self.assertHTMLEqual(html, expected_html)
def test_render_with_custom_default_attrs(self):
block = ImageChooserBlock()
with unittest.mock.patch.object(
apps.get_app_config("wagtailimages"),
"default_attrs",
new={"decoding": "async", "loading": "lazy"},
):
html = block.render(self.bad_image)
self.assertHTMLEqual(
html,
'<img alt="missing image" src="/media/not-found" width="0" height="0" decoding="async" loading="lazy">',
)
def test_render_missing(self):
block = ImageChooserBlock()
html = block.render(self.bad_image)
expected_html = (
'<img alt="missing image" src="/media/not-found" width="0" height="0">'
)
self.assertHTMLEqual(html, expected_html)
def test_deconstruct(self):
block = ImageChooserBlock(required=False)
path, args, kwargs = block.deconstruct()
self.assertEqual(path, "wagtail.images.blocks.ImageChooserBlock")
self.assertEqual(args, ())
self.assertEqual(kwargs, {"required": False})
def test_extract_references(self):
block = ImageChooserBlock()
self.assertListEqual(
list(block.extract_references(self.image)),
[(Image, str(self.image.id), "", "")],
)
# None should not yield any references
self.assertListEqual(list(block.extract_references(None)), [])

View File

@@ -0,0 +1,84 @@
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.test.utils import WagtailTestUtils
Image = get_image_model()
test_file = get_test_image_file()
def get_tag_list(image):
return [tag.name for tag in image.tags.all()]
class TestBulkAddTags(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
self.new_tags = ["first", "second"]
self.images = [
Image.objects.create(title=f"Test image - {i}", file=test_file)
for i in range(1, 6)
]
self.url = (
reverse(
"wagtail_bulk_action",
args=(
"wagtailimages",
"image",
"add_tags",
),
)
+ "?"
)
for image in self.images:
self.url += f"id={image.id}&"
self.post_data = {"tags": ",".join(self.new_tags)}
def test_add_tags_with_limited_permissions(self):
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
html = response.content.decode()
self.assertInHTML(
"<p>You don't have permission to add tags to these images</p>", html
)
for image in self.images:
self.assertInHTML(f"<li>{image.title}</li>", html)
self.client.post(self.url, self.post_data)
# New tags should not be added to the images
for image in self.images:
self.assertCountEqual(get_tag_list(Image.objects.get(id=image.id)), [])
def test_simple(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailimages/bulk_actions/confirm_bulk_add_tags.html"
)
def test_add_tags(self):
# Make post request
response = self.client.post(self.url, self.post_data)
# User should be redirected back to the index
self.assertEqual(response.status_code, 302)
# New tags should not be added to the images
for image in self.images:
self.assertCountEqual(
get_tag_list(Image.objects.get(id=image.id)), self.new_tags
)

View File

@@ -0,0 +1,84 @@
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.models import Collection
from wagtail.test.utils import WagtailTestUtils
Image = get_image_model()
test_file = get_test_image_file()
class TestBulkAddImagesToCollection(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
self.root_collection = Collection.get_first_root_node()
self.dest_collection = self.root_collection.add_child(name="Destination")
self.images = [
Image.objects.create(title=f"Test image - {i}", file=test_file)
for i in range(1, 6)
]
self.url = (
reverse(
"wagtail_bulk_action",
args=(
"wagtailimages",
"image",
"add_to_collection",
),
)
+ "?"
)
for image in self.images:
self.url += f"id={image.id}&"
self.post_data = {"collection": str(self.dest_collection.id)}
def test_add_to_collection_with_limited_permissions(self):
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
html = response.content.decode()
self.assertInHTML(
"<p>You don't have permission to add these images to a collection</p>", html
)
for image in self.images:
self.assertInHTML(f"<li>{image.title}</li>", html)
self.client.post(self.url, self.post_data)
# Images should not be moved to new collection
for image in self.images:
self.assertEqual(
Image.objects.get(id=image.id).collection_id, self.root_collection.id
)
def test_simple(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailimages/bulk_actions/confirm_bulk_add_to_collection.html"
)
def test_add_to_collection(self):
# Make post request
response = self.client.post(self.url, self.post_data)
# User should be redirected back to the index
self.assertEqual(response.status_code, 302)
# Images should be moved to new collection
for image in self.images:
self.assertEqual(
Image.objects.get(id=image.id).collection_id, self.dest_collection.id
)

View File

@@ -0,0 +1,91 @@
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.test.utils import WagtailTestUtils
Image = get_image_model()
test_file = get_test_image_file()
class TestImageBulkDeleteView(WagtailTestUtils, TestCase):
def setUp(self):
self.user = self.login()
# Create images to delete
self.images = [
Image.objects.create(title=f"Test image - {i}", file=test_file)
for i in range(1, 6)
]
self.url = (
reverse(
"wagtail_bulk_action",
args=(
"wagtailimages",
"image",
"delete",
),
)
+ "?"
)
for image in self.images:
self.url += f"id={image.id}&"
def test_delete_with_limited_permissions(self):
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.user.save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
html = response.content.decode()
self.assertInHTML(
"<p>You don't have permission to delete these images</p>", html
)
for image in self.images:
self.assertInHTML(f"<li>{image.title}</li>", html)
response = self.client.post(self.url)
# User should be redirected back to the index
self.assertEqual(response.status_code, 302)
# Images should not be deleted
for image in self.images:
self.assertTrue(Image.objects.filter(id=image.id).exists())
def test_simple(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailimages/bulk_actions/confirm_bulk_delete.html"
)
def test_delete(self):
# Make post request
response = self.client.post(self.url)
# User should be redirected back to the index
self.assertEqual(response.status_code, 302)
# Images should be deleted
for image in self.images:
self.assertFalse(Image.objects.filter(id=image.id).exists())
def test_usage_link(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(
response, "wagtailimages/bulk_actions/confirm_bulk_delete.html"
)
for image in self.images:
self.assertContains(response, image.usage_url)
# usage count should be printed for each image
self.assertContains(response, "Used 0 times", count=5)

View File

@@ -0,0 +1,83 @@
from django import forms
from django.test import TestCase, override_settings
from taggit import models as taggit_models
from wagtail.admin import widgets
from wagtail.admin.widgets import AdminDateTimeInput
from wagtail.images import models
from wagtail.images.forms import BaseImageForm, get_image_base_form, get_image_form
from wagtail.test.testapp.media_forms import AlternateImageForm, OverriddenWidget
from wagtail.test.testapp.models import CustomRestaurantImage, RestaurantTag
class TestImageFormOverride(TestCase):
def test_get_image_base_form(self):
self.assertIs(get_image_base_form(), BaseImageForm)
def test_get_image_form(self):
bases = get_image_form(models.Image).__bases__
self.assertIn(BaseImageForm, bases)
self.assertNotIn(AlternateImageForm, bases)
def test_get_image_form_widgets(self):
form_cls = get_image_form(models.Image)
form = form_cls()
self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
self.assertEqual(form.fields["tags"].widget.tag_model, taggit_models.Tag)
self.assertIsInstance(form.fields["file"].widget, forms.FileInput)
self.assertIsInstance(form.fields["focal_point_x"].widget, forms.HiddenInput)
def test_tags_widget_with_custom_tag_model(self):
form_cls = get_image_form(CustomRestaurantImage)
form = form_cls()
self.assertIsInstance(form.fields["tags"].widget, widgets.AdminTagWidget)
self.assertEqual(form.fields["tags"].widget.tag_model, RestaurantTag)
def test_tags_longer_than_max_characters(self):
long_value = "longtag" * 20
form_data = {
"title": "Image",
"file": OverriddenWidget,
"tags": [long_value],
}
form_cls = get_image_form(models.Image)
form = form_cls(form_data)
self.assertFalse(form.is_valid())
self.assertIn("tags", form.errors)
self.assertEqual(
form.errors["tags"][0],
"Tag(s) ['{val}'] are over {max_tag_length} characters".format(
val=long_value,
max_tag_length=taggit_models.TagBase._meta.get_field("name").max_length,
),
)
@override_settings(
WAGTAILIMAGES_IMAGE_FORM_BASE="wagtail.test.testapp.media_forms.AlternateImageForm"
)
def test_overridden_base_form(self):
self.assertIs(get_image_base_form(), AlternateImageForm)
@override_settings(
WAGTAILIMAGES_IMAGE_FORM_BASE="wagtail.test.testapp.media_forms.AlternateImageForm"
)
def test_get_overridden_image_form(self):
bases = get_image_form(models.Image).__bases__
self.assertNotIn(BaseImageForm, bases)
self.assertIn(AlternateImageForm, bases)
@override_settings(
WAGTAILIMAGES_IMAGE_FORM_BASE="wagtail.test.testapp.media_forms.AlternateImageForm"
)
def test_get_overridden_image_form_widgets(self):
form_cls = get_image_form(models.Image)
form = form_cls()
self.assertIsInstance(form.fields["tags"].widget, OverriddenWidget)
self.assertIsInstance(form.fields["file"].widget, OverriddenWidget)
self.assertIsInstance(form.fields["focal_point_x"].widget, forms.HiddenInput)
self.assertIn("form_only_field", form.fields)
self.assertIs(form.Meta.widgets["form_only_field"], AdminDateTimeInput)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,462 @@
import unittest.mock
from django.apps import apps
from django.template import TemplateSyntaxError, engines
from django.test import TestCase
from wagtail.models import Site
from .utils import (
Image,
get_test_bad_image,
get_test_image_file,
get_test_image_filename,
)
class JinjaImagesTestCase(TestCase):
maxDiff = None
def setUp(self):
self.engine = engines["jinja2"]
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
self.bad_image = get_test_bad_image()
self.bad_image.save()
def render(self, string, context=None, request_context=True):
if context is None:
context = {}
# Add a request to the template, to simulate a RequestContext
if request_context:
site = Site.objects.get(is_default_site=True)
request = self.client.get("/test/", HTTP_HOST=site.hostname)
context["request"] = request
template = self.engine.from_string(string)
return template.render(context)
class TestImageJinja(JinjaImagesTestCase):
def test_image(self):
self.assertHTMLEqual(
self.render('{{ image(myimage, "width-200") }}', {"myimage": self.image}),
'<img alt="Test image" src="{}" width="200" height="150">'.format(
get_test_image_filename(self.image, "width-200")
),
)
def test_no_image(self):
rendered = self.render('{{ image(myimage, "width-2") }}', {"myimage": None})
self.assertEqual(rendered, "")
def test_image_attributes(self):
self.assertHTMLEqual(
self.render(
'{{ image(myimage, "width-200", alt="alternate", class="test") }}',
{"myimage": self.image},
),
'<img alt="alternate" src="{}" width="200" height="150" class="test">'.format(
get_test_image_filename(self.image, "width-200")
),
)
def test_image_assignment(self):
template = (
'{% set bg=image(myimage, "width-200") %}'
"width: {{ bg.width }}, url: {{ bg.url }}"
)
output = "width: 200, url: " + get_test_image_filename(self.image, "width-200")
self.assertHTMLEqual(self.render(template, {"myimage": self.image}), output)
def test_image_assignment_render_as_is(self):
self.assertHTMLEqual(
self.render(
'{% set bg=image(myimage, "width-200") %}{{ bg }}',
{"myimage": self.image},
),
'<img alt="Test image" src="{}" width="200" height="150">'.format(
get_test_image_filename(self.image, "width-200")
),
)
def test_missing_image(self):
self.assertHTMLEqual(
self.render(
'{{ image(myimage, "width-200") }}', {"myimage": self.bad_image}
),
'<img alt="missing image" src="/media/not-found" width="0" height="0">',
)
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in 'image' tag may only"
):
self.render('{{ image(myimage, "fill-200×200") }}', {"myimage": self.image})
def test_custom_default_attrs(self):
with unittest.mock.patch.object(
apps.get_app_config("wagtailimages"),
"default_attrs",
new={"decoding": "async", "loading": "lazy"},
):
self.assertHTMLEqual(
self.render(
'{{ image(myimage, "width-200") }}', {"myimage": self.bad_image}
),
'<img alt="missing image" src="/media/not-found" width="0" height="0" decoding="async" loading="lazy">',
)
def test_chaining_filterspecs(self):
self.assertHTMLEqual(
self.render(
'{{ image(myimage, "width-200|jpegquality-40") }}',
{"myimage": self.image},
),
'<img alt="Test image" src="{}" width="200" height="150">'.format(
get_test_image_filename(self.image, "width-200.jpegquality-40")
),
)
class TestImageURLJinja(JinjaImagesTestCase):
def test_image_url(self):
self.assertRegex(
self.render(
'{{ image_url(myimage, "width-200") }}', {"myimage": self.image}
),
"/images/.*/width-200/{}".format(self.image.file.name.split("/")[-1]),
)
def test_image_url_custom_view(self):
self.assertRegex(
self.render(
'{{ image_url(myimage, "width-200", "wagtailimages_serve_custom_view") }}',
{"myimage": self.image},
),
"/testimages/custom_view/.*/width-200/{}".format(
self.image.file.name.split("/")[-1]
),
)
class TestSrcsetImageJinja(JinjaImagesTestCase):
def test_srcset_image(self):
filename_200 = get_test_image_filename(self.image, "width-200")
filename_400 = get_test_image_filename(self.image, "width-400")
rendered = self.render(
'{{ srcset_image(myimage, "width-{200,400}", sizes="100vw") }}',
{"myimage": self.image},
)
expected = f"""
<img
sizes="100vw"
src="{filename_200}"
srcset="{filename_200} 200w, {filename_400} 400w"
alt="Test image"
width="200"
height="150"
>
"""
self.assertHTMLEqual(rendered, expected)
def test_no_image(self):
rendered = self.render(
'{{ srcset_image(myimage, "width-2") }}', {"myimage": None}
)
self.assertEqual(rendered, "")
def test_srcset_output_single_image(self):
self.assertHTMLEqual(
self.render(
'{{ srcset_image(myimage, "width-200") }}',
{"myimage": self.image},
),
self.render(
'{{ image(myimage, "width-200") }}',
{"myimage": self.image},
),
)
def test_srcset_image_assignment(self):
template = (
'{% set bg=srcset_image(myimage, "width-{200,400}") %}'
"width: {{ bg.renditions[0].width }}, url: {{ bg.renditions[0].url }} "
"width: {{ bg.renditions[1].width }}, url: {{ bg.renditions[1].url }} "
)
rendered = self.render(template, {"myimage": self.image})
expected = f"""
width: 200, url: {get_test_image_filename(self.image, "width-200")}
width: 400, url: {get_test_image_filename(self.image, "width-400")}
"""
self.assertHTMLEqual(rendered, expected)
def test_srcset_image_assignment_render_as_is(self):
rendered = self.render(
'{% set bg=srcset_image(myimage, "width-{200,400}") %}{{ bg }}',
{"myimage": self.image},
)
expected = self.render(
'{{ srcset_image(myimage, "width-{200,400}") }}',
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_missing_srcset_image(self):
rendered = self.render(
'{{ srcset_image(myimage, "width-{200,400}", sizes="100vw") }}',
{"myimage": self.bad_image},
)
expected = """
<img
sizes="100vw"
src="/media/not-found"
srcset="/media/not-found 0w, /media/not-found 0w"
alt="missing image"
width="0"
height="0"
>
"""
self.assertHTMLEqual(rendered, expected)
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in 'srcset_image' tag may only"
):
self.render(
'{{ srcset_image(myimage, "fill-{20×20,40×40}", sizes="100vw") }}',
{"myimage": self.image},
)
def test_custom_default_attrs(self):
with unittest.mock.patch.object(
apps.get_app_config("wagtailimages"),
"default_attrs",
new={"decoding": "async", "loading": "lazy"},
):
rendered = self.render(
'{{ srcset_image(myimage, "width-{20,40}", sizes="100vw") }}',
{"myimage": self.bad_image},
)
expected = """
<img
sizes="100vw"
src="/media/not-found"
srcset="/media/not-found 0w, /media/not-found 0w"
alt="missing image"
width="0"
height="0"
decoding="async"
loading="lazy"
>
"""
self.assertHTMLEqual(rendered, expected)
def test_chaining_filterspecs(self):
filenames = [
get_test_image_filename(self.image, "width-200.jpegquality-40"),
get_test_image_filename(self.image, "width-400.jpegquality-40"),
]
rendered = self.render(
'{{ srcset_image(myimage, "width-{200,400}|jpegquality-40", sizes="100vw") }}',
{"myimage": self.image},
)
expected = f"""
<img
sizes="100vw"
src="{filenames[0]}"
srcset="{filenames[0]} 200w, {filenames[1]} 400w"
alt="Test image"
width="200"
height="150"
>
"""
self.assertHTMLEqual(rendered, expected)
class TestPictureJinja(JinjaImagesTestCase):
def test_picture_formats_multi_sizes(self):
filenames = [
get_test_image_filename(self.image, "width-200.format-jpeg"),
get_test_image_filename(self.image, "width-400.format-jpeg"),
get_test_image_filename(self.image, "width-200.format-webp"),
get_test_image_filename(self.image, "width-400.format-webp"),
get_test_image_filename(self.image, "width-200.format-gif"),
get_test_image_filename(self.image, "width-400.format-gif"),
]
rendered = self.render(
'{{ picture(myimage, "width-{200,400}|format-{jpeg,webp,gif}", sizes="100vw") }}',
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filenames[2]} 200w, {filenames[3]} 400w" sizes="100vw" type="image/webp">
<source srcset="{filenames[0]} 200w, {filenames[1]} 400w" sizes="100vw" type="image/jpeg">
<img
sizes="100vw"
src="{filenames[4]}"
srcset="{filenames[4]} 200w, {filenames[5]} 400w"
alt="Test image"
width="200"
height="150"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_formats_only(self):
filename_jpeg = get_test_image_filename(self.image, "format-jpeg")
filename_webp = get_test_image_filename(self.image, "format-webp")
rendered = self.render(
'{{ picture(myimage, "format-{jpeg,webp}") }}',
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filename_webp}" type="image/webp">
<img
src="{filename_jpeg}"
alt="Test image"
width="640"
height="480"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_sizes_only(self):
rendered = self.render(
'{{ picture(myimage, "width-{200,400}", sizes="100vw") }}',
{"myimage": self.image},
)
expected = self.render(
'<picture>{{ srcset_image(myimage, "width-{200,400}", sizes="100vw") }}</picture>',
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_picture_single_format(self):
rendered = self.render(
'{{ picture(myimage, "format-jpeg") }}',
{"myimage": self.image},
)
expected = self.render(
'<picture>{{ image(myimage, "format-jpeg") }}</picture>',
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_no_image(self):
rendered = self.render('{{ picture(myimage, "width-2") }}', {"myimage": None})
self.assertEqual(rendered, "")
def test_picture_assignment(self):
template = (
'{% set bg=picture(myimage, "width-{200,400}|format-{jpeg,webp}") %}'
"width: {{ bg.formats['jpeg'][0].width }}, url: {{ bg.formats['jpeg'][0].url }} "
"width: {{ bg.formats['jpeg'][1].width }}, url: {{ bg.formats['jpeg'][1].url }} "
"width: {{ bg.formats['webp'][0].width }}, url: {{ bg.formats['webp'][0].url }} "
"width: {{ bg.formats['webp'][1].width }}, url: {{ bg.formats['webp'][1].url }} "
)
rendered = self.render(template, {"myimage": self.image})
expected = f"""
width: 200, url: {get_test_image_filename(self.image, "width-200.format-jpeg")}
width: 400, url: {get_test_image_filename(self.image, "width-400.format-jpeg")}
width: 200, url: {get_test_image_filename(self.image, "width-200.format-webp")}
width: 400, url: {get_test_image_filename(self.image, "width-400.format-webp")}
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_assignment_render_as_is(self):
rendered = self.render(
'{% set bg=picture(myimage, "width-{200,400}|format-{jpeg,webp,gif}", sizes="100vw") %}{{ bg }}',
{"myimage": self.image},
)
expected = self.render(
'{{ picture(myimage, "width-{200,400}|format-{jpeg,webp,gif}", sizes="100vw") }}',
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_missing_picture(self):
rendered = self.render(
'{{ picture(myimage, "format-{jpeg,webp}") }}',
{"myimage": self.bad_image},
)
expected = """
<picture>
<source srcset="/media/not-found" type="image/webp">
<img
src="/media/not-found"
alt="missing image"
width="0"
height="0"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in 'picture' tag may only"
):
self.render(
'{{ picture(myimage, "fill-{20×20,40×40}", sizes="100vw") }}',
{"myimage": self.image},
)
def test_custom_default_attrs(self):
with unittest.mock.patch.object(
apps.get_app_config("wagtailimages"),
"default_attrs",
new={"decoding": "async", "loading": "lazy"},
):
rendered = self.render(
'{{ picture(myimage, "format-{jpeg,webp}") }}',
{"myimage": self.bad_image},
)
expected = """
<picture>
<source srcset="/media/not-found" type="image/webp">
<img
src="/media/not-found"
alt="missing image"
width="0"
height="0"
decoding="async"
loading="lazy"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_chaining_filterspecs(self):
filename_jpeg = get_test_image_filename(
self.image, "format-jpeg.jpegquality-40.webpquality-40"
)
filename_webp = get_test_image_filename(
self.image, "format-webp.jpegquality-40.webpquality-40"
)
rendered = self.render(
'{{ picture(myimage, "format-{jpeg,webp}|jpegquality-40|webpquality-40") }}',
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filename_webp}" type="image/webp">
<img
src="{filename_jpeg}"
alt="Test image"
width="640"
height="480"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)

View File

@@ -0,0 +1,140 @@
import re
import warnings
from io import StringIO
from django.core import management
from django.test import TestCase, override_settings
from ..management.commands.wagtail_update_image_renditions import progress_bar
from .utils import Image, get_test_image_file
# note .utils.Image already does get_image_model()
Rendition = Image.get_rendition_model()
class TestUpdateImageRenditions(TestCase):
REAESC = re.compile(r"\x1b[^m]*m")
@classmethod
def setUpTestData(cls):
cls.image = Image.objects.create(
title="Test image",
file=get_test_image_file(filename="test_image.png", colour="white"),
)
cls.rendition = Rendition.objects.create(
image=cls.image,
filter_spec="original",
width=1000,
height=1000,
file=get_test_image_file(
filename="test_rendition.png", colour="white", size=(1000, 1000)
),
)
def delete_renditions(self):
renditions = Rendition.objects.all()
for rendition in renditions:
try:
rendition_image = rendition.image
rendition.delete()
except Exception: # noqa: BLE001
warnings.warn(f"Could not delete rendition for {rendition_image}")
def run_command(self, **options):
output = StringIO()
management.call_command(
"wagtail_update_image_renditions", stdout=output, **options
)
output.seek(0)
return output
def test_progress_bar(self):
total_rendition = 10
out = StringIO()
for current in range(1, total_rendition + 1):
progress_bar_output = progress_bar(current, total_rendition)[0]
out.write(progress_bar_output)
out.seek(0)
expected_output = "".join(
[
"Progress: [----> ] 10%",
"Progress: [---------> ] 20%",
"Progress: [--------------> ] 30%",
"Progress: [-------------------> ] 40%",
"Progress: [------------------------> ] 50%",
"Progress: [-----------------------------> ] 60%",
"Progress: [----------------------------------> ] 70%",
"Progress: [---------------------------------------> ] 80%",
"Progress: [--------------------------------------------> ] 90%",
"Progress: [------------------------------------------------->] 100%",
]
)
self.assertIn(expected_output, out.getvalue())
def test_exits_early_for_no_renditions(self):
self.delete_renditions()
# checking when command is called without any arguments
output = self.run_command()
output_string = self.REAESC.sub("", output.read())
self.assertEqual(output_string, "No image renditions found.\n")
# checking when command is called with '--purge-only'
output = self.run_command(purge_only=True)
output_string = self.REAESC.sub("", output.read())
self.assertEqual(output_string, "No image renditions found.\n")
def test_image_renditions(self):
renditions = Rendition.objects.all()
total_renditions = len(renditions)
output = self.run_command()
output_string = self.REAESC.sub("", output.read())
# checking if the number of renditions regenerated equal total_renditions
self.assertEqual(
output_string,
f"Regenerating {total_renditions} rendition(s)\n"
f"Progress: [------------------------------------------------->] 100%\n"
f"Successfully processed {total_renditions} rendition(s)\n",
)
# checking if the number of renditions now equal total_renditions
renditions_now = Rendition.objects.all()
total_renditions_now = len(renditions_now)
self.assertEqual(total_renditions_now, total_renditions)
def test_image_renditions_with_purge_only(self):
renditions = Rendition.objects.all()
total_renditions = len(renditions)
output = self.run_command(purge_only=True)
output_string = self.REAESC.sub("", output.read())
# checking if the number of renditions purged equal total_renditions
self.assertEqual(
output_string,
f"Purging {total_renditions} rendition(s)\n"
f"Progress: [------------------------------------------------->] 100%\n"
f"Successfully processed {total_renditions} rendition(s)\n",
)
# checking if the number of renditions now equal 0
renditions_now = Rendition.objects.all()
total_renditions_now = len(renditions_now)
self.assertEqual(total_renditions_now, 0)
@override_settings(
CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
)
def test_image_renditions_with_cache(self):
total_renditions = Rendition.objects.count()
output = self.run_command()
output_string = self.REAESC.sub("", output.read())
self.assertIn(
f"Successfully processed {total_renditions} rendition(s)\n", output_string
)
# Run the command again with a warmed cache
output = self.run_command()
output_string = self.REAESC.sub("", output.read())
self.assertIn(
f"Successfully processed {total_renditions} rendition(s)\n", output_string
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
from django.test import TestCase
from django.urls import reverse_lazy
from wagtail.fields import RichTextField
from wagtail.images.rich_text import ImageEmbedHandler as FrontendImageEmbedHandler
from wagtail.images.rich_text.editor_html import (
ImageEmbedHandler as EditorHtmlImageEmbedHandler,
)
from wagtail.rich_text.feature_registry import FeatureRegistry
from wagtail.test.utils import WagtailTestUtils
from .utils import Image, get_test_image_file
class TestEditorHtmlImageEmbedHandler(WagtailTestUtils, TestCase):
def test_get_db_attributes(self):
soup = self.get_soup(
'<b data-id="test-id" data-format="test-format" data-alt="test-alt">foo</b>',
)
tag = soup.b
result = EditorHtmlImageEmbedHandler.get_db_attributes(tag)
self.assertEqual(
result,
{
"alt": "test-alt",
"id": "test-id",
"format": "test-format",
},
)
def test_expand_db_attributes_for_editor(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = EditorHtmlImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"alt": "test-alt",
"format": "left",
}
)
self.assertTagInHTML(
(
'<img data-embedtype="image" data-id="1" data-format="left" '
'data-alt="test-alt" class="richtext-image left" />'
),
result,
allow_extra_attrs=True,
)
def test_expand_db_attributes_for_editor_nonexistent_image(self):
self.assertEqual(
EditorHtmlImageEmbedHandler.expand_db_attributes({"id": 0}), '<img alt="">'
)
def test_expand_db_attributes_for_editor_escapes_alt_text(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = EditorHtmlImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"alt": 'Arthur "two sheds" Jackson',
"format": "left",
}
)
self.assertTagInHTML(
(
'<img data-embedtype="image" data-id="1" data-format="left" '
'data-alt="Arthur &quot;two sheds&quot; Jackson" class="richtext-image left" />'
),
result,
allow_extra_attrs=True,
)
self.assertIn('alt="Arthur &quot;two sheds&quot; Jackson"', result)
def test_expand_db_attributes_for_editor_with_missing_alt(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = EditorHtmlImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"format": "left",
}
)
self.assertTagInHTML(
(
'<img data-embedtype="image" data-id="1" data-format="left" data-alt="" '
'class="richtext-image left" />'
),
result,
allow_extra_attrs=True,
)
class TestFrontendImageEmbedHandler(WagtailTestUtils, TestCase):
def test_expand_db_attributes_for_frontend(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = FrontendImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"alt": "test-alt",
"format": "left",
}
)
self.assertTagInHTML(
'<img class="richtext-image left" />', result, allow_extra_attrs=True
)
def test_expand_db_attributes_for_frontend_with_nonexistent_image(self):
result = FrontendImageEmbedHandler.expand_db_attributes({"id": 0})
self.assertEqual(result, '<img alt="">')
def test_expand_db_attributes_for_frontend_escapes_alt_text(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = FrontendImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"alt": 'Arthur "two sheds" Jackson',
"format": "left",
}
)
self.assertIn('alt="Arthur &quot;two sheds&quot; Jackson"', result)
def test_expand_db_attributes_for_frontend_with_missing_alt(self):
Image.objects.create(id=1, title="Test", file=get_test_image_file())
result = FrontendImageEmbedHandler.expand_db_attributes(
{
"id": 1,
"format": "left",
}
)
self.assertTagInHTML(
'<img class="richtext-image left" alt="" />', result, allow_extra_attrs=True
)
class TestExtractReferencesWithImage(WagtailTestUtils, TestCase):
def test_extract_references(self):
self.assertEqual(
list(
RichTextField().extract_references(
'<embed alt="Olivia Ava" embedtype="image" format="left" id="52"/>'
)
),
[(Image, "52", "", "")],
)
class TestEntityFeatureChooserUrls(TestCase):
def test_chooser_urls_exist(self):
features = FeatureRegistry()
image = features.get_editor_plugin("draftail", "image")
self.assertIsNotNone(image.data.get("chooserUrls"))
self.assertEqual(
image.data["chooserUrls"]["imageChooser"],
reverse_lazy("wagtailimages_chooser:choose"),
)

View File

@@ -0,0 +1,64 @@
from django.test import TestCase
from wagtail.images.models import Filter
from wagtail.images.shortcuts import (
get_rendition_or_not_found,
get_renditions_or_not_found,
)
from .utils import Image, get_test_image_file
class TestShortcuts(TestCase):
fixtures = ["test.json"]
def test_fallback_to_not_found(self):
bad_image = Image.objects.get(id=1)
good_image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
rendition = get_rendition_or_not_found(good_image, "width-400")
self.assertEqual(rendition.width, 400)
rendition = get_rendition_or_not_found(bad_image, "width-400")
self.assertEqual(rendition.file.name, "not-found")
def test_multiple_fallback_to_not_found(self):
bad_image = Image.objects.get(id=1)
good_image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
renditions = get_renditions_or_not_found(good_image, ("width-200", "width-400"))
self.assertEqual(tuple(renditions.keys()), ("width-200", "width-400"))
self.assertEqual(renditions["width-200"].width, 200)
self.assertEqual(renditions["width-400"].width, 400)
renditions = get_renditions_or_not_found(bad_image, ("width-200", "width-400"))
self.assertEqual(tuple(renditions.keys()), ("width-200", "width-400"))
self.assertEqual(renditions["width-200"].file.name, "not-found")
self.assertEqual(renditions["width-400"].file.name, "not-found")
def test_multiple_fallback_to_not_found_with_filters(self):
bad_image = Image.objects.get(id=1)
good_image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
renditions = get_renditions_or_not_found(
good_image, (Filter("width-200"), Filter("width-400"))
)
self.assertEqual(tuple(renditions.keys()), ("width-200", "width-400"))
self.assertEqual(renditions["width-200"].width, 200)
self.assertEqual(renditions["width-400"].width, 400)
renditions = get_renditions_or_not_found(
bad_image, (Filter("width-200"), Filter("width-400"))
)
self.assertEqual(tuple(renditions.keys()), ("width-200", "width-400"))
self.assertEqual(renditions["width-200"].file.name, "not-found")
self.assertEqual(renditions["width-400"].file.name, "not-found")

View File

@@ -0,0 +1,95 @@
from django.db import transaction
from django.test import TestCase, TransactionTestCase, override_settings
from wagtail.images import get_image_model, signal_handlers
from wagtail.images.tests.utils import get_test_image_file
from wagtail.models import Collection
from .utils import Image
class TestFilesDeletedForDefaultModels(TransactionTestCase):
"""
Because we expect file deletion to only happen once a transaction is
successfully committed, we must run these tests using TransactionTestCase
per the following documentation:
Django's TestCase class wraps each test in a transaction and rolls back that
transaction after each test, in order to provide test isolation. This means
that no transaction is ever actually committed, thus your on_commit()
callbacks will never be run. If you need to test the results of an
on_commit() callback, use a TransactionTestCase instead.
https://docs.djangoproject.com/en/1.10/topics/db/transactions/#use-in-tests
"""
def setUp(self):
# Required to create root collection because the TransactionTestCase
# does not make initial data loaded in migrations available and
# serialized_rollback=True causes other problems in the test suite.
# ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
Collection.objects.get_or_create(
name="Root",
path="0001",
depth=1,
numchild=0,
)
def test_image_file_deleted_oncommit(self):
with transaction.atomic():
image = get_image_model().objects.create(
title="Test Image", file=get_test_image_file()
)
filename = image.file.name
self.assertTrue(image.file.storage.exists(filename))
image.delete()
self.assertTrue(image.file.storage.exists(filename))
self.assertFalse(image.file.storage.exists(filename))
def test_rendition_file_deleted_oncommit(self):
with transaction.atomic():
image = get_image_model().objects.create(
title="Test Image", file=get_test_image_file()
)
rendition = image.get_rendition("original")
filename = rendition.file.name
self.assertTrue(rendition.file.storage.exists(filename))
rendition.delete()
self.assertTrue(rendition.file.storage.exists(filename))
self.assertFalse(rendition.file.storage.exists(filename))
@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.CustomImage")
class TestFilesDeletedForCustomModels(TestFilesDeletedForDefaultModels):
def setUp(self):
# Required to create root collection because the TransactionTestCase
# does not make initial data loaded in migrations available and
# serialized_rollback=True causes other problems in the test suite.
# ref: https://docs.djangoproject.com/en/1.10/topics/testing/overview/#rollback-emulation
Collection.objects.get_or_create(
name="Root",
path="0001",
depth=1,
numchild=0,
)
#: Sadly signal receivers only get connected when starting django.
#: We will re-attach them here to mimic the django startup behaviour
#: and get the signals connected to our custom model..
signal_handlers.register_signal_handlers()
def test_image_model(self):
cls = get_image_model()
self.assertEqual(f"{cls._meta.app_label}.{cls.__name__}", "tests.CustomImage")
@override_settings(WAGTAILIMAGES_FEATURE_DETECTION_ENABLED=True)
class TestRawForPreSaveImageFeatureDetection(TestCase):
fixtures = ["test.json"]
# just to test the file is from a fixture doesn't actually exists.
# raw check in pre_save_image_feature_detection skips on the provided condition of this test
# hence avoiding an error
def test_image_does_not_exist(self):
bad_image = Image.objects.get(pk=1)
self.assertFalse(bad_image.file.storage.exists(bad_image.file.name))

View File

@@ -0,0 +1,136 @@
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.urls import reverse
from wagtail.images.tests.utils import Image
from wagtail.images.wagtail_hooks import ImagesSummaryItem
from wagtail.models import Collection, GroupCollectionPermission, Site
from wagtail.test.utils import WagtailTestUtils
class TestImagesSummary(WagtailTestUtils, TestCase):
@classmethod
def setUpTestData(self):
# Permissions
image_content_type = ContentType.objects.get_for_model(Image)
add_image_permission = Permission.objects.get(
content_type=image_content_type, codename="add_image"
)
change_image_permission = Permission.objects.get(
content_type=image_content_type, codename="change_image"
)
choose_image_permission = Permission.objects.get(
content_type=image_content_type, codename="choose_image"
)
# Collections
self.root_collection = Collection.get_first_root_node()
self.birds_collection = self.root_collection.add_child(name="Birds")
# Groups
image_changers_group = Group.objects.create(name="Image changers")
GroupCollectionPermission.objects.create(
group=image_changers_group,
collection=self.root_collection,
permission=change_image_permission,
)
bird_adders_group = Group.objects.create(name="Bird adders")
GroupCollectionPermission.objects.create(
group=bird_adders_group,
collection=self.birds_collection,
permission=add_image_permission,
)
bird_choosers_group = Group.objects.create(name="Bird choosers")
GroupCollectionPermission.objects.create(
group=bird_choosers_group,
collection=self.birds_collection,
permission=choose_image_permission,
)
# Users
self.superuser = self.create_superuser(
"superuser", "superuser@example.com", "password"
)
# a user with add_image permission on birds via the bird_adders group
self.bird_adder = self.create_user(
"birdadder", "birdadder@example.com", "password"
)
self.bird_adder.groups.add(bird_adders_group)
# a user with choose_image permission on birds via the bird_choosers group
self.bird_chooser = self.create_user(
"birdchooser", "birdchooser@example.com", "password"
)
self.bird_chooser.groups.add(bird_choosers_group)
# Images
# an image in the root owned by 'birdadder'
self.changer_image = Image.objects.create(
title="birdadder's image",
collection=self.root_collection,
uploaded_by_user=self.bird_adder,
width=1,
height=1,
)
# an image in birds owned by 'birdadder'
self.changer_bird = Image.objects.create(
title="birdadder's bird",
collection=self.birds_collection,
uploaded_by_user=self.bird_adder,
width=2,
height=2,
)
# an image in birds owned by 'birdadder'
self.adder_bird = Image.objects.create(
title="birdadder's bird",
collection=self.birds_collection,
uploaded_by_user=self.bird_adder,
width=3,
height=3,
)
def setUp(self):
self.login(self.superuser)
def get_request(self):
return self.client.get(reverse("wagtailadmin_home")).wsgi_request
def assertSummaryContains(self, content):
summary = ImagesSummaryItem(self.get_request()).render_html()
self.assertIn(content, summary)
def test_site_name_is_shown(self):
self.assertEqual(Site.objects.count(), 1)
site = Site.objects.first()
self.assertSummaryContains(site.site_name)
def test_user_with_permissions_is_shown_panel(self):
self.assertTrue(ImagesSummaryItem(self.get_request()).is_shown())
def test_user_with_no_permissions_is_not_shown_panel(self):
self.superuser.is_superuser = False
self.superuser.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
self.superuser.save()
self.assertFalse(ImagesSummaryItem(self.get_request()).is_shown())
def test_user_sees_proper_image_count(self):
cases = (
(self.superuser, "<span>3</span> Images"),
(self.bird_adder, "<span>2</span> Images"),
(self.bird_chooser, "<span>2</span> Images"),
)
for user, content in cases:
with self.subTest(user=user):
self.login(user)
self.assertSummaryContains(content)

View File

@@ -0,0 +1,480 @@
from django.template import Context, Engine, TemplateSyntaxError, Variable
from django.test import TestCase
from wagtail.images.models import Image, Rendition
from wagtail.images.templatetags.wagtailimages_tags import ImageNode
from wagtail.images.tests.utils import (
get_test_bad_image,
get_test_image_file,
get_test_image_file_svg,
get_test_image_filename,
)
LIBRARIES = {
"wagtailimages_tags": "wagtail.images.templatetags.wagtailimages_tags",
}
class ImageNodeTestCase(TestCase):
@classmethod
def setUpTestData(cls):
# Create an image for running tests on
cls.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
cls.svg_image = Image.objects.create(
title="Test SVG image",
file=get_test_image_file_svg(),
)
def test_render_valid_image_to_string(self):
"""
Tests that an ImageNode with a valid image renders an img tag
"""
context = {"image": self.image}
node = ImageNode(Variable("image"), ["original"])
rendered = node.render(context)
self.assertIn('<img alt="Test image"', rendered)
def test_render_none_to_string(self):
"""
Tests that an ImageNode without image renders an empty string
"""
context = {"image": None}
node = ImageNode(Variable("image"), ["original"])
rendered = node.render(context)
self.assertEqual(rendered, "")
def test_render_valid_image_as_context_variable(self):
"""
Tests that an ImageNode with a valid image and a context variable name
renders an empty string and puts a rendition in the context variable
"""
context = {"image": self.image, "image_node": "fake value"}
node = ImageNode(Variable("image"), ["original"], "image_node")
rendered = node.render(context)
self.assertEqual(rendered, "")
self.assertIsInstance(context["image_node"], Rendition)
def test_render_none_as_context_variable(self):
"""
Tests that an ImageNode without an image and a context variable name
renders an empty string and puts None in the context variable
"""
context = {"image": None, "image_node": "fake value"}
node = ImageNode(Variable("image"), ["original"], "image_node")
rendered = node.render(context)
self.assertEqual(rendered, "")
self.assertIsNone(context["image_node"])
def test_filters_preserve_svg(self):
"""
If the image is an SVG, and we set the preserve_svg parameter of ImageNode
to True, we should only use filters that don't require rasterisation (at this
time, resize and crop operations only).
"""
params = [
(self.svg_image, ["original"], "original"),
(self.svg_image, ["fill-400x400", "bgcolor-000"], "fill-400x400"),
(
self.svg_image,
["fill-400x400", "format-webp", "webpquality-50"],
"fill-400x400",
),
(self.image, ["fill-400x400", "bgcolor-000"], "fill-400x400|bgcolor-000"),
(self.image, ["fill-400x400", "format-webp"], "fill-400x400|format-webp"),
(
self.image,
["fill-400x400", "format-webp", "webpquality-50"],
"fill-400x400|format-webp|webpquality-50",
),
(self.svg_image, ["max-400x400"], "max-400x400"),
(self.svg_image, ["min-400x400"], "min-400x400"),
(self.svg_image, ["width-300"], "width-300"),
(self.svg_image, ["height-300"], "height-300"),
(self.svg_image, ["scale-50"], "scale-50"),
(self.svg_image, ["fill-400x400"], "fill-400x400"),
]
for image, filter_specs, expected in params:
with self.subTest(img=image, filter_specs=filter_specs, expected=expected):
context = {"image": image, "image_node": "fake_value"}
node = ImageNode(Variable("image"), filter_specs, preserve_svg=True)
node.render(context)
self.assertEqual(
node.get_filter(preserve_svg=image.is_svg()).spec, expected
)
class ImagesTestCase(TestCase):
maxDiff = None
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.engine = Engine(
app_dirs=True,
libraries=LIBRARIES,
builtins=[LIBRARIES["wagtailimages_tags"]],
)
@classmethod
def setUpTestData(cls):
# Create an image for running tests on
cls.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
cls.svg_image = Image.objects.create(
title="Test SVG image",
file=get_test_image_file_svg(),
)
cls.bad_image = get_test_bad_image()
cls.bad_image.save()
def render(self, string, context=None):
if context is None:
context = {}
template = self.engine.from_string(string)
return template.render(Context(context, autoescape=False))
class ImageTagTestCase(ImagesTestCase):
def test_image(self):
filename_200 = get_test_image_filename(self.image, "width-200")
rendered = self.render("{% image myimage width-200 %}", {"myimage": self.image})
self.assertHTMLEqual(
rendered,
f'<img alt="Test image" height="150" src="{filename_200}" width="200" />',
)
def test_none(self):
rendered = self.render("{% image myimage width-2 %}", {"myimage": None})
self.assertEqual(rendered, "")
def test_missing_image(self):
rendered = self.render(
"{% image myimage width-200 %}", {"myimage": self.bad_image}
)
self.assertHTMLEqual(
rendered,
'<img alt="missing image" src="/media/not-found" width="0" height="0">',
)
def test_not_an_image(self):
with self.assertRaisesMessage(
ValueError, "Image template tags expect an Image object, got 'not a pipe'"
):
self.render(
"{% image myimage width-200 %}",
{"myimage": "not a pipe"},
)
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in image tags may only"
):
self.render(
"{% image myimage fill-200×200 %}",
{"myimage": self.image},
)
def test_multiple_as_variable(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "More than one variable name after 'as'"
):
self.render(
"{% image myimage width-200 as a b %}",
{"myimage": self.image},
)
def test_missing_as_variable(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "Missing a variable name after 'as'"
):
self.render(
"{% image myimage width-200 as %}",
{"myimage": self.image},
)
def test_mixing_as_variable_and_attrs(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "Do not use attributes with 'as' context assignments"
):
self.render(
"{% image myimage width-200 alt='Test' as test %}",
{"myimage": self.image},
)
def test_missing_filter_spec(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "Image tags must be used with at least one filter spec"
):
self.render(
"{% image myimage %}",
{"myimage": self.image},
)
class SrcsetImageTagTestCase(ImagesTestCase):
def test_srcset_image(self):
filename_20 = get_test_image_filename(self.image, "width-20")
filename_40 = get_test_image_filename(self.image, "width-40")
rendered = self.render(
"{% srcset_image myimage width-{20,40} sizes='100vw' %}",
{"myimage": self.image},
)
expected = f"""
<img
sizes="100vw"
src="{filename_20}"
srcset="{filename_20} 20w, {filename_40} 40w"
alt="Test image"
width="20"
height="15"
>
"""
self.assertHTMLEqual(rendered, expected)
def test_srcset_output_single_image(self):
self.assertHTMLEqual(
self.render(
"{% srcset_image myimage width-20 %}",
{"myimage": self.image},
),
self.render(
"{% image myimage width-20 %}",
{"myimage": self.image},
),
)
def test_none(self):
rendered = self.render("{% srcset_image myimage width-2 %}", {"myimage": None})
self.assertEqual(rendered, "")
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in image tags may only contain"
):
self.render(
"{% srcset_image myimage fill-{200×200,400×400} sizes='100vw' %}",
{"myimage": self.image},
)
def test_srcset_image_assignment(self):
template = (
"{% srcset_image myimage width-{30,60} as bg %}"
"width: {{ bg.renditions.0.width }}, url: {{ bg.renditions.0.url }} "
"width: {{ bg.renditions.1.width }}, url: {{ bg.renditions.1.url }} "
)
rendered = self.render(template, {"myimage": self.image})
expected = f"""
width: 30, url: {get_test_image_filename(self.image, "width-30")}
width: 60, url: {get_test_image_filename(self.image, "width-60")}
"""
self.assertHTMLEqual(rendered, expected)
def test_srcset_image_assignment_render_as_is(self):
filename_35 = get_test_image_filename(self.image, "width-35")
filename_70 = get_test_image_filename(self.image, "width-70")
rendered = self.render(
"{% srcset_image myimage width-{35,70} as bg %}{{ bg }}",
{"myimage": self.image},
)
expected = f"""
<img
src="{filename_35}"
srcset="{filename_35} 35w, {filename_70} 70w"
alt="Test image"
width="35"
height="26"
>
"""
self.assertHTMLEqual(rendered, expected)
def test_missing_srcset_image(self):
rendered = self.render(
"{% srcset_image myimage width-{200,400} sizes='100vw' %}",
{"myimage": self.bad_image},
)
expected = """
<img
sizes="100vw"
src="/media/not-found"
srcset="/media/not-found 0w, /media/not-found 0w"
alt="missing image"
width="0"
height="0"
>
"""
self.assertHTMLEqual(rendered, expected)
class PictureTagTestCase(ImagesTestCase):
def test_picture_formats_multi_sizes(self):
filenames = [
get_test_image_filename(self.image, "width-200.format-jpeg"),
get_test_image_filename(self.image, "width-400.format-jpeg"),
get_test_image_filename(self.image, "width-200.format-webp"),
get_test_image_filename(self.image, "width-400.format-webp"),
get_test_image_filename(self.image, "width-200.format-gif"),
get_test_image_filename(self.image, "width-400.format-gif"),
]
rendered = self.render(
'{% picture myimage width-{200,400} format-{jpeg,webp,gif} sizes="100vw" %}',
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filenames[2]} 200w, {filenames[3]} 400w" sizes="100vw" type="image/webp">
<source srcset="{filenames[0]} 200w, {filenames[1]} 400w" sizes="100vw" type="image/jpeg">
<img
sizes="100vw"
src="{filenames[4]}"
srcset="{filenames[4]} 200w, {filenames[5]} 400w"
alt="Test image"
width="200"
height="150"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_formats_only(self):
filename_jpeg = get_test_image_filename(self.image, "format-jpeg")
filename_webp = get_test_image_filename(self.image, "format-webp")
rendered = self.render(
"{% picture myimage format-{jpeg,webp} %}",
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filename_webp}" type="image/webp">
<img
src="{filename_jpeg}"
alt="Test image"
width="640"
height="480"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_sizes_only(self):
rendered = self.render(
'{% picture myimage width-{350,450} sizes="100vw" %}',
{"myimage": self.image},
)
expected = self.render(
'<picture>{% srcset_image myimage width-{350,450} sizes="100vw" %}</picture>',
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_picture_single_format(self):
rendered = self.render(
"{% picture myimage format-jpeg %}",
{"myimage": self.image},
)
expected = self.render(
"<picture>{% image myimage format-jpeg %}</picture>",
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_none(self):
rendered = self.render("{% picture myimage width-2 %}", {"myimage": None})
self.assertEqual(rendered, "")
def test_picture_assignment(self):
template = (
"{% picture myimage width-{550,600} format-{jpeg,webp} as bg %}"
"width: {{ bg.formats.jpeg.0.width }}, url: {{ bg.formats.jpeg.0.url }} "
"width: {{ bg.formats.jpeg.1.width }}, url: {{ bg.formats.jpeg.1.url }} "
"width: {{ bg.formats.webp.0.width }}, url: {{ bg.formats.webp.0.url }} "
"width: {{ bg.formats.webp.1.width }}, url: {{ bg.formats.webp.1.url }} "
)
rendered = self.render(template, {"myimage": self.image})
expected = f"""
width: 550, url: {get_test_image_filename(self.image, "width-550.format-jpeg")}
width: 600, url: {get_test_image_filename(self.image, "width-600.format-jpeg")}
width: 550, url: {get_test_image_filename(self.image, "width-550.format-webp")}
width: 600, url: {get_test_image_filename(self.image, "width-600.format-webp")}
"""
self.assertHTMLEqual(rendered, expected)
def test_picture_assignment_render_as_is(self):
rendered = self.render(
"{% picture myimage width-{2000,4000} format-{jpeg,webp} as bg %}{{ bg }}",
{"myimage": self.image},
)
expected = self.render(
"{% picture myimage width-{2000,4000} format-{jpeg,webp} %}",
{"myimage": self.image},
)
self.assertHTMLEqual(rendered, expected)
def test_missing_picture(self):
rendered = self.render(
"{% picture myimage width-{200,400} %}",
{"myimage": self.bad_image},
)
expected = """
<picture>
<img
src="/media/not-found"
srcset="/media/not-found 0w, /media/not-found 0w"
alt="missing image"
width="0"
height="0"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)
def test_invalid_character(self):
with self.assertRaisesRegex(
TemplateSyntaxError, "filter specs in image tags may only"
):
self.render(
'{% picture myimage fill-{20×20,40×40} sizes="100vw" %}',
{"myimage": self.image},
)
def test_chaining_filterspecs(self):
filename_jpeg = get_test_image_filename(
self.image, "format-jpeg.jpegquality-40.webpquality-40"
)
filename_webp = get_test_image_filename(
self.image, "format-webp.jpegquality-40.webpquality-40"
)
rendered = self.render(
"{% picture myimage format-{jpeg,webp} jpegquality-40 webpquality-40 %}",
{"myimage": self.image},
)
expected = f"""
<picture>
<source srcset="{filename_webp}" type="image/webp">
<img
src="{filename_jpeg}"
alt="Test image"
width="640"
height="480"
>
</picture>
"""
self.assertHTMLEqual(rendered, expected)

View File

@@ -0,0 +1,60 @@
from unittest import TestCase
from wagtail.images.image_operations import ImageTransform
from wagtail.images.rect import Rect, Vector
class TestTransform(TestCase):
def test_resize(self):
context = ImageTransform((640, 480))
resized = context.resize((320, 240))
vector = Vector(100, 200)
transformed = resized.transform_vector(vector)
self.assertEqual(transformed, Vector(50, 100))
untransformed = resized.untransform_vector(transformed)
self.assertEqual(untransformed, vector)
def test_crop(self):
context = ImageTransform((640, 480))
cropped = context.crop(Rect(200, 100, 300, 200))
vector = Vector(250, 150)
transformed = cropped.transform_vector(vector)
self.assertEqual(transformed, Vector(50, 50))
untransformed = cropped.untransform_vector(transformed)
self.assertEqual(untransformed, vector)
def test_resize_then_crop(self):
context = ImageTransform((640, 480))
resized = context.resize((320, 240))
cropped = resized.crop(Rect(200, 100, 300, 200))
vector = Vector(500, 300)
transformed = cropped.transform_vector(vector)
self.assertEqual(transformed, Vector(50, 50))
untransformed = cropped.untransform_vector(transformed)
self.assertEqual(untransformed, vector)
def test_crop_then_resize(self):
context = ImageTransform((640, 480))
cropped = context.crop(Rect(200, 100, 300, 200))
resized = cropped.resize((50, 50))
vector = Vector(250, 150)
transformed = resized.transform_vector(vector)
self.assertEqual(transformed, Vector(25, 25))
untransformed = resized.untransform_vector(transformed)
self.assertEqual(untransformed, vector)

View File

@@ -0,0 +1,958 @@
import os
import unittest
from io import BytesIO
import willow
from django import forms, template
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
from django.test import TestCase, override_settings
from django.test.signals import setting_changed
from django.urls import reverse
from taggit.forms import TagField, TagWidget
from willow.image import (
AvifImageFile,
PNGImageFile,
SvgImageFile,
)
from willow.image import ImageFile as WillowImageFile
from wagtail.images import get_image_model, get_image_model_string
from wagtail.images.fields import WagtailImageField
from wagtail.images.formats import Format, get_image_format, register_image_format
from wagtail.images.forms import get_image_form
from wagtail.images.models import Image as WagtailImage
from wagtail.images.permissions import update_permission_policy
from wagtail.images.rect import Rect, Vector
from wagtail.images.utils import generate_signature, verify_signature
from wagtail.images.views.serve import ServeView
from wagtail.test.testapp.models import CustomImage, CustomImageFilePath
from wagtail.test.utils import WagtailTestUtils, disconnect_signal_receiver
from wagtail.utils.deprecation import RemovedInWagtail70Warning
from .utils import (
Image,
get_test_image_file,
get_test_image_file_avif,
get_test_image_file_svg,
)
try:
import sendfile # noqa: F401
sendfile_mod = True
except ImportError:
sendfile_mod = False
class TestImageTag(TestCase):
def setUp(self):
# Create an image for running tests on
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def render_image_tag(self, image, filter_spec):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_obj " + filter_spec + "%}"
)
context = template.Context({"image_obj": image})
return temp.render(context)
def test_image_tag(self):
result = self.render_image_tag(self.image, "width-400")
# Check that all the required HTML attributes are set
self.assertIn('width="400"', result)
self.assertIn('height="300"', result)
self.assertIn('alt="Test image"', result)
def test_image_tag_none(self):
result = self.render_image_tag(None, "width-500")
self.assertEqual(result, "")
def test_image_tag_wrong_type(self):
with self.assertRaises(ValueError):
self.render_image_tag("foobar", "width-500")
def render_image_tag_as(self, image, filter_spec):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_obj "
+ filter_spec
+ " as test_img %}<img {{ test_img.attrs }} />"
)
context = template.Context({"image_obj": image})
return temp.render(context)
def test_image_tag_attrs(self):
result = self.render_image_tag_as(self.image, "width-400")
# Check that all the required HTML attributes are set
self.assertIn('width="400"', result)
self.assertIn('height="300"', result)
self.assertIn('alt="Test image"', result)
def render_image_tag_with_extra_attributes(self, image, title):
temp = template.Template(
'{% load wagtailimages_tags %}{% image image_obj width-400 \
class="photo" title=title|lower alt="Alternate" %}'
)
context = template.Context({"image_obj": image, "title": title})
return temp.render(context)
def test_image_tag_with_extra_attributes(self):
result = self.render_image_tag_with_extra_attributes(
self.image, "My Wonderful Title"
)
# Check that all the required HTML attributes are set
self.assertIn('width="400"', result)
self.assertIn('height="300"', result)
self.assertIn('class="photo"', result)
self.assertIn('alt="Alternate"', result)
self.assertIn('title="my wonderful title"', result)
def render_image_tag_with_filters(self, image):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_primary|default:image_alternate width-400 %}"
)
context = template.Context({"image_primary": None, "image_alternate": image})
return temp.render(context)
def test_image_tag_with_filters(self):
result = self.render_image_tag_with_filters(self.image)
self.assertIn('width="400"', result)
self.assertIn('height="300"', result)
def test_image_tag_with_chained_filters(self):
result = self.render_image_tag(self.image, "fill-200x200 height-150")
self.assertIn('width="150"', result)
self.assertIn('height="150"', result)
def test_filter_specs_must_match_allowed_pattern(self):
with self.assertRaises(template.TemplateSyntaxError):
self.render_image_tag(self.image, "fill-200x200|height-150")
with self.assertRaises(template.TemplateSyntaxError):
self.render_image_tag(self.image, 'fill-800x600 alt"test"')
def test_context_may_only_contain_one_argument(self):
with self.assertRaises(template.TemplateSyntaxError):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_obj fill-200x200"
" as test_img this_one_should_not_be_there %}<img {{ test_img.attrs }} />"
)
context = template.Context({"image_obj": self.image})
temp.render(context)
def test_no_image_filter_provided(self):
# if image template gets the image but no filters
with self.assertRaises(template.TemplateSyntaxError):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_obj %}"
)
context = template.Context({"image_obj": self.image})
temp.render(context)
def test_no_image_filter_provided_when_using_as(self):
# if image template gets the image but no filters
with self.assertRaises(template.TemplateSyntaxError):
temp = template.Template(
"{% load wagtailimages_tags %}{% image image_obj as foo %}"
)
context = template.Context({"image_obj": self.image})
temp.render(context)
def test_no_image_filter_provided_but_attributes_provided(self):
# if image template gets the image but no filters
with self.assertRaises(template.TemplateSyntaxError):
temp = template.Template(
'{% load wagtailimages_tags %}{% image image_obj class="cover-image"%}'
)
context = template.Context({"image_obj": self.image})
temp.render(context)
def render_image_url_tag(self, image, view_name):
temp = template.Template(
'{% load wagtailimages_tags %}{% image_url image_obj "width-400" "'
+ view_name
+ '" %}'
)
context = template.Context({"image_obj": image})
return temp.render(context)
def test_image_url(self):
result = self.render_image_url_tag(self.image, "wagtailimages_serve")
self.assertRegex(
result,
"/images/.*/width-400/{}".format(self.image.file.name.split("/")[-1]),
)
def test_image_url_custom_view(self):
result = self.render_image_url_tag(
self.image, "wagtailimages_serve_custom_view"
)
self.assertRegex(
result,
"/testimages/custom_view/.*/width-400/{}".format(
self.image.file.name.split("/")[-1]
),
)
def test_image_url_no_imageserve_view_added(self):
# if image_url tag is used, but the image serve view was not defined.
with self.assertRaises(ImproperlyConfigured):
temp = template.Template(
'{% load wagtailimages_tags %}{% image_url image_obj "width-400" "mynonexistingimageserve_view" %}'
)
context = template.Context({"image_obj": self.image})
temp.render(context)
class TestMissingImage(TestCase):
"""
Missing image files in media/original_images should be handled gracefully, to cope with
pulling live databases to a development instance without copying the corresponding image files.
In this case, it's acceptable to render broken images, but not to fail rendering the page outright.
"""
fixtures = ["test.json"]
def test_image_tag_with_missing_image(self):
# the page /events/christmas/ has a missing image as the feed image
response = self.client.get("/events/christmas/")
self.assertContains(
response,
'<img src="/media/not-found" width="0" height="0" alt="A missing image" \
class="feed-image">',
html=True,
)
def test_rich_text_with_missing_image(self):
# the page /events/final-event/ has a missing image in the rich text body
response = self.client.get("/events/final-event/")
self.assertContains(
response,
'<img class="richtext-image full-width" src="/media/not-found" \
width="0" height="0" alt="where did my image go?">',
html=True,
)
class TestFormat(WagtailTestUtils, TestCase):
def setUp(self):
# test format
self.format = Format("test name", "test label", "test is-primary", "original")
# test image
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def test_editor_attributes(self):
result = self.format.editor_attributes(self.image, "test alt text")
self.assertEqual(
result,
{
"data-alt": "test alt text",
"data-embedtype": "image",
"data-format": "test name",
"data-id": self.image.pk,
},
)
def test_image_to_editor_html(self):
result = self.format.image_to_editor_html(self.image, "test alt text")
self.assertTagInHTML(
'<img data-embedtype="image" data-id="%d" data-format="test name" '
'data-alt="test alt text" class="test is-primary" '
'width="640" height="480" alt="test alt text" >' % self.image.pk,
result,
allow_extra_attrs=True,
)
def test_image_to_editor_html_with_quoting(self):
result = self.format.image_to_editor_html(
self.image, 'Arthur "two sheds" Jackson'
)
expected_html = (
'<img data-embedtype="image" data-id="%d" data-format="test name" '
'data-alt="Arthur &quot;two sheds&quot; Jackson" class="test is-primary" '
'width="640" height="480" alt="Arthur &quot;two sheds&quot; Jackson" >'
% self.image.pk
)
self.assertTagInHTML(expected_html, result, allow_extra_attrs=True)
def test_image_to_html_no_classnames(self):
self.format.classname = None
result = self.format.image_to_html(self.image, "test alt text")
self.assertTagInHTML(
'<img width="640" height="480" alt="test alt text">',
result,
allow_extra_attrs=True,
)
self.format.classname = (
"test is-primary" # reset to original value for other tests
)
def test_image_to_html_with_quoting(self):
result = self.format.image_to_html(self.image, 'Arthur "two sheds" Jackson')
self.assertTagInHTML(
'<img class="test is-primary" width="640" height="480" '
'alt="Arthur &quot;two sheds&quot; Jackson">',
result,
allow_extra_attrs=True,
)
def test_get_image_format(self):
register_image_format(self.format)
result = get_image_format("test name")
self.assertEqual(result, self.format)
def test_deprecated_classnames_property_access(self):
with self.assertWarns(RemovedInWagtail70Warning):
classname = self.format.classnames
self.assertEqual(classname, "test is-primary")
class TestSignatureGeneration(TestCase):
def test_signature_generation(self):
self.assertEqual(
generate_signature(100, "fill-800x600"), "xnZOzQyUg6pkfciqcfRJRosOrGg="
)
def test_signature_verification(self):
self.assertTrue(
verify_signature("xnZOzQyUg6pkfciqcfRJRosOrGg=", 100, "fill-800x600")
)
def test_signature_changes_on_image_id(self):
self.assertFalse(
verify_signature("xnZOzQyUg6pkfciqcfRJRosOrGg=", 200, "fill-800x600")
)
def test_signature_changes_on_filter_spec(self):
self.assertFalse(
verify_signature("xnZOzQyUg6pkfciqcfRJRosOrGg=", 100, "fill-800x700")
)
class TestFrontendServeView(TestCase):
def setUp(self):
# Create an image for running tests on
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def test_get(self):
"""
Test a valid GET request to the view
"""
# Generate signature
signature = generate_signature(self.image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve", args=(signature, self.image.id, "fill-800x600")
)
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTrue(response.streaming)
self.assertEqual(response["Content-Type"], "image/png")
# Ensure the file can actually be read
image = willow.Image.open(b"".join(response.streaming_content))
self.assertIsInstance(image, PNGImageFile)
def test_get_svg(self):
image = Image.objects.create(title="Test SVG", file=get_test_image_file_svg())
# Generate signature
signature = generate_signature(image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse("wagtailimages_serve", args=(signature, image.id, "fill-800x600"))
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTrue(response.streaming)
self.assertEqual(response["Content-Type"], "image/svg+xml")
# Ensure the file can actually be read
image = willow.Image.open(BytesIO(b"".join(response.streaming_content)))
self.assertIsInstance(image, SvgImageFile)
@override_settings(WAGTAILIMAGES_FORMAT_CONVERSIONS={"avif": "avif"})
def test_get_avif(self):
image = Image.objects.create(title="Test AVIF", file=get_test_image_file_avif())
# Generate signature
signature = generate_signature(image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse("wagtailimages_serve", args=(signature, image.id, "fill-800x600"))
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTrue(response.streaming)
self.assertEqual(response["Content-Type"], "image/avif")
# Ensure the file can actually be read
image = willow.Image.open(b"".join(response.streaming_content))
self.assertIsInstance(image, AvifImageFile)
def test_get_with_extra_component(self):
"""
Test that a filename can be optionally added to the end of the URL.
"""
# Generate signature
signature = generate_signature(self.image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve", args=(signature, self.image.id, "fill-800x600")
)
+ "test.png"
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertTrue(response.streaming)
self.assertEqual(response["Content-Type"], "image/png")
# Ensure the file can actually be read
image = willow.Image.open(b"".join(response.streaming_content))
self.assertIsInstance(image, PNGImageFile)
def test_get_with_too_many_extra_components(self):
"""
A filename can be appended to the end of the URL, but it must not contain a '/'
"""
# Generate signature
signature = generate_signature(self.image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve", args=(signature, self.image.id, "fill-800x600")
)
+ "test/test.png"
)
# URL pattern should not match
self.assertEqual(response.status_code, 404)
def test_get_with_serve_action(self):
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_serve_action_serve",
args=(signature, self.image.id, "fill-800x600"),
)
)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.streaming)
self.assertEqual(response["Content-Type"], "image/png")
# Ensure the file can actually be read
image = willow.Image.open(b"".join(response.streaming_content))
self.assertIsInstance(image, PNGImageFile)
def test_get_with_redirect_action(self):
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_serve_action_redirect",
args=(signature, self.image.id, "fill-800x600"),
)
)
expected_redirect_url = (
"/media/images/{filename[0]}.2e16d0ba.fill-800x600{filename[1]}".format(
filename=os.path.splitext(os.path.basename(self.image.file.path))
)
)
self.assertRedirects(
response,
expected_redirect_url,
status_code=302,
fetch_redirect_response=False,
)
def test_init_with_unknown_action_raises_error(self):
with self.assertRaises(ImproperlyConfigured):
ServeView.as_view(action="unknown")
def test_get_with_custom_key(self):
"""
Test that the key can be changed on the view
"""
# Generate signature
signature = generate_signature(self.image.id, "fill-800x600", key="custom")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve_custom_key",
args=(signature, self.image.id, "fill-800x600"),
)
+ "test.png"
)
# Check response
self.assertEqual(response.status_code, 200)
# Ensure the file can actually be read
image = willow.Image.open(b"".join(response.streaming_content))
self.assertIsInstance(image, PNGImageFile)
def test_get_with_custom_key_using_default_key(self):
"""
Test that the key can be changed on the view
This tests that the default key no longer works when the key is changed on the view
"""
# Generate signature
signature = generate_signature(self.image.id, "fill-800x600")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve_custom_key",
args=(signature, self.image.id, "fill-800x600"),
)
+ "test.png"
)
# Check response
self.assertEqual(response.status_code, 403)
def test_get_invalid_signature(self):
"""
Test that an invalid signature returns a 403 response
"""
# Generate a signature for the incorrect image id
signature = generate_signature(self.image.id + 1, "fill-800x600")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve", args=(signature, self.image.id, "fill-800x600")
)
)
# Check response
self.assertEqual(response.status_code, 403)
def test_get_invalid_filter_spec(self):
"""
Test that an invalid filter spec returns a 400 response
This is very unlikely to happen in reality. A user would have
to create signature for the invalid filter spec which can't be
done with Wagtails built in URL generator. We should test it
anyway though.
"""
# Generate a signature with the invalid filterspec
signature = generate_signature(self.image.id, "bad-filter-spec")
# Get the image
response = self.client.get(
reverse(
"wagtailimages_serve",
args=(signature, self.image.id, "bad-filter-spec"),
)
)
# Check response
self.assertEqual(response.status_code, 400)
def test_get_missing_source_image_file(self):
"""
Test that a missing image file gives a 410 response
When the source image file is missing, it is presumed deleted so we
return a 410 "Gone" response.
"""
# Delete the image file
os.remove(self.image.file.path)
# Get the image
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_serve", args=(signature, self.image.id, "fill-800x600")
)
)
# Check response
self.assertEqual(response.status_code, 410)
def test_get_cache_control(self):
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_serve_action_serve",
args=(signature, self.image.id, "fill-800x600"),
)
)
self.assertEqual(response["Cache-Control"], "max-age=3600, public")
class TestFrontendSendfileView(TestCase):
def setUp(self):
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
@override_settings(SENDFILE_BACKEND="sendfile.backends.development")
@unittest.skipIf(not sendfile_mod, "Missing django-sendfile app.")
def test_sendfile_nobackend(self):
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_sendfile",
args=(signature, self.image.id, "fill-800x600"),
)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "image/png")
@override_settings(SENDFILE_BACKEND="sendfile.backends.development")
def test_sendfile_dummy_backend(self):
signature = generate_signature(self.image.id, "fill-800x600")
response = self.client.get(
reverse(
"wagtailimages_sendfile_dummy",
args=(signature, self.image.id, "fill-800x600"),
)
)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.content, msg="Dummy backend response")
class TestRect(TestCase):
def test_init(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect.left, 100)
self.assertEqual(rect.top, 150)
self.assertEqual(rect.right, 200)
self.assertEqual(rect.bottom, 250)
def test_equality(self):
self.assertEqual(Rect(100, 150, 200, 250), Rect(100, 150, 200, 250))
self.assertNotEqual(Rect(100, 150, 200, 250), Rect(10, 15, 20, 25))
def test_getitem(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect[0], 100)
self.assertEqual(rect[1], 150)
self.assertEqual(rect[2], 200)
self.assertEqual(rect[3], 250)
self.assertRaises(IndexError, rect.__getitem__, 4)
def test_as_tuple(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(rect.as_tuple(), (100, 150, 200, 250))
def test_size(self):
rect = Rect(100, 150, 200, 350)
self.assertIsInstance(rect.size, Vector)
self.assertEqual(rect.size, (100, 200))
self.assertEqual(rect.width, 100)
self.assertEqual(rect.height, 200)
def test_set_size_with_tuple(self):
rect = Rect(100, 150, 200, 350)
rect.size = (200, 400)
self.assertEqual(rect, (50, 50, 250, 450))
def test_set_size_with_vector(self):
rect = Rect(100, 150, 200, 350)
rect.size = Vector(200, 400)
self.assertEqual(rect, (50, 50, 250, 450))
def test_centroid(self):
rect = Rect(100, 150, 200, 350)
self.assertIsInstance(rect.centroid, Vector)
self.assertEqual(rect.centroid, (150, 250))
self.assertEqual(rect.x, 150)
self.assertEqual(rect.y, 250)
self.assertEqual(rect.centroid_x, 150)
self.assertEqual(rect.centroid_y, 250)
def test_set_centroid_with_tuple(self):
rect = Rect(100, 150, 200, 350)
rect.centroid = (500, 500)
self.assertEqual(rect, (450, 400, 550, 600))
def test_set_centroid_with_vector(self):
rect = Rect(100, 150, 200, 350)
rect.centroid = Vector(500, 500)
self.assertEqual(rect, (450, 400, 550, 600))
def test_repr(self):
rect = Rect(100, 150, 200, 250)
self.assertEqual(
repr(rect), "Rect(left: 100, top: 150, right: 200, bottom: 250)"
)
def test_from_point(self):
rect = Rect.from_point(100, 200, 50, 20)
self.assertEqual(rect, Rect(75, 190, 125, 210))
class TestGetImageForm(WagtailTestUtils, TestCase):
def test_fields(self):
form = get_image_form(Image)
self.assertEqual(
list(form.base_fields.keys()),
[
"title",
"file",
"collection",
"tags",
"focal_point_x",
"focal_point_y",
"focal_point_width",
"focal_point_height",
],
)
def test_admin_form_fields_attribute(self):
form = get_image_form(CustomImage)
self.assertEqual(
list(form.base_fields.keys()),
[
"title",
"file",
"collection",
"tags",
"focal_point_x",
"focal_point_y",
"focal_point_width",
"focal_point_height",
"caption",
"fancy_caption",
],
)
def test_file_field(self):
form = get_image_form(WagtailImage)
self.assertIsInstance(form.base_fields["file"], WagtailImageField)
self.assertIsInstance(form.base_fields["file"].widget, forms.FileInput)
def test_tags_field(self):
form = get_image_form(WagtailImage)
self.assertIsInstance(form.base_fields["tags"], TagField)
self.assertIsInstance(form.base_fields["tags"].widget, TagWidget)
def test_focal_point_fields(self):
form = get_image_form(WagtailImage)
self.assertIsInstance(form.base_fields["focal_point_x"], forms.IntegerField)
self.assertIsInstance(form.base_fields["focal_point_y"], forms.IntegerField)
self.assertIsInstance(form.base_fields["focal_point_width"], forms.IntegerField)
self.assertIsInstance(
form.base_fields["focal_point_height"], forms.IntegerField
)
self.assertIsInstance(
form.base_fields["focal_point_x"].widget, forms.HiddenInput
)
self.assertIsInstance(
form.base_fields["focal_point_y"].widget, forms.HiddenInput
)
self.assertIsInstance(
form.base_fields["focal_point_width"].widget, forms.HiddenInput
)
self.assertIsInstance(
form.base_fields["focal_point_height"].widget, forms.HiddenInput
)
class TestRenditionFilenames(TestCase):
# Can't create image in setUp as we need a unique filename for each test.
# This stops Django appending some rubbish to the filename which makes
# the assertions difficult.
def test_normal_filter(self):
image = Image.objects.create(
title="Test image",
file=get_test_image_file(filename="test_rf1.png"),
)
rendition = image.get_rendition("width-100")
self.assertEqual(rendition.file.name, "images/test_rf1.width-100.png")
def test_fill_filter(self):
image = Image.objects.create(
title="Test image",
file=get_test_image_file(filename="test_rf2.png"),
)
rendition = image.get_rendition("fill-100x100")
self.assertEqual(
rendition.file.name, "images/test_rf2.2e16d0ba.fill-100x100.png"
)
def test_fill_filter_with_focal_point(self):
image = Image.objects.create(
title="Test image",
file=get_test_image_file(filename="test_rf3.png"),
)
image.set_focal_point(Rect(100, 100, 200, 200))
image.save()
rendition = image.get_rendition("fill-100x100")
self.assertEqual(
rendition.file.name, "images/test_rf3.15ee4958.fill-100x100.png"
)
def test_filter_with_pipe_gets_dotted(self):
image = Image.objects.create(
title="Test image",
file=get_test_image_file(filename="test_rf4.png"),
)
image.set_focal_point(Rect(100, 100, 200, 200))
image.save()
rendition = image.get_rendition("fill-200x200|height-150")
self.assertEqual(
rendition.file.name, "images/test_rf4.15ee4958.fill-200x200.height-150.png"
)
class TestDifferentUpload(TestCase):
def test_upload_path(self):
image = CustomImageFilePath.objects.create(
title="Test image",
file=get_test_image_file(),
)
second_image = CustomImageFilePath.objects.create(
title="Test Image",
file=get_test_image_file(colour="black"),
)
# The files should be uploaded based on it's content, not just
# it's filename
self.assertNotEqual(image.file.url, second_image.file.url)
class TestGetImageModel(WagtailTestUtils, TestCase):
@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.CustomImage")
def test_custom_get_image_model(self):
"""Test get_image_model with a custom image model"""
self.assertIs(get_image_model(), CustomImage)
@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.CustomImage")
def test_custom_get_image_model_string(self):
"""Test get_image_model_string with a custom image model"""
self.assertEqual(get_image_model_string(), "tests.CustomImage")
@override_settings()
def test_standard_get_image_model(self):
"""Test get_image_model with no WAGTAILIMAGES_IMAGE_MODEL"""
del settings.WAGTAILIMAGES_IMAGE_MODEL
from wagtail.images.models import Image
self.assertIs(get_image_model(), Image)
@override_settings()
def test_standard_get_image_model_string(self):
"""Test get_image_model_STRING with no WAGTAILIMAGES_IMAGE_MODEL"""
del settings.WAGTAILIMAGES_IMAGE_MODEL
self.assertEqual(get_image_model_string(), "wagtailimages.Image")
@disconnect_signal_receiver(
signal=setting_changed, receiver=update_permission_policy
)
@override_settings(WAGTAILIMAGES_IMAGE_MODEL="tests.UnknownModel")
def test_unknown_get_image_model(self):
"""Test get_image_model with an unknown model"""
with self.assertRaises(ImproperlyConfigured):
get_image_model()
@disconnect_signal_receiver(
signal=setting_changed, receiver=update_permission_policy
)
@override_settings(WAGTAILIMAGES_IMAGE_MODEL="invalid-string")
def test_invalid_get_image_model(self):
"""Test get_image_model with an invalid model string"""
with self.assertRaises(ImproperlyConfigured):
get_image_model()
class TestWagtailImageField(TestCase):
@classmethod
def setUpTestData(cls):
cls.filename = "wagtailimagefield.png"
cls.image = get_test_image_file(filename=cls.filename).file
cls.image_size = cls.image.getbuffer().nbytes
def test_to_python_with_inmemoryfile(self):
f = WagtailImageField()
self.image.seek(0)
file = InMemoryUploadedFile(
self.image, "", self.filename, "image/png", self.image_size, None
)
to_python = f.to_python(file)
self.assertIsInstance(to_python.image, WillowImageFile)
self.assertEqual(to_python.content_type, "image/png")
def test_to_python_gets_content_type_from_willow(self):
f = WagtailImageField()
self.image.seek(0)
file = InMemoryUploadedFile(
self.image, "", self.filename, "image/jpeg", self.image_size, None
)
to_python = f.to_python(file)
self.assertIsInstance(to_python.image, WillowImageFile)
self.assertEqual(to_python.content_type, "image/png")
def test_to_python_with_temporary_file(self):
f = WagtailImageField()
with TemporaryUploadedFile(
"test_temp.png", "image/png", self.image_size, None
) as tmp_file:
self.image.seek(0)
tmp_file.write(self.image.read())
tmp_file.seek(0)
to_python = f.to_python(tmp_file)
self.assertIsInstance(to_python.image, WillowImageFile)
self.assertEqual(to_python.content_type, "image/png")
def test_to_python_raises_error_with_invalid_image_file(self):
msg = (
"Upload a valid image. The file you uploaded was either not an "
"image or a corrupted image."
)
f = WagtailImageField()
with TemporaryUploadedFile("test_temp.png", "image/png", 32, None) as tmp_file:
with self.assertRaisesMessage(ValidationError, msg):
f.to_python(tmp_file)
with self.assertRaisesMessage(
ValidationError,
"No file was submitted. Check the encoding type on the form.",
):
f.to_python(self.image)

View File

@@ -0,0 +1,38 @@
from django.urls import re_path
from wagtail.images.views.serve import SendFileView, ServeView
from wagtail.test import dummy_sendfile_backend
urlpatterns = [
# Format: signature, image_id, filter_spec, filename=None
re_path(
r"^actions/serve/(.*)/(\d*)/(.*)/[^/]*",
ServeView.as_view(action="serve"),
name="wagtailimages_serve_action_serve",
),
re_path(
r"^actions/redirect/(.*)/(\d*)/(.*)/[^/]*",
ServeView.as_view(action="redirect"),
name="wagtailimages_serve_action_redirect",
),
re_path(
r"^custom_key/(.*)/(\d*)/(.*)/[^/]*",
ServeView.as_view(key="custom"),
name="wagtailimages_serve_custom_key",
),
re_path(
r"^custom_view/([^/]*)/(\d*)/([^/]*)/[^/]*$",
ServeView.as_view(),
name="wagtailimages_serve_custom_view",
),
re_path(
r"^sendfile/(.*)/(\d*)/(.*)/[^/]*",
SendFileView.as_view(),
name="wagtailimages_sendfile",
),
re_path(
r"^sendfile-dummy/(.*)/(\d*)/(.*)/[^/]*",
SendFileView.as_view(backend=dummy_sendfile_backend.sendfile),
name="wagtailimages_sendfile_dummy",
),
]

View File

@@ -0,0 +1,93 @@
import os
from io import BytesIO
import PIL.Image
from django.conf import settings
from django.core import serializers
from django.core.files.images import ImageFile
from wagtail.images import get_image_model
Image = get_image_model()
def get_test_image_filename(image, filterspec):
"""
Get the generated filename for a resized image
"""
name, ext = os.path.splitext(os.path.basename(image.file.name))
# Use the correct extension if the filterspec is a format operation.
if "format-" in filterspec:
ext = "." + filterspec.split("format-")[1].split("-")[0].split(".")[0].replace(
"jpeg", "jpg"
)
return f"{settings.MEDIA_URL}images/{name}.{filterspec}{ext}"
def get_test_image_file(filename="test.png", colour="white", size=(640, 480)):
f = BytesIO()
image = PIL.Image.new("RGBA", size, colour)
image.save(f, "PNG")
return ImageFile(f, name=filename)
def get_test_image_file_avif(filename="test.png", colour="white", size=(640, 480)):
f = BytesIO()
image = PIL.Image.new("RGBA", size, colour)
image.save(f, "AVIF")
return ImageFile(f, name=filename)
def get_test_image_file_jpeg(filename="test.jpg", colour="white", size=(640, 480)):
f = BytesIO()
image = PIL.Image.new("RGB", size, colour)
image.save(f, "JPEG")
return ImageFile(f, name=filename)
def get_test_image_file_webp(filename="test.webp", colour="white", size=(640, 480)):
f = BytesIO()
image = PIL.Image.new("RGB", size, colour)
image.save(f, "WEBP")
return ImageFile(f, name=filename)
def get_test_image_file_tiff(filename="test.tiff", colour="white", size=(640, 480)):
f = BytesIO()
image = PIL.Image.new("RGB", size, colour)
image.save(f, "TIFF")
return ImageFile(f, name=filename)
def get_test_image_file_svg(
filename="test.svg", width=100, height=100, view_box="0 0 100 100"
):
img = f"""
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="{view_box}">
</svg>
"""
f = BytesIO(img.strip().encode("utf-8"))
return ImageFile(f, filename)
def get_test_bad_image():
# Create an image with a missing file, by deserializing from a python object
# (which bypasses FileField's attempt to read the file)
return list(
serializers.deserialize(
"python",
[
{
"fields": {
"title": "missing image",
"height": 100,
"file": "original_images/missing-image.jpg",
"width": 100,
},
"model": "wagtailimages.image",
}
],
)
)[0].object