diff --git a/src/newsreader/news/core/endpoints.py b/src/newsreader/news/core/endpoints.py index ab47cca..b224024 100644 --- a/src/newsreader/news/core/endpoints.py +++ b/src/newsreader/news/core/endpoints.py @@ -13,11 +13,19 @@ from rest_framework.response import Response from newsreader.accounts.permissions import IsPostOwner from newsreader.core.pagination import CursorPagination from newsreader.news.collection.serializers import RuleSerializer -from newsreader.news.core.filters import ReadFilter +from newsreader.news.core.filters import ReadFilter, SavedFilter from newsreader.news.core.models import Category, Post from newsreader.news.core.serializers import CategorySerializer, PostSerializer +class ListPostView(ListAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + permission_classes = (IsAuthenticated, IsPostOwner) + pagination_class = CursorPagination + filter_backends = [ReadFilter, SavedFilter] + + class DetailPostView(RetrieveUpdateAPIView): queryset = Post.objects.all() serializer_class = PostSerializer diff --git a/src/newsreader/news/core/filters.py b/src/newsreader/news/core/filters.py index d322d83..ba3ea48 100644 --- a/src/newsreader/news/core/filters.py +++ b/src/newsreader/news/core/filters.py @@ -30,3 +30,30 @@ class ReadFilter(filters.BaseFilterBackend): ), ) ] + + +class SavedFilter(filters.BaseFilterBackend): + query_param = "saved" + + 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(saved=value) + + def get_schema_fields(self, view): + return [ + coreapi.Field( + name=self.query_param, + required=False, + location="query", + schema=coreschema.String( + title=force_text(self.query_param), + description=force_text(_("Wether posts should be saved or not")), + ), + ) + ] diff --git a/src/newsreader/news/core/migrations/0008_post_saved.py b/src/newsreader/news/core/migrations/0008_post_saved.py new file mode 100644 index 0000000..08ae2a8 --- /dev/null +++ b/src/newsreader/news/core/migrations/0008_post_saved.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.5 on 2021-02-19 20:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("core", "0007_auto_20200706_2312")] + + operations = [ + migrations.AddField( + model_name="post", name="saved", field=models.BooleanField(default=False) + ) + ] diff --git a/src/newsreader/news/core/models.py b/src/newsreader/news/core/models.py index ff44c81..2f7d571 100644 --- a/src/newsreader/news/core/models.py +++ b/src/newsreader/news/core/models.py @@ -14,6 +14,7 @@ class Post(TimeStampedModel): url = models.URLField(max_length=1024, blank=True, null=True) read = models.BooleanField(default=False) + saved = models.BooleanField(default=False) rule = models.ForeignKey( CollectionRule, on_delete=models.CASCADE, editable=False, related_name="posts" diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py index d4353c9..38619a1 100644 --- a/src/newsreader/news/core/serializers.py +++ b/src/newsreader/news/core/serializers.py @@ -19,6 +19,7 @@ class PostSerializer(serializers.ModelSerializer): "url", "rule", "read", + "saved", "publicationDate", "remoteIdentifier", ) diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py index 2d25a89..92444cc 100644 --- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -22,8 +22,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["id"], post.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["id"], post.pk) self.assertTrue("title" in data) self.assertTrue("body" in data) @@ -37,8 +37,8 @@ class PostDetailViewTestCase(TestCase): response = self.client.get(reverse("api:news:core:posts-detail", args=[100])) data = response.json() - self.assertEquals(response.status_code, 404) - self.assertEquals(data["detail"], "Not found.") + self.assertEqual(response.status_code, 404) + self.assertEqual(data["detail"], "Not found.") def test_post(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -49,8 +49,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 405) - self.assertEquals(data["detail"], 'Method "POST" not allowed.') + self.assertEqual(response.status_code, 405) + self.assertEqual(data["detail"], 'Method "POST" not allowed.') def test_patch(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -63,8 +63,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["title"], "This title is very accurate") + self.assertEqual(response.status_code, 200) + self.assertEqual(data["title"], "This title is very accurate") def test_identifier_cannot_be_changed(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -77,8 +77,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["id"], post.pk) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["id"], post.pk) def test_rule_cannot_be_changed(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -98,7 +98,7 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) self.assertTrue(data["rule"], rule.pk) @@ -113,8 +113,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["title"], "This title is very accurate") + self.assertEqual(response.status_code, 200) + self.assertEqual(data["title"], "This title is very accurate") def test_delete(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -125,8 +125,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 405) - self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + self.assertEqual(response.status_code, 405) + self.assertEqual(data["detail"], 'Method "DELETE" not allowed.') def test_post_with_unauthenticated_user_without_category(self): self.client.logout() @@ -138,7 +138,7 @@ class PostDetailViewTestCase(TestCase): reverse("api:news:core:posts-detail", args=[post.pk]) ) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def test_post_with_unauthenticated_user_with_category(self): self.client.logout() @@ -150,7 +150,7 @@ class PostDetailViewTestCase(TestCase): reverse("api:news:core:posts-detail", args=[post.pk]) ) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def test_post_with_unauthorized_user_without_category(self): other_user = UserFactory() @@ -161,7 +161,7 @@ class PostDetailViewTestCase(TestCase): reverse("api:news:core:posts-detail", args=[post.pk]) ) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def test_post_with_unauthorized_user_with_category(self): other_user = UserFactory() @@ -172,7 +172,7 @@ class PostDetailViewTestCase(TestCase): reverse("api:news:core:posts-detail", args=[post.pk]) ) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def test_post_with_different_user_for_category_and_rule(self): other_user = UserFactory() @@ -183,7 +183,7 @@ class PostDetailViewTestCase(TestCase): reverse("api:news:core:posts-detail", args=[post.pk]) ) - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 403) def test_mark_read(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -196,8 +196,8 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["read"], True) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["read"], True) def test_mark_unread(self): rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) @@ -210,5 +210,33 @@ class PostDetailViewTestCase(TestCase): ) data = response.json() - self.assertEquals(response.status_code, 200) - self.assertEquals(data["read"], False) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["read"], False) + + def test_mark_saved(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = FeedPostFactory(rule=rule, saved=False) + + response = self.client.patch( + reverse("api:news:core:posts-detail", args=[post.pk]), + data=json.dumps({"saved": True}), + content_type="application/json", + ) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertEqual(data["saved"], True) + + def test_mark_unsaved(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = FeedPostFactory(rule=rule, saved=True) + + response = self.client.patch( + reverse("api:news:core:posts-detail", args=[post.pk]), + data=json.dumps({"saved": False}), + content_type="application/json", + ) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertEqual(data["saved"], False) diff --git a/src/newsreader/news/core/tests/endpoints/post/list/__init__.py b/src/newsreader/news/core/tests/endpoints/post/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/list/tests.py b/src/newsreader/news/core/tests/endpoints/post/list/tests.py new file mode 100644 index 0000000..37f83b0 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -0,0 +1,96 @@ +from datetime import datetime + +from django.test import TestCase +from django.urls import reverse + +import pytz + +from newsreader.accounts.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import FeedFactory +from newsreader.news.core.tests.factories import CategoryFactory, FeedPostFactory + + +class PostListViewTestCase(TestCase): + def setUp(self): + self.user = UserFactory(is_staff=True, password="test") + self.client.force_login(self.user) + + def test_simple(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) + FeedPostFactory.create_batch(size=3, rule=rule) + + response = self.client.get(reverse("api:news:core:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 3) + + def test_ordering(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) + + posts = [ + FeedPostFactory( + title="I'm the first post", + rule=rule, + publication_date=datetime(2019, 5, 20, 16, 7, 38, tzinfo=pytz.utc), + ), + FeedPostFactory( + title="I'm the second post", + rule=rule, + publication_date=datetime(2019, 5, 20, 16, 7, 37, tzinfo=pytz.utc), + ), + FeedPostFactory( + title="I'm the third post", + rule=rule, + publication_date=datetime(2019, 5, 20, 16, 7, 36, tzinfo=pytz.utc), + ), + ] + + response = self.client.get(reverse("api:news:core:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + + for index, post in enumerate(posts, start=0): + with self.subTest(post=post): + self.assertEqual(data["results"][index]["id"], post.pk) + + def test_read_posts(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(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:core:posts-list"), {"read": "true"} + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 10) + + for post in posts: + with self.subTest(post=post): + self.assertEqual(post["read"], True) + + def test_saved_posts(self): + rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user)) + + FeedPostFactory.create_batch(size=20, rule=rule, saved=False) + FeedPostFactory.create_batch(size=10, rule=rule, saved=True) + + response = self.client.get( + reverse("api:news:core:posts-list"), {"saved": "true"} + ) + + data = response.json() + posts = data["results"] + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 10) + + for post in posts: + with self.subTest(post=post): + self.assertEqual(post["saved"], True) diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py index 21db59d..8096cf8 100644 --- a/src/newsreader/news/core/urls.py +++ b/src/newsreader/news/core/urls.py @@ -6,6 +6,7 @@ from newsreader.news.core.endpoints import ( DetailCategoryView, DetailPostView, ListCategoryView, + ListPostView, NestedPostCategoryView, NestedRuleCategoryView, ) @@ -32,6 +33,7 @@ urlpatterns = [ ] endpoints = [ + path("posts/", ListPostView.as_view(), name="posts-list"), path("posts//", DetailPostView.as_view(), name="posts-detail"), path("categories/", ListCategoryView.as_view(), name="categories-list"), path(