Browse Source

WIP remove api/url/acl keys from serializers and create basic api assertions mixin for API tests (#988)

Rafał Pitoń 7 years ago
parent
commit
bc99d5c4da

+ 20 - 0
misago/api/testutils.py

@@ -0,0 +1,20 @@
+class ApiTestsMixin(object):
+    def assertApiResultsAreEmpty(self, response):
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json()['results'], [])
+
+    def assertApiResultsEqual(self, response, items):
+        self.assertEqual(response.status_code, 200)
+        results_ids = [r['id'] for r in response.json()['results']]
+        items_ids = [i.id for i in items]
+        self.assertEqual(results_ids, items_ids)
+
+    def assertInApiResults(self, response, item):
+        self.assertEqual(response.status_code, 200)
+        results_ids = [r['id'] for r in response.json()['results']]
+        self.assertIn(item.id, results_ids)
+
+    def assertNotInApiResults(self, response, item):
+        self.assertEqual(response.status_code, 200)
+        results_ids = [r['id'] for r in response.json()['results']]
+        self.assertNotIn(item.id, results_ids)

+ 1 - 32
misago/categories/serializers.py

@@ -38,9 +38,6 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
     description = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
     subcategories = serializers.SerializerMethodField()
-    acl = serializers.SerializerMethodField()
-
-    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Category
@@ -59,11 +56,9 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
             'css_class',
             'is_read',
             'subcategories',
-            'acl',
             'level',
             'lft',
             'rght',
-            'url',
         ]
 
     def get_description(self, obj):
@@ -86,12 +81,6 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return []
 
-    def get_acl(self, obj):
-        try:
-            return obj.acl
-        except AttributeError:
-            return {}
-
     @last_activity_detail
     def get_last_poster(self, obj):
         if obj.last_poster_id:
@@ -107,26 +96,6 @@ class CategorySerializer(serializers.ModelSerializer, MutableFields):
             }
         return None
 
-    def get_url(self, obj):
-        return {
-            'index': obj.get_absolute_url(),
-            'last_thread': self.get_last_thread_url(obj),
-            'last_thread_new': self.get_last_thread_new_url(obj),
-            'last_post': self.get_last_post_url(obj),
-        }
-
-    @last_activity_detail
-    def get_last_thread_url(self, obj):
-        return obj.get_last_thread_url()
-
-    @last_activity_detail
-    def get_last_thread_new_url(self, obj):
-        return obj.get_last_thread_new_url()
-
-    @last_activity_detail
-    def get_last_post_url(self, obj):
-        return obj.get_last_post_url()
-
 
 class CategoryWithPosterSerializer(CategorySerializer):
     last_poster = serializers.SerializerMethodField()
@@ -142,5 +111,5 @@ CategoryWithPosterSerializer = CategoryWithPosterSerializer.extend_fields('last_
 
 BasicCategorySerializer = CategorySerializer.subset_fields(
     'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'level', 'lft', 'rght', 'url'
+    'level', 'lft', 'rght'
 )

+ 0 - 1
misago/threads/api/postingendpoint/attachments.py

@@ -113,7 +113,6 @@ class AttachmentsSerializer(serializers.Serializer):
         if attachments:
             post.attachments_cache = AttachmentSerializer(attachments, many=True).data
             for attachment in post.attachments_cache:
-                del attachment['acl']
                 del attachment['post']
                 del attachment['uploader_ip']
         else:

+ 5 - 32
misago/threads/serializers/attachment.py

@@ -13,17 +13,15 @@ IMAGE_EXTENSIONS = ('jpg', 'jpeg', 'png', 'gif')
 class AttachmentSerializer(serializers.ModelSerializer):
     post = serializers.PrimaryKeyRelatedField(read_only=True)
 
-    acl = serializers.SerializerMethodField()
-    is_image = serializers.SerializerMethodField()
     filetype = serializers.SerializerMethodField()
     uploader_ip = serializers.SerializerMethodField()
-
-    url = serializers.SerializerMethodField()
+    has_thumbnail = serializers.SerializerMethodField()
 
     class Meta:
         model = Attachment
         fields = [
             'id',
+            'secret',
             'filetype',
             'post',
             'uploaded_on',
@@ -31,20 +29,10 @@ class AttachmentSerializer(serializers.ModelSerializer):
             'uploader_ip',
             'filename',
             'size',
-            'acl',
             'is_image',
-            'url',
+            'has_thumbnail',
         ]
 
-    def get_acl(self, obj):
-        try:
-            return obj.acl
-        except AttributeError:
-            return None
-
-    def get_is_image(self, obj):
-        return obj.is_image
-
     def get_filetype(self, obj):
         return obj.filetype.name
 
@@ -55,23 +43,8 @@ class AttachmentSerializer(serializers.ModelSerializer):
         else:
             return None
 
-    def get_url(self, obj):
-        return {
-            'index': obj.get_absolute_url(),
-            'thumb': obj.get_thumbnail_url(),
-            'uploader': self.get_uploader_url(obj),
-        }
-
-    def get_uploader_url(self, obj):
-        if obj.uploader_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj.uploader_slug,
-                    'pk': obj.uploader_id,
-                }
-            )
-        else:
-            return None
+    def get_has_thumbnail(self, obj):
+        return bool(obj.thumbnail)
 
 
 class NewAttachmentSerializer(serializers.Serializer):

+ 1 - 2
misago/threads/serializers/feed.py

@@ -12,12 +12,11 @@ FeedUserSerializer = UserSerializer.subset_fields(
     'id',
     'username',
     'avatars',
-    'url',
     'title',
     'rank',
 )
 
-FeedCategorySerializer = CategorySerializer.subset_fields('name', 'css_class', 'url')
+FeedCategorySerializer = CategorySerializer.subset_fields('name', 'css_class')
 
 
 class FeedSerializer(PostSerializer, MutableFields):

+ 2 - 35
misago/threads/serializers/poll.py

@@ -12,17 +12,15 @@ MAX_POLL_OPTIONS = 16
 
 
 class PollSerializer(serializers.ModelSerializer):
-    acl = serializers.SerializerMethodField()
     choices = serializers.SerializerMethodField()
 
-    api = serializers.SerializerMethodField()
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = Poll
         fields = [
             'id',
+            'poster',
             'poster_name',
+            'poster_slug',
             'posted_on',
             'length',
             'question',
@@ -30,40 +28,9 @@ class PollSerializer(serializers.ModelSerializer):
             'allow_revotes',
             'votes',
             'is_public',
-            'acl',
             'choices',
-            'api',
-            'url',
         ]
 
-    def get_api(self, obj):
-        return {
-            'index': obj.get_api_url(),
-            'votes': obj.get_votes_api_url(),
-        }
-
-    def get_url(self, obj):
-        return {
-            'poster': self.get_poster_url(obj),
-        }
-
-    def get_poster_url(self, obj):
-        if obj.poster_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj.poster_slug,
-                    'pk': obj.poster_id,
-                }
-            )
-        else:
-            return None
-
-    def get_acl(self, obj):
-        try:
-            return obj.acl
-        except AttributeError:
-            return None
-
     def get_choices(self, obj):
         return obj.choices
 

+ 11 - 13
misago/threads/serializers/pollvote.py

@@ -41,26 +41,24 @@ class NewVoteSerializer(serializers.Serializer):
 
 
 class PollVoteSerializer(serializers.Serializer):
-    voted_on = serializers.DateTimeField()
+    id = serializers.SerializerMethodField()
     username = serializers.SerializerMethodField()
-
-    url = serializers.SerializerMethodField()
+    slug = serializers.SerializerMethodField()
+    voted_on = serializers.DateTimeField()
 
     class Meta:
         fields = [
-            'voted_on',
+            'id',
             'username',
-            'url',
+            'slug'
+            'voted_on',
         ]
 
+    def get_id(self, obj):
+        return obj['voter_id']
+
     def get_username(self, obj):
         return obj['voter_name']
 
-    def get_url(self, obj):
-        if obj['voter_id']:
-            return reverse(
-                'misago:user', kwargs={
-                    'pk': obj['voter_id'],
-                    'slug': obj['voter_slug'],
-                }
-            )
+    def get_slug(self, obj):
+        return obj['voter_slug']

+ 0 - 57
misago/threads/serializers/post.py

@@ -16,7 +16,6 @@ UserSerializer = BaseUserSerializer.subset_fields(
     'title',
     'status',
     'posts',
-    'url',
 )
 
 
@@ -28,16 +27,12 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
     last_editor = serializers.PrimaryKeyRelatedField(read_only=True)
     hidden_by = serializers.PrimaryKeyRelatedField(read_only=True)
 
-    acl = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
     is_new = serializers.SerializerMethodField()
     is_liked = serializers.SerializerMethodField()
     last_likes = serializers.SerializerMethodField()
     likes = serializers.SerializerMethodField()
 
-    api = serializers.SerializerMethodField()
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = Post
         fields = [
@@ -63,14 +58,11 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
             'is_event',
             'event_type',
             'event_context',
-            'acl',
             'is_liked',
             'is_new',
             'is_read',
             'last_likes',
             'likes',
-            'api',
-            'url',
         ]
 
     def get_poster_ip(self, obj):
@@ -88,12 +80,6 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
     def get_attachments(self, obj):
         return obj.attachments_cache
 
-    def get_acl(self, obj):
-        try:
-            return obj.acl
-        except AttributeError:
-            return None
-
     def get_is_liked(self, obj):
         try:
             return obj.is_liked
@@ -131,46 +117,3 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
                 return obj.likes
         except AttributeError:
             return None
-
-    def get_api(self, obj):
-        api_links = {
-            'index': obj.get_api_url(),
-            'likes': obj.get_likes_api_url(),
-            'editor': obj.get_editor_api_url(),
-            'edits': obj.get_edits_api_url(),
-            'read': obj.get_read_api_url(),
-        }
-
-        if obj.is_event:
-            del api_links['likes']
-
-        return api_links
-
-    def get_url(self, obj):
-        return {
-            'index': obj.get_absolute_url(),
-            'last_editor': self.get_last_editor_url(obj),
-            'hidden_by': self.get_hidden_by_url(obj),
-        }
-
-    def get_last_editor_url(self, obj):
-        if obj.last_editor_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'pk': obj.last_editor_id,
-                    'slug': obj.last_editor_slug,
-                }
-            )
-        else:
-            return None
-
-    def get_hidden_by_url(self, obj):
-        if obj.hidden_by_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'pk': obj.hidden_by_id,
-                    'slug': obj.hidden_by_slug,
-                }
-            )
-        else:
-            return None

+ 2 - 19
misago/threads/serializers/postedit.py

@@ -6,36 +6,19 @@ from misago.threads.models import PostEdit
 
 
 class PostEditSerializer(serializers.ModelSerializer):
+    editor = serializers.PrimaryKeyRelatedField(read_only=True)
     diff = serializers.SerializerMethodField()
 
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = PostEdit
         fields = [
             'id',
             'edited_on',
+            'editor',
             'editor_name',
             'editor_slug',
             'diff',
-            'url',
         ]
 
     def get_diff(self, obj):
         return obj.get_diff()
-
-    def get_url(self, obj):
-        return {
-            'editor': self.get_editor_url(obj),
-        }
-
-    def get_editor_url(self, obj):
-        if obj.editor_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj.editor_slug,
-                    'pk': obj.editor_id,
-                }
-            )
-        else:
-            return None

+ 6 - 16
misago/threads/serializers/postlike.py

@@ -9,36 +9,26 @@ class PostLikeSerializer(serializers.ModelSerializer):
     avatars = serializers.SerializerMethodField()
     liker_id = serializers.SerializerMethodField()
     username = serializers.SerializerMethodField()
-
-    url = serializers.SerializerMethodField()
+    slug = serializers.SerializerMethodField()
 
     class Meta:
         model = PostLike
         fields = [
             'id',
-            'avatars',
             'liked_on',
             'liker_id',
             'username',
-            'url',
+            'slug',
+            'avatars',
         ]
-
     def get_liker_id(self, obj):
         return obj['liker_id']
 
     def get_username(self, obj):
         return obj['liker_name']
 
+    def get_slug(self, obj):
+        return obj['liker_slug']
+
     def get_avatars(self, obj):
         return obj.get('liker__avatars')
-
-    def get_url(self, obj):
-        if obj['liker_id']:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj['liker_slug'],
-                    'pk': obj['liker_id'],
-                }
-            )
-        else:
-            return None

+ 24 - 73
misago/threads/serializers/thread.py

@@ -13,7 +13,6 @@ from .threadparticipant import ThreadParticipantSerializer
 class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     category = BasicCategorySerializer(many=False, read_only=True)
 
-    acl = serializers.SerializerMethodField()
     has_unapproved_posts = serializers.SerializerMethodField()
     is_new = serializers.SerializerMethodField()
     is_read = serializers.SerializerMethodField()
@@ -23,9 +22,6 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
     best_answer_marked_by = serializers.PrimaryKeyRelatedField(read_only=True)
     subscription = serializers.SerializerMethodField()
 
-    api = serializers.SerializerMethodField()
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = Thread
         fields = [
@@ -50,22 +46,13 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
             'best_answer_marked_by',
             'best_answer_marked_by_name',
             'best_answer_marked_by_slug',
-            'acl',
             'is_new',
             'is_read',
             'path',
             'poll',
             'subscription',
-            'api',
-            'url',
         ]
 
-    def get_acl(self, obj):
-        try:
-            return obj.acl
-        except AttributeError:
-            return {}
-
     def get_has_unapproved_posts(self, obj):
         try:
             acl = obj.acl
@@ -94,60 +81,13 @@ class ThreadSerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return None
 
-    def get_api(self, obj):
-        return {
-            'index': obj.get_api_url(),
-            'editor': obj.get_editor_api_url(),
-            'merge': obj.get_merge_api_url(),
-            'poll': obj.get_poll_api_url(),
-            'posts': {
-                'index': obj.get_posts_api_url(),
-                'merge': obj.get_post_merge_api_url(),
-                'move': obj.get_post_move_api_url(),
-                'split': obj.get_post_split_api_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(),
-            'best_answer': obj.get_best_answer_url(),
-            'unapproved_post': obj.get_unapproved_post_url(),
-            'starter': self.get_starter_url(obj),
-            'last_poster': self.get_last_poster_url(obj),
-        }
-
-    def get_starter_url(self, obj):
-        if obj.starter_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj.starter_slug,
-                    'pk': obj.starter_id,
-                }
-            )
-        return None
-
-    def get_last_poster_url(self, obj):
-        if obj.last_poster_id:
-            return reverse(
-                'misago:user', kwargs={
-                    'slug': obj.last_poster_slug,
-                    'pk': obj.last_poster_id,
-                }
-            )
-        return None
-
 
 class PrivateThreadSerializer(ThreadSerializer):
     participants = serializers.SerializerMethodField()
 
     class Meta:
         model = Thread
-        fields = ThreadSerializer.Meta.fields + [
-            'participants',
-        ]
+        fields = ThreadSerializer.Meta.fields + ['participants']
 
 
 class ThreadsListSerializer(ThreadSerializer):
@@ -165,20 +105,31 @@ class ThreadsListSerializer(ThreadSerializer):
         ]
 
     def get_starter(self, obj):
-        if obj.starter_id:
-            return {
-                'id': obj.starter_id,
-                'avatars': obj.starter.avatars,
-            }
-        return None
+        if obj.starter:
+            avatars = obj.starter.avatars
+        else:
+            avatars = None
+
+        return {
+            'id': obj.starter_id,
+            'username': obj.starter_name,
+            'slug': obj.starter_slug,
+            'avatars': avatars,
+        }
 
     def get_last_poster(self, obj):
-        if obj.last_poster_id:
-            return {
-                'id': obj.last_poster_id,
-                'avatars': obj.last_poster.avatars,
-            }
-        return None
+        if obj.last_poster:
+            avatars = obj.last_poster.avatars
+        else:
+            avatars = None
+
+        return {
+            'id': obj.last_poster_id,
+            'username': obj.last_poster_name,
+            'slug': obj.last_poster_slug,
+            'avatars': avatars,
+        }
+
 
 
 ThreadsListSerializer = ThreadsListSerializer.exclude_fields('path', 'poll')

+ 5 - 6
misago/threads/serializers/threadparticipant.py

@@ -6,13 +6,12 @@ from misago.threads.models import ThreadParticipant
 class ThreadParticipantSerializer(serializers.ModelSerializer):
     id = serializers.SerializerMethodField()
     username = serializers.SerializerMethodField()
+    slug = serializers.SerializerMethodField()
     avatars = serializers.SerializerMethodField()
 
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = ThreadParticipant
-        fields = ['id', 'username', 'avatars', 'url', 'is_owner']
+        fields = ['id', 'username', 'slug', 'avatars', 'is_owner']
 
     def get_id(self, obj):
         return obj.user.id
@@ -20,8 +19,8 @@ class ThreadParticipantSerializer(serializers.ModelSerializer):
     def get_username(self, obj):
         return obj.user.username
 
+    def get_slug(self, obj):
+        return obj.user.slug
+
     def get_avatars(self, obj):
         return obj.user.avatars
-
-    def get_url(self, obj):
-        return obj.user.get_absolute_url()

+ 4 - 12
misago/threads/tests/test_attachments_api.py

@@ -254,9 +254,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
-        self.assertIsNone(response_json['url']['thumb'])
-        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        self.assertFalse(response_json['has_thumbnail'])
 
         # files associated with attachment are deleted on its deletion
         file_path = attachment.file.path
@@ -295,9 +293,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
-        self.assertIsNone(response_json['url']['thumb'])
-        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        self.assertFalse(response_json['has_thumbnail'])
 
     def test_large_image_upload(self):
         """successful large image upload creates orphan attachment with thumbnail"""
@@ -333,9 +329,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
-        self.assertEqual(response_json['url']['thumb'], attachment.get_thumbnail_url())
-        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        self.assertTrue(response_json['has_thumbnail'])
 
         # thumbnail was scaled down
         thumbnail = Image.open(attachment.thumbnail.path)
@@ -386,6 +380,4 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
         self.assertIsNone(response_json['post'])
         self.assertEqual(response_json['uploader_name'], self.user.username)
-        self.assertEqual(response_json['url']['index'], attachment.get_absolute_url())
-        self.assertEqual(response_json['url']['thumb'], attachment.get_thumbnail_url())
-        self.assertEqual(response_json['url']['uploader'], self.user.get_absolute_url())
+        self.assertTrue(response_json['has_thumbnail'])

+ 59 - 232
misago/threads/tests/test_threadslists.py

@@ -5,6 +5,7 @@ from django.utils import timezone
 from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
+from misago.api.testutils import ApiTestsMixin
 from misago.categories.models import Category
 from misago.conf import settings
 from misago.readtracker import poststracker
@@ -16,7 +17,7 @@ from misago.users.testutils import AuthenticatedUserTestCase
 LISTS_URLS = ('', 'my/', 'new/', 'unread/', 'subscribed/', )
 
 
-class ThreadsListTestCase(AuthenticatedUserTestCase):
+class ThreadsListTestCase(AuthenticatedUserTestCase, ApiTestsMixin):
     def setUp(self):
         """
         Create categories tree for test cases:
@@ -212,10 +213,7 @@ class AllThreadsListTests(ThreadsListTestCase):
             self.access_all_categories()
 
             response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all'))
-            self.assertEqual(response.status_code, 200)
-
-            response_json = response.json()
-            self.assertEqual(len(response_json['results']), 0)
+            self.assertApiResultsAreEmpty(response)
 
         # empty lists render for anonymous user?
         self.logout_user()
@@ -239,10 +237,7 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.access_all_categories()
 
         response = self.client.get('%s?list=all' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_authenticated_only_views(self):
         """authenticated only views return 403 for guests"""
@@ -379,32 +374,17 @@ class AllThreadsListTests(ThreadsListTestCase):
         self.assertTrue(positions['s'] < positions['l'])
         self.assertTrue(positions['s'] > positions['g'])
 
-        # pinned last
+        # locally pinned last
         self.assertTrue(positions['l'] > positions['g'])
         self.assertTrue(positions['l'] > positions['s'])
 
         # API behaviour is identic
         response = self.client.get('/api/threads/')
-        self.assertEqual(response.status_code, 200)
-
-        content = smart_str(response.content)
-        positions = {
-            'g': content.find(globally.get_absolute_url()),
-            'l': content.find(locally.get_absolute_url()),
-            's': content.find(standard.get_absolute_url()),
-        }
-
-        # global announcement before others
-        self.assertTrue(positions['g'] < positions['l'])
-        self.assertTrue(positions['g'] < positions['s'])
-
-        # standard in the middle
-        self.assertTrue(positions['s'] < positions['l'])
-        self.assertTrue(positions['s'] > positions['g'])
-
-        # pinned last
-        self.assertTrue(positions['l'] > positions['g'])
-        self.assertTrue(positions['l'] > positions['s'])
+        self.assertApiResultsEqual(response, [
+            globally,  # global announcement before others
+            standard,  # standard in the middle
+            locally,  # locally pinned last
+        ])
 
     def test_noscript_pagination(self):
         """threads list is paginated for users with js disabled"""
@@ -552,25 +532,11 @@ class CategoryThreadsListTests(ThreadsListTestCase):
         # API behaviour is identic
         response = self.client.get('/api/threads/?category=%s' % self.first_category.pk)
         self.assertEqual(response.status_code, 200)
-
-        content = smart_str(response.content)
-        positions = {
-            'g': content.find(globally.get_absolute_url()),
-            'l': content.find(locally.get_absolute_url()),
-            's': content.find(standard.get_absolute_url()),
-        }
-
-        # global announcement before others
-        self.assertTrue(positions['g'] < positions['l'])
-        self.assertTrue(positions['g'] < positions['s'])
-
-        # pinned in the middle
-        self.assertTrue(positions['l'] < positions['s'])
-        self.assertTrue(positions['l'] > positions['g'])
-
-        # standard last
-        self.assertTrue(positions['s'] > positions['g'])
-        self.assertTrue(positions['s'] > positions['g'])
+        self.assertApiResultsEqual(response, [
+            globally,  # global announcement before others
+            locally,  # locally pinned in the middle
+            standard,  # standard last
+        ])
 
 
 class ThreadsVisibilityTests(ThreadsListTestCase):
@@ -581,8 +547,6 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         )
 
         response = self.client.get('/')
-        self.assertEqual(response.status_code, 200)
-
         self.assertContainsThread(response, test_thread)
 
         self.assertContains(response, 'subcategory-%s' % self.category_a.css_class)
@@ -594,10 +558,9 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # api displays same data
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
+        self.assertInApiResults(response, test_thread)
 
         response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 3)
         self.assertIn(self.category_a.pk, response_json['subcategories'])
 
@@ -615,10 +578,9 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # api displays same data
         self.access_all_categories()
         response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk))
-        self.assertEqual(response.status_code, 200)
+        self.assertInApiResults(response, test_thread)
 
         response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
         self.assertEqual(len(response_json['subcategories']), 2)
         self.assertEqual(response_json['subcategories'][0], self.category_c.pk)
 
@@ -659,10 +621,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         )
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_user_see_own_unapproved_thread(self):
         """list renders unapproved thread that belongs to viewer"""
@@ -679,10 +638,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_user_cant_see_unapproved_thread(self):
         """list hides unapproved thread that belongs to other user"""
@@ -698,10 +654,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_user_cant_see_hidden_thread(self):
         """list hides hidden thread that belongs to other user"""
@@ -717,10 +670,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_user_cant_see_own_hidden_thread(self):
         """list hides hidden thread that belongs to viewer"""
@@ -737,10 +687,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_user_can_see_own_hidden_thread(self):
         """list shows hidden thread that belongs to viewer due to permission"""
@@ -760,10 +707,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_user_can_see_hidden_thread(self):
         """list shows hidden thread that belongs to other user due to permission"""
@@ -782,10 +726,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_hide_threads': 1})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_user_can_see_unapproved_thread(self):
         """list shows hidden thread that belongs to other user due to permission"""
@@ -804,10 +745,7 @@ class ThreadsVisibilityTests(ThreadsListTestCase):
         self.access_all_categories({'can_approve_content': 1})
 
         response = self.client.get(self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
 
 class MyThreadsListTests(ThreadsListTestCase):
@@ -828,16 +766,11 @@ class MyThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_renders_test_thread(self):
         """list renders only threads posted by user"""
@@ -865,19 +798,11 @@ class MyThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=my' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
 
 class NewThreadsListTests(ThreadsListTestCase):
@@ -898,16 +823,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_renders_new_thread(self):
         """list renders new thread"""
@@ -928,19 +848,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_renders_thread_bumped_after_user_cutoff(self):
         """list renders new thread bumped after user cutoff"""
@@ -972,19 +884,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread started before global cutoff"""
@@ -1011,17 +915,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
     def test_list_hides_user_cutoff_thread(self):
         """list hides thread started before users cutoff"""
@@ -1048,17 +946,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
     def test_list_hides_user_read_thread(self):
         """list hides thread already read by user"""
@@ -1084,17 +976,11 @@ class NewThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=new' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk))
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
 
 class UnreadThreadsListTests(ThreadsListTestCase):
@@ -1115,19 +1001,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertApiResultsAreEmpty(response)
 
     def test_list_renders_unread_thread(self):
         """list renders thread with unread posts"""
@@ -1155,21 +1035,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertEqual(response_json['results'][0]['id'], test_thread.pk)
+        self.assertInApiResults(response, test_thread)
 
     def test_list_hides_never_read_thread(self):
         """list hides never read thread"""
@@ -1193,19 +1065,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
     def test_list_hides_read_thread(self):
         """list hides read thread"""
@@ -1231,19 +1097,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
     def test_list_hides_global_cutoff_thread(self):
         """list hides thread replied before global cutoff"""
@@ -1274,19 +1134,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
     def test_list_hides_user_cutoff_thread(self):
         """list hides thread replied before user cutoff"""
@@ -1320,19 +1174,13 @@ class UnreadThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=unread' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
+        self.assertNotInApiResults(response, test_thread)
 
 
 class SubscribedThreadsListTests(ThreadsListTestCase):
@@ -1360,21 +1208,13 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertContains(response, test_thread.get_absolute_url())
+        self.assertInApiResults(response, test_thread)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 1)
-        self.assertContains(response, test_thread.get_absolute_url())
+        self.assertInApiResults(response, test_thread)
 
     def test_list_hides_unsubscribed_thread(self):
         """list shows subscribed thread"""
@@ -1395,22 +1235,13 @@ class SubscribedThreadsListTests(ThreadsListTestCase):
         # test api
         self.access_all_categories()
         response = self.client.get('%s?list=subscribed' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
-        self.assertNotContainsThread(response, test_thread)
+        self.assertApiResultsAreEmpty(response)
 
         self.access_all_categories()
         response = self.client.get(
             '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk)
         )
-        self.assertEqual(response.status_code, 200)
-
-        response_json = response.json()
-        self.assertEqual(len(response_json['results']), 0)
-        self.assertNotContainsThread(response, test_thread)
-
+        self.assertApiResultsAreEmpty(response)
 
 class UnapprovedListTests(ThreadsListTestCase):
     def test_list_errors_without_permission(self):
@@ -1485,9 +1316,7 @@ class UnapprovedListTests(ThreadsListTestCase):
         })
 
         response = self.client.get('%s?list=unapproved' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, visible_thread.get_absolute_url())
-        self.assertNotContains(response, hidden_thread.get_absolute_url())
+        self.assertInApiResults(response, visible_thread)
 
     def test_list_shows_owned_threads_for_unapproving_user(self):
         """list shows owned threads with unapproved posts for user without perm"""
@@ -1523,9 +1352,7 @@ class UnapprovedListTests(ThreadsListTestCase):
             'can_see_unapproved_content_lists': True,
         })
         response = self.client.get('%s?list=unapproved' % self.api_link)
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, visible_thread.get_absolute_url())
-        self.assertNotContains(response, hidden_thread.get_absolute_url())
+        self.assertInApiResults(response, visible_thread)
 
 
 class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase):

+ 1 - 0
misago/threads/threadtypes/__init__.py

@@ -1,6 +1,7 @@
 from .treesmap import trees_map
 
 
+# fixme: a lot of those getters may be superficial
 class ThreadType(object):
     """Abstract class for thread type strategy"""
     root_name = 'undefined'

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

@@ -88,5 +88,5 @@ class PrivateThreadsCategory(ViewModel):
 
 BasicCategorySerializer = CategorySerializer.subset_fields(
     'id', 'parent', 'name', 'description', 'is_closed', 'css_class',
-    'level', 'lft', 'rght', 'is_read', 'url'
+    'level', 'lft', 'rght', 'is_read'
 )

+ 0 - 3
misago/users/api/users.py

@@ -324,10 +324,7 @@ UserProfileSerializer = UserSerializer.subset_fields(
     'following',
     'threads',
     'posts',
-    'acl',
     'is_followed',
     'is_blocked',
     'status',
-    'api',
-    'url',
 )

+ 0 - 5
misago/users/serializers/rank.py

@@ -6,7 +6,6 @@ from misago.users.models import Rank
 
 class RankSerializer(serializers.ModelSerializer):
     description = serializers.SerializerMethodField()
-    url = serializers.SerializerMethodField()
 
     class Meta:
         model = Rank
@@ -19,7 +18,6 @@ class RankSerializer(serializers.ModelSerializer):
             'css_class',
             'is_default',
             'is_tab',
-            'url',
         ]
 
     def get_description(self, obj):
@@ -27,6 +25,3 @@ class RankSerializer(serializers.ModelSerializer):
             return format_plaintext_for_html(obj.description)
         else:
             return ''
-
-    def get_url(self, obj):
-        return obj.get_absolute_url()

+ 0 - 91
misago/users/serializers/user.py

@@ -28,15 +28,11 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
     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()
 
