mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-22 03:13:02 +00:00
Add RSS feeds for shared bookmarks (#656)
* Add shared bookmarks feed * Add public shared bookmarks feed
This commit is contained in:
parent
afb752765d
commit
d0d5c15345
6 changed files with 235 additions and 5 deletions
|
@ -6,12 +6,12 @@ from django.db.models import QuerySet
|
|||
from django.urls import reverse
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken
|
||||
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken, UserProfile
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeedContext:
|
||||
feed_token: FeedToken
|
||||
feed_token: FeedToken | None
|
||||
query_set: QuerySet[Bookmark]
|
||||
|
||||
|
||||
|
@ -67,3 +67,39 @@ class UnreadBookmarksFeed(BaseBookmarksFeed):
|
|||
|
||||
def items(self, context: FeedContext):
|
||||
return context.query_set.filter(unread=True)
|
||||
|
||||
|
||||
class SharedBookmarksFeed(BaseBookmarksFeed):
|
||||
title = "Shared bookmarks"
|
||||
description = "All shared bookmarks"
|
||||
|
||||
def get_object(self, request, feed_key: str):
|
||||
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
||||
search = BookmarkSearch(q=request.GET.get("q", ""))
|
||||
query_set = queries.query_shared_bookmarks(
|
||||
None, feed_token.user.profile, search, False
|
||||
)
|
||||
return FeedContext(feed_token, query_set)
|
||||
|
||||
def link(self, context: FeedContext):
|
||||
return reverse("bookmarks:feeds.shared", args=[context.feed_token.key])
|
||||
|
||||
def items(self, context: FeedContext):
|
||||
return context.query_set
|
||||
|
||||
|
||||
class PublicSharedBookmarksFeed(BaseBookmarksFeed):
|
||||
title = "Public shared bookmarks"
|
||||
description = "All public shared bookmarks"
|
||||
|
||||
def get_object(self, request):
|
||||
search = BookmarkSearch(q=request.GET.get("q", ""))
|
||||
default_profile = UserProfile()
|
||||
query_set = queries.query_shared_bookmarks(None, default_profile, search, True)
|
||||
return FeedContext(None, query_set)
|
||||
|
||||
def link(self, context: FeedContext):
|
||||
return reverse("bookmarks:feeds.public_shared")
|
||||
|
||||
def items(self, context: FeedContext):
|
||||
return context.query_set
|
||||
|
|
|
@ -49,9 +49,11 @@
|
|||
<section class="content-area">
|
||||
<h2>RSS Feeds</h2>
|
||||
<p>The following URLs provide RSS feeds for your bookmarks:</p>
|
||||
<ul>
|
||||
<ul style="list-style-position: outside;">
|
||||
<li><a href="{{ all_feed_url }}">All bookmarks</a></li>
|
||||
<li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li>
|
||||
<li><a href="{{ shared_feed_url }}">Shared bookmarks</a></li>
|
||||
<li><a href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-gray">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span></li>
|
||||
</ul>
|
||||
<p>
|
||||
All URLs support appending a <code>q</code> URL parameter for specifying a search query.
|
||||
|
|
|
@ -194,7 +194,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.setup_bookmark(unread=True)
|
||||
self.setup_bookmark(unread=True)
|
||||
|
||||
feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
|
||||
feed_url = reverse("bookmarks:feeds.unread", args=[self.token.key])
|
||||
|
||||
url = feed_url + f"?q={bookmark1.title}"
|
||||
response = self.client.get(url)
|
||||
|
@ -229,3 +229,172 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "<item>", count=0)
|
||||
|
||||
def test_shared_returns_404_for_unknown_feed_token(self):
|
||||
response = self.client.get(reverse("bookmarks:feeds.shared", args=["foo"]))
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_shared_metadata(self):
|
||||
feed_url = reverse("bookmarks:feeds.shared", args=[self.token.key])
|
||||
response = self.client.get(feed_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "<title>Shared bookmarks</title>")
|
||||
self.assertContains(response, "<description>All shared bookmarks</description>")
|
||||
self.assertContains(response, f"<link>http://testserver{feed_url}</link>")
|
||||
self.assertContains(
|
||||
response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>'
|
||||
)
|
||||
|
||||
def test_shared_returns_shared_bookmarks_only(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=False)
|
||||
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(shared=False, user=user1)
|
||||
self.setup_bookmark(shared=True, user=user2)
|
||||
|
||||
shared_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("bookmarks:feeds.shared", args=[self.token.key])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "<item>", count=len(shared_bookmarks))
|
||||
|
||||
for bookmark in shared_bookmarks:
|
||||
expected_item = (
|
||||
"<item>"
|
||||
f"<title>{bookmark.resolved_title}</title>"
|
||||
f"<link>{bookmark.url}</link>"
|
||||
f"<description>{bookmark.resolved_description}</description>"
|
||||
f"<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>"
|
||||
f"<guid>{bookmark.url}</guid>"
|
||||
"</item>"
|
||||
)
|
||||
self.assertContains(response, expected_item, count=1)
|
||||
|
||||
def test_shared_with_query(self):
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
|
||||
tag1 = self.setup_tag(user=user)
|
||||
bookmark1 = self.setup_bookmark(shared=True, user=user)
|
||||
bookmark2 = self.setup_bookmark(shared=True, tags=[tag1], user=user)
|
||||
bookmark3 = self.setup_bookmark(shared=True, tags=[tag1], user=user)
|
||||
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
|
||||
feed_url = reverse("bookmarks:feeds.shared", args=[self.token.key])
|
||||
|
||||
url = feed_url + f"?q={bookmark1.title}"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark1.url}</guid>", count=1)
|
||||
|
||||
url = feed_url + "?q=" + urllib.parse.quote("#" + tag1.name)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=2)
|
||||
self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark3.url}</guid>", count=1)
|
||||
|
||||
url = feed_url + "?q=" + urllib.parse.quote(f"#{tag1.name} {bookmark2.title}")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
|
||||
|
||||
def test_public_shared_does_not_require_auth(self):
|
||||
response = self.client.get(reverse("bookmarks:feeds.public_shared"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_public_shared_metadata(self):
|
||||
feed_url = reverse("bookmarks:feeds.public_shared")
|
||||
response = self.client.get(feed_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "<title>Public shared bookmarks</title>")
|
||||
self.assertContains(
|
||||
response, "<description>All public shared bookmarks</description>"
|
||||
)
|
||||
self.assertContains(response, f"<link>http://testserver{feed_url}</link>")
|
||||
self.assertContains(
|
||||
response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>'
|
||||
)
|
||||
|
||||
def test_public_shared_returns_publicly_shared_bookmarks_only(self):
|
||||
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=False)
|
||||
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(shared=False, user=user1)
|
||||
self.setup_bookmark(shared=False, user=user2)
|
||||
self.setup_bookmark(shared=True, user=user2)
|
||||
self.setup_bookmark(shared=True, user=user3)
|
||||
|
||||
public_shared_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
self.setup_bookmark(shared=True, user=user1, description="test"),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse("bookmarks:feeds.public_shared"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "<item>", count=len(public_shared_bookmarks))
|
||||
|
||||
for bookmark in public_shared_bookmarks:
|
||||
expected_item = (
|
||||
"<item>"
|
||||
f"<title>{bookmark.resolved_title}</title>"
|
||||
f"<link>{bookmark.url}</link>"
|
||||
f"<description>{bookmark.resolved_description}</description>"
|
||||
f"<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>"
|
||||
f"<guid>{bookmark.url}</guid>"
|
||||
"</item>"
|
||||
)
|
||||
self.assertContains(response, expected_item, count=1)
|
||||
|
||||
def test_public_shared_with_query(self):
|
||||
user = self.setup_user(enable_sharing=True, enable_public_sharing=True)
|
||||
|
||||
tag1 = self.setup_tag(user=user)
|
||||
bookmark1 = self.setup_bookmark(shared=True, user=user)
|
||||
bookmark2 = self.setup_bookmark(shared=True, tags=[tag1], user=user)
|
||||
bookmark3 = self.setup_bookmark(shared=True, tags=[tag1], user=user)
|
||||
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
|
||||
feed_url = reverse("bookmarks:feeds.shared", args=[self.token.key])
|
||||
|
||||
url = feed_url + f"?q={bookmark1.title}"
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark1.url}</guid>", count=1)
|
||||
|
||||
url = feed_url + "?q=" + urllib.parse.quote("#" + tag1.name)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=2)
|
||||
self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark3.url}</guid>", count=1)
|
||||
|
||||
url = feed_url + "?q=" + urllib.parse.quote(f"#{tag1.name} {bookmark2.title}")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<item>", count=1)
|
||||
self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
|
||||
|
|
|
@ -74,3 +74,11 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
f'<a href="http://testserver/feeds/{token.key}/unread">Unread bookmarks</a>',
|
||||
html,
|
||||
)
|
||||
self.assertInHTML(
|
||||
f'<a href="http://testserver/feeds/{token.key}/shared">Shared bookmarks</a>',
|
||||
html,
|
||||
)
|
||||
self.assertInHTML(
|
||||
f'<a href="http://testserver/feeds/shared">Public shared bookmarks</a>',
|
||||
html,
|
||||
)
|
||||
|
|
|
@ -4,7 +4,12 @@ from django.views.generic import RedirectView
|
|||
|
||||
from bookmarks import views
|
||||
from bookmarks.api.routes import router
|
||||
from bookmarks.feeds import AllBookmarksFeed, UnreadBookmarksFeed
|
||||
from bookmarks.feeds import (
|
||||
AllBookmarksFeed,
|
||||
UnreadBookmarksFeed,
|
||||
SharedBookmarksFeed,
|
||||
PublicSharedBookmarksFeed,
|
||||
)
|
||||
from bookmarks.views import partials
|
||||
|
||||
app_name = "bookmarks"
|
||||
|
@ -77,6 +82,8 @@ urlpatterns = [
|
|||
# Feeds
|
||||
path("feeds/<str:feed_key>/all", AllBookmarksFeed(), name="feeds.all"),
|
||||
path("feeds/<str:feed_key>/unread", UnreadBookmarksFeed(), name="feeds.unread"),
|
||||
path("feeds/<str:feed_key>/shared", SharedBookmarksFeed(), name="feeds.shared"),
|
||||
path("feeds/shared", PublicSharedBookmarksFeed(), name="feeds.public_shared"),
|
||||
# Health check
|
||||
path("health", views.health, name="health"),
|
||||
# Manifest
|
||||
|
|
|
@ -114,6 +114,12 @@ def integrations(request):
|
|||
unread_feed_url = request.build_absolute_uri(
|
||||
reverse("bookmarks:feeds.unread", args=[feed_token.key])
|
||||
)
|
||||
shared_feed_url = request.build_absolute_uri(
|
||||
reverse("bookmarks:feeds.shared", args=[feed_token.key])
|
||||
)
|
||||
public_shared_feed_url = request.build_absolute_uri(
|
||||
reverse("bookmarks:feeds.public_shared")
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"settings/integrations.html",
|
||||
|
@ -122,6 +128,8 @@ def integrations(request):
|
|||
"api_token": api_token.key,
|
||||
"all_feed_url": all_feed_url,
|
||||
"unread_feed_url": unread_feed_url,
|
||||
"shared_feed_url": shared_feed_url,
|
||||
"public_shared_feed_url": public_shared_feed_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue