Add bundles for organizing bookmarks (#1097)

* add bundle model and query logic

* cleanup tests

* add basic form

* add success message

* Add form tests

* Add bundle list view

* fix edit view

* Add remove button

* Add basic preview logic

* Make pagination use absolute URLs

* Hide bookmark edits when rendering preview

* Render bookmark list in preview

* Reorder bundles

* Show bundles in bookmark view

* Make bookmark search respect selected bundle

* UI tweaks

* Fix bookmark scope

* Improve bundle preview

* Skip preview if form is submitted

* Show correct preview after invalid form submission

* Add option to hide bundles

* Merge new migrations

* Add tests for bundle menu

* Improve check for preview being removed
This commit is contained in:
Sascha Ißbrücker
2025-06-19 16:47:29 +02:00
committed by GitHub
parent 8be72a5d1f
commit 1672dc0152
59 changed files with 2290 additions and 267 deletions

View File

@@ -2,16 +2,23 @@ from django.http import QueryDict
from django.test import TestCase
from bookmarks.models import BookmarkSearch
from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkSearchModelTest(TestCase):
class MockRequest:
def __init__(self, user):
self.user = user
class BookmarkSearchModelTest(TestCase, BookmarkFactoryMixin):
def test_from_request(self):
# no params
query_dict = QueryDict()
search = BookmarkSearch.from_request(query_dict)
search = BookmarkSearch.from_request(None, query_dict)
self.assertEqual(search.q, "")
self.assertEqual(search.user, "")
self.assertEqual(search.bundle, None)
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
@@ -19,7 +26,7 @@ class BookmarkSearchModelTest(TestCase):
# some params
query_dict = QueryDict("q=search query&user=user123")
bookmark_search = BookmarkSearch.from_request(query_dict)
bookmark_search = BookmarkSearch.from_request(None, query_dict)
self.assertEqual(bookmark_search.q, "search query")
self.assertEqual(bookmark_search.user, "user123")
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
@@ -27,13 +34,16 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
# all params
bundle = self.setup_bundle()
request = MockRequest(self.get_or_create_test_user())
query_dict = QueryDict(
"q=search query&sort=title_asc&user=user123&shared=yes&unread=yes"
f"q=search query&sort=title_asc&user=user123&bundle={bundle.id}&shared=yes&unread=yes"
)
search = BookmarkSearch.from_request(query_dict)
search = BookmarkSearch.from_request(request, query_dict)
self.assertEqual(search.q, "search query")
self.assertEqual(search.user, "user123")
self.assertEqual(search.bundle, bundle)
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
@@ -45,7 +55,7 @@ class BookmarkSearchModelTest(TestCase):
}
query_dict = QueryDict("q=search query")
search = BookmarkSearch.from_request(query_dict, preferences)
search = BookmarkSearch.from_request(None, query_dict, preferences)
self.assertEqual(search.q, "search query")
self.assertEqual(search.user, "")
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
@@ -60,13 +70,110 @@ class BookmarkSearchModelTest(TestCase):
}
query_dict = QueryDict("sort=title_desc&shared=no&unread=off")
search = BookmarkSearch.from_request(query_dict, preferences)
search = BookmarkSearch.from_request(None, query_dict, preferences)
self.assertEqual(search.q, "")
self.assertEqual(search.user, "")
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
def test_from_request_ignores_invalid_bundle_param(self):
self.setup_bundle()
# bundle does not exist
request = MockRequest(self.get_or_create_test_user())
query_dict = QueryDict("bundle=99999")
search = BookmarkSearch.from_request(request, query_dict)
self.assertIsNone(search.bundle)
# bundle belongs to another user
other_user = self.setup_user()
bundle = self.setup_bundle(user=other_user)
query_dict = QueryDict(f"bundle={bundle.id}")
search = BookmarkSearch.from_request(request, query_dict)
self.assertIsNone(search.bundle)
def test_query_params(self):
# no params
search = BookmarkSearch()
self.assertEqual(search.query_params, {})
# params are default values
search = BookmarkSearch(
q="", sort=BookmarkSearch.SORT_ADDED_DESC, user="", bundle=None, shared=""
)
self.assertEqual(search.query_params, {})
# some modified params
search = BookmarkSearch(q="search query", sort=BookmarkSearch.SORT_ADDED_ASC)
self.assertEqual(
search.query_params,
{"q": "search query", "sort": BookmarkSearch.SORT_ADDED_ASC},
)
# all modified params
bundle = self.setup_bundle()
search = BookmarkSearch(
q="search query",
sort=BookmarkSearch.SORT_ADDED_ASC,
user="user123",
bundle=bundle,
shared=BookmarkSearch.FILTER_SHARED_SHARED,
unread=BookmarkSearch.FILTER_UNREAD_YES,
)
self.assertEqual(
search.query_params,
{
"q": "search query",
"sort": BookmarkSearch.SORT_ADDED_ASC,
"user": "user123",
"bundle": bundle.id,
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# preferences are not query params if they match default
preferences = {
"sort": BookmarkSearch.SORT_TITLE_ASC,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
}
search = BookmarkSearch(preferences=preferences)
self.assertEqual(search.query_params, {})
# param is not a query param if it matches the preference
preferences = {
"sort": BookmarkSearch.SORT_TITLE_ASC,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
}
search = BookmarkSearch(
sort=BookmarkSearch.SORT_TITLE_ASC,
unread=BookmarkSearch.FILTER_UNREAD_YES,
preferences=preferences,
)
self.assertEqual(search.query_params, {})
# overriding preferences is a query param
preferences = {
"sort": BookmarkSearch.SORT_TITLE_ASC,
"shared": BookmarkSearch.FILTER_SHARED_SHARED,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
}
search = BookmarkSearch(
sort=BookmarkSearch.SORT_TITLE_DESC,
shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
unread=BookmarkSearch.FILTER_UNREAD_OFF,
preferences=preferences,
)
self.assertEqual(
search.query_params,
{
"sort": BookmarkSearch.SORT_TITLE_DESC,
"shared": BookmarkSearch.FILTER_SHARED_UNSHARED,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
def test_modified_params(self):
# no params
bookmark_search = BookmarkSearch()
@@ -88,16 +195,18 @@ class BookmarkSearchModelTest(TestCase):
self.assertCountEqual(modified_params, ["q", "sort"])
# all modified params
bundle = self.setup_bundle()
bookmark_search = BookmarkSearch(
q="search query",
sort=BookmarkSearch.SORT_ADDED_ASC,
user="user123",
bundle=bundle,
shared=BookmarkSearch.FILTER_SHARED_SHARED,
unread=BookmarkSearch.FILTER_UNREAD_YES,
)
modified_params = bookmark_search.modified_params
self.assertCountEqual(
modified_params, ["q", "sort", "user", "shared", "unread"]
modified_params, ["q", "sort", "user", "bundle", "shared", "unread"]
)
# preferences are not modified params
@@ -180,7 +289,10 @@ class BookmarkSearchModelTest(TestCase):
)
# only returns preferences
bookmark_search = BookmarkSearch(q="search query", user="user123")
bundle = self.setup_bundle()
bookmark_search = BookmarkSearch(
q="search query", user="user123", bundle=bundle
)
self.assertEqual(
bookmark_search.preferences_dict,
{