Просмотр исходного кода

hash user avatars for cache versioning

Rafał Pitoń 10 лет назад
Родитель
Сommit
0741a5e7f8

+ 2 - 2
misago/users/avatars/__init__.py

@@ -23,5 +23,5 @@ def set_default_avatar(user):
             dynamic.set_avatar(user)
 
 
-def delete_avatar(user):
-    store.delete_avatar(user)
+get_avatar_hash = store.get_avatar_hash
+delete_avatar = store.delete_avatar

+ 17 - 0
misago/users/avatars/store.py

@@ -15,6 +15,23 @@ def normalize_image(image):
     return image.copy().convert('RGBA')
 
 
+def get_avatar_hash(user):
+    avatars_dir = get_existing_avatars_dir(user)
+
+    avatar_file = '%s_%s.png' % (user.pk, max(settings.MISAGO_AVATARS_SIZES))
+    avatar_file = Path(os.path.join(avatars_dir, avatar_file))
+
+    md5_hash = md5()
+
+    with open(avatar_file, 'rb') as f:
+        while True:
+            data = f.read(128)
+            if not data:
+                break
+            md5_hash.update(data)
+    return md5_hash.hexdigest()[:8]
+
+
 def store_avatar(user, image):
     avatars_dir = get_existing_avatars_dir(user)
 

+ 1 - 0
misago/users/migrations/0001_initial.py

@@ -40,6 +40,7 @@ class Migration(migrations.Migration):
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
                 ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
                 ('is_avatar_locked', models.BooleanField(default=False)),
+                ('avatar_hash', models.CharField(max_length=8, null=True, blank=True)),
                 ('avatar_crop', models.CharField(max_length=255, null=True, blank=True)),
                 ('avatar_lock_user_message', models.TextField(null=True, blank=True)),
                 ('avatar_lock_staff_message', models.TextField(null=True, blank=True)),

+ 6 - 2
misago/users/models/user.py

@@ -117,13 +117,16 @@ class UserManager(BaseUserManager):
 
             if set_default_avatar:
                 avatars.set_default_avatar(user)
+                user.avatar_hash = avatars.get_avatar_hash(user)
+            else:
+                user.avatar_hash = 'abcdef01'
 
             authenticated_role = Role.objects.get(special_role='authenticated')
             if authenticated_role not in user.roles.all():
                 user.roles.add(authenticated_role)
-
             user.update_acl_key()
-            user.save(update_fields=['acl_key'])
+
+            user.save(update_fields=['avatar_hash', 'acl_key'])
 
             return user
 
@@ -193,6 +196,7 @@ class User(AbstractBaseUser, PermissionsMixin):
     acl_key = models.CharField(max_length=12, null=True, blank=True)
 
     is_avatar_locked = models.BooleanField(default=False)
+    avatar_hash = models.CharField(max_length=8)
     avatar_crop = models.CharField(max_length=255, null=True, blank=True)
     avatar_lock_user_message = models.TextField(null=True, blank=True)
     avatar_lock_staff_message = models.TextField(null=True, blank=True)

+ 5 - 7
misago/users/templatetags/misago_avatars.py

@@ -7,13 +7,11 @@ register = template.Library()
 
 @register.filter(name='avatar')
 def avatar(user, size=200):
-    try:
-        user_pk = user.pk
-    except:
-        user_pk = user
-
-    return reverse('misago:user_avatar',
-                   kwargs={'user_id': user_pk, 'size': size})
+    return reverse('misago:user_avatar', kwargs={
+        'user_id': user.id,
+        'hash': user.avatar_hash,
+        'size': size
+    })
 
 
 @register.simple_tag

+ 8 - 0
misago/users/tests/test_avatars.py

@@ -40,8 +40,16 @@ class AvatarsStoreTests(TestCase):
             self.assertTrue(avatar.exists())
             self.assertTrue(avatar.isfile())
 
+        # Compute avatar hash
+        test_user.avatar_hash = store.get_avatar_hash(test_user)
+        self.assertEqual(len(test_user.avatar_hash), 8)
+        test_user.save(update_fields=['avatar_hash'])
+
         # Delete avatar
         store.delete_avatar(test_user)
+        for size in settings.MISAGO_AVATARS_SIZES:
+            avatar = Path('%s/%s_%s.png' % (avatar_dir, test_user.pk, size))
+            self.assertFalse(avatar.exists())
 
 
 class AvatarSetterTests(TestCase):

+ 2 - 2
misago/users/urls/__init__.py

@@ -78,13 +78,13 @@ urlpatterns += patterns('',
         url(r'^ban/$', 'ban_user', name='ban_user'),
         url(r'^lift-ban/$', 'lift_user_ban', name='lift_user_ban'),
         url(r'^delete/$', 'delete', name='delete_user'),
-    ))),
+    )))
 )
 
 
 urlpatterns += patterns('',
     url(r'^user-avatar/', include(patterns('misago.users.views.avatarserver',
-        url(r'^(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar', name="user_avatar"),
+        url(r'^(?P<hash>[a-f0-9]+)/(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar', name="user_avatar"),
         url(r'^tmp:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', name="user_avatar_tmp", kwargs={'type': 'tmp'}),
         url(r'^org:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', name="user_avatar_org", kwargs={'type': 'org'}),
         url(r'^(?P<size>\d+)\.png$', 'serve_blank_avatar', name="blank_avatar"),

+ 5 - 1
misago/users/views/avatarserver.py

@@ -4,6 +4,7 @@ from path import Path
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
+from django.views.decorators.cache import cache_control, never_cache
 
 from misago.core.fileserver import make_file_response
 
@@ -11,6 +12,7 @@ from misago.users.avatars import store
 from misago.users.avatars.uploaded import avatar_source_token
 
 
+@cache_control(private=True, must_revalidate=True, max_age=5 * 24 * 3600)
 def serve_blank_avatar(request, size):
     size = clean_size(size)
     avatar_dir = store.get_avatars_dir_path()
@@ -19,7 +21,8 @@ def serve_blank_avatar(request, size):
     return make_file_response(avatar_path, 'image/png')
 
 
-def serve_user_avatar(request, user_id, size):
+@cache_control(private=True, must_revalidate=False)
+def serve_user_avatar(request, hash, user_id, size):
     size = clean_size(size)
 
     if int(user_id) > 0:
@@ -36,6 +39,7 @@ def serve_user_avatar(request, user_id, size):
         return serve_blank_avatar(request, size)
 
 
+@never_cache
 def serve_user_avatar_source(request, user_id, token, type):
     fallback_avatar = get_blank_avatar_file(min(settings.MISAGO_AVATARS_SIZES))
     User = get_user_model()