Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions bookmarks/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.urls import reverse

from bookmarks import queries
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken, UserProfile
from bookmarks.models import Bookmark, BookmarkSearch, FeedToken, User, UserProfile
from bookmarks.views import access


Expand All @@ -18,6 +18,10 @@ class FeedContext:
query_set: QuerySet[Bookmark]


def resolve_user(search: BookmarkSearch) -> User | None:
return User.objects.filter(username=search.user).first() if search.user else None


def sanitize(text: str):
if not text:
return ""
Expand All @@ -38,6 +42,7 @@ def get_object(self, request, feed_key: str | None):

search = BookmarkSearch(
q=request.GET.get("q", ""),
user=request.GET.get("user", ""),
unread=request.GET.get("unread", ""),
shared=request.GET.get("shared", ""),
bundle=bundle,
Expand Down Expand Up @@ -99,8 +104,11 @@ class SharedBookmarksFeed(BaseBookmarksFeed):
description = "All shared bookmarks"

def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
user = resolve_user(search)
if search.user and not user:
return Bookmark.objects.none()
return queries.query_shared_bookmarks(
None, feed_token.user.profile, search, False
user, feed_token.user.profile, search, False
)

def link(self, context: FeedContext):
Expand All @@ -115,7 +123,10 @@ def get_object(self, request):
return super().get_object(request, None)

def get_query_set(self, feed_token: FeedToken, search: BookmarkSearch):
return queries.query_shared_bookmarks(None, UserProfile(), search, True)
user = resolve_user(search)
if search.user and not user:
return Bookmark.objects.none()
return queries.query_shared_bookmarks(user, UserProfile(), search, True)

def link(self, context: FeedContext):
return reverse("linkding:feeds.public_shared")
12 changes: 3 additions & 9 deletions bookmarks/templates/bookmarks/tag_cloud.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
{% if tag_cloud.has_selected_tags %}
<p class="selected-tags">
{% for tag in tag_cloud.selected_tags %}
<a href="?{{ tag.query_string }}" class="text-bold mr-2">
<span>-{{ tag.name }}</span>
</a>
<a href="?{{ tag.query_string }}" class="text-bold mr-2"><span>-{{ tag.name }}</span></a>
{% endfor %}
</p>
{% endif %}
Expand All @@ -16,14 +14,10 @@
{% for tag in group.tags %}
{# Highlight first char of first tag in group if grouping is enabled #}
{% if group.highlight_first_char and forloop.counter == 1 %}
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
<span class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
</a>
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item><span class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span></a>
{% else %}
{# Render tags normally #}
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
<span>{{ tag.name }}</span>
</a>
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item><span>{{ tag.name }}</span></a>
{% endif %}
{% endfor %}
</p>
Expand Down
5 changes: 4 additions & 1 deletion bookmarks/templates/settings/integrations.html
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ <h2 id="rss-feeds-heading">RSS Feeds</h2>
<span class="text-small text-secondary">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 the following URL parameters:</p>
<p>Feed URLs support the following URL parameters:</p>
<ul style="list-style-position: outside;">
<li>
A <code>limit</code> parameter for specifying the maximum number of bookmarks to include in the feed. By
Expand All @@ -220,6 +220,9 @@ <h2 id="rss-feeds-heading">RSS Feeds</h2>
A <code>shared</code> parameter for filtering for shared or unshared bookmarks. Use <code>yes</code> for
shared bookmarks and <code>no</code> for unshared bookmarks.
</li>
<li>
A <code>user</code> parameter for filtering bookmarks by user name. Only applies to the shared bookmark feeds.
</li>
</ul>
<p>
<strong>Please note that these URLs include an authentication token that should be treated like any other
Expand Down
62 changes: 62 additions & 0 deletions bookmarks/tests/test_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,37 @@ def test_shared_returns_shared_bookmarks_only(self):
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, shared_bookmarks)

def test_shared_user_parameter(self):
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)

user1_bookmarks = [
self.setup_bookmark(shared=True, user=user1),
self.setup_bookmark(shared=True, user=user1),
]
user2_bookmarks = [self.setup_bookmark(shared=True, user=user2)]

feed_url = reverse("linkding:feeds.shared", args=[self.token.key])

# without user parameter
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user1_bookmarks + user2_bookmarks)

# with user parameter
response = self.client.get(feed_url + f"?user={user1.username}")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user1_bookmarks)

response = self.client.get(feed_url + f"?user={user2.username}")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user2_bookmarks)

# with unknown user
response = self.client.get(feed_url + "?user=unknown")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, [])

def test_public_shared_does_not_require_auth(self):
response = self.client.get(reverse("linkding:feeds.public_shared"))

Expand Down Expand Up @@ -224,6 +255,37 @@ def test_public_shared_returns_publicly_shared_bookmarks_only(self):
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, public_shared_bookmarks)

def test_public_shared_user_parameter(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True, enable_public_sharing=True)

user1_bookmarks = [
self.setup_bookmark(shared=True, user=user1),
self.setup_bookmark(shared=True, user=user1),
]
user2_bookmarks = [self.setup_bookmark(shared=True, user=user2)]

feed_url = reverse("linkding:feeds.public_shared")

# without user parameter
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user1_bookmarks + user2_bookmarks)

# with user parameter
response = self.client.get(feed_url + f"?user={user1.username}")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user1_bookmarks)

response = self.client.get(feed_url + f"?user={user2.username}")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, user2_bookmarks)

# with unknown user
response = self.client.get(feed_url + "?user=unknown")
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, [])

def test_with_query(self):
tag1 = self.setup_tag()
bookmark1 = self.setup_bookmark()
Expand Down
25 changes: 25 additions & 0 deletions bookmarks/tests/test_tag_cloud_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,3 +546,28 @@ def test_with_anonymous_user(self):
""",
rendered_template,
)

def test_tag_links_have_no_whitespace_around_contents(self):
tags = [
self.setup_tag(name="tag1"),
self.setup_tag(name="tag2"),
self.setup_tag(name="tag3"),
]
self.setup_bookmark(tags=tags)

rendered_template = self.render_template(url="/test?q=%23tag1")

soup = self.make_soup(rendered_template)
link_elements = soup.select(".tag-cloud a")
self.assertEqual(len(link_elements), 3)

for link_element in link_elements:
contents = link_element.decode_contents()
self.assertTrue(
contents.startswith("<span"),
f"unexpected characters after opening anchor tag: {contents!r}",
)
self.assertTrue(
contents.endswith("</span>"),
f"unexpected characters before closing anchor tag: {contents!r}",
)
Loading