Quellcode durchsuchen

Add docs and releases notes for `prefetch_renditions` method

Also add tests to ensure example in docs works fine.
Tidiane Dia vor 2 Jahren
Ursprung
Commit
25c43273a9
4 geänderte Dateien mit 108 neuen und 11 gelöschten Zeilen
  1. 1 0
      CHANGELOG.txt
  2. 46 11
      docs/advanced_topics/images/renditions.md
  3. 5 0
      docs/releases/4.0.md
  4. 56 0
      wagtail/images/tests/test_models.py

+ 1 - 0
CHANGELOG.txt

@@ -24,6 +24,7 @@ Changelog
  * Implement redesign of the Workflow Status dialog, fixing accessibility issues (Steven Steinwand)
  * Add the ability to change the number of images displayed per page in the image library (Tidiane Dia, with sponsorship from YouGov)
  * Allow users to sort by different fields in the image library (Tidiane Dia, with sponsorship from YouGov)
+ * Add `prefetch_renditions` method to `ImageQueryset` for performance optimisation on image listings (Tidiane Dia, Karl Hobley)
  * Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer)
  * Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke)
  * Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand)

+ 46 - 11
docs/advanced_topics/images/renditions.md

@@ -38,11 +38,45 @@ See also: [](image_tag)
 
 ## Prefetching image renditions
 
-```{versionadded} 3.0
-This following guidance is only applicable in Wagtail versions 3.0 and above.
+When using a queryset to render a list of images or objects with images, you can prefetch the renditions needed with a single additional query. For long lists of items, or where multiple renditions are used for each item, this can provide a significant boost to performance.
+
+```{versionadded} 4.0
+The `prefetch_renditions` method is only applicable in Wagtail versions 4.0 and above.
+```
+
+### Image QuerySets
+
+When working with an Image QuerySet, you can make use of Wagtail's built-in ``prefetch_renditions`` queryset method to prefetch the renditions needed.
+
+For example, say you were rendering a list of all the images uploaded by a certain user:
+
+```python
+def get_images_uploaded_by_user(user):
+    return ImageModel.objects.filter(uploaded_by_user=user)
+```
+
+The above can be modified slightly to prefetch the renditions of the images returned:
+
+```python
+def get_images_uploaded_by_user(user)::
+    return ImageModel.objects.filter(uploaded_by_user=user).prefetch_renditions()
 ```
 
-When using a queryset to render a list of objects with images, you can make use of Django's built-in `prefetch_related()` queryset method to prefetch the renditions needed for rendering with a single additional query. For long lists of items, or where multiple renditions are used for each item, this can provide a significant boost to performance.
+The above will prefetch all renditions even if we may not need them.
+
+If images in your project tend to have very large numbers of renditions, and you know in advance the ones you need, you might want to consider specifying a set of filters to the ``prefetch_renditions`` method and only select the renditions you need for rendering. For example:
+
+```python
+def get_images_uploaded_by_user(user):
+    # Only specify the renditions required for rendering
+    return ImageModel.objects.filter(uploaded_by_user=user).prefetch_renditions(
+        "fill-700x586", "min-600x400", "max-940x680"
+    )
+```
+
+### Non Image Querysets
+
+If you're working with a non Image Model, you can make use of Django's built-in `prefetch_related()` queryset method to prefetch renditions.
 
 For example, say you were rendering a list of events (with thumbnail images for each). Your code might look something like this:
 
@@ -58,7 +92,8 @@ def get_events():
     return EventPage.objects.live().select_related("listing_image").prefetch_related("listing_image__renditions")
 ```
 
-If images in your project tend to have very large numbers of renditions, and you know in advance the ones you need, you might want to consider using a `Prefetch` object to select only the renditions you need for rendering. For example:
+
+If you know in advance the renditions you'll need, you can filter the renditions queryset to use:
 
 ```python
 from django.db.models import Prefetch
@@ -66,15 +101,15 @@ from wagtail.images import get_image_model
 
 
 def get_events():
-    # These are the renditions required for rendering
-    renditions_queryset = get_image_model().get_rendition_model().objects.filter(
-        filter_spec__in=["fill-300x186", "fill-600x400", "fill-940x680"]
-    )
-
+    Image = get_image_model()
+    filters = ["fill-300x186", "fill-600x400", "fill-940x680"]
+    
     # `Prefetch` is used to fetch only the required renditions
-    return EventPage.objects.live().select_related("listing_image").prefetch_related(
-        Prefetch("listing_image__renditions", queryset=renditions_queryset)
+    prefetch_images_and_renditions = Prefetch(
+        "listing_image",
+        queryset=Image.objects.prefetch_renditions(*filters)
     )
+    return EventPage.objects.live().prefetch_related(prefetch_images_and_renditions)
 ```
 
 (image_rendition_methods)=

+ 5 - 0
docs/releases/4.0.md

@@ -9,6 +9,10 @@ depth: 1
 
 ## What's new
 
+### Image renditions can now be prefetched by filter
+
+When using a queryset to render a list of images, you can now use the ``prefetch_renditions()`` queryset method to prefetch the renditions needed for rendering with a single extra query, similar to ``prefetch_related``. If you have many renditions per image, you can also call it with filters as arguments - ``prefetch_renditions("fill-700x586", "min-600x400")`` - to fetch only the renditions you intend on using for a smaller query. For long lists of images, this can provide a significant boost to performance. See [](prefetching_image_renditions) for more examples. This feature was developed by Tidiane Dia and Karl Hobley.
+
 ### Other features
 
  * Add clarity to confirmation when being asked to convert an external link to an internal one (Thijs Kramer)
@@ -29,6 +33,7 @@ depth: 1
  * Implement redesign of the Workflow Status dialog, fixing accessibility issues (Steven Steinwand)
  * Add the ability to change the number of images displayed per page in the image library (Tidiane Dia, with sponsorship from YouGov)
  * Allow users to sort by different fields in the image library (Tidiane Dia, with sponsorship from YouGov)
+ * Add `prefetch_renditions` method to `ImageQueryset` for performance optimisation on image listings (Tidiane Dia, Karl Hobley)
 
 ### Bug fixes
 

+ 56 - 0
wagtail/images/tests/test_models.py

@@ -5,6 +5,7 @@ from django.core.cache import caches
 from django.core.files import File
 from django.core.files.storage import DefaultStorage, Storage
 from django.core.files.uploadedfile import SimpleUploadedFile
+from django.db.models import Prefetch
 from django.db.utils import IntegrityError
 from django.test import TestCase
 from django.test.utils import override_settings
@@ -465,6 +466,61 @@ class TestRenditions(TestCase):
         settings = bkp
 
 
+class TestPrefetchRenditions(TestCase):
+    fixtures = ["test.json"]
+
+    def setUp(self):
+        self.images = []
+        self.event_pages_pks = []
+
+        event_pages = EventPage.objects.all()[:3]
+        for i, page in enumerate(event_pages):
+            page.feed_image = image = Image.objects.create(
+                title="Test image {i}",
+                file=get_test_image_file(),
+            )
+            page.save(update_fields=["feed_image"])
+            self.images.append(image)
+            self.event_pages_pks.append(page.pk)
+
+        # Generate renditions
+        self.small_renditions = [
+            image.get_rendition("max-100x100") for image in self.images
+        ]
+        self.large_renditions = [
+            image.get_rendition("min-300x600") for image in self.images
+        ]
+
+    def test_prefetch_renditions_on_non_image_querysets(self):
+        prefetch_images_and_small_renditions = Prefetch(
+            "feed_image", queryset=Image.objects.prefetch_renditions("max-100x100")
+        )
+        with self.assertNumQueries(3):
+            # One query to get the `EventPage`s, another one to fetch the feed images
+            # and a last one to select matching renditions.
+            pages = list(
+                EventPage.objects.prefetch_related(
+                    prefetch_images_and_small_renditions
+                ).filter(pk__in=self.event_pages_pks)
+            )
+
+        with self.assertNumQueries(0):
+            # No additional query since small renditions were prefetched.
+            small_renditions = [
+                page.feed_image.get_rendition("max-100x100") for page in pages
+            ]
+
+        self.assertListEqual(self.small_renditions, small_renditions)
+
+        with self.assertNumQueries(3):
+            # Additional queries since large renditions weren't prefetched.
+            large_renditions = [
+                page.feed_image.get_rendition("min-300x600") for page in pages
+            ]
+
+        self.assertListEqual(self.large_renditions, large_renditions)
+
+
 class TestUsageCount(TestCase):
     fixtures = ["test.json"]