Browse Source

Merge pull request #736 from rafalp/serializer-cleanup

fix #644
Rafał Pitoń 8 years ago
parent
commit
dd80204009

+ 0 - 1
frontend/src/utils/test-utils.js

@@ -38,7 +38,6 @@ export function mockUser(overrides) {
     },
     avatar_hash: "5c6a04b4",
     email: "test@example.com",
-    full_title: "Forum team",
     is_hiding_presence: false,
     joined_on: "2015-05-09T16:13:33.973603Z",
     limits_private_thread_invites_to: 0,

+ 2 - 2
misago/categories/serializers.py

@@ -2,7 +2,7 @@ from rest_framework import serializers
 
 from django.urls import reverse
 
-from misago.core.serializers import Subsettable
+from misago.core.serializers import MutableFields
 from misago.core.utils import format_plaintext_for_html
 
 from .models import Category
@@ -29,7 +29,7 @@ def last_activity_detail(f):
     return decorator
 
 
-class CategorySerializer(serializers.ModelSerializer, Subsettable):
+class CategorySerializer(serializers.ModelSerializer, MutableFields):
     parent = serializers.PrimaryKeyRelatedField(read_only=True)
     description = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()

+ 27 - 8
misago/core/serializers.py

@@ -1,32 +1,51 @@
-class Subsettable(object):
+class MutableFields(object):
     @classmethod
-    def subset(cls, *fields):
+    def subset_fields(cls, *fields):
         fields_in_name = [f.title().replace('_', '') for f in fields]
         name = '{}{}Subset'.format(cls.__name__, ''.join(fields_in_name)[:100])
 
         class Meta(cls.Meta):
             pass
 
-        Meta.fields = fields
+        Meta.fields = tuple(fields)
 
         return type(name, (cls,), {
             'Meta': Meta
         })
 
     @classmethod
-    def subset_exclude(cls, *fields):
-        clean_fields = []
+    def exclude_fields(cls, *fields):
+        final_fields = []
         for field in cls.Meta.fields:
             if field not in fields:
-                clean_fields.append(field)
+                final_fields.append(field)
 
-        fields_in_name = [f.title().replace('_', '') for f in clean_fields]
+        fields_in_name = [f.title().replace('_', '') for f in final_fields]
         name = '{}{}Subset'.format(cls.__name__, ''.join(fields_in_name)[:100])
 
         class Meta(cls.Meta):
             pass
 