-    api = serializers.SerializerMethodField()
-    url = serializers.SerializerMethodField()
-
     class Meta:
         model = UserModel
         fields = [
@@ -55,20 +51,13 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
             'following',
             'threads',
             'posts',
-            'acl',
             'is_followed',
             'is_blocked',
 
             'meta',
             'status',
-
-            '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_cache['can_see_users_emails']):
             return obj.email
@@ -102,85 +91,6 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
         except AttributeError:
             return None
 
-    def get_api(self, obj):
-        return {
-            'index': 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
-                }
-            ),
-            'details': reverse(
-                'misago:api:user-details',
-                kwargs={
-                    'pk': obj.pk,
-                }
-            ),
-            'edit_details': reverse(
-                'misago:api:user-edit-details',
-                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
-                }
-            ),
-            'followers': reverse(
-                'misago:api:user-followers',
-                kwargs={
-                    'pk': obj.pk
-                }
-            ),
-            'follows': reverse(
-                'misago:api:user-follows',
-                kwargs={
-                    'pk': obj.pk
-                }
-            ),
-            'threads': reverse(
-                'misago:api:user-threads',
-                kwargs={
-                    'pk': obj.pk
-                }
-            ),
-            'posts': reverse(
-                'misago:api:user-posts',
-                kwargs={
-                    'pk': obj.pk
-                }
-            ),
-        }
-
-    def get_url(self, obj):
-        return obj.get_absolute_url()
-
 
 UserCardSerializer = UserSerializer.subset_fields(
     'id',
@@ -193,5 +103,4 @@ UserCardSerializer = UserSerializer.subset_fields(
     'threads',
     'posts',
     'status',
-    'url',
 )

+ 1 - 1
misago/users/serializers/usernamechange.py

@@ -5,7 +5,7 @@ from misago.users.models import UsernameChange
 from .user import UserSerializer as BaseUserSerializer
 
 
-UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'avatars', 'url')
+UserSerializer = BaseUserSerializer.subset_fields('id', 'username', 'slug', 'avatars')
 
 
 class UsernameChangeSerializer(serializers.ModelSerializer):

+ 4 - 4
misago/users/tests/test_usernamechanges_api.py

@@ -35,14 +35,14 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
                     'user': {
                         'id': self.user.id,
                         'username': 'NewUsername',
+                        'slug': 'newusername',
                         'avatars': self.user.avatars,
-                        'url': self.user.get_absolute_url(),
                     },
                     'changed_by': {
                         'id': self.user.id,
                         'username': 'NewUsername',
+                        'slug': 'newusername',
                         'avatars': self.user.avatars,
-                        'url': self.user.get_absolute_url(),
                     },
                     'changed_by_username': 'NewUsername',
                     'changed_on': serialize_datetime(username_change.changed_on),
@@ -99,14 +99,14 @@ class UsernameChangesApiTests(AuthenticatedUserTestCase):
                     'user': {
                         'id': self.user.id,
                         'username': 'NewUsername',
+                        'slug': 'newusername',
                         'avatars': self.user.avatars,
-                        'url': self.user.get_absolute_url(),
                     },
                     'changed_by': {
                         'id': self.user.id,
                         'username': 'NewUsername',
+                        'slug': 'newusername',
                         'avatars': self.user.avatars,
-                        'url': self.user.get_absolute_url(),
                     },
                     'changed_by_username': 'NewUsername',
                     'changed_on': serialize_datetime(username_change.changed_on),

+ 5 - 3
misago/users/tests/test_users_api.py

@@ -214,7 +214,7 @@ class RankListTests(AuthenticatedUserTestCase):
             is_tab=True,
         )
 
-        test_user = UserModel.objects.create_user(
+        UserModel.objects.create_user(
             'Visible',
             'visible@te.com',
             'Pass.123',
@@ -223,14 +223,16 @@ class RankListTests(AuthenticatedUserTestCase):
         )
 
         response = self.client.get(self.link % test_rank.pk)
-        self.assertNotContains(response, test_user.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json()['count'], 0)
 
         # api shows disabled accounts to staff
         self.user.is_staff = True
         self.user.save()
 
         response = self.client.get(self.link % test_rank.pk)
-        self.assertContains(response, test_user.get_absolute_url())
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json()['count'], 1)
 
 
 class SearchNamesListTests(AuthenticatedUserTestCase):

+ 2 - 2
misago/users/views/profile.py

@@ -196,6 +196,6 @@ class UserBanView(ProfileView):
 
 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', 'api', 'url'
+    'signature', 'is_signature_locked', 'followers', 'following', 'threads', 'posts',
+    'is_followed', 'is_blocked', 'status',
 )