Browse Source

change avatar api

Rafał Pitoń 8 years ago
parent
commit
e535052a2b

+ 2 - 2
misago/threads/migrations/0001_initial.py

@@ -258,8 +258,8 @@ class Migration(migrations.Migration):
                 ('uploader_ip', models.GenericIPAddressField()),
                 ('uploader_ip', models.GenericIPAddressField()),
                 ('filename', models.CharField(max_length=255, db_index=True)),
                 ('filename', models.CharField(max_length=255, db_index=True)),
                 ('size', models.PositiveIntegerField(default=0, db_index=True)),
                 ('size', models.PositiveIntegerField(default=0, db_index=True)),
-                ('thumbnail', models.ImageField(blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
-                ('image', models.ImageField(blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
+                ('thumbnail', models.ImageField(max_length=255, blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
+                ('image', models.ImageField(max_length=255, blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
                 ('file', models.FileField(blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
                 ('file', models.FileField(blank=True, null=True, upload_to=misago.threads.models.attachment.upload_to)),
                 ('post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='misago_threads.Post')),
                 ('post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='misago_threads.Post')),
             ],
             ],

+ 14 - 3
misago/threads/models/attachment.py

@@ -30,7 +30,8 @@ def upload_to(instance, filename):
         extension
         extension
     ))
     ))
 
 
-    return os.path.join('attachments', spread_path[:2], spread_path[2:4], secret, filename_clean)
+    return os.path.join(
+        'attachments', spread_path[:2], spread_path[2:4], secret, filename_clean)
 
 
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
@@ -60,8 +61,18 @@ class Attachment(models.Model):
     size = models.PositiveIntegerField(default=0, db_index=True)
     size = models.PositiveIntegerField(default=0, db_index=True)
 
 
     thumbnail = models.ImageField(blank=True, null=True, upload_to=upload_to)
     thumbnail = models.ImageField(blank=True, null=True, upload_to=upload_to)
-    image = models.ImageField(blank=True, null=True, upload_to=upload_to)
-    file = models.FileField(blank=True, null=True, upload_to=upload_to)
+    image = models.ImageField(
+        max_length=255,
+        blank=True,
+        null=True,
+        upload_to=upload_to
+    )
+    file = models.FileField(
+        max_length=255,
+        blank=True,
+        null=True,
+        upload_to=upload_to
+    )
 
 
     def __str__(self):
     def __str__(self):
         return self.filename
         return self.filename

+ 27 - 30
misago/users/api/userendpoints/avatar.py

@@ -22,10 +22,9 @@ def avatar_endpoint(request, pk=None):
             reason = None
             reason = None
 
 
         return Response({
         return Response({
-                'detail': _("Your avatar is locked. You can't change it."),
-                'reason': reason
-            },
-            status=status.HTTP_403_FORBIDDEN)
+            'detail': _("Your avatar is locked. You can't change it."),
+            'reason': reason
+        }, status=status.HTTP_403_FORBIDDEN)
 
 
     avatar_options = get_avatar_options(request.user)
     avatar_options = get_avatar_options(request.user)
     if request.method == 'POST':
     if request.method == 'POST':
@@ -36,11 +35,11 @@ def avatar_endpoint(request, pk=None):
 
 
 def get_avatar_options(user):
 def get_avatar_options(user):
     options = {
     options = {
-        'avatar_hash': user.avatar_hash,
+        'avatars': user.avatars,
 
 
         'generated': True,
         'generated': True,
         'gravatar': False,
         'gravatar': False,
-        'crop_org': False,
+        'crop_src': False,
         'crop_tmp': False,
         'crop_tmp': False,
         'upload': False,
         'upload': False,
         'galleries': False
         'galleries': False
@@ -57,14 +56,11 @@ def get_avatar_options(user):
     # Allow Gravatar download
     # Allow Gravatar download
     options['gravatar'] = True
     options['gravatar'] = True
 
 
-    # Get avatar tokens
-    tokens = avatars.get_user_avatar_tokens(user)
-
-    # Allow crop with token if we have uploaded avatar
-    if avatars.uploaded.has_original_avatar(user):
+    # Allow crop if we have uploaded temporary avatar
+    if avatars.uploaded.has_source_avatar(user):
         try:
         try:
-            options['crop_org'] = {
-                'secret': tokens['org'],
+            options['crop_src'] = {
+                'url': user.avatar_src.url,
                 'crop': json.loads(user.avatar_crop),
                 'crop': json.loads(user.avatar_crop),
                 'size': max(settings.MISAGO_AVATARS_SIZES)
                 'size': max(settings.MISAGO_AVATARS_SIZES)
             }
             }
@@ -74,7 +70,7 @@ def get_avatar_options(user):
     # Allow crop of uploaded avatar
     # Allow crop of uploaded avatar
     if avatars.uploaded.has_temporary_avatar(user):
     if avatars.uploaded.has_temporary_avatar(user):
         options['crop_tmp'] = {
         options['crop_tmp'] = {
-            'secret': tokens['tmp'],
+            'url': user.avatar_tmp.url,
             'size': max(settings.MISAGO_AVATARS_SIZES)
             'size': max(settings.MISAGO_AVATARS_SIZES)
         }
         }
 
 
@@ -96,23 +92,25 @@ def avatar_post(options, user, data):
     try:
     try:
         type_options = options[data.get('avatar', 'nope')]
         type_options = options[data.get('avatar', 'nope')]
         if not type_options:
         if not type_options:
-            return Response({'detail': _("This avatar type is not allowed.")},
-                            status=status.HTTP_400_BAD_REQUEST)
+            return Response({
+                'detail': _("This avatar type is not allowed.")
+            }, status=status.HTTP_400_BAD_REQUEST)
 
 
         rpc_handler = AVATAR_TYPES[data.get('avatar', 'nope')]
         rpc_handler = AVATAR_TYPES[data.get('avatar', 'nope')]
     except KeyError:
     except KeyError:
-        return Response({'detail': _("Unknown avatar type.")},
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response({
+            'detail': _("Unknown avatar type.")
+        }, status=status.HTTP_400_BAD_REQUEST)
 
 
     try:
     try:
         response_dict = {'detail': rpc_handler(user, data)}
         response_dict = {'detail': rpc_handler(user, data)}
     except AvatarError as e:
     except AvatarError as e:
-        return Response({'detail': e.args[0]},
-                        status=status.HTTP_400_BAD_REQUEST)
+        return Response({
+            'detail': e.args[0]
+        }, status=status.HTTP_400_BAD_REQUEST)
 
 
-    user.avatar_hash = avatars.get_avatar_hash(user)
-    user.save(update_fields=['avatar_hash', 'avatar_crop'])
-    response_dict['avatar_hash'] = user.avatar_hash
+    user.save()
+    response_dict['avatars'] = user.avatars
 
 
     response_dict['options'] = get_avatar_options(user)
     response_dict['options'] = get_avatar_options(user)
 
 
@@ -157,12 +155,12 @@ def avatar_upload(user, data):
     except ValidationError as e:
     except ValidationError as e:
         raise AvatarError(e.args[0])
         raise AvatarError(e.args[0])
 
 
-    # send back token for temp image
-    return avatars.get_avatar_hash(user, 'tmp')
+    # send back url for temp image
+    return user.avatar_tmp.url
 
 
 
 
-def avatar_crop_org(user, data):
-    avatar_crop(user, data, 'org')
+def avatar_crop_src(user, data):
+    avatar_crop(user, data, 'src')
     return _("Avatar was re-cropped.")
     return _("Avatar was re-cropped.")
 
 
 
 
@@ -186,7 +184,7 @@ AVATAR_TYPES = {
     'galleries': avatar_gallery,
     'galleries': avatar_gallery,
     'upload': avatar_upload,
     'upload': avatar_upload,
 
 
-    'crop_org': avatar_crop_org,
+    'crop_src': avatar_crop_src,
     'crop_tmp': avatar_crop_tmp,
     'crop_tmp': avatar_crop_tmp,
 }
 }
 
 
@@ -198,11 +196,10 @@ def moderate_avatar_endpoint(request, profile):
         if form.is_valid():
         if form.is_valid():
             if form.cleaned_data['is_avatar_locked'] and not is_avatar_locked:
             if form.cleaned_data['is_avatar_locked'] and not is_avatar_locked:
                 avatars.dynamic.set_avatar(profile)
                 avatars.dynamic.set_avatar(profile)
-                profile.avatar_hash = avatars.get_avatar_hash(profile)
             form.save()
             form.save()
 
 
             return Response({
             return Response({
-                'avatar_hash': profile.avatar_hash,
+                'avatars': profile.avatars,
                 'is_avatar_locked': int(profile.is_avatar_locked),
                 'is_avatar_locked': int(profile.is_avatar_locked),
                 'avatar_lock_user_message': profile.avatar_lock_user_message,
                 'avatar_lock_user_message': profile.avatar_lock_user_message,
                 'avatar_lock_staff_message': profile.avatar_lock_staff_message,
                 'avatar_lock_staff_message': profile.avatar_lock_staff_message,

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

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

+ 1 - 1
misago/users/avatars/dynamic.py

@@ -17,7 +17,7 @@ def set_avatar(user):
     drawer_function = getattr(drawer_module, name_bits[-1])
     drawer_function = getattr(drawer_module, name_bits[-1])
 
 
     image = drawer_function(user)
     image = drawer_function(user)
-    store.store_new_avatar(user, image)
+    store.store_avatar(user, image)
 
 
 
 
 """
 """

+ 53 - 63
misago/users/avatars/store.py

@@ -1,94 +1,84 @@
 import os
 import os
 from hashlib import md5
 from hashlib import md5
+from io import BytesIO
 
 
-from path import Path
 from PIL import Image
 from PIL import Image
 
 
-from django.utils.encoding import force_bytes
+from django.core.files.base import ContentFile
+from django.utils.crypto import get_random_string
 
 
 from misago.conf import settings
 from misago.conf import settings
 
 
-from .paths import AVATARS_STORE
 
 
+def normalize_image(image):
+    """strip image of animation, convert to RGBA"""
+    image.seek(0)
+    return image.copy().convert('RGBA')
 
 
-from .store_ import *
 
 
+def delete_avatar(user):
+    if user.avatar_tmp:
+        user.avatar_tmp.delete(False)
 
 
-def get_avatar_hash(user, suffix=None):
-    avatars_dir = get_existing_avatars_dir(user)
+    if user.avatar_src:
+        user.avatar_src.delete(False)
 
 
-    avatar_suffix = suffix or max(settings.MISAGO_AVATARS_SIZES)
-    avatar_file = '%s_%s.png' % (user.pk, avatar_suffix)
-    avatar_file = Path(os.path.join(avatars_dir, avatar_file))
+    for avatar in user.avatar_set.all():
+        avatar.image.delete(False)
+    user.avatar_set.all().delete()
 
 
-    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):
+    from ..models import Avatar
+    image = normalize_image(image)
 
 
+    avatars = []
+    for size in sorted(settings.MISAGO_AVATARS_SIZES, reverse=True):
+        image_stream = BytesIO()
 
 
-def get_user_avatar_tokens(user):
-    token_seeds = (user.email, user.avatar_hash, settings.SECRET_KEY)
+        image = image.resize((size, size), Image.ANTIALIAS)
+        image.save(image_stream, "PNG")
 
 
-    tokens = {
-        'org': md5(force_bytes('org:%s:%s:%s' % token_seeds)).hexdigest()[:8],
-        'tmp': md5(force_bytes('tmp:%s:%s:%s' % token_seeds)).hexdigest()[:8],
-    }
+        avatars.append(Avatar.objects.create(
+            user=user,
+            size=size,
+            image=ContentFile(image_stream.getvalue(), 'avatar')
+        ))
 
 
-    tokens.update({
-        tokens['org']: 'org',
-        tokens['tmp']: 'tmp',
-    })
+    user.avatars = [{'size': a.size, 'url': a.url} for a in avatars]
+    user.save(update_fields=['avatars'])
 
 
-    return tokens
-
-
-def store_original_avatar(user):
-    org_path = avatar_file_path(user, 'org')
-    if org_path.exists():
-        org_path.remove()
-    avatar_file_path(user, 'tmp').rename(org_path)
 
 
+def store_new_avatar(user, image):
+    delete_avatar(user)
+    store_avatar(user, image)
 
 
-def avatar_file_path(user, size):
-    avatars_dir = get_existing_avatars_dir(user)
-    avatar_file = '%s_%s.png' % (user.pk, size)
-    return Path(os.path.join(avatars_dir, avatar_file))
 
 
+def store_temporary_avatar(user, image):
+    image_stream = BytesIO()
 
 
-def avatar_file_exists(user, size):
-    return avatar_file_path(user, size).exists()
+    normalize_image(image)
+    image.save(image_stream, "PNG")
 
 
+    if user.avatar_tmp:
+        user.avatar_tmp.delete(False)
 
 
-def store_new_avatar(user, image):
-    """
-    Deletes old image before storing new one
-    """
-    delete_avatar(user)
-    store_avatar(user, image)
+    user.avatar_tmp = ContentFile(image_stream.getvalue(), 'avatar')
+    user.save(update_fields=['avatar_tmp'])
 
 
 
 
-def get_avatars_dir_path(user=None):
-    if user:
-        try:
-            user_pk = user.pk
-        except AttributeError:
-            user_pk = user
+def store_original_avatar(user):
+    if user.avatar_src:
+        user.avatar_src.delete(False)
+    user.avatar_src = user.avatar_tmp.path
+    user.avatar_tmp = None
+    user.save(update_fields=['avatar_tmp', 'avatar_src'])
 
 
-        dir_hash = md5(str(user_pk).encode()).hexdigest()
-        hash_path = [dir_hash[0:1], dir_hash[2:3]]
-        return Path(os.path.join(AVATARS_STORE, *hash_path))
-    else:
-        return Path(os.path.join(AVATARS_STORE, 'blank'))
 
 
+def upload_to(instance, filename):
+    spread_path = md5(get_random_string(64).encode()).hexdigest()
+    secret = get_random_string(32)
+    filename_clean = '%s.png' % get_random_string(32)
 
 
-def get_existing_avatars_dir(user=None):
-    avatars_dir = get_avatars_dir_path(user)
-    if not avatars_dir.exists():
-        avatars_dir.makedirs()
-    return avatars_dir
+    return os.path.join(
+        'avatars', spread_path[:2], spread_path[2:4], secret, filename_clean)

+ 0 - 77
misago/users/avatars/store_.py

@@ -1,77 +0,0 @@
-import os
-from hashlib import md5
-from io import BytesIO
-
-from PIL import Image
-
-from django.core.files.base import ContentFile
-from django.db import transaction
-from django.utils.crypto import get_random_string
-
-from misago.conf import settings
-
-
-def normalize_image(image):
-    """strip image of animation, convert to RGBA"""
-    image.seek(0)
-    return image.copy().convert('RGBA')
-
-
-def delete_avatar(user, exclude=None):
-    exclude = exclude or []
-    for avatar in user.avatar_set.exclude(id__in=exclude):
-        avatar.image.delete(False)
-    user.avatar_set.exclude(id__in=exclude).delete()
-
-
-def store_avatar(user, image):
-    from ..models import Avatar
-    image = normalize_image(image)
-
-    avatars = []
-    for size in sorted(settings.MISAGO_AVATARS_SIZES, reverse=True):
-        image_stream = BytesIO()
-
-        image = image.resize((size, size), Image.ANTIALIAS)
-        image.save(image_stream, "PNG")
-
-        avatars.append(Avatar.objects.create(
-            user=user,
-            size=size,
-            image=ContentFile(image_stream.getvalue(), 'avatar')
-        ))
-
-    with transaction.atomic():
-        user.avatars = [{'size': a.size, 'url': a.url} for a in avatars]
-        user.save(update_fields=['avatars'])
-        delete_avatar(user, exclude=[a.id for a in avatars])
-
-
-def store_temporary_avatar(user, image):
-    image_stream = BytesIO()
-
-    normalize_image(image)
-    image.save(image_stream, "PNG")
-
-    if user.avatar_tmp:
-        user.avatar_tmp.delete(False)
-
-    user.avatar_tmp = ContentFile(image_stream.getvalue(), 'avatar')
-    user.save(update_fields=['avatar_tmp'])
-
-
-def store_original_avatar(user):
-    if user.avatar_src:
-        user.avatar_src.delete(False)
-    user.avatar_src = user.avatar_tmp.path
-    user.avatar_tmp = None
-    user.save(update_fields=['avatar_tmp', 'avatar_src'])
-
-
-def upload_to(instance, filename):
-    spread_path = md5(get_random_string(64).encode()).hexdigest()
-    secret = get_random_string(32)
-    filename_clean = '%s.png' % get_random_string(32)
-
-    return os.path.join(
-        'avatars', spread_path[:2], spread_path[2:4], secret, filename_clean)

+ 12 - 9
misago/users/avatars/uploaded.py

@@ -122,7 +122,10 @@ def clean_crop(image, crop):
 
 
 
 
 def crop_source_image(user, source, crop):
 def crop_source_image(user, source, crop):
-    image = Image.open(store.avatar_file_path(user, source))
+    if source == 'tmp':
+        image = Image.open(user.avatar_tmp)
+    else:
+        image = Image.open(user.avatar_src)
     crop = clean_crop(image, crop)
     crop = clean_crop(image, crop)
 
 
     min_size = max(settings.MISAGO_AVATARS_SIZES)
     min_size = max(settings.MISAGO_AVATARS_SIZES)
@@ -131,11 +134,11 @@ def crop_source_image(user, source, crop):
     else:
     else:
         upscale = 1.0 / crop['zoom']
         upscale = 1.0 / crop['zoom']
         cropped_image = image.crop((
         cropped_image = image.crop((
-                int(round(crop['x'] * upscale * -1, 0)),
-                int(round(crop['y'] * upscale * -1, 0)),
-                int(round((crop['x'] - min_size) * upscale * -1, 0)),
-                int(round((crop['y'] - min_size) * upscale * -1, 0)),
-            ))
+            int(round(crop['x'] * upscale * -1, 0)),
+            int(round(crop['y'] * upscale * -1, 0)),
+            int(round((crop['x'] - min_size) * upscale * -1, 0)),
+            int(round((crop['y'] - min_size) * upscale * -1, 0)),
+        ))
 
 
     store.store_avatar(user, cropped_image)
     store.store_avatar(user, cropped_image)
     if source == 'tmp':
     if source == 'tmp':
@@ -145,8 +148,8 @@ def crop_source_image(user, source, crop):
 
 
 
 
 def has_temporary_avatar(user):
 def has_temporary_avatar(user):
-    return store.avatar_file_exists(user, 'tmp')
+    return bool(user.avatar_tmp)
 
 
 
 
-def has_original_avatar(user):
-    return store.avatar_file_exists(user, 'org')
+def has_source_avatar(user):
+    return bool(user.avatar_src)

+ 3 - 3
misago/users/migrations/0001_initial.py

@@ -48,8 +48,8 @@ class Migration(migrations.Migration):
                 ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
                 ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
                 ('roles', models.ManyToManyField(to='misago_acl.Role')),
                 ('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')),
                 ('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')),
-                ('avatar_tmp', models.ImageField(upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
-                ('avatar_src', models.ImageField(upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
+                ('avatar_tmp', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
+                ('avatar_src', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to, null=True, blank=True)),
                 ('avatar_crop', models.CharField(max_length=255, null=True, blank=True)),
                 ('avatar_crop', models.CharField(max_length=255, null=True, blank=True)),
                 ('avatars', JSONField(null=True, blank=True)),
                 ('avatars', JSONField(null=True, blank=True)),
                 ('is_avatar_locked', models.BooleanField(default=False)),
                 ('is_avatar_locked', models.BooleanField(default=False)),
@@ -172,7 +172,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
                 ('size', models.PositiveIntegerField(default=0)),
                 ('size', models.PositiveIntegerField(default=0)),
-                ('image', models.ImageField(upload_to=misago.users.avatars.store.upload_to)),
+                ('image', models.ImageField(max_length=255, upload_to=misago.users.avatars.store.upload_to)),
             ],
             ],
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(

+ 1 - 1
misago/users/models/avatar.py

@@ -10,7 +10,7 @@ class Avatar(models.Model):
         on_delete=models.CASCADE
         on_delete=models.CASCADE
     )
     )
     size = models.PositiveIntegerField(default=0)
     size = models.PositiveIntegerField(default=0)
-    image = models.ImageField(upload_to=store.upload_to)
+    image = models.ImageField(max_length=255, upload_to=store.upload_to)
 
 
     @property
     @property
     def url(self):
     def url(self):

+ 25 - 16
misago/users/tests/test_avatars.py

@@ -18,20 +18,18 @@ class AvatarsStoreTests(TestCase):
         user = User.objects.create_user('Bob', 'bob@bob.com', 'pass123')
         user = User.objects.create_user('Bob', 'bob@bob.com', 'pass123')
 
 
         test_image = Image.new("RGBA", (100, 100), 0)
         test_image = Image.new("RGBA", (100, 100), 0)
-        store.store_avatar(user, test_image)
+        store.store_new_avatar(user, test_image)
 
 
         # reload user
         # reload user
         test_user = User.objects.get(pk=user.pk)
         test_user = User.objects.get(pk=user.pk)
 
 
         # assert that avatars were stored in media
         # assert that avatars were stored in media
         avatars_dict = {}
         avatars_dict = {}
-        for size in settings.MISAGO_AVATARS_SIZES:
-            avatar = user.avatar_set.get(size=size)
-
+        for avatar in user.avatar_set.all():
             self.assertTrue(avatar.image.url)
             self.assertTrue(avatar.image.url)
             self.assertEqual(avatar.url, avatar.image.url)
             self.assertEqual(avatar.url, avatar.image.url)
 
 
-            avatars_dict[size] = avatar
+            avatars_dict[avatar.size] = avatar
 
 
         # asserts that user.avatars cache was set
         # asserts that user.avatars cache was set
         self.assertEqual(len(avatars_dict), len(settings.MISAGO_AVATARS_SIZES))
         self.assertEqual(len(avatars_dict), len(settings.MISAGO_AVATARS_SIZES))
@@ -43,7 +41,7 @@ class AvatarsStoreTests(TestCase):
             self.assertEqual(avatar['url'], avatars_dict[avatar['size']].url)
             self.assertEqual(avatar['url'], avatars_dict[avatar['size']].url)
 
 
         # another avatar change deleted old avatars
         # another avatar change deleted old avatars
-        store.store_avatar(user, test_image)
+        store.store_new_avatar(user, test_image)
         for old_avatar in avatars_dict.values():
         for old_avatar in avatars_dict.values():
             avatar_path = Path(old_avatar.image.path)
             avatar_path = Path(old_avatar.image.path)
             self.assertFalse(avatar_path.exists())
             self.assertFalse(avatar_path.exists())
@@ -87,23 +85,34 @@ class AvatarSetterTests(TestCase):
         User = get_user_model()
         User = get_user_model()
         self.user = User.objects.create_user(
         self.user = User.objects.create_user(
             'Bob', 'kontakt@rpiton.com', 'pass123')
             'Bob', 'kontakt@rpiton.com', 'pass123')
-        store.delete_avatar(self.user)
+
+        self.user.avatars = None
+        self.user.save()
 
 
     def tearDown(self):
     def tearDown(self):
         store.delete_avatar(self.user)
         store.delete_avatar(self.user)
 
 
+    def get_current_user(self):
+        User = get_user_model()
+        return User.objects.get(pk=self.user.pk)
+
     def assertNoAvatarIsSet(self):
     def assertNoAvatarIsSet(self):
-        avatar_dir = store.get_existing_avatars_dir(self.user)
-        for size in settings.MISAGO_AVATARS_SIZES:
-            avatar = Path('%s/%s_%s.png' % (avatar_dir, self.user.pk, size))
-            self.assertFalse(avatar.exists())
+        user = self.get_current_user()
+        self.assertFalse(user.avatars)
 
 
     def assertAvatarWasSet(self):
     def assertAvatarWasSet(self):
-        avatar_dir = store.get_existing_avatars_dir(self.user)
-        for size in settings.MISAGO_AVATARS_SIZES:
-            avatar = Path('%s/%s_%s.png' % (avatar_dir, self.user.pk, size))
-            self.assertTrue(avatar.exists())
-            self.assertTrue(avatar.isfile())
+        user = self.get_current_user()
+
+        avatars_dict = {}
+        for avatar in user.avatar_set.all():
+            avatar_path = Path(avatar.image.path)
+            self.assertTrue(avatar_path.exists())
+            self.assertTrue(avatar_path.isfile())
+
+            avatars_dict[avatar.size] = avatar
+
+        self.assertEqual(len(user.avatars), len(avatars_dict))
+        self.assertEqual(len(user.avatars), len(settings.MISAGO_AVATARS_SIZES))
 
 
     def test_dynamic_avatar(self):
     def test_dynamic_avatar(self):
         """dynamic avatar gets created"""
         """dynamic avatar gets created"""

+ 88 - 108
misago/users/tests/test_user_avatar_api.py

@@ -1,4 +1,5 @@
 import json
 import json
+import os
 
 
 from path import Path
 from path import Path
 
 
@@ -13,6 +14,10 @@ from ..avatars import store
 from ..testutils import AuthenticatedUserTestCase
 from ..testutils import AuthenticatedUserTestCase
 
 
 
 
+TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
+TEST_AVATAR_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
+
+
 class UserAvatarTests(AuthenticatedUserTestCase):
 class UserAvatarTests(AuthenticatedUserTestCase):
     """
     """
     tests for user avatar RPC (/api/users/1/avatar/)
     tests for user avatar RPC (/api/users/1/avatar/)
@@ -21,6 +26,10 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         super(UserAvatarTests, self).setUp()
         super(UserAvatarTests, self).setUp()
         self.link = '/api/users/%s/avatar/' % self.user.pk
         self.link = '/api/users/%s/avatar/' % self.user.pk
 
 
+    def get_current_user(self):
+        UserModel = get_user_model()
+        return UserModel.objects.get(pk=self.user.pk)
+
     def test_avatars_off(self):
     def test_avatars_off(self):
         """custom avatars are not allowed"""
         """custom avatars are not allowed"""
         with self.settings(allow_custom_avatars=False):
         with self.settings(allow_custom_avatars=False):
@@ -30,7 +39,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             options = json.loads(smart_str(response.content))
             options = json.loads(smart_str(response.content))
             self.assertTrue(options['generated'])
             self.assertTrue(options['generated'])
             self.assertFalse(options['gravatar'])
             self.assertFalse(options['gravatar'])
-            self.assertFalse(options['crop_org'])
+            self.assertFalse(options['crop_src'])
             self.assertFalse(options['crop_tmp'])
             self.assertFalse(options['crop_tmp'])
             self.assertFalse(options['upload'])
             self.assertFalse(options['upload'])
             self.assertTrue(options['galleries'])
             self.assertTrue(options['galleries'])
@@ -44,7 +53,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             options = json.loads(smart_str(response.content))
             options = json.loads(smart_str(response.content))
             self.assertTrue(options['generated'])
             self.assertTrue(options['generated'])
             self.assertTrue(options['gravatar'])
             self.assertTrue(options['gravatar'])
-            self.assertFalse(options['crop_org'])
+            self.assertFalse(options['crop_src'])
             self.assertFalse(options['crop_tmp'])
             self.assertFalse(options['crop_tmp'])
             self.assertTrue(options['upload'])
             self.assertTrue(options['upload'])
             self.assertTrue(options['galleries'])
             self.assertTrue(options['galleries'])
@@ -52,30 +61,30 @@ class UserAvatarTests(AuthenticatedUserTestCase):
     def test_avatar_locked(self):
     def test_avatar_locked(self):
         """requests to api error if user's avatar is locked"""
         """requests to api error if user's avatar is locked"""
         self.user.is_avatar_locked = True
         self.user.is_avatar_locked = True
-        self.user.avatar_lock_user_message = 'Your avatar is pwnt.'
+        self.user.avatar_lock_user_message = "Your avatar is pwnt."
         self.user.save()
         self.user.save()
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
-        self.assertContains(response, 'Your avatar is pwnt', status_code=403)
+        self.assertContains(response, "Your avatar is pwnt", status_code=403)
 
 
     def test_other_user_avatar(self):
     def test_other_user_avatar(self):
         """requests to api error if user tries to access other user"""
         """requests to api error if user tries to access other user"""
         self.logout_user();
         self.logout_user();
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
-        self.assertContains(response, 'You have to sign in', status_code=403)
+        self.assertContains(response, "You have to sign in", status_code=403)
 
 
         User = get_user_model()
         User = get_user_model()
         self.login_user(User.objects.create_user(
         self.login_user(User.objects.create_user(
             "BobUser", "bob@bob.com", self.USER_PASSWORD))
             "BobUser", "bob@bob.com", self.USER_PASSWORD))
 
 
         response = self.client.get(self.link)
         response = self.client.get(self.link)
-        self.assertContains(response, 'can\'t change other users avatars', status_code=403)
+        self.assertContains(response, "can't change other users avatars", status_code=403)
 
 
     def test_empty_requests(self):
     def test_empty_requests(self):
         """empty request errors with code 400"""
         """empty request errors with code 400"""
         response = self.client.post(self.link)
         response = self.client.post(self.link)
-        self.assertContains(response, 'Unknown avatar type.', status_code=400)
+        self.assertContains(response, "Unknown avatar type.", status_code=400)
 
 
     def test_failed_gravatar_request(self):
     def test_failed_gravatar_request(self):
         """no gravatar RPC fails"""
         """no gravatar RPC fails"""
@@ -83,20 +92,20 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.user.save()
         self.user.save()
 
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertContains(response, 'No Gravatar is associated', status_code=400)
+        self.assertContains(response, "No Gravatar is associated", status_code=400)
 
 
     def test_successful_gravatar_request(self):
     def test_successful_gravatar_request(self):
-        """gravatar RPC fails"""
+        """gravatar RPC passes"""
         self.user.set_email('rafio.xudb@gmail.com')
         self.user.set_email('rafio.xudb@gmail.com')
         self.user.save()
         self.user.save()
 
 
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
-        self.assertContains(response, 'Gravatar was downloaded and set')
+        self.assertContains(response, "Gravatar was downloaded and set")
 
 
     def test_generation_request(self):
     def test_generation_request(self):
         """generated avatar is set"""
         """generated avatar is set"""
         response = self.client.post(self.link, data={'avatar': 'generated'})
         response = self.client.post(self.link, data={'avatar': 'generated'})
-        self.assertContains(response, 'New avatar based on your account')
+        self.assertContains(response, "New avatar based on your account")
 
 
     def test_avatar_upload_and_crop(self):
     def test_avatar_upload_and_crop(self):
         """avatar can be uploaded and cropped"""
         """avatar can be uploaded and cropped"""
@@ -104,91 +113,63 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(self.link, data={'avatar': 'upload'})
         response = self.client.post(self.link, data={'avatar': 'upload'})
-        self.assertContains(response, 'No file was sent.', status_code=400)
-
-        avatar_path = (settings.MEDIA_ROOT, 'avatars', 'blank.png')
-        with open('/'.join(avatar_path), 'rb') as avatar:
-            response = self.client.post(self.link,
-                                        data={
-                                            'avatar': 'upload',
-                                            'image': avatar
-                                        })
+        self.assertContains(response, "No file was sent.", status_code=400)
+
+        with open(TEST_AVATAR_PATH, 'rb') as avatar:
+            response = self.client.post(self.link, data={
+                'avatar': 'upload',
+                'image': avatar
+            })
             self.assertEqual(response.status_code, 200)
             self.assertEqual(response.status_code, 200)
 
 
-            response_json = json.loads(smart_str(response.content))
+            response_json = response.json()
             self.assertTrue(response_json['options']['crop_tmp'])
             self.assertTrue(response_json['options']['crop_tmp'])
 
 
-            avatar_dir = store.get_existing_avatars_dir(self.user)
-            avatar = Path('%s/%s_tmp.png' % (avatar_dir, self.user.pk))
-            self.assertTrue(avatar.exists())
-            self.assertTrue(avatar.isfile())
+        avatar = Path(self.get_current_user().avatar_tmp.path)
+        self.assertTrue(avatar.exists())
+        self.assertTrue(avatar.isfile())
 
 
-            tmp_avatar_kwargs = {
-                'pk': self.user.pk,
-                'secret': response_json['options']['crop_tmp']['secret'],
-                'hash': response_json['avatar_hash'],
+        response = self.client.post(self.link, json.dumps({
+            'avatar': 'crop_tmp',
+            'crop': {
+                'offset': {
+                    'x': 0, 'y': 0
+                },
+                'zoom': 1
             }
             }
-            tmp_avatar_path = reverse('misago:user-avatar-source',
-                                      kwargs=tmp_avatar_kwargs)
-            response = self.client.get(tmp_avatar_path)
-            self.assertEqual(response.status_code, 200)
+        }), content_type="application/json")
 
 
-            response = self.client.post(self.link, json.dumps({
-                    'avatar': 'crop_tmp',
-                    'crop': {
-                        'offset': {
-                            'x': 0, 'y': 0
-                        },
-                        'zoom': 1
-                    }
-                }),
-                content_type="application/json")
-            response_json = json.loads(smart_str(response.content))
-
-            self.assertEqual(response.status_code, 200)
-            self.assertContains(response, 'Uploaded avatar was set.')
+        response_json = response.json()
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "Uploaded avatar was set.")
 
 
-            avatar_dir = store.get_existing_avatars_dir(self.user)
-            avatar = Path('%s/%s_tmp.png' % (avatar_dir, self.user.pk))
-            self.assertFalse(avatar.exists())
+        self.assertFalse(self.get_current_user().avatar_tmp)
 
 
-            avatar = Path('%s/%s_org.png' % (avatar_dir, self.user.pk))
-            self.assertTrue(avatar.exists())
-            self.assertTrue(avatar.isfile())
+        avatar = Path(self.get_current_user().avatar_src.path)
+        self.assertTrue(avatar.exists())
+        self.assertTrue(avatar.isfile())
 
 
-            org_avatar_kwargs = {
-                'pk': self.user.pk,
-                'secret': response_json['options']['crop_org']['secret'],
-                'hash': response_json['avatar_hash'],
+        response = self.client.post(self.link, json.dumps({
+            'avatar': 'crop_tmp',
+            'crop': {
+                'offset': {
+                    'x': 0, 'y': 0
+                },
+                'zoom': 1
             }
             }
-            org_avatar_path = reverse('misago:user-avatar-source',
-                                      kwargs=tmp_avatar_kwargs)
-            response = self.client.get(org_avatar_path)
-            self.assertEqual(response.status_code, 200)
+        }), content_type="application/json")
+        self.assertContains(response, "This avatar type is not allowed.", status_code=400)
 
 
-            response = self.client.post(self.link, json.dumps({
-                    'avatar': 'crop_tmp',
-                    'crop': {
-                        'offset': {
-                            'x': 0, 'y': 0
-                        },
-                        'zoom': 1
-                    }
-                }),
-                content_type="application/json")
-            self.assertContains(response, 'This avatar type is not allowed.', status_code=400)
-
-            response = self.client.post(self.link, json.dumps({
-                    'avatar': 'crop_org',
-                    'crop': {
-                        'offset': {
-                            'x': 0, 'y': 0
-                        },
-                        'zoom': 1
-                    }
-                }),
-                content_type="application/json")
-            self.assertContains(response, 'Avatar was re-cropped.')
+        response = self.client.post(self.link, json.dumps({
+            'avatar': 'crop_src',
+            'crop': {
+                'offset': {
+                    'x': 0, 'y': 0
+                },
+                'zoom': 1
+            }
+        }), content_type="application/json")
+        self.assertContains(response, "Avatar was re-cropped.")
 
 
     def test_gallery(self):
     def test_gallery(self):
         """its possible to set avatar from gallery"""
         """its possible to set avatar from gallery"""
@@ -247,12 +228,12 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         options = json.loads(smart_str(response.content))
         options = json.loads(smart_str(response.content))
-        self.assertEqual(options['is_avatar_locked'],
-                         self.other_user.is_avatar_locked)
-        self.assertEqual(options['avatar_lock_user_message'],
-                         self.other_user.avatar_lock_user_message)
-        self.assertEqual(options['avatar_lock_staff_message'],
-                         self.other_user.avatar_lock_staff_message)
+        self.assertEqual(
+            options['is_avatar_locked'], self.other_user.is_avatar_locked)
+        self.assertEqual(
+            options['avatar_lock_user_message'], self.other_user.avatar_lock_user_message)
+        self.assertEqual(
+            options['avatar_lock_staff_message'], self.other_user.avatar_lock_staff_message)
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
@@ -276,14 +257,14 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         self.assertEqual(
         self.assertEqual(
             other_user.avatar_lock_staff_message, "Test staff message.")
             other_user.avatar_lock_staff_message, "Test staff message.")
 
 
-        self.assertEqual(options['avatar_hash'],
-                         other_user.avatar_hash)
-        self.assertEqual(options['is_avatar_locked'],
-                         other_user.is_avatar_locked)
-        self.assertEqual(options['avatar_lock_user_message'],
-                         other_user.avatar_lock_user_message)
-        self.assertEqual(options['avatar_lock_staff_message'],
-                         other_user.avatar_lock_staff_message)
+        self.assertEqual(
+            options['avatars'], other_user.avatars)
+        self.assertEqual(
+            options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(
+            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
+        self.assertEqual(
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
 
 
         override_acl(self.user, {
         override_acl(self.user, {
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
@@ -300,14 +281,14 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
         other_user = User.objects.get(pk=self.other_user.pk)
         other_user = User.objects.get(pk=self.other_user.pk)
 
 
         options = json.loads(smart_str(response.content))
         options = json.loads(smart_str(response.content))
-        self.assertEqual(options['avatar_hash'],
-                         other_user.avatar_hash)
-        self.assertEqual(options['is_avatar_locked'],
-                         other_user.is_avatar_locked)
-        self.assertEqual(options['avatar_lock_user_message'],
-                         other_user.avatar_lock_user_message)
-        self.assertEqual(options['avatar_lock_staff_message'],
-                         other_user.avatar_lock_staff_message)
+        self.assertEqual(
+            options['avatars'], other_user.avatars)
+        self.assertEqual(
+            options['is_avatar_locked'], other_user.is_avatar_locked)
+        self.assertEqual(
+            options['avatar_lock_user_message'], other_user.avatar_lock_user_message)
+        self.assertEqual(
+            options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message)
 
 
     def test_moderate_own_avatar(self):
     def test_moderate_own_avatar(self):
         """moderate own avatar"""
         """moderate own avatar"""
@@ -315,6 +296,5 @@ class UserAvatarModerationTests(AuthenticatedUserTestCase):
             'can_moderate_avatars': 1,
             'can_moderate_avatars': 1,
         })
         })
 
 
-        response = self.client.get(
-            '/api/users/%s/moderate-avatar/' % self.user.pk)
+        response = self.client.get( '/api/users/%s/moderate-avatar/' % self.user.pk)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 0 - 0
misago/users/tests/testfiles/avatar_src.png → misago/users/tests/testfiles/avatar.png