-        Meta.fields = tuple(clean_fields)
+        Meta.fields = tuple(final_fields)
+
+        return type(name, (cls,), {
+            'Meta': Meta
+        })
+
+    @classmethod
+    def extend_fields(cls, *fields):
+        final_fields = list(cls.Meta.fields)
+        for field in fields:
+            if field not in final_fields:
+                final_fields.append(field)
+
+        fields_in_name = [f.title().replace('_', '') for f in final_fields]
+        name = '{}{}Subset'.format(cls.__name__, ''.join(fields_in_name)[:100])
+
+        class Meta(cls.Meta):
+            pass
+
+        Meta.fields = tuple(final_fields)
 
         return type(name, (cls,), {
             'Meta': Meta

+ 18 - 39
misago/core/tests/test_serializers.py

@@ -3,20 +3,20 @@ from rest_framework import serializers
 from django.test import TestCase
 
 from misago.categories.models import Category
-from misago.core.serializers import Subsettable
+from misago.core.serializers import MutableFields
 from misago.threads import testutils
 from misago.threads.models import Thread
 
 
-class SubsettableSerializerTests(TestCase):
-    def test_create_subset_serializer(self):
-        """classmethod subset creates new serializer"""
+class MutableFieldsSerializerTests(TestCase):
+    def test_subset_fields(self):
+        """classmethod subset_fields creates new serializer"""
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
 
         fields = ('id', 'title', 'replies', 'last_poster_name')
 
-        serializer = TestSerializer.subset(*fields)
+        serializer = TestSerializer.subset_fields(*fields)
         self.assertEqual(
             serializer.__name__,
             'TestSerializerIdTitleRepliesLastPosterNameSubset'
@@ -33,15 +33,15 @@ class SubsettableSerializerTests(TestCase):
 
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
 
-    def test_create_subset_serializer_exclude(self):
-        """classmethod exclude creates new serializer"""
+    def test_exclude_fields(self):
+        """classmethod exclude_fields creates new serializer"""
         category = Category.objects.get(slug='first-category')
         thread = testutils.post_thread(category=category)
 
         kept_fields = ('id', 'title', 'weight')
         removed_fields = tuple(set(TestSerializer.Meta.fields) - set(kept_fields))
 
-        serializer = TestSerializer.subset_exclude(*removed_fields)
+        serializer = TestSerializer.exclude_fields(*removed_fields)
         self.assertEqual(serializer.__name__, 'TestSerializerIdTitleWeightSubset')
         self.assertEqual(serializer.Meta.fields, kept_fields)
 
@@ -54,30 +54,20 @@ class SubsettableSerializerTests(TestCase):
 
         self.assertFalse(TestSerializer.Meta.fields == serializer.Meta.fields)
 
+    def test_extend_fields(self):
+        """classmethod extend_fields creates new serializer"""
+        category = Category.objects.get(slug='first-category')
+        thread = testutils.post_thread(category=category)
 
-class TestRelatedSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Category
-        fields = (
-            'id',
-            'title',
-            'replies',
-            'has_unapproved_posts',
-            'started_on',
-            'last_post_on',
-            'last_post_is_event',
-            'last_post',
-            'last_poster_name',
-            'is_unapproved',
-            'is_hidden',
-            'is_closed',
-            'weight',
+        added_fields = ('category',)
 
-            'url',
-        )
+        serializer = TestSerializer.extend_fields(*added_fields)
+
+        serialized_thread = serializer(thread).data
+        self.assertEqual(serialized_thread['category'], category.pk)
 
 
-class TestSerializer(serializers.ModelSerializer, Subsettable):
+class TestSerializer(serializers.ModelSerializer, MutableFields):
     url = serializers.SerializerMethodField()
 
     class Meta:
@@ -96,15 +86,4 @@ class TestSerializer(serializers.ModelSerializer, Subsettable):
             'is_hidden',
             'is_closed',
             'weight',
-
-            'url',
         )
-
-    def get_url(self, obj):
-        return {
-            'index': obj.get_absolute_url(),
-            'new_post': obj.get_new_post_url(),
-            'last_post': obj.get_last_post_url(),
-            'unapproved_post': obj.get_unapproved_post_url(),
-            'last_poster': self.get_last_poster_url(obj),
-        }

+ 12 - 7
misago/threads/serializers/feed.py

@@ -3,9 +3,9 @@ from rest_framework import serializers
 from django.urls import reverse
 
 from misago.categories.serializers import CategorySerializer
-from misago.core.serializers import Subsettable
+from misago.core.serializers import MutableFields
 from misago.threads.models import Post
-from misago.users.serializers import BasicUserSerializer
+from misago.users.serializers import UserSerializer
 
 from .post import PostSerializer
 
@@ -15,13 +15,18 @@ __all__ = [
 ]
 
 
-CategoryFeedSerializer = CategorySerializer.subset(
+
+FeedUserSerializer = UserSerializer.subset_fields(
+    'id', 'username', 'avatars', 'absolute_url')
+
+
+FeedCategorySerializer = CategorySerializer.subset_fields(
     'name', 'css_class', 'absolute_url')
 
 
-class FeedSerializer(PostSerializer, Subsettable):
-    poster = BasicUserSerializer(many=False, read_only=True)
-    category = CategoryFeedSerializer(many=False, read_only=True)
+class FeedSerializer(PostSerializer, MutableFields):
+    poster = FeedUserSerializer(many=False, read_only=True)
+    category = FeedCategorySerializer(many=False, read_only=True)
 
     thread = serializers.SerializerMethodField()
     top_category = serializers.SerializerMethodField()
@@ -43,6 +48,6 @@ class FeedSerializer(PostSerializer, Subsettable):
 
     def get_top_category(self, obj):
         try:
-            return CategoryFeedSerializer(obj.top_category).data
+            return FeedCategorySerializer(obj.top_category).data
         except AttributeError:
             return None

+ 8 - 5
misago/threads/serializers/post.py

@@ -3,16 +3,19 @@ from rest_framework import serializers
 from django.urls import reverse
 
 from misago.categories.serializers import CategorySerializer
+from misago.core.serializers import MutableFields
 from misago.threads.models import Post
-from misago.users.serializers import BasicUserSerializer, UserSerializer
+from misago.users.serializers import UserSerializer as BaseUserSerializer
 
 
-__all__ = [
-    'PostSerializer',
-]
+__all__ = ['PostSerializer']
 
 
-class PostSerializer(serializers.ModelSerializer):
+UserSerializer = BaseUserSerializer.subset_fields(
+    'id', 'username', 'rank', 'avatars', 'signature', 'short_title', 'status', 'absolute_url')
+
+
+class PostSerializer(serializers.ModelSerializer, MutableFields):
     poster = UserSerializer(many=False, read_only=True)
     poster_ip = serializers.SerializerMethodField()
     content = serializers.SerializerMethodField()

+ 4 - 4
misago/threads/serializers/thread.py

@@ -3,7 +3,7 @@ from rest_framework import serializers
 from django.urls import reverse
 
 from misago.categories.serializers import CategorySerializer
-from misago.core.serializers import Subsettable
+from misago.core.serializers import MutableFields
 from misago.threads.models import Thread
 
 from .poll import PollSerializer
@@ -17,12 +17,12 @@ __all__ = [
 ]
 
 
-BasicCategorySerializer = CategorySerializer.subset(
+BasicCategorySerializer = CategorySerializer.subset_fields(
     'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
     'absolute_url', 'api_url', 'level', 'lft', 'rght', 'is_read')
 
 
-class ThreadSerializer(serializers.ModelSerializer, Subsettable):
+class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     category = BasicCategorySerializer(many=False, read_only=True)
 
     acl = serializers.SerializerMethodField()
@@ -151,4 +151,4 @@ class ThreadsListSerializer(ThreadSerializer):
         fields = ThreadSerializer.Meta.fields + (
             'has_poll', 'top_category'
         )
-ThreadsListSerializer = ThreadsListSerializer.subset_exclude('path', 'poll')
+ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')

+ 1 - 1
misago/threads/viewmodels/category.py

@@ -90,6 +90,6 @@ class PrivateThreadsCategory(ViewModel):
         return categories[0]
 
 
-BasicCategorySerializer = CategorySerializer.subset(
+BasicCategorySerializer = CategorySerializer.subset_fields(
     'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
     'absolute_url', 'api_url', 'level', 'lft', 'rght', 'is_read')

+ 5 - 2
misago/users/api/userendpoints/list.py

@@ -13,7 +13,7 @@ from misago.core.shortcuts import get_int_or_404, get_object_or_404, paginate, p
 from misago.users.activepostersranking import get_active_posters_ranking
 from misago.users.models import Rank
 from misago.users.online.utils import make_users_status_aware
-from misago.users.serializers import ScoredUserSerializer, UserSerializer
+from misago.users.serializers import UserCardSerializer
 
 
 UserModel = get_user_model()
@@ -69,7 +69,7 @@ def generic(request):
 
     make_users_status_aware(request.user, list_page.object_list)
 
-    return paginated_response(list_page, serializer=UserSerializer)
+    return paginated_response(list_page, serializer=UserCardSerializer)
 
 
 LISTS = {
@@ -85,3 +85,6 @@ def list_endpoint(request):
         return list_handler(request)
     else:
         return generic(request)
+
+
+ScoredUserSerializer = UserCardSerializer.extend_fields('meta')

+ 8 - 1
misago/users/api/users.py

@@ -25,7 +25,7 @@ from misago.users.permissions.delete import allow_delete_user
 from misago.users.permissions.moderation import allow_moderate_avatar, allow_rename_user
 from misago.users.permissions.profiles import (
     allow_browse_users_list, allow_follow_user, allow_see_ban_details)
-from misago.users.serializers import BanDetailsSerializer, UserProfileSerializer, UserSerializer
+from misago.users.serializers import BanDetailsSerializer, UserSerializer
 from misago.users.viewmodels import UserPosts, UserThreads
 
 from .rest_permissions import BasePermission, UnbannedAnonOnly
@@ -256,3 +256,10 @@ class UserViewSet(viewsets.GenericViewSet):
         feed = UserPosts(request, profile, page)
 
         return Response(feed.get_frontend_context())
+
+
+UserProfileSerializer = UserSerializer.subset_fields(
+    'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars',
+    'is_avatar_locked', 'signature', 'is_signature_locked', 'followers', 'following',
+    'threads', 'posts', 'acl', 'is_followed', 'is_blocked', 'status', 'absolute_url',
+    'api_url')

+ 0 - 4
misago/users/models/user.py

@@ -305,10 +305,6 @@ class User(AbstractBaseUser, PermissionsMixin):
         raise TypeError('Cannot make User instances ACL aware')
 
     @property
-    def full_title(self):
-        return self.title or self.rank.name
-
-    @property
     def short_title(self):
         return self.title or self.rank.title or self.rank.name
 

+ 2 - 2
misago/users/search.py

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy
 
 from misago.search import SearchProvider
 
-from .serializers import UserSerializer
+from .serializers import UserCardSerializer
 
 
 HEAD_RESULTS = 8
@@ -33,7 +33,7 @@ class SearchUsers(SearchProvider):
             results = []
 
         return {
-            'results': UserSerializer(results, many=True).data,
+            'results': UserCardSerializer(results, many=True).data,
             'count': len(results)
         }
 

+ 1 - 0
misago/users/serializers/__init__.py

@@ -1,3 +1,4 @@
 from .ban import *
 from .rank import *
 from .user import *
+from .auth import *

+ 72 - 0
misago/users/serializers/auth.py

@@ -0,0 +1,72 @@
+from rest_framework import serializers
+
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from misago.acl import serialize_acl
+
+from .user import UserSerializer
+
+
+UserModel = get_user_model()
+
+__all__ = ['AuthenticatedUserSerializer', 'AnonymousUserSerializer',]
+
+
+class AuthFlags(object):
+    def get_is_authenticated(self, obj):
+        return bool(obj.is_authenticated)
+
+    def get_is_anonymous(self, obj):
+        return bool(obj.is_anonymous)
+
+
+class AuthenticatedUserSerializer(UserSerializer, AuthFlags):
+    email = serializers.SerializerMethodField()
+    is_authenticated = serializers.SerializerMethodField()
+    is_anonymous = serializers.SerializerMethodField()
+
+    class Meta:
+        model = UserModel
+        fields = UserSerializer.Meta.fields + (
+            'is_authenticated',
+            'is_anonymous',
+        )
+
+    def get_acl(self, obj):
+        return serialize_acl(obj)
+
+    def get_email(self, obj):
+        return obj.email
+
+    def get_api_url(self, obj):
+        return {
+            'avatar': reverse(
+                'misago:api:user-avatar', kwargs={'pk': obj.pk}),
+            'options': reverse(
+                'misago:api:user-forum-options', kwargs={'pk': obj.pk}),
+            'username': reverse(
+                'misago:api:user-username', kwargs={'pk': obj.pk}),
+            'change_email': reverse(
+                'misago:api:user-change-email', kwargs={'pk': obj.pk}),
+            'change_password': reverse(
+                'misago:api:user-change-password', kwargs={'pk': obj.pk}),
+        }
+
+AuthenticatedUserSerializer = AuthenticatedUserSerializer.exclude_fields(
+    'is_avatar_locked', 'is_blocked', 'is_followed', 'is_signature_locked',
+    'meta', 'signature', 'status',
+)
+
+
+class AnonymousUserSerializer(serializers.Serializer, AuthFlags):
+    id = serializers.ReadOnlyField()
+    acl = serializers.SerializerMethodField()
+    is_authenticated = serializers.SerializerMethodField()
+    is_anonymous = serializers.SerializerMethodField()
+
+    def get_acl(self, obj):
+        if hasattr(obj, 'acl'):
+            return serialize_acl(obj)
+        else:
+            return {}

+ 51 - 172
misago/users/serializers/user.py

@@ -4,18 +4,14 @@ from django.contrib.auth import get_user_model
 from django.urls import reverse
 
 from misago.acl import serialize_acl
+from misago.core.serializers import MutableFields
 
 from . import RankSerializer
 
 
-__all__ = [
-    'AuthenticatedUserSerializer',
-    'AnonymousUserSerializer',
-    'BasicUserSerializer',
-    'UserSerializer',
-    'ScoredUserSerializer',
-    'UserProfileSerializer',
-]
+UserModel = get_user_model()
+
+__all__ = ['StatusSerializer', 'UserSerializer', 'UserCardSerializer']
 
 
 class StatusSerializer(serializers.Serializer):
@@ -30,193 +26,53 @@ class StatusSerializer(serializers.Serializer):
     banned_until = serializers.DateTimeField()
 
 
-class AuthenticatedUserSerializer(serializers.ModelSerializer):
-    acl = serializers.SerializerMethodField()
+class UserSerializer(serializers.ModelSerializer, MutableFields):
+    email = serializers.SerializerMethodField()
     rank = RankSerializer(many=False, read_only=True)
+    signature = serializers.SerializerMethodField()
+
+    acl = serializers.SerializerMethodField()
+    is_followed = serializers.SerializerMethodField()
+    is_blocked = serializers.SerializerMethodField()
+    meta = serializers.SerializerMethodField()
+    status = serializers.SerializerMethodField()
 
     absolute_url = serializers.SerializerMethodField()
     api_url = serializers.SerializerMethodField()
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = (
             'id',
             'username',
             'slug',
             'email',
             'joined_on',
-            'is_hiding_presence',
-            'title',
-            'full_title',
-            'short_title',
             'rank',
-            'avatars',
-            'limits_private_thread_invites_to',
-            'unread_private_threads',
-            'subscribe_to_started_threads',
-            'subscribe_to_replied_threads',
-            'threads',
-            'posts',
-            'followers',
-            'following',
-            'acl',
-            'absolute_url',
-            'api_url'
-        )
-
-    def get_acl(self, obj):
-        return serialize_acl(obj)
-
-    def get_absolute_url(self, obj):
-        return obj.get_absolute_url()
-
-    def get_api_url(self, obj):
-        return {
-            'avatar': reverse(
-                'misago:api:user-avatar', kwargs={'pk': obj.pk}),
-            'options': reverse(
-                'misago:api:user-forum-options', kwargs={'pk': obj.pk}),
-            'username': reverse(
-                'misago:api:user-username', kwargs={'pk': obj.pk}),
-            'change_email': reverse(
-                'misago:api:user-change-email', kwargs={'pk': obj.pk}),
-            'change_password': reverse(
-                'misago:api:user-change-password', kwargs={'pk': obj.pk}),
-        }
-
-
-class AnonymousUserSerializer(serializers.Serializer):
-    id = serializers.ReadOnlyField()
-    acl = serializers.SerializerMethodField()
-
-    def get_acl(self, obj):
-        if hasattr(obj, 'acl'):
-            return serialize_acl(obj)
-        else:
-            return {}
-
-
-class BaseSerializer(serializers.ModelSerializer):
-    absolute_url = serializers.SerializerMethodField()
-
-    def get_absolute_url(self, obj):
-        return obj.get_absolute_url()
-
-    def get_short_title(self, obj):
-        return obj.short_title
-
-
-class BasicUserSerializer(BaseSerializer):
-    class Meta:
-        model = get_user_model()
-        fields = (
-            'id',
-            'username',
-            'slug',
-            'avatars',
-            'absolute_url',
-        )
-
-
-class UserSerializer(BaseSerializer):
-    rank = RankSerializer(many=False, read_only=True)
-    short_title = serializers.SerializerMethodField()
-    status = serializers.SerializerMethodField()
-    signature = serializers.SerializerMethodField()
-
-    class Meta:
-        model = get_user_model()
-        fields = (
-            'id',
-            'username',
-            'slug',
-            'joined_on',
-            'avatars',
             'title',
             'short_title',
-            'rank',
-            'signature',
-            'threads',
-            'posts',
-            'followers',
-            'following',
-            'status',
-            'absolute_url',
-        )
-
-    def get_status(self, obj):
-        try:
-            return StatusSerializer(obj.status).data
-        except AttributeError:
-            return None
-
-    def get_signature(self, obj):
-        if obj.has_valid_signature:
-            return obj.signature_parsed
-        else:
-            return None
-
-
-class ScoredUserSerializer(UserSerializer):
-    meta = serializers.SerializerMethodField()
-
-    class Meta:
-        model = get_user_model()
-        fields = (
-            'id',
-            'username',
-            'slug',
-            'joined_on',
             'avatars',
-            'title',
-            'rank',
-            'signature',
-            'threads',
-            'posts',
-            'followers',
-            'following',
-            'meta',
-            'status',
-            'absolute_url',
-        )
-
-    def get_meta(self, obj):
-        return {'score': obj.score}
-
-
-class UserProfileSerializer(UserSerializer):
-    email = serializers.SerializerMethodField()
-    is_followed = serializers.SerializerMethodField()
-    is_blocked = serializers.SerializerMethodField()
-    acl = serializers.SerializerMethodField()
-    api_url = serializers.SerializerMethodField()
-
-    class Meta:
-        model = get_user_model()
-        fields = (
-            'id',
-            'username',
-            'slug',
-            'email',
-            'joined_on',
             'is_avatar_locked',
-            'avatars',
-            'title',
-            'rank',
             'signature',
             'is_signature_locked',
-            'threads',
-            'posts',
             'followers',
             'following',
+            'threads',
+            'posts',
+
+            'acl',
             'is_followed',
             'is_blocked',
+            'meta',
             'status',
-            'acl',
+
             'absolute_url',
             'api_url',
         )
 
+    def get_acl(self, obj):
+        return obj.acl_
+
     def get_email(self, obj):
         if (obj == self.context['user'] or
                 self.context['user'].acl['can_see_users_emails']):
@@ -224,9 +80,6 @@ class UserProfileSerializer(UserSerializer):
         else:
             return None
 
-    def get_acl(self, obj):
-        return obj.acl_
-
     def get_is_followed(self, obj):
         if obj.acl_['can_follow']:
             return self.context['user'].is_following(obj)
@@ -239,16 +92,42 @@ class UserProfileSerializer(UserSerializer):
         else:
             return False
 
+    def get_meta(self, obj):
+        return {'score': obj.score}
+
+    def get_short_title(self, obj):
+        return obj.short_title
+
+    def get_signature(self, obj):
+        if obj.has_valid_signature:
+            return obj.signature_parsed
+        else:
+            return None
+
+    def get_status(self, obj):
+        try:
+            return StatusSerializer(obj.status).data
+        except AttributeError:
+            return None
+
+    def get_absolute_url(self, obj):
+        return obj.get_absolute_url()
+
     def get_api_url(self, obj):
         return {
             'root': reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
             'follow': reverse('misago:api:user-follow', kwargs={'pk': obj.pk}),
             'ban': reverse('misago:api:user-ban', kwargs={'pk': obj.pk}),
             'moderate_avatar': reverse(
-                'misago:api:user-moderate-avatar',kwargs={'pk': obj.pk}),
+                'misago:api:user-moderate-avatar', kwargs={'pk': obj.pk}),
             'moderate_username': reverse(
-                'misago:api:user-moderate-username',kwargs={'pk': obj.pk}),
+                'misago:api:user-moderate-username', kwargs={'pk': obj.pk}),
             'delete': reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
             'threads': reverse('misago:api:user-threads', kwargs={'pk': obj.pk}),
             'posts': reverse('misago:api:user-posts', kwargs={'pk': obj.pk}),
         }
+
+
+UserCardSerializer = UserSerializer.subset_fields(
+    'id', 'username', 'joined_on', 'rank', 'title', 'avatars', 'followers',
+    'threads', 'posts', 'status', 'absolute_url')

+ 256 - 0
misago/users/serializers/user_.py

@@ -0,0 +1,256 @@
+from rest_framework import serializers
+
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from misago.acl import serialize_acl
+
+from . import RankSerializer
+
+
+UserModel = get_user_model()
+
+__all__ = [
+    'AuthenticatedUserSerializer',
+    'AnonymousUserSerializer',
+    'BasicUserSerializer',
+    'UserSerializer',
+    'ScoredUserSerializer',
+    'UserProfileSerializer',
+]
+
+
+class StatusSerializer(serializers.Serializer):
+    is_offline = serializers.BooleanField()
+    is_online = serializers.BooleanField()
+    is_hidden = serializers.BooleanField()
+    is_offline_hidden = serializers.BooleanField()
+    is_online_hidden = serializers.BooleanField()
+    last_click = serializers.DateTimeField()
+
+    is_banned = serializers.BooleanField()
+    banned_until = serializers.DateTimeField()
+
+
+class AuthenticatedUserSerializer(serializers.ModelSerializer):
+    acl = serializers.SerializerMethodField()
+    rank = RankSerializer(many=False, read_only=True)
+
+    absolute_url = serializers.SerializerMethodField()
+    api_url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = UserModel
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'email',
+            'joined_on',
+            'is_hiding_presence',
+            'title',
+            'full_title',
+            'short_title',
+            'rank',
+            'avatars',
+            'limits_private_thread_invites_to',
+            'unread_private_threads',
+            'subscribe_to_started_threads',
+            'subscribe_to_replied_threads',
+            'threads',
+            'posts',
+            'followers',
+            'following',
+            'acl',
+            'absolute_url',
+            'api_url'
+        )
+
+    def get_acl(self, obj):
+        return serialize_acl(obj)
+
+    def get_absolute_url(self, obj):
+        return obj.get_absolute_url()
+
+    def get_api_url(self, obj):
+        return {
+            'avatar': reverse(
+                'misago:api:user-avatar', kwargs={'pk': obj.pk}),
+            'options': reverse(
+                'misago:api:user-forum-options', kwargs={'pk': obj.pk}),
+            'username': reverse(
+                'misago:api:user-username', kwargs={'pk': obj.pk}),
+            'change_email': reverse(
+                'misago:api:user-change-email', kwargs={'pk': obj.pk}),
+            'change_password': reverse(
+                'misago:api:user-change-password', kwargs={'pk': obj.pk}),
+        }
+
+
+class AnonymousUserSerializer(serializers.Serializer):
+    id = serializers.ReadOnlyField()
+    acl = serializers.SerializerMethodField()
+
+    def get_acl(self, obj):
+        if hasattr(obj, 'acl'):
+            return serialize_acl(obj)
+        else:
+            return {}
+
+
+class BaseSerializer(serializers.ModelSerializer):
+    absolute_url = serializers.SerializerMethodField()
+
+    def get_absolute_url(self, obj):
+        return obj.get_absolute_url()
+
+    def get_short_title(self, obj):
+        return obj.short_title
+
+
+class BasicUserSerializer(BaseSerializer):
+    class Meta:
+        model = UserModel
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'avatars',
+            'absolute_url',
+        )
+
+
+class UserSerializer(BaseSerializer):
+    rank = RankSerializer(many=False, read_only=True)
+    short_title = serializers.SerializerMethodField()
+    status = serializers.SerializerMethodField()
+    signature = serializers.SerializerMethodField()
+
+    class Meta:
+        model = UserModel
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'joined_on',
+            'avatars',
+            'title',
+            'short_title',
+            'rank',
+            'signature',
+            'threads',
+            'posts',
+            'followers',
+            'following',
+            'status',
+            'absolute_url',
+        )
+
+    def get_status(self, obj):
+        try:
+            return StatusSerializer(obj.status).data
+        except AttributeError:
+            return None
+
+    def get_signature(self, obj):
+        if obj.has_valid_signature:
+            return obj.signature_parsed
+        else:
+            return None
+
+
+class ScoredUserSerializer(UserSerializer):
+    meta = serializers.SerializerMethodField()
+
+    class Meta:
+        model = UserModel
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'joined_on',
+            'avatars',
+            'title',
+            'rank',
+            'signature',
+            'threads',
+            'posts',
+            'followers',
+            'following',
+            'meta',
+            'status',
+            'absolute_url',
+        )
+
+    def get_meta(self, obj):
+        return {'score': obj.score}
+
+
+class UserProfileSerializer(UserSerializer):
+    email = serializers.SerializerMethodField()
+    is_followed = serializers.SerializerMethodField()
+    is_blocked = serializers.SerializerMethodField()
+    acl = serializers.SerializerMethodField()
+    api_url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = UserModel
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'email',
+            'joined_on',
+            'is_avatar_locked',
+            'avatars',
+            'title',
+            'rank',
+            'signature',
+            'is_signature_locked',
+            'threads',
+            'posts',
+            'followers',
+            'following',
+            'is_followed',
+            'is_blocked',
+            'status',
+            'acl',
+            'absolute_url',
+            'api_url',
+        )
+
+    def get_email(self, obj):
+        if (obj == self.context['user'] or
+                self.context['user'].acl['can_see_users_emails']):
+            return obj.email
+        else:
+            return None
+
+    def get_acl(self, obj):
+        return obj.acl_
+
+    def get_is_followed(self, obj):
+        if obj.acl_['can_follow']:
+            return self.context['user'].is_following(obj)
+        else:
+            return False
+
+    def get_is_blocked(self, obj):
+        if obj.acl_['can_block']:
+            return self.context['user'].is_blocking(obj)
+        else:
+            return False
+
+    def get_api_url(self, obj):
+        return {
+            'root': reverse('misago:api:user-detail', kwargs={'pk': obj.pk}),
+            'follow': reverse('misago:api:user-follow', kwargs={'pk': obj.pk}),
+            'ban': reverse('misago:api:user-ban', kwargs={'pk': obj.pk}),
+            'moderate_avatar': reverse(
+                'misago:api:user-moderate-avatar',kwargs={'pk': obj.pk}),
+            'moderate_username': reverse(
+                'misago:api:user-moderate-username',kwargs={'pk': obj.pk}),
+            'delete': reverse('misago:api:user-delete', kwargs={'pk': obj.pk}),
+            'threads': reverse('misago:api:user-threads', kwargs={'pk': obj.pk}),
+            'posts': reverse('misago:api:user-posts', kwargs={'pk': obj.pk}),
+        }

