From 10ee97b9028f172003f12b7d5a6fbb42b0af3f83 Mon Sep 17 00:00:00 2001 From: Rafiq Hilali Date: Thu, 7 Feb 2019 16:27:18 +0000 Subject: [PATCH 1/5] cached the sizes of avatars that have been added to the cache, and then used these cached sizes to clear the cache in invalidate_cache --- avatar/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/avatar/utils.py b/avatar/utils.py index 38db5b3..9978cfc 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -65,6 +65,11 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): if result is None: result = func(user, size or default_size, **kwargs) cache_set(key, result) + # add image size to set of cached sizes so we can invalidate them later + sizes_key = get_cache_key(user, '', prefix='cached_sizes') + sizes = cache.get(sizes_key, set()) + sizes.add(size) + cache_set(sizes_key, sizes) return result return cached_func return decorator @@ -74,7 +79,8 @@ def invalidate_cache(user, size=None): """ Function to be called when saving or changing an user's avatars. """ - sizes = set(settings.AVATAR_AUTO_GENERATE_SIZES) + sizes_key = get_cache_key(user, '', prefix='cached_sizes') + sizes = cache.get(sizes_key, set()) if size is not None: sizes.add(size) for prefix in cached_funcs: From 0e66ae5de79f592bee691e84a95cfe4f8fc2dd1c Mon Sep 17 00:00:00 2001 From: Rafiq Hilali Date: Fri, 8 Feb 2019 10:03:23 +0000 Subject: [PATCH 2/5] simplified chained comparison --- avatar/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/avatar/forms.py b/avatar/forms.py index c52b42a..04d07c4 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -49,8 +49,7 @@ class UploadAvatarForm(forms.Form): }) count = Avatar.objects.filter(user=self.user).count() - if (settings.AVATAR_MAX_AVATARS_PER_USER > 1 and - count >= settings.AVATAR_MAX_AVATARS_PER_USER): + if 1 < settings.AVATAR_MAX_AVATARS_PER_USER <= count: error = _("You already have %(nb_avatars)d avatars, " "and the maximum allowed is %(nb_max_avatars)d.") raise forms.ValidationError(error % { From 942e08080bf568ca815b4af7753b1956928efbab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 08:32:03 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- avatar/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avatar/utils.py b/avatar/utils.py index 652dca1..5446453 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -68,7 +68,7 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): result = func(user, width or default_size, height, **kwargs) cache_set(key, result) # add image size to set of cached sizes so we can invalidate them later - sizes_key = get_cache_key(user, '', prefix='cached_sizes') + sizes_key = get_cache_key(user, "", prefix="cached_sizes") sizes = cache.get(sizes_key, set()) sizes.add((width or default_size, height or width)) cache_set(sizes_key, sizes) @@ -83,7 +83,7 @@ def invalidate_cache(user, width=None, height=None): """ Function to be called when saving or changing a user's avatars. """ - sizes_key = get_cache_key(user, '', prefix='cached_sizes') + sizes_key = get_cache_key(user, "", prefix="cached_sizes") sizes = cache.get(sizes_key, set()) if width is not None: sizes.add((width, height or width)) From 72c1cae3455cb42ad41846a1d39a333a849724cd Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Mon, 15 Aug 2022 10:44:04 +0200 Subject: [PATCH 4/5] Make get_cache_key work when there is no width/height --- avatar/utils.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/avatar/utils.py b/avatar/utils.py index 5446453..4c7b5d5 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -28,13 +28,17 @@ def get_user(userdescriptor): return User.objects.get_by_natural_key(userdescriptor) -def get_cache_key(user_or_username, width, height, prefix): +def get_cache_key(user_or_username, prefix, width=False, height=False): """ Returns a cache key consisten of a username and image size. """ if isinstance(user_or_username, get_user_model()): user_or_username = get_username(user_or_username) - key = "%s_%s_%s_%s" % (prefix, user_or_username, width, height or width) + key = f"{prefix}_{user_or_username}" + if width: + key += f"_{width}" + if height or width: + key += f"x{height or width}" return "%s_%s" % ( slugify(key)[:100], hashlib.md5(force_bytes(key)).hexdigest(), @@ -62,13 +66,13 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): def cached_func(user, width=None, height=None, **kwargs): prefix = func.__name__ cached_funcs.add(prefix) - key = get_cache_key(user, width or default_size, height, prefix=prefix) + key = get_cache_key(user, prefix, width or default_size, height) result = cache.get(key) if result is None: result = func(user, width or default_size, height, **kwargs) cache_set(key, result) # add image size to set of cached sizes so we can invalidate them later - sizes_key = get_cache_key(user, "", prefix="cached_sizes") + sizes_key = get_cache_key(user, "cached_sizes") sizes = cache.get(sizes_key, set()) sizes.add((width or default_size, height or width)) cache_set(sizes_key, sizes) @@ -83,17 +87,17 @@ def invalidate_cache(user, width=None, height=None): """ Function to be called when saving or changing a user's avatars. """ - sizes_key = get_cache_key(user, "", prefix="cached_sizes") + sizes_key = get_cache_key(user, "cached_sizes") sizes = cache.get(sizes_key, set()) if width is not None: sizes.add((width, height or width)) for prefix in cached_funcs: for size in sizes: if isinstance(size, int): - cache.delete(get_cache_key(user, size, size, prefix)) + cache.delete(get_cache_key(user, prefix, size)) else: # Size is specified with height and width. - cache.delete(get_cache_key(user, size[0], size[1], prefix)) + cache.delete(get_cache_key(user, prefix, size[0], size[1])) def get_default_avatar_url(): From 28302c52053573a84b7fcbe59eafffac3be161b3 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Mon, 15 Aug 2022 11:34:35 +0200 Subject: [PATCH 5/5] Add invalidate_cache test --- CHANGELOG.rst | 1 + avatar/models.py | 1 + avatar/utils.py | 5 +++-- tests/tests.py | 35 +++++++++++++++++++++++++++-------- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 22902f0..fc0ad3a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog * Unreleased * Allowed for rectangular avatars. Custom avatar tag templates now require the specification of both a ``width`` and ``height`` attribute instead of ``size``. * Made ``True`` the default value of ``AVATAR_CLEANUP_DELETED``. (Set to ``False`` to obtain previous behavior). + * Fix invalidate_cache for on-the-fly created thumbnails * 6.0.1 (August 12, 2022) * Exclude tests folder from distribution. diff --git a/avatar/models.py b/avatar/models.py index 8a625f1..a3e7413 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -188,6 +188,7 @@ class Avatar(models.Model): thumb = self.avatar.storage.save( self.avatar_name(width, height), thumb_file ) + invalidate_cache(self.user, width, height) def avatar_url(self, width, height=None): return self.avatar.storage.url(self.avatar_name(width, height)) diff --git a/avatar/utils.py b/avatar/utils.py index 4c7b5d5..51776b8 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -28,7 +28,7 @@ def get_user(userdescriptor): return User.objects.get_by_natural_key(userdescriptor) -def get_cache_key(user_or_username, prefix, width=False, height=False): +def get_cache_key(user_or_username, prefix, width=None, height=None): """ Returns a cache key consisten of a username and image size. """ @@ -74,7 +74,7 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): # add image size to set of cached sizes so we can invalidate them later sizes_key = get_cache_key(user, "cached_sizes") sizes = cache.get(sizes_key, set()) - sizes.add((width or default_size, height or width)) + sizes.add((width or default_size, height or width or default_size)) cache_set(sizes_key, sizes) return result @@ -98,6 +98,7 @@ def invalidate_cache(user, width=None, height=None): else: # Size is specified with height and width. cache.delete(get_cache_key(user, prefix, size[0], size[1])) + cache.set(sizes_key, set()) def get_default_avatar_url(): diff --git a/tests/tests.py b/tests/tests.py index a99fb75..cce39ac 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -5,6 +5,7 @@ from shutil import rmtree from django.contrib.admin.sites import AdminSite from django.core import management +from django.core.cache import cache from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse @@ -15,7 +16,12 @@ from avatar.conf import settings from avatar.models import Avatar from avatar.signals import avatar_deleted from avatar.templatetags import avatar_tags -from avatar.utils import get_primary_avatar, get_user_model +from avatar.utils import ( + get_cache_key, + get_primary_avatar, + get_user_model, + invalidate_cache, +) class AssertSignal: @@ -465,10 +471,23 @@ class AvatarTests(TestCase): self.assertMediaFileExists(avatar_80_url) self.assertNotEqual(avatar_80_mtime, self.get_media_file_mtime(avatar_80_url)) - # def testAvatarOrder - # def testReplaceAvatarWhenMaxIsOne - # def testHashFileName - # def testHashUserName - # def testChangePrimaryAvatar - # def testDeleteThumbnailAndRecreation - # def testAutomaticThumbnailCreation + def test_invalidate_cache(self): + upload_helper(self, "test.png") + sizes_key = get_cache_key(self.user, "cached_sizes") + sizes = cache.get(sizes_key, set()) + # Only default 80x80 thumbnail is cached + self.assertEqual(len(sizes), 1) + # Invalidate cache + invalidate_cache(self.user) + sizes = cache.get(sizes_key, set()) + # No thumbnail is cached. + self.assertEqual(len(sizes), 0) + # Create a custom 25x25 thumbnail and check that it is cached + avatar_tags.avatar(self.user, 25) + sizes = cache.get(sizes_key, set()) + self.assertEqual(len(sizes), 1) + # Invalidate cache again. + invalidate_cache(self.user) + sizes = cache.get(sizes_key, set()) + # It should now be empty again + self.assertEqual(len(sizes), 0)