Apply query optimizations for posts

This commit is contained in:
Sonny Bakker 2024-10-13 10:16:57 +02:00
parent 2d5801f226
commit e33497569a
10 changed files with 37 additions and 107 deletions

View file

@ -124,10 +124,10 @@ export const fetchPostsBySection = (section, next = false) => {
switch (section.type) { switch (section.type) {
case RULE_TYPE: case RULE_TYPE:
url = next ? next : `/api/rules/${section.id}/posts/?read=false`; url = next ? next : `/api/rules/${section.id}/posts/`;
break; break;
case CATEGORY_TYPE: case CATEGORY_TYPE:
url = next ? next : `/api/categories/${section.id}/posts/?read=false`; url = next ? next : `/api/categories/${section.id}/posts/`;
break; break;
} }

View file

@ -304,10 +304,10 @@ describe('post actions', () => {
type: constants.RULE_TYPE, type: constants.RULE_TYPE,
}; };
fetchMock.getOnce('/api/rules/4/posts/?read=false', { fetchMock.getOnce('/api/rules/4/posts/', {
body: { body: {
count: 2, count: 2,
next: 'https://durp.com/api/rules/4/posts/?cursor=jabadabar&read=false', next: 'https://durp.com/api/rules/4/posts/?cursor=jabadabar',
previous: null, previous: null,
results: posts, results: posts,
}, },
@ -325,7 +325,7 @@ describe('post actions', () => {
{ type: actions.REQUEST_POSTS }, { type: actions.REQUEST_POSTS },
{ {
type: actions.RECEIVE_POSTS, type: actions.RECEIVE_POSTS,
next: 'https://durp.com/api/rules/4/posts/?cursor=jabadabar&read=false', next: 'https://durp.com/api/rules/4/posts/?cursor=jabadabar',
posts, posts,
}, },
]; ];
@ -373,10 +373,10 @@ describe('post actions', () => {
type: constants.CATEGORY_TYPE, type: constants.CATEGORY_TYPE,
}; };
fetchMock.getOnce('/api/categories/1/posts/?read=false', { fetchMock.getOnce('/api/categories/1/posts/', {
body: { body: {
count: 2, count: 2,
next: 'https://durp.com/api/categories/4/posts/?cursor=jabadabar&read=false', next: 'https://durp.com/api/categories/4/posts/?cursor=jabadabar',
previous: null, previous: null,
results: posts, results: posts,
}, },
@ -394,7 +394,7 @@ describe('post actions', () => {
{ type: actions.REQUEST_POSTS }, { type: actions.REQUEST_POSTS },
{ {
type: actions.RECEIVE_POSTS, type: actions.RECEIVE_POSTS,
next: 'https://durp.com/api/categories/4/posts/?cursor=jabadabar&read=false', next: 'https://durp.com/api/categories/4/posts/?cursor=jabadabar',
posts, posts,
}, },
]; ];
@ -600,7 +600,7 @@ describe('post actions', () => {
const errorMessage = 'Page not found'; const errorMessage = 'Page not found';
fetchMock.getOnce(`/api/rules/${rule.id}/posts/?read=false`, () => { fetchMock.getOnce(`/api/rules/${rule.id}/posts/`, () => {
throw new Error(errorMessage); throw new Error(errorMessage);
}); });

View file

@ -81,7 +81,7 @@ describe('post actions', () => {
const action = { const action = {
type: actions.RECEIVE_POSTS, type: actions.RECEIVE_POSTS,
next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', next: 'https://durp.com/api/rules/4/posts/?page=2',
posts, posts,
}; };

View file

@ -254,13 +254,13 @@ describe('selected reducer', () => {
const action = { const action = {
type: postActions.RECEIVE_POSTS, type: postActions.RECEIVE_POSTS,
next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', next: 'https://durp.com/api/rules/4/posts/?page=2',
posts, posts,
}; };
const expectedState = { const expectedState = {
...defaultState, ...defaultState,
next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', next: 'https://durp.com/api/rules/4/posts/?page=2',
lastReached: false, lastReached: false,
}; };

View file

@ -1,3 +1,4 @@
from django.db.models import Prefetch
from rest_framework import status from rest_framework import status
from rest_framework.generics import ( from rest_framework.generics import (
GenericAPIView, GenericAPIView,
@ -10,7 +11,6 @@ from rest_framework.response import Response
from newsreader.core.pagination import CursorPagination from newsreader.core.pagination import CursorPagination
from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.models import CollectionRule
from newsreader.news.collection.serializers import RuleSerializer from newsreader.news.collection.serializers import RuleSerializer
from newsreader.news.core.filters import ReadFilter
from newsreader.news.core.models import Post from newsreader.news.core.models import Post
from newsreader.news.core.serializers import PostSerializer from newsreader.news.core.serializers import PostSerializer
@ -24,7 +24,6 @@ class NestedRuleView(ListAPIView):
queryset = CollectionRule.objects.prefetch_related("posts").all() queryset = CollectionRule.objects.prefetch_related("posts").all()
serializer_class = PostSerializer serializer_class = PostSerializer
pagination_class = CursorPagination pagination_class = CursorPagination
filter_backends = [ReadFilter]
def get_queryset(self): def get_queryset(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
@ -33,7 +32,9 @@ class NestedRuleView(ListAPIView):
# filtered on the user. # filtered on the user.
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
rule = get_object_or_404(self.queryset, **filter_kwargs) prefetch = Prefetch("posts", queryset=Post.objects.filter(read=False))
queryset = CollectionRule.objects.prefetch_related(prefetch)
rule = get_object_or_404(queryset, **filter_kwargs)
self.check_object_permissions(self.request, rule) self.check_object_permissions(self.request, rule)
return rule.posts.order_by("-publication_date") return rule.posts.order_by("-publication_date")

View file

@ -202,7 +202,7 @@ class NestedRuleListViewTestCase(TestCase):
with self.subTest(post=post): with self.subTest(post=post):
self.assertEqual(post["rule"]["id"], rule.pk) self.assertEqual(post["rule"]["id"], rule.pk)
def test_unread_posts(self): def test_posts(self):
rule = FeedFactory.create(user=self.user) rule = FeedFactory.create(user=self.user)
FeedPostFactory.create_batch(size=10, rule=rule, read=False) FeedPostFactory.create_batch(size=10, rule=rule, read=False)
@ -210,7 +210,6 @@ class NestedRuleListViewTestCase(TestCase):
response = self.client.get( response = self.client.get(
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}), reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}),
{"read": "false"},
) )
data = response.json() data = response.json()
@ -221,23 +220,3 @@ class NestedRuleListViewTestCase(TestCase):
for post in data["results"]: for post in data["results"]:
with self.subTest(post=post): with self.subTest(post=post):
self.assertEqual(post["read"], False) self.assertEqual(post["read"], False)
def test_read_posts(self):
rule = FeedFactory.create(user=self.user)
FeedPostFactory.create_batch(size=20, rule=rule, read=False)
FeedPostFactory.create_batch(size=10, rule=rule, read=True)
response = self.client.get(
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk}),
{"read": "true"},
)
data = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(data["results"]), 10)
for post in data["results"]:
with self.subTest(post=post):
self.assertEqual(post["read"], True)

View file

@ -1,3 +1,4 @@
from django.db.models import Prefetch
from rest_framework import status from rest_framework import status
from rest_framework.generics import ( from rest_framework.generics import (
GenericAPIView, GenericAPIView,
@ -11,18 +12,19 @@ from rest_framework.response import Response
from newsreader.accounts.permissions import IsPostOwner from newsreader.accounts.permissions import IsPostOwner
from newsreader.core.pagination import CursorPagination from newsreader.core.pagination import CursorPagination
from newsreader.news.collection.models import CollectionRule
from newsreader.news.collection.serializers import RuleSerializer from newsreader.news.collection.serializers import RuleSerializer
from newsreader.news.core.filters import ReadFilter, SavedFilter from newsreader.news.core.filters import SavedFilter
from newsreader.news.core.models import Category, Post from newsreader.news.core.models import Category, Post
from newsreader.news.core.serializers import CategorySerializer, PostSerializer from newsreader.news.core.serializers import CategorySerializer, PostSerializer
class ListPostView(ListAPIView): class ListPostView(ListAPIView):
queryset = Post.objects.all() queryset = Post.objects.filter(read=False)
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = (IsAuthenticated, IsPostOwner) permission_classes = (IsAuthenticated, IsPostOwner)
pagination_class = CursorPagination pagination_class = CursorPagination
filter_backends = [ReadFilter, SavedFilter] filter_backends = [SavedFilter]
class DetailPostView(RetrieveUpdateAPIView): class DetailPostView(RetrieveUpdateAPIView):
@ -63,10 +65,8 @@ class NestedRuleCategoryView(ListAPIView):
class NestedPostCategoryView(ListAPIView): class NestedPostCategoryView(ListAPIView):
queryset = Category.objects.prefetch_related("rules", "rules__posts").all()
serializer_class = PostSerializer serializer_class = PostSerializer
pagination_class = CursorPagination pagination_class = CursorPagination
filter_backends = [ReadFilter]
def get_queryset(self): def get_queryset(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
@ -75,13 +75,16 @@ class NestedPostCategoryView(ListAPIView):
# filtered on the user. # filtered on the user.
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
category = get_object_or_404(self.queryset, **filter_kwargs) rules_queryset = CollectionRule.objects.filter(user=self.request.user)
prefetch = Prefetch("rules", queryset=rules_queryset, to_attr="user_rules")
category_queryset = Category.objects.prefetch_related(prefetch).filter(
user=self.request.user
)
category = get_object_or_404(category_queryset, **filter_kwargs)
self.check_object_permissions(self.request, category) self.check_object_permissions(self.request, category)
rules = category.rules.values_list("id", flat=True) return Post.objects.filter(rule__in=category.user_rules, read=False)
queryset = Post.objects.filter(rule__in=rules)
return queryset
class CategoryReadView(GenericAPIView): class CategoryReadView(GenericAPIView):

View file

@ -4,33 +4,6 @@ from rest_framework import filters
from rest_framework.compat import coreapi, coreschema from rest_framework.compat import coreapi, coreschema
class ReadFilter(filters.BaseFilterBackend):
query_param = "read"
def filter_queryset(self, request, queryset, view):
key = request.query_params.get(self.query_param, None)
available_values = {"True": True, "true": True, "False": False, "false": False}
if not key or key not in available_values.keys():
return queryset
value = available_values[key]
return queryset.filter(read=value)
def get_schema_fields(self, view):
return [
coreapi.Field(
name=self.query_param,
required=False,
location="query",
schema=coreschema.String(
title=str(self.query_param),
description=str(_("Wether posts should be read or not")),
),
)
]
class SavedFilter(filters.BaseFilterBackend): class SavedFilter(filters.BaseFilterBackend):
query_param = "saved" query_param = "saved"

View file

@ -409,7 +409,7 @@ class NestedCategoryPostView(TestCase):
reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk}) reverse("api:news:core:categories-nested-posts", kwargs={"pk": category.pk})
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_ordering(self): def test_ordering(self):
category = CategoryFactory.create(user=self.user) category = CategoryFactory.create(user=self.user)
@ -503,9 +503,9 @@ class NestedCategoryPostView(TestCase):
self.assertEqual(posts[0]["rule"]["id"], guardian_rule.pk) self.assertEqual(posts[0]["rule"]["id"], guardian_rule.pk)
self.assertEqual(posts[1]["rule"]["id"], guardian_rule.pk) self.assertEqual(posts[1]["rule"]["id"], guardian_rule.pk)
def test_unread_posts(self): def test_posts(self):
category = CategoryFactory.create(user=self.user) category = CategoryFactory.create(user=self.user)
rule = FeedFactory(category=category) rule = FeedFactory(category=category, user=self.user)
FeedPostFactory.create_batch(size=10, rule=rule, read=False) FeedPostFactory.create_batch(size=10, rule=rule, read=False)
FeedPostFactory.create_batch(size=10, rule=rule, read=True) FeedPostFactory.create_batch(size=10, rule=rule, read=True)
@ -514,7 +514,6 @@ class NestedCategoryPostView(TestCase):
reverse( reverse(
"api:news:core:categories-nested-posts", kwargs={"pk": category.pk} "api:news:core:categories-nested-posts", kwargs={"pk": category.pk}
), ),
{"read": "false"},
) )
data = response.json() data = response.json()
@ -525,26 +524,3 @@ class NestedCategoryPostView(TestCase):
for post in posts: for post in posts:
self.assertEqual(post["read"], False) self.assertEqual(post["read"], False)
def test_read_posts(self):
category = CategoryFactory.create(user=self.user)
rule = FeedFactory(category=category)
FeedPostFactory.create_batch(size=20, rule=rule, read=False)
FeedPostFactory.create_batch(size=10, rule=rule, read=True)
response = self.client.get(
reverse(
"api:news:core:categories-nested-posts", kwargs={"pk": category.pk}
),
{"read": "true"},
)
data = response.json()
posts = data["results"]
self.assertEqual(response.status_code, 200)
self.assertEqual(len(data["results"]), 10)
for post in posts:
self.assertEqual(post["read"], True)

View file

@ -53,25 +53,23 @@ class PostListViewTestCase(TestCase):
with self.subTest(post=post): with self.subTest(post=post):
self.assertEqual(data["results"][index]["id"], post.pk) self.assertEqual(data["results"][index]["id"], post.pk)
def test_read_posts(self): def test_posts(self):
rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user))
FeedPostFactory.create_batch(size=20, rule=rule, read=False) FeedPostFactory.create_batch(size=20, rule=rule, read=False)
FeedPostFactory.create_batch(size=10, rule=rule, read=True) FeedPostFactory.create_batch(size=10, rule=rule, read=True)
response = self.client.get( response = self.client.get(reverse("api:news:core:posts-list"))
reverse("api:news:core:posts-list"), {"read": "true"}
)
data = response.json() data = response.json()
posts = data["results"] posts = data["results"]
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(data["results"]), 10) self.assertEqual(len(data["results"]), 20)
for post in posts: for post in posts:
with self.subTest(post=post): with self.subTest(post=post):
self.assertEqual(post["read"], True) self.assertEqual(post["read"], False)
def test_saved_posts(self): def test_saved_posts(self):
rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user))