+ 7 - 3
misago/users/serializers/usernamechange.py

@@ -2,15 +2,19 @@ from rest_framework import serializers
 
 from misago.users.models import UsernameChange
 
-from .user import BasicUserSerializer
+from .user import UserSerializer as BaseUserSerializer
 
 
 __all__ = ['UsernameChangeSerializer']
 
 
+UserSerializer = BaseUserSerializer.subset_fields(
+    'id', 'username', 'avatars', 'absolute_url')
+
+
 class UsernameChangeSerializer(serializers.ModelSerializer):
-    user = BasicUserSerializer(many=False, read_only=True)
-    changed_by = BasicUserSerializer(many=False, read_only=True)
+    user = UserSerializer(many=False, read_only=True)
+    changed_by = UserSerializer(many=False, read_only=True)
 
     class Meta:
         model = UsernameChange

+ 1 - 1
misago/users/viewmodels/threads.py

@@ -77,4 +77,4 @@ class UserThreads(object):
         }
 
 
-UserFeedSerializer = FeedSerializer.subset_exclude('poster')
+UserFeedSerializer = FeedSerializer.exclude_fields('poster')

+ 5 - 2
misago/users/views/lists.py

@@ -11,7 +11,7 @@ from misago.users.activepostersranking import get_active_posters_ranking
 from misago.users.models import Rank
 from misago.users.pages import users_list
 from misago.users.permissions.profiles import allow_browse_users_list
