Compare commits
4 commits
main
...
autocomple
| Author | SHA1 | Date | |
|---|---|---|---|
| a8ca688456 | |||
| b016230500 | |||
| 8a6421b4c1 | |||
| 29c5b562d4 |
8 changed files with 250 additions and 102 deletions
|
|
@ -228,6 +228,10 @@ REST_FRAMEWORK = {
|
||||||
"newsreader.accounts.permissions.IsOwner",
|
"newsreader.accounts.permissions.IsOwner",
|
||||||
),
|
),
|
||||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||||
|
"DEFAULT_THROTTLE_RATES": {
|
||||||
|
"burst_search": "100/min",
|
||||||
|
"sustained_search": "2000/day",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,9 @@ class CheckboxInput(forms.CheckboxInput):
|
||||||
def get_context(self, name, value, attrs):
|
def get_context(self, name, value, attrs):
|
||||||
context = super().get_context(name, value, attrs)
|
context = super().get_context(name, value, attrs)
|
||||||
return {**context, **attrs}
|
return {**context, **attrs}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiAutoCompleteWidget(forms.TextInput):
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
return context
|
||||||
|
|
|
||||||
20
src/newsreader/core/throttling.py
Normal file
20
src/newsreader/core/throttling.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from rest_framework.throttling import UserRateThrottle
|
||||||
|
|
||||||
|
|
||||||
|
class SearchThrottle(UserRateThrottle):
|
||||||
|
"""
|
||||||
|
Only applies throttling to requests with the search param
|
||||||
|
"""
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
if not "search" in request.GET.keys():
|
||||||
|
return True
|
||||||
|
return super().allow_request(request, view)
|
||||||
|
|
||||||
|
|
||||||
|
class BurstSearchThrottle(SearchThrottle):
|
||||||
|
scope = "burst_search"
|
||||||
|
|
||||||
|
|
||||||
|
class SustainedSearchThrottle(SearchThrottle):
|
||||||
|
scope = "sustained_search"
|
||||||
29
src/newsreader/js/components/AutoCompleteInput.js
Normal file
29
src/newsreader/js/components/AutoCompleteInput.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class AutoCompleteInput extends React.Component {
|
||||||
|
state = {
|
||||||
|
filteredSuggestions: [],
|
||||||
|
activeSuggestions: [],
|
||||||
|
showSuggestions: false,
|
||||||
|
userInput: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange(e) {}
|
||||||
|
|
||||||
|
onClick(e) {}
|
||||||
|
|
||||||
|
onKeyDown(e) {}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={this.onChange}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
value={this.state.userInput}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCompleteInput;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from rest_framework import status
|
from rest_framework import filters, status
|
||||||
from rest_framework.generics import (
|
from rest_framework.generics import (
|
||||||
GenericAPIView,
|
GenericAPIView,
|
||||||
ListAPIView,
|
ListAPIView,
|
||||||
|
|
@ -8,6 +8,7 @@ from rest_framework.generics import (
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
|
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
|
||||||
|
from newsreader.core.throttling import BurstSearchThrottle, SustainedSearchThrottle
|
||||||
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.filters import ReadFilter
|
||||||
|
|
@ -20,9 +21,14 @@ class ListRuleView(ListAPIView):
|
||||||
serializer_class = RuleSerializer
|
serializer_class = RuleSerializer
|
||||||
pagination_class = ResultSetPagination
|
pagination_class = ResultSetPagination
|
||||||
|
|
||||||
|
filter_backends = [filters.SearchFilter]
|
||||||
|
search_fields = ["name", "screen_name", "url"]
|
||||||
|
|
||||||
|
throttle_classes = [BurstSearchThrottle, SustainedSearchThrottle]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
return self.queryset.filter(user=user).order_by("-created")
|
return self.queryset.filter(user=user).order_by("name", "screen_name")
|
||||||
|
|
||||||
|
|
||||||
class DetailRuleView(RetrieveUpdateDestroyAPIView):
|
class DetailRuleView(RetrieveUpdateDestroyAPIView):
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from datetime import date, datetime, time
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from newsreader.accounts.tests.factories import UserFactory
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
from newsreader.news.collection.tests.factories import FeedFactory
|
from newsreader.news.collection.tests.factories import (
|
||||||
|
FeedFactory,
|
||||||
|
SubredditFactory,
|
||||||
|
TwitterTimelineFactory,
|
||||||
|
)
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, FeedPostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, FeedPostFactory
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,44 +32,29 @@ class RuleListViewTestCase(TestCase):
|
||||||
response = self.client.get(reverse("api:news:collection:rules-list"))
|
response = self.client.get(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
self.assertEquals(data["count"], 3)
|
self.assertEqual(data["count"], 3)
|
||||||
|
|
||||||
def test_ordering(self):
|
def test_ordering(self):
|
||||||
rules = [
|
rules = {
|
||||||
FeedFactory(
|
"foo": FeedFactory(name="foo", user=self.user),
|
||||||
created=datetime.combine(
|
"bar": FeedFactory(name="bar", user=self.user),
|
||||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
"dar": FeedFactory(name="dar", user=self.user),
|
||||||
),
|
}
|
||||||
user=self.user,
|
|
||||||
),
|
|
||||||
FeedFactory(
|
|
||||||
created=datetime.combine(
|
|
||||||
date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc
|
|
||||||
),
|
|
||||||
user=self.user,
|
|
||||||
),
|
|
||||||
FeedFactory(
|
|
||||||
created=datetime.combine(
|
|
||||||
date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc
|
|
||||||
),
|
|
||||||
user=self.user,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
response = self.client.get(reverse("api:news:collection:rules-list"))
|
response = self.client.get(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
self.assertEquals(data["count"], 3)
|
self.assertEqual(data["count"], 3)
|
||||||
|
|
||||||
self.assertEquals(data["results"][0]["id"], rules[1].pk)
|
self.assertEqual(data["results"][0]["id"], rules["bar"].pk)
|
||||||
self.assertEquals(data["results"][1]["id"], rules[2].pk)
|
self.assertEqual(data["results"][1]["id"], rules["dar"].pk)
|
||||||
self.assertEquals(data["results"][2]["id"], rules[0].pk)
|
self.assertEqual(data["results"][2]["id"], rules["foo"].pk)
|
||||||
|
|
||||||
def test_pagination_count(self):
|
def test_pagination_count(self):
|
||||||
FeedFactory.create_batch(size=80, user=self.user)
|
FeedFactory.create_batch(size=80, user=self.user)
|
||||||
|
|
@ -70,20 +64,20 @@ class RuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEquals(data["count"], 80)
|
self.assertEqual(data["count"], 80)
|
||||||
self.assertEquals(len(data["results"]), 30)
|
self.assertEqual(len(data["results"]), 30)
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
response = self.client.get(reverse("api:news:collection:rules-list"))
|
response = self.client.get(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
|
|
||||||
self.assertEquals(data["count"], 0)
|
self.assertEqual(data["count"], 0)
|
||||||
self.assertEquals(len(data["results"]), 0)
|
self.assertEqual(len(data["results"]), 0)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
@ -97,29 +91,29 @@ class RuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
self.assertEqual(data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
def test_patch(self):
|
||||||
response = self.client.patch(reverse("api:news:collection:rules-list"))
|
response = self.client.patch(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
self.assertEqual(data["detail"], 'Method "PATCH" not allowed.')
|
||||||
|
|
||||||
def test_put(self):
|
def test_put(self):
|
||||||
response = self.client.put(reverse("api:news:collection:rules-list"))
|
response = self.client.put(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
self.assertEqual(data["detail"], 'Method "PUT" not allowed.')
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
response = self.client.delete(reverse("api:news:collection:rules-list"))
|
response = self.client.delete(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
self.assertEqual(data["detail"], 'Method "DELETE" not allowed.')
|
||||||
|
|
||||||
def test_rules_with_unauthenticated_user(self):
|
def test_rules_with_unauthenticated_user(self):
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
@ -128,7 +122,7 @@ class RuleListViewTestCase(TestCase):
|
||||||
|
|
||||||
response = self.client.get(reverse("api:news:collection:rules-list"))
|
response = self.client.get(reverse("api:news:collection:rules-list"))
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_rules_with_unauthorized_user(self):
|
def test_rules_with_unauthorized_user(self):
|
||||||
other_user = UserFactory()
|
other_user = UserFactory()
|
||||||
|
|
@ -137,10 +131,108 @@ class RuleListViewTestCase(TestCase):
|
||||||
response = self.client.get(reverse("api:news:collection:rules-list"))
|
response = self.client.get(reverse("api:news:collection:rules-list"))
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertEquals(data["count"], 0)
|
self.assertEqual(data["count"], 0)
|
||||||
self.assertEquals(len(data["results"]), 0)
|
self.assertEqual(len(data["results"]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class RuleListViewSearchTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = UserFactory(password="test")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_feed_rules(self):
|
||||||
|
rules = {
|
||||||
|
"foo": FeedFactory(name="foo", user=self.user),
|
||||||
|
"BarFoo": FeedFactory(name="BarFoo", user=self.user),
|
||||||
|
"FooBar": FeedFactory(name="FooBar", user=self.user),
|
||||||
|
"boo": FeedFactory(name="boo", user=self.user),
|
||||||
|
"Boobar": FeedFactory(name="Boobar", user=self.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
params = urlencode({"search": "foo"})
|
||||||
|
url = reverse("api:news:collection:rules-list")
|
||||||
|
|
||||||
|
response = self.client.get(f"{url}?{params}")
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
self.assertEqual(response_data["count"], 3)
|
||||||
|
self.assertEqual(response_data["results"][0]["id"], rules["BarFoo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][1]["id"], rules["foo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][2]["id"], rules["FooBar"].pk)
|
||||||
|
|
||||||
|
def test_twitter_profiles(self):
|
||||||
|
rules = {
|
||||||
|
"foo": TwitterTimelineFactory(
|
||||||
|
name="foo", screen_name="foo", user=self.user
|
||||||
|
),
|
||||||
|
"BarFoo": TwitterTimelineFactory(
|
||||||
|
name="BarFoo", screen_name="foodiddy", user=self.user
|
||||||
|
),
|
||||||
|
"BarFoo dup": TwitterTimelineFactory(
|
||||||
|
name="BarFoo", screen_name="foodadda", user=self.user
|
||||||
|
),
|
||||||
|
"something else": TwitterTimelineFactory(
|
||||||
|
name="something else", screen_name="foo", user=self.user
|
||||||
|
),
|
||||||
|
"not found": TwitterTimelineFactory(
|
||||||
|
name="no result", screen_name="boo", user=self.user
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
params = urlencode({"search": "foo"})
|
||||||
|
url = reverse("api:news:collection:rules-list")
|
||||||
|
|
||||||
|
response = self.client.get(f"{url}?{params}")
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
self.assertEqual(response_data["count"], 4)
|
||||||
|
self.assertEqual(response_data["results"][0]["id"], rules["BarFoo dup"].pk)
|
||||||
|
self.assertEqual(response_data["results"][1]["id"], rules["BarFoo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][2]["id"], rules["foo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][3]["id"], rules["something else"].pk)
|
||||||
|
|
||||||
|
def test_subreddits(self):
|
||||||
|
rules = {
|
||||||
|
"foo": SubredditFactory(name="foo", user=self.user),
|
||||||
|
"BarFoo": SubredditFactory(name="BarFoo", user=self.user),
|
||||||
|
"FooBar": SubredditFactory(name="FooBar", user=self.user),
|
||||||
|
"boo": SubredditFactory(name="boo", user=self.user),
|
||||||
|
"Boobar": SubredditFactory(name="Boobar", user=self.user),
|
||||||
|
}
|
||||||
|
|
||||||
|
params = urlencode({"search": "foo"})
|
||||||
|
url = reverse("api:news:collection:rules-list")
|
||||||
|
|
||||||
|
response = self.client.get(f"{url}?{params}")
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
self.assertEqual(response_data["count"], 3)
|
||||||
|
self.assertEqual(response_data["results"][0]["id"], rules["BarFoo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][1]["id"], rules["foo"].pk)
|
||||||
|
self.assertEqual(response_data["results"][2]["id"], rules["FooBar"].pk)
|
||||||
|
|
||||||
|
@freeze_time("2020-10-30 14:00")
|
||||||
|
def test_ratelimitting(self):
|
||||||
|
# Trigger ratelimit
|
||||||
|
cache.set(
|
||||||
|
f"throttle_burst_search_{self.user.pk}", [time.time() for i in range(100)]
|
||||||
|
)
|
||||||
|
|
||||||
|
params = urlencode({"search": "foo"})
|
||||||
|
url = reverse("api:news:collection:rules-list")
|
||||||
|
|
||||||
|
response = self.client.get(f"{url}?{params}")
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 429)
|
||||||
|
|
||||||
|
message = response_data["detail"]
|
||||||
|
|
||||||
|
self.assertIn("Request was throttled", message)
|
||||||
|
|
||||||
|
cache.delete(f"throttle_burst_search_{self.user.pk}")
|
||||||
|
|
||||||
|
|
||||||
class NestedRuleListViewTestCase(TestCase):
|
class NestedRuleListViewTestCase(TestCase):
|
||||||
|
|
@ -157,11 +249,11 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
self.assertEquals(data["count"], 5)
|
self.assertEqual(data["count"], 5)
|
||||||
|
|
||||||
def test_pagination(self):
|
def test_pagination(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -178,11 +270,11 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEquals(data["count"], 80)
|
self.assertEqual(data["count"], 80)
|
||||||
self.assertEquals(len(data["results"]), 30)
|
self.assertEqual(len(data["results"]), 30)
|
||||||
|
|
||||||
self.assertEquals(
|
self.assertEqual(
|
||||||
[post["id"] for post in data["results"]], [post.id for post in posts[:30]]
|
[post["id"] for post in data["results"]], [post.id for post in posts[:30]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -194,16 +286,16 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEquals(data["count"], 0)
|
self.assertEqual(data["count"], 0)
|
||||||
self.assertEquals(len(data["results"]), 0)
|
self.assertEqual(len(data["results"]), 0)
|
||||||
|
|
||||||
def test_not_known(self):
|
def test_not_known(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": 0})
|
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": 0})
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -215,8 +307,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
self.assertEqual(data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
def test_patch(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -228,8 +320,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
self.assertEqual(data["detail"], 'Method "PATCH" not allowed.')
|
||||||
|
|
||||||
def test_put(self):
|
def test_put(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -241,8 +333,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
self.assertEqual(data["detail"], 'Method "PUT" not allowed.')
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -254,8 +346,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
self.assertEqual(data["detail"], 'Method "DELETE" not allowed.')
|
||||||
|
|
||||||
def test_rule_with_unauthenticated_user(self):
|
def test_rule_with_unauthenticated_user(self):
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
@ -266,7 +358,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk})
|
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_rule_with_unauthorized_user(self):
|
def test_rule_with_unauthorized_user(self):
|
||||||
other_user = UserFactory()
|
other_user = UserFactory()
|
||||||
|
|
@ -276,7 +368,7 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk})
|
reverse("api:news:collection:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_posts_ordering(self):
|
def test_posts_ordering(self):
|
||||||
rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user))
|
rule = FeedFactory(user=self.user, category=CategoryFactory(user=self.user))
|
||||||
|
|
@ -285,23 +377,17 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
FeedPostFactory(
|
FeedPostFactory(
|
||||||
title="I'm the first post",
|
title="I'm the first post",
|
||||||
rule=rule,
|
rule=rule,
|
||||||
publication_date=datetime.combine(
|
publication_date=datetime(2019, 5, 20, 16, 7, 37, tzinfo=pytz.utc),
|
||||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
FeedPostFactory(
|
FeedPostFactory(
|
||||||
title="I'm the second post",
|
title="I'm the second post",
|
||||||
rule=rule,
|
rule=rule,
|
||||||
publication_date=datetime.combine(
|
publication_date=datetime(2019, 7, 20, 18, 7, 37, tzinfo=pytz.utc),
|
||||||
date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
FeedPostFactory(
|
FeedPostFactory(
|
||||||
title="I'm the third post",
|
title="I'm the third post",
|
||||||
rule=rule,
|
rule=rule,
|
||||||
publication_date=datetime.combine(
|
publication_date=datetime(2019, 7, 20, 16, 7, 37, tzinfo=pytz.utc),
|
||||||
date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -310,14 +396,14 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
self.assertEquals(data["count"], 3)
|
self.assertEqual(data["count"], 3)
|
||||||
|
|
||||||
self.assertEquals(data["results"][0]["id"], posts[1].pk)
|
self.assertEqual(data["results"][0]["id"], posts[1].pk)
|
||||||
self.assertEquals(data["results"][1]["id"], posts[2].pk)
|
self.assertEqual(data["results"][1]["id"], posts[2].pk)
|
||||||
self.assertEquals(data["results"][2]["id"], posts[0].pk)
|
self.assertEqual(data["results"][2]["id"], posts[0].pk)
|
||||||
|
|
||||||
def test_only_posts_from_rule_are_returned(self):
|
def test_only_posts_from_rule_are_returned(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -331,14 +417,14 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertTrue("results" in data)
|
self.assertTrue("results" in data)
|
||||||
self.assertTrue("count" in data)
|
self.assertTrue("count" in data)
|
||||||
self.assertEquals(data["count"], 5)
|
self.assertEqual(data["count"], 5)
|
||||||
|
|
||||||
for post in data["results"]:
|
for post in data["results"]:
|
||||||
self.assertEquals(post["rule"], rule.pk)
|
self.assertEqual(post["rule"], rule.pk)
|
||||||
|
|
||||||
def test_unread_posts(self):
|
def test_unread_posts(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -354,11 +440,11 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
data = response.json()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEquals(data["count"], 10)
|
self.assertEqual(data["count"], 10)
|
||||||
|
|
||||||
for post in posts:
|
for post in posts:
|
||||||
self.assertEquals(post["read"], False)
|
self.assertEqual(post["read"], False)
|
||||||
|
|
||||||
def test_read_posts(self):
|
def test_read_posts(self):
|
||||||
rule = FeedFactory.create(user=self.user)
|
rule = FeedFactory.create(user=self.user)
|
||||||
|
|
@ -374,8 +460,8 @@ class NestedRuleListViewTestCase(TestCase):
|
||||||
data = response.json()
|
data = response.json()
|
||||||
posts = data["results"]
|
posts = data["results"]
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEquals(data["count"], 10)
|
self.assertEqual(data["count"], 10)
|
||||||
|
|
||||||
for post in posts:
|
for post in posts:
|
||||||
self.assertEquals(post["read"], True)
|
self.assertEqual(post["read"], True)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from django import forms
|
||||||
from django.forms.widgets import CheckboxSelectMultiple
|
from django.forms.widgets import CheckboxSelectMultiple
|
||||||
|
|
||||||
from newsreader.accounts.models import User
|
from newsreader.accounts.models import User
|
||||||
|
from newsreader.core.forms import MultiAutoCompleteWidget
|
||||||
from newsreader.news.collection.models import CollectionRule
|
from newsreader.news.collection.models import CollectionRule
|
||||||
from newsreader.news.core.models import Category
|
from newsreader.news.core.models import Category
|
||||||
|
|
||||||
|
|
@ -21,7 +22,9 @@ class RulesWidget(CheckboxSelectMultiple):
|
||||||
|
|
||||||
class CategoryForm(forms.ModelForm):
|
class CategoryForm(forms.ModelForm):
|
||||||
rules = forms.ModelMultipleChoiceField(
|
rules = forms.ModelMultipleChoiceField(
|
||||||
required=False, queryset=CollectionRule.objects.none(), widget=RulesWidget
|
required=False,
|
||||||
|
queryset=CollectionRule.objects.none(),
|
||||||
|
widget=MultiAutoCompleteWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
|
|
@ -35,7 +38,6 @@ class CategoryForm(forms.ModelForm):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user)
|
self.fields["rules"].queryset = CollectionRule.objects.filter(user=self.user)
|
||||||
self.fields["rules"].widget.category = self.instance
|
|
||||||
|
|
||||||
self.fields["user"].queryset = User.objects.filter(pk=self.user.pk)
|
self.fields["user"].queryset = User.objects.filter(pk=self.user.pk)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,6 @@ class CategoryDetailMixin:
|
||||||
success_url = reverse_lazy("news:core:categories")
|
success_url = reverse_lazy("news:core:categories")
|
||||||
form_class = CategoryForm
|
form_class = CategoryForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context_data = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
rules = CollectionRule.objects.filter(user=self.request.user).order_by("name")
|
|
||||||
context_data["rules"] = rules
|
|
||||||
|
|
||||||
return context_data
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
return {**super().get_form_kwargs(), "user": self.request.user}
|
return {**super().get_form_kwargs(), "user": self.request.user}
|
||||||
|
|
||||||
|
|
@ -63,12 +55,15 @@ class CategoryListView(CategoryViewMixin, ListView):
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
|
rules = CollectionRule.objects.filter(user=self.request.user).order_by("name")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**context,
|
**context,
|
||||||
"categories_create_url": reverse_lazy("news:core:category-create"),
|
"categories_create_url": reverse_lazy("news:core:category-create"),
|
||||||
"categories_update_url": (
|
"categories_update_url": (
|
||||||
reverse_lazy("news:core:category-update", args=(0,))
|
reverse_lazy("news:core:category-update", args=(0,))
|
||||||
),
|
),
|
||||||
|
"rules": rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue