mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-10 06:04:15 +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 django.urls import reverse
|
||||||
|
|
||||||
from bookmarks import queries
|
from bookmarks import queries
|
||||||
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken
|
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken, UserProfile
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FeedContext:
|
class FeedContext:
|
||||||
feed_token: FeedToken
|
feed_token: FeedToken | None
|
||||||
query_set: QuerySet[Bookmark]
|
query_set: QuerySet[Bookmark]
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,3 +67,39 @@ class UnreadBookmarksFeed(BaseBookmarksFeed):
|
||||||
|
|
||||||
def items(self, context: FeedContext):
|
def items(self, context: FeedContext):
|
||||||
return context.query_set.filter(unread=True)
|
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">
|
<section class="content-area">
|
||||||
<h2>RSS Feeds</h2>
|
<h2>RSS Feeds</h2>
|
||||||
<p>The following URLs provide RSS feeds for your bookmarks:</p>
|
<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="{{ all_feed_url }}">All bookmarks</a></li>
|
||||||
<li><a href="{{ unread_feed_url }}">Unread 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>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
All URLs support appending a <code>q</code> URL parameter for specifying a search query.
|
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)
|
||||||
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}"
|
url = feed_url + f"?q={bookmark1.title}"
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
@ -229,3 +229,172 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
self.assertContains(response, "<item>", count=0)
|
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>',
|
f'<a href="http://testserver/feeds/{token.key}/unread">Unread bookmarks</a>',
|
||||||
html,
|
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 import views
|
||||||
from bookmarks.api.routes import router
|
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
|
from bookmarks.views import partials
|
||||||
|
|
||||||
app_name = "bookmarks"
|
app_name = "bookmarks"
|
||||||
|
@ -77,6 +82,8 @@ urlpatterns = [
|
||||||
# Feeds
|
# Feeds
|
||||||
path("feeds/<str:feed_key>/all", AllBookmarksFeed(), name="feeds.all"),
|
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>/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
|
# Health check
|
||||||
path("health", views.health, name="health"),
|
path("health", views.health, name="health"),
|
||||||
# Manifest
|
# Manifest
|
||||||
|
|
|
@ -114,6 +114,12 @@ def integrations(request):
|
||||||
unread_feed_url = request.build_absolute_uri(
|
unread_feed_url = request.build_absolute_uri(
|
||||||
reverse("bookmarks:feeds.unread", args=[feed_token.key])
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
"settings/integrations.html",
|
"settings/integrations.html",
|
||||||
|
@ -122,6 +128,8 @@ def integrations(request):
|
||||||
"api_token": api_token.key,
|
"api_token": api_token.key,
|
||||||
"all_feed_url": all_feed_url,
|
"all_feed_url": all_feed_url,
|
||||||
"unread_feed_url": unread_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