Compare commits

...
Sign in to create a new pull request.

4 commits

8 changed files with 250 additions and 102 deletions

View file

@ -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 = {

View file

@ -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

View 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"

View 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;

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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,
} }