kopia lustrzana https://github.com/wagtail/wagtail
Implement get_renditions(), find_existing_renditions() and create_renditions() to mirror get_rendition(), find_existing_rendition() and create_rendition()
rodzic
3e23ae32e0
commit
feb6aea70d
|
@ -2,10 +2,11 @@ import hashlib
|
|||
import logging
|
||||
import os.path
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, defaultdict
|
||||
from contextlib import contextmanager
|
||||
from tempfile import SpooledTemporaryFile
|
||||
from typing import Union
|
||||
from io import BytesIO
|
||||
from typing import Dict, Iterable, List, Union
|
||||
|
||||
import willow
|
||||
from django.apps import apps
|
||||
|
@ -13,8 +14,10 @@ from django.conf import settings
|
|||
from django.core import checks
|
||||
from django.core.cache import InvalidCacheBackendError, caches
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.forms.utils import flatatt
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -451,10 +454,10 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
|
|||
self._add_to_prefetched_renditions(rendition)
|
||||
|
||||
if self.renditions_cache is not None:
|
||||
key = Rendition.construct_cache_key(
|
||||
cache_key = Rendition.construct_cache_key(
|
||||
self.id, filter.get_cache_key(self), filter.spec
|
||||
)
|
||||
self.renditions_cache.set(key, rendition)
|
||||
self.renditions_cache.set(cache_key, rendition)
|
||||
|
||||
return rendition
|
||||
|
||||
|
@ -520,6 +523,207 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
|
|||
)
|
||||
return rendition
|
||||
|
||||
def get_renditions(self, *filter_specs: str) -> Dict[str, "AbstractRendition"]:
|
||||
"""
|
||||
Returns a ``dict`` of ``Rendition`` instances with image files reflecting
|
||||
the supplied ``filter_specs``, keyed by the relevant ``filter_spec`` string.
|
||||
|
||||
Note: If using custom image models, instances of the custom rendition
|
||||
model will be returned.
|
||||
"""
|
||||
Rendition = self.get_rendition_model()
|
||||
filters = tuple(Filter(spec) for spec in set(filter_specs))
|
||||
|
||||
# Find existing renditions where possible
|
||||
renditions = self.find_existing_renditions(*filters)
|
||||
|
||||
# Create any renditions not found in prefetched values, cache or database
|
||||
not_found = tuple(f for f in filters if f not in renditions)
|
||||
if not_found:
|
||||
if len(not_found) == 1:
|
||||
# create_rendition() is better for creating single items, as it
|
||||
# can use QuerySet.get_or_create(), which has better handling
|
||||
# of race conditions
|
||||
filter = not_found[0]
|
||||
rendition = self.create_rendition(filter)
|
||||
self._add_to_prefetched_renditions(rendition)
|
||||
renditions[filter] = rendition
|
||||
else:
|
||||
# For multiple, create_renditions() is more performant
|
||||
new_renditions = self.create_renditions(*not_found)
|
||||
for filter, rendition in new_renditions.items():
|
||||
self._add_to_prefetched_renditions(rendition)
|
||||
renditions[filter] = rendition
|
||||
|
||||
# If rendition caching is enabled, update the cache
|
||||
if self.renditions_cache is not None:
|
||||
cache_additions = {
|
||||
Rendition.construct_cache_key(
|
||||
self.id, filter.get_cache_key(self), filter.spec
|
||||
): rendition
|
||||
for filter, rendition in renditions.items()
|
||||
# prevent writing of cached data back to the cache
|
||||
if not getattr(rendition, "_from_cache", False)
|
||||
}
|
||||
if cache_additions:
|
||||
self.renditions_cache.set_many(cache_additions)
|
||||
|
||||
# Return a dict in the expected format
|
||||
return {filter.spec: rendition for filter, rendition in renditions.items()}
|
||||
|
||||
def find_existing_renditions(
|
||||
self, *filters: "Filter"
|
||||
) -> Dict["Filter", "AbstractRendition"]:
|
||||
"""
|
||||
Returns a dictionary of existing ``Rendition`` instances with ``file``
|
||||
values (images) reflecting the supplied ``filters`` and the focal point
|
||||
values from this object.
|
||||
|
||||
Filters for which an existing rendition cannot be found are ommitted
|
||||
from the return value. If none of the requested renditions have been
|
||||
created before, the return value will be an empty dict.
|
||||
"""
|
||||
Rendition = self.get_rendition_model()
|
||||
filters_by_spec: Dict[str, Filter] = {f.spec: f for f in filters}
|
||||
found: Dict[Filter, AbstractRendition] = {}
|
||||
|
||||
# Interrogate prefetched values first (where available)
|
||||
prefetched_renditions = self._get_prefetched_renditions()
|
||||
if prefetched_renditions is not None:
|
||||
# NOTE: When renditions are prefetched, it's assumed that if the
|
||||
# requested renditions exist, they will be present in the
|
||||
# prefetched value, and further cache/database lookups are avoided.
|
||||
|
||||
# group renditions by the filters of interest
|
||||
potential_matches: Dict[Filter, List[AbstractRendition]] = defaultdict(list)
|
||||
for rendition in prefetched_renditions:
|
||||
try:
|
||||
filter = filters_by_spec[rendition.filter_spec]
|
||||
except KeyError:
|
||||
continue # this rendition can be ignored
|
||||
else:
|
||||
potential_matches[filter].append(rendition)
|
||||
|
||||
# For each filter we have rendtions for, look for one with a
|
||||
# 'focal_point_key' value matching filter.get_cache_key()
|
||||
for filter, renditions in potential_matches.items():
|
||||
focal_point_key = filter.get_cache_key(self)
|
||||
for rendition in renditions:
|
||||
if rendition.focal_point_key == focal_point_key:
|
||||
# to prevent writing of cached data back to the cache
|
||||
rendition._from_cache = True
|
||||
# use this rendition
|
||||
found[filter] = rendition
|
||||
# skip to the next filter
|
||||
break
|
||||
else:
|
||||
# Renditions are not prefetched, so attempt to find suitable
|
||||
# items in the cache or database
|
||||
|
||||
# Query the cache first (if enabled)
|
||||
if self.renditions_cache is not None:
|
||||
cache_keys = [
|
||||
Rendition.construct_cache_key(
|
||||
self.id, filter.get_cache_key(self), spec
|
||||
)
|
||||
for spec, filter in filters_by_spec.items()
|
||||
]
|
||||
for rendition in self.renditions_cache.get_many(cache_keys).values():
|
||||
filter = filters_by_spec[rendition.filter_spec]
|
||||
found[filter] = rendition
|
||||
|
||||
# For items not found in the cache, look in the database
|
||||
not_found = tuple(f for f in filters if f not in found)
|
||||
if not_found:
|
||||
lookup_q = Q()
|
||||
for filter in not_found:
|
||||
lookup_q |= Q(
|
||||
filter_spec=filter.spec,
|
||||
focal_point_key=filter.get_cache_key(self),
|
||||
)
|
||||
for rendition in self.renditions.filter(lookup_q):
|
||||
filter = filters_by_spec[rendition.filter_spec]
|
||||
found[filter] = rendition
|
||||
return found
|
||||
|
||||
def create_renditions(
|
||||
self, *filters: "Filter"
|
||||
) -> Dict["Filter", "AbstractRendition"]:
|
||||
"""
|
||||
Creates multiple ``Rendition`` instances with image files reflecting the supplied
|
||||
``filters``, and returns them as a ``dict`` keyed by the relevant ``Filter`` instance.
|
||||
Where suitable renditions already exist in the database, they will be returned instead,
|
||||
so as not to create duplicates.
|
||||
|
||||
This method is usually called by ``Image.get_renditions()``, after first
|
||||
checking that a suitable rendition does not already exist.
|
||||
|
||||
Note: If using custom image models, an instance of the custom rendition
|
||||
model will be returned.
|
||||
"""
|
||||
Rendition = self.get_rendition_model()
|
||||
|
||||
created: Dict[Filter, AbstractRendition] = {}
|
||||
filter_map: Dict[str, Filter] = {f.spec: f for f in filters}
|
||||
|
||||
with self.open_file() as file:
|
||||
in_memory_file = ContentFile(file.read(), name=self.file.name)
|
||||
|
||||
to_create = []
|
||||
for filter in filters:
|
||||
image_file = self.generate_rendition_file(filter, source=in_memory_file)
|
||||
# Reset in-memory file for next use
|
||||
in_memory_file.seek(0)
|
||||
# Add for bulk creation
|
||||
to_create.append(
|
||||
Rendition(
|
||||
image=self,
|
||||
filter_spec=filter.spec,
|
||||
focal_point_key=filter.get_cache_key(self),
|
||||
file=image_file,
|
||||
)
|
||||
)
|
||||
|
||||
# Rendition generation can take a while. So, if other processes have created
|
||||
# identical renditions in the meantime, we should find them to avoid clashes.
|
||||
# NB: Clashes can still occur, because there is no get_or_create() equivalent
|
||||
# for multiple objects. However, this will reduce that risk considerably.
|
||||
files_for_deletion: List[File] = []
|
||||
|
||||
# Assemble Q() to identify potential clashes
|
||||
lookup_q = Q()
|
||||
for rendition in to_create:
|
||||
lookup_q |= Q(
|
||||
filter_spec=rendition.filter_spec,
|
||||
focal_point_key=rendition.focal_point_key,
|
||||
)
|
||||
|
||||
for existing in self.renditions.filter(lookup_q):
|
||||
# Include the existing rendition in the return value
|
||||
filter = filter_map[existing.filter_spec]
|
||||
created[filter] = existing
|
||||
|
||||
for new in tuple(to_create):
|
||||
if (
|
||||
new.filter_spec == existing.filter_spec
|
||||
and new.focal_point_key == existing.focal_point_key
|
||||
):
|
||||
# Avoid creating the new version
|
||||
to_create.remove(new)
|
||||
# Mark for deletion later, so as not to hold up creation
|
||||
files_for_deletion.append(new.file)
|
||||
|
||||
for rendition in Rendition.objects.bulk_create(
|
||||
to_create, ignore_conflicts=True
|
||||
):
|
||||
created[filter_map[rendition.filter_spec]] = rendition
|
||||
|
||||
# Delete redundant rendition image files
|
||||
for file in files_for_deletion:
|
||||
file.delete(save=False)
|
||||
|
||||
return created
|
||||
|
||||
def generate_rendition_file(self, filter: "Filter", *, source: File = None) -> File:
|
||||
"""
|
||||
Generates an in-memory image matching the supplied ``filter`` value
|
||||
|
|
Ładowanie…
Reference in New Issue