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 2522081..51776b8 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=None, height=None): """ 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,11 +66,16 @@ 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, "cached_sizes") + sizes = cache.get(sizes_key, set()) + sizes.add((width or default_size, height or width or default_size)) + cache_set(sizes_key, sizes) return result return cached_func @@ -78,16 +87,18 @@ def invalidate_cache(user, width=None, height=None): """ Function to be called when saving or changing a user's avatars. """ - sizes = set(settings.AVATAR_AUTO_GENERATE_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])) + 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)