diff --git a/filer/admin/folderadmin.py b/filer/admin/folderadmin.py index 1bc486417..75e33975f 100644 --- a/filer/admin/folderadmin.py +++ b/filer/admin/folderadmin.py @@ -376,8 +376,8 @@ def directory_listing(self, request, folder_id=None, viewtype=None): .order_by("-modified") ) file_qs = file_qs.annotate( - thumbnail_name=Subquery(thumbnail_qs.filter(name__contains=f"__{size}_").values_list("name")[:1]), - thumbnailx2_name=Subquery(thumbnail_qs.filter(name__contains=f"__{size_x2}_").values_list("name")[:1]) + thumbnail_name=Subquery(thumbnail_qs.filter(name__contains=f".{size}_").values_list("name")[:1]), + thumbnailx2_name=Subquery(thumbnail_qs.filter(name__contains=f".{size_x2}_").values_list("name")[:1]) ).select_related("owner") try: diff --git a/filer/utils/filer_easy_thumbnails.py b/filer/utils/filer_easy_thumbnails.py index 00b2f4b43..ac3bb93d8 100644 --- a/filer/utils/filer_easy_thumbnails.py +++ b/filer/utils/filer_easy_thumbnails.py @@ -1,14 +1,15 @@ import os import re +from contextlib import contextmanager from easy_thumbnails.files import Thumbnailer +from easy_thumbnails.namers import default - -# match the source filename using `__` as the seperator. ``opts_and_ext`` is non -# greedy so it should match the last occurence of `__`. -# in ``ThumbnailerNameMixin.get_thumbnail_name`` we ensure that there is no `__` -# in the opts part. -RE_ORIGINAL_FILENAME = re.compile(r"^(?P.*)__(?P.*?)$") +# easy-thumbnails default pattern +# e.g: source.jpg.100x100_q80_crop_upscale.jpg +RE_ORIGINAL_FILENAME = re.compile( + r"^(?P.*?)\.(?P[^.]+\.[^.]+)$" +) def thumbnail_to_original_filename(thumbnail_name): @@ -18,59 +19,51 @@ def thumbnail_to_original_filename(thumbnail_name): return None +@contextmanager +def use_default_namer(thumbnailer): + """ + Context manager to use the default easy-thumbnails namer for private files. + """ + original_namer = thumbnailer.thumbnail_namer + thumbnailer.thumbnail_namer = default + try: + yield + finally: + thumbnailer.thumbnail_namer = original_namer + + class ThumbnailerNameMixin: - thumbnail_basedir = '' - thumbnail_subdir = '' - thumbnail_prefix = '' + thumbnail_basedir = "" + thumbnail_subdir = "" + thumbnail_prefix = "" def get_thumbnail_name(self, thumbnail_options, transparent=False): """ - A version of ``Thumbnailer.get_thumbnail_name`` that produces a - reproducible thumbnail name that can be converted back to the original - filename. + Get thumbnail name using easy-thumbnails pattern. + For public files: Uses configurable naming via THUMBNAIL_NAMER + For private files: Uses easy-thumbnails default naming pattern regardless of THUMBNAIL_NAMER """ - path, source_filename = os.path.split(self.name) - source_extension = os.path.splitext(source_filename)[1][1:].lower() - preserve_extensions = self.thumbnail_preserve_extensions - if preserve_extensions is True or source_extension == 'svg' or \ - isinstance(preserve_extensions, (list, tuple)) and source_extension in preserve_extensions: - extension = source_extension - elif transparent: - extension = self.thumbnail_transparency_extension - else: - extension = self.thumbnail_extension - extension = extension or 'jpg' - - thumbnail_options = thumbnail_options.copy() - size = tuple(thumbnail_options.pop('size')) - initial_opts = ['{}x{}'.format(*size)] - quality = thumbnail_options.pop('quality', self.thumbnail_quality) - if extension == 'jpg': - initial_opts.append(f'q{quality}') - elif extension == 'svg': - thumbnail_options.pop('subsampling', None) - thumbnail_options.pop('upscale', None) - - opts = list(thumbnail_options.items()) - opts.sort() # Sort the options so the file name is consistent. - opts = ['{}'.format(v is not True and f'{k}-{v}' or k) - for k, v in opts if v] - all_opts = '_'.join(initial_opts + opts) + is_public = False + if hasattr(self, "thumbnail_storage"): + is_public = "PrivateFileSystemStorage" not in str( + self.thumbnail_storage.__class__ + ) - basedir = self.thumbnail_basedir - subdir = self.thumbnail_subdir - - # make sure our magic delimiter is not used in all_opts - all_opts = all_opts.replace('__', '_') - filename = f'{source_filename}__{all_opts}.{extension}' + if is_public: + return super(ThumbnailerNameMixin, self).get_thumbnail_name( + thumbnail_options, transparent + ) - return os.path.join(basedir, path, subdir, filename) + with use_default_namer(self): + return super(ThumbnailerNameMixin, self).get_thumbnail_name( + thumbnail_options, transparent + ) class ActionThumbnailerMixin: - thumbnail_basedir = '' - thumbnail_subdir = '' - thumbnail_prefix = '' + thumbnail_basedir = "" + thumbnail_subdir = "" + thumbnail_prefix = "" def get_thumbnail_name(self, thumbnail_options, transparent=False): """ @@ -90,7 +83,7 @@ def thumbnail_exists(self, thumbnail_name): class FilerThumbnailer(ThumbnailerNameMixin, Thumbnailer): def __init__(self, *args, **kwargs): - self.thumbnail_basedir = kwargs.pop('thumbnail_basedir', '') + self.thumbnail_basedir = kwargs.pop("thumbnail_basedir", "") super().__init__(*args, **kwargs) diff --git a/tests/test_models.py b/tests/test_models.py index 3d52e2502..f9466a5d5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -87,7 +87,7 @@ def test_create_icons(self): self.assertEqual(len(icons), len(filer_settings.FILER_ADMIN_ICON_SIZES)) for size in filer_settings.FILER_ADMIN_ICON_SIZES: self.assertEqual(os.path.basename(icons[size]), - file_basename + '__{}x{}_q85_crop_subsampling-2_upscale.jpg'.format(size, size)) + file_basename + '.{}x{}_q85_crop_upscale.jpg'.format(size, size)) def test_access_icons_property(self): """Test IconsMixin that calls static on a non-existent file""" diff --git a/tests/test_thumbnails.py b/tests/test_thumbnails.py new file mode 100644 index 000000000..b758d4ff7 --- /dev/null +++ b/tests/test_thumbnails.py @@ -0,0 +1,71 @@ +import os + +from django.conf import settings +from django.core.files import File as DjangoFile +from django.test import TestCase, override_settings + +from filer.models.filemodels import File +from filer.settings import FILER_IMAGE_MODEL +from filer.utils.loader import load_model +from tests.helpers import create_image, create_superuser + +Image = load_model(FILER_IMAGE_MODEL) + + +def custom_namer(thumbnailer, **kwargs): + path, filename = os.path.split(thumbnailer.name) + return os.path.join(path, f"custom_prefix_{filename}") + + +class ThumbnailNameTests(TestCase): + def setUp(self): + self.superuser = create_superuser() + self.img = create_image() + self.image_name = "test_file.jpg" + self.filename = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, self.image_name) + self.img.save(self.filename, "JPEG") + + def tearDown(self): + os.remove(self.filename) + for f in File.objects.all(): + f.delete() + + def create_filer_image(self, is_public=True): + with open(self.filename, "rb") as f: + file_obj = DjangoFile(f) + image = Image.objects.create( + owner=self.superuser, + original_filename=self.image_name, + file=file_obj, + is_public=is_public, + ) + return image + + def test_thumbnailer_class_for_public_files(self): + image = self.create_filer_image(is_public=True) + thumbnailer = image.easy_thumbnails_thumbnailer + name = thumbnailer.get_thumbnail_name({"size": (100, 100)}) + self.assertRegex(name, r"^.*\..*\.[^.]+$") + + def test_thumbnailer_class_for_private_files(self): + image = self.create_filer_image(is_public=False) + thumbnailer = image.easy_thumbnails_thumbnailer + name = thumbnailer.get_thumbnail_name({"size": (100, 100)}) + self.assertRegex(name, r"^.*\..*\.[^.]+$") + + @override_settings(THUMBNAIL_NAMER="tests.test_thumbnails.custom_namer") + def test_thumbnail_custom_namer(self): + image = self.create_filer_image(is_public=True) + thumbnailer = image.easy_thumbnails_thumbnailer + name = thumbnailer.get_thumbnail_name({"size": (100, 100)}) + filename = os.path.basename(name) + self.assertTrue(filename.startswith("custom_prefix_")) + + @override_settings(THUMBNAIL_NAMER="tests.test_thumbnails.custom_namer") + def test_private_thumbnail_ignores_custom_namer(self): + image = self.create_filer_image(is_public=False) + thumbnailer = image.easy_thumbnails_thumbnailer + name = thumbnailer.get_thumbnail_name({"size": (100, 100)}) + filename = os.path.basename(name) + self.assertFalse(filename.startswith("custom_prefix_")) + self.assertRegex(name, r"^.*\..*\.[^.]+$")