-from misago.users.serializers import ScoredUserSerializer, UserSerializer
+from misago.users.serializers import UserCardSerializer
 
 
 def render(request, template, context):
@@ -104,7 +104,7 @@ def rank(request, slug, page=0):
 
     data = pagination_dict(page)
     data.update({
-        'results': UserSerializer(page.object_list, many=True).data
+        'results': UserCardSerializer(page.object_list, many=True).data
     })
 
     request.frontend_context['USERS'] = data
@@ -124,3 +124,6 @@ def rank(request, slug, page=0):
 
         'paginator': data
     })
+
+
+ScoredUserSerializer = UserCardSerializer.extend_fields('meta')

+ 10 - 3
misago/users/views/profile.py

@@ -20,7 +20,7 @@ from misago.users.decorators import deny_guests
 from misago.users.online.utils import get_user_status
 from misago.users.pages import user_profile
 from misago.users.permissions.profiles import allow_block_user, allow_follow_user
-from misago.users.serializers import BanDetailsSerializer, UserProfileSerializer, UserSerializer
+from misago.users.serializers import BanDetailsSerializer, UserSerializer, UserCardSerializer
 from misago.users.serializers.usernamechange import UsernameChangeSerializer
 from misago.users.viewmodels import UserPosts, UserThreads
 
@@ -142,7 +142,7 @@ def followers(request, profile):
     paginator = pagination_dict(page)
 
     request.frontend_context['PROFILE_FOLLOWERS'] = dict(
-        results=UserSerializer(page.object_list, many=True).data,
+        results=UserCardSerializer(page.object_list, many=True).data,
         **paginator
     )
 
@@ -161,7 +161,7 @@ def follows(request, profile):
     paginator = pagination_dict(page)
 
     request.frontend_context['PROFILE_FOLLOWS'] = dict(
-        results=UserSerializer(page.object_list, many=True).data,
+        results=UserCardSerializer(page.object_list, many=True).data,
         **paginator
     )
 
@@ -203,3 +203,10 @@ def user_ban(request, profile):
         'profile': profile,
         'ban': ban,
     })
+
+
+UserProfileSerializer = UserSerializer.subset_fields(
+    'id', 'username', 'slug', 'email', 'joined_on', 'rank', 'title', 'avatars',
+    'is_avatar_locked', 'signature', 'is_signature_locked', 'followers', 'following',
+    'threads', 'posts', 'acl', 'is_followed', 'is_blocked', 'status', 'absolute_url',
+    'api_url')