diff --git a/bookmarks/models.py b/bookmarks/models.py
index 07ae443..ddcf4ec 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -134,12 +134,17 @@ class BookmarkSearch:
FILTER_SHARED_SHARED = 'shared'
FILTER_SHARED_UNSHARED = 'unshared'
- params = ['q', 'user', 'sort', 'shared']
+ FILTER_UNREAD_OFF = ''
+ FILTER_UNREAD_YES = 'yes'
+ FILTER_UNREAD_NO = 'no'
+
+ params = ['q', 'user', 'sort', 'shared', 'unread']
defaults = {
'q': '',
'user': '',
'sort': SORT_ADDED_DESC,
'shared': FILTER_SHARED_OFF,
+ 'unread': FILTER_UNREAD_OFF,
}
def __init__(self,
@@ -147,11 +152,13 @@ class BookmarkSearch:
query: str = defaults['q'], # alias for q
user: str = defaults['user'],
sort: str = defaults['sort'],
- shared: str = defaults['shared']):
+ shared: str = defaults['shared'],
+ unread: str = defaults['unread']):
self.q = q or query
self.user = user
self.sort = sort
self.shared = shared
+ self.unread = unread
@property
def query(self):
@@ -192,11 +199,17 @@ class BookmarkSearchForm(forms.Form):
(BookmarkSearch.FILTER_SHARED_SHARED, 'Shared'),
(BookmarkSearch.FILTER_SHARED_UNSHARED, 'Unshared'),
]
+ FILTER_UNREAD_CHOICES = [
+ (BookmarkSearch.FILTER_UNREAD_OFF, 'Off'),
+ (BookmarkSearch.FILTER_UNREAD_YES, 'Unread'),
+ (BookmarkSearch.FILTER_UNREAD_NO, 'Read'),
+ ]
q = forms.CharField()
user = forms.ChoiceField()
sort = forms.ChoiceField(choices=SORT_CHOICES)
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
+ unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect)
def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None):
super().__init__()
diff --git a/bookmarks/queries.py b/bookmarks/queries.py
index a529cd3..63d7895 100644
--- a/bookmarks/queries.py
+++ b/bookmarks/queries.py
@@ -63,12 +63,18 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
query_set = query_set.filter(
tags=None
)
- # Unread bookmarks
+ # Legacy unread bookmarks filter from query
if query['unread']:
query_set = query_set.filter(
unread=True
)
+ # Unread filter from bookmark search
+ if search.unread == BookmarkSearch.FILTER_UNREAD_YES:
+ query_set = query_set.filter(unread=True)
+ elif search.unread == BookmarkSearch.FILTER_UNREAD_NO:
+ query_set = query_set.filter(unread=False)
+
# Shared filter
if search.shared == BookmarkSearch.FILTER_SHARED_SHARED:
query_set = query_set.filter(shared=True)
diff --git a/bookmarks/styles/bookmark-page.scss b/bookmarks/styles/bookmark-page.scss
index 22cf14d..1fb92fc 100644
--- a/bookmarks/styles/bookmark-page.scss
+++ b/bookmarks/styles/bookmark-page.scss
@@ -87,6 +87,7 @@
}
.radio-group {
+ margin-bottom: $unit-1;
.form-label {
padding-bottom: 0;
}
diff --git a/bookmarks/templates/bookmarks/nav_menu.html b/bookmarks/templates/bookmarks/nav_menu.html
index 1ec5c5c..f3935c6 100644
--- a/bookmarks/templates/bookmarks/nav_menu.html
+++ b/bookmarks/templates/bookmarks/nav_menu.html
@@ -26,7 +26,7 @@
{% endif %}
- Unread
+ Unread
Untagged
@@ -65,7 +65,7 @@
{% endif %}
- Unread
+ Unread
Untagged
diff --git a/bookmarks/templates/bookmarks/search.html b/bookmarks/templates/bookmarks/search.html
index bf970b7..0ed20cb 100644
--- a/bookmarks/templates/bookmarks/search.html
+++ b/bookmarks/templates/bookmarks/search.html
@@ -37,6 +37,16 @@
{% endfor %}
+
@@ -58,6 +68,7 @@
q: '{{ search.query }}',
user: '{{ search.user }}',
shared: '{{ search.shared }}',
+ unread: '{{ search.unread }}',
}
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
const input = document.querySelector('#search input[name="q"]')
diff --git a/bookmarks/templatetags/bookmarks.py b/bookmarks/templatetags/bookmarks.py
index 478b33f..f3c9f51 100644
--- a/bookmarks/templatetags/bookmarks.py
+++ b/bookmarks/templatetags/bookmarks.py
@@ -22,7 +22,7 @@ def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''):
tag_names = [tag.name for tag in tags]
tags_string = build_tag_string(tag_names, ' ')
- form = BookmarkSearchForm(search, editable_fields=['q', 'sort', 'shared'])
+ form = BookmarkSearchForm(search, editable_fields=['q', 'sort', 'shared', 'unread'])
return {
'request': context['request'],
'search': search,
diff --git a/bookmarks/tests/helpers.py b/bookmarks/tests/helpers.py
index f52d41c..d2812dc 100644
--- a/bookmarks/tests/helpers.py
+++ b/bookmarks/tests/helpers.py
@@ -77,6 +77,7 @@ class BookmarkFactoryMixin:
suffix: str = '',
tag_prefix: str = '',
archived: bool = False,
+ unread: bool = False,
shared: bool = False,
with_tags: bool = False,
user: User = None):
@@ -106,7 +107,12 @@ class BookmarkFactoryMixin:
if with_tags:
tag_name = f'{tag_prefix} {i}{suffix}'
tags = [self.setup_tag(name=tag_name)]
- bookmark = self.setup_bookmark(url=url, title=title, is_archived=archived, shared=shared, tags=tags,
+ bookmark = self.setup_bookmark(url=url,
+ title=title,
+ is_archived=archived,
+ unread=unread,
+ shared=shared,
+ tags=tags,
user=user)
bookmarks.append(bookmark)
diff --git a/bookmarks/tests/test_bookmark_search_form.py b/bookmarks/tests/test_bookmark_search_form.py
index 90980cb..2d71fa8 100644
--- a/bookmarks/tests/test_bookmark_search_form.py
+++ b/bookmarks/tests/test_bookmark_search_form.py
@@ -13,17 +13,20 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(form['user'].initial, '')
self.assertEqual(form['shared'].initial, '')
+ self.assertEqual(form['unread'].initial, '')
# with params
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123',
- shared=BookmarkSearch.FILTER_SHARED_SHARED)
+ shared=BookmarkSearch.FILTER_SHARED_SHARED,
+ unread=BookmarkSearch.FILTER_UNREAD_YES)
form = BookmarkSearchForm(search)
self.assertEqual(form['q'].initial, 'search query')
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
self.assertEqual(form['user'].initial, 'user123')
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED)
+ self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_YES)
def test_user_options(self):
users = [
@@ -57,9 +60,11 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123',
- shared=BookmarkSearch.FILTER_SHARED_SHARED)
+ shared=BookmarkSearch.FILTER_SHARED_SHARED,
+ unread=BookmarkSearch.FILTER_UNREAD_YES)
form = BookmarkSearchForm(search)
- self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort'], form['user'], form['shared']])
+ self.assertCountEqual(form.hidden_fields(),
+ [form['q'], form['sort'], form['user'], form['shared'], form['unread']])
# some modified params are editable fields
search = BookmarkSearch(q='search query',
diff --git a/bookmarks/tests/test_bookmark_search_model.py b/bookmarks/tests/test_bookmark_search_model.py
index a876ca1..5d2c650 100644
--- a/bookmarks/tests/test_bookmark_search_model.py
+++ b/bookmarks/tests/test_bookmark_search_model.py
@@ -14,6 +14,7 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(search.user, '')
self.assertEqual(search.shared, '')
+ self.assertEqual(search.unread, '')
# some params
mock_request.GET = {
@@ -26,6 +27,7 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(bookmark_search.user, 'user123')
self.assertEqual(bookmark_search.shared, '')
+ self.assertEqual(bookmark_search.unread, '')
# all params
mock_request.GET = {
@@ -33,6 +35,7 @@ class BookmarkSearchModelTest(TestCase):
'user': 'user123',
'sort': BookmarkSearch.SORT_TITLE_ASC,
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
+ 'unread': BookmarkSearch.FILTER_UNREAD_YES,
}
search = BookmarkSearch.from_request(mock_request)
@@ -40,6 +43,7 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(search.user, 'user123')
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
+ self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
def test_modified_params(self):
# no params
@@ -58,6 +62,10 @@ class BookmarkSearchModelTest(TestCase):
self.assertCountEqual(modified_params, ['q', 'sort'])
# all modified params
- bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123', shared=BookmarkSearch.FILTER_SHARED_SHARED)
+ bookmark_search = BookmarkSearch(q='search query',
+ sort=BookmarkSearch.SORT_ADDED_ASC,
+ user='user123',
+ shared=BookmarkSearch.FILTER_SHARED_SHARED,
+ unread=BookmarkSearch.FILTER_UNREAD_YES)
modified_params = bookmark_search.modified_params
- self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared'])
+ self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared', 'unread'])
diff --git a/bookmarks/tests/test_bookmark_search_tag.py b/bookmarks/tests/test_bookmark_search_tag.py
index 9df79a7..02f3912 100644
--- a/bookmarks/tests/test_bookmark_search_tag.py
+++ b/bookmarks/tests/test_bookmark_search_tag.py
@@ -45,12 +45,14 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')
+ self.assertNoHiddenInput(rendered_template, 'unread')
# With params
- url = '/test?q=foo&user=john&sort=title_asc&shared=shared'
+ url = '/test?q=foo&user=john&sort=title_asc&shared=shared&unread=yes'
rendered_template = self.render_template(url)
self.assertHiddenInput(rendered_template, 'user', 'john')
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')
+ self.assertNoHiddenInput(rendered_template, 'unread')
diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py
index d9bca4f..0f0b798 100644
--- a/bookmarks/tests/test_bookmarks_api.py
+++ b/bookmarks/tests/test_bookmarks_api.py
@@ -70,6 +70,25 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], bookmarks)
+ def test_list_bookmarks_filter_unread(self):
+ self.authenticate()
+ unread_bookmarks = self.setup_numbered_bookmarks(5, unread=True)
+ read_bookmarks = self.setup_numbered_bookmarks(5, unread=False)
+
+ # Filter off
+ response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
+ self.assertBookmarkListEqual(response.data['results'], unread_bookmarks + read_bookmarks)
+
+ # Filter shared
+ response = self.get(reverse('bookmarks:bookmark-list') + '?unread=yes',
+ expected_status_code=status.HTTP_200_OK)
+ self.assertBookmarkListEqual(response.data['results'], unread_bookmarks)
+
+ # Filter unshared
+ response = self.get(reverse('bookmarks:bookmark-list') + '?unread=no',
+ expected_status_code=status.HTTP_200_OK)
+ self.assertBookmarkListEqual(response.data['results'], read_bookmarks)
+
def test_list_bookmarks_filter_shared(self):
self.authenticate()
unshared_bookmarks = self.setup_numbered_bookmarks(5)
diff --git a/bookmarks/tests/test_queries.py b/bookmarks/tests/test_queries.py
index 50f29d3..0f3e4f4 100644
--- a/bookmarks/tests/test_queries.py
+++ b/bookmarks/tests/test_queries.py
@@ -394,31 +394,51 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(list(query), [])
def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
- unread_bookmarks = [
- self.setup_bookmark(unread=True),
- self.setup_bookmark(unread=True),
- self.setup_bookmark(unread=True),
- ]
- self.setup_bookmark()
- self.setup_bookmark()
- self.setup_bookmark()
+ unread_bookmarks = self.setup_numbered_bookmarks(5, unread=True)
+ read_bookmarks = self.setup_numbered_bookmarks(5, unread=False)
+ # Legacy query filter
query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
self.assertCountEqual(list(query), unread_bookmarks)
- def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
- unread_bookmarks = [
- self.setup_bookmark(is_archived=True, unread=True),
- self.setup_bookmark(is_archived=True, unread=True),
- self.setup_bookmark(is_archived=True, unread=True),
- ]
- self.setup_bookmark(is_archived=True)
- self.setup_bookmark(is_archived=True)
- self.setup_bookmark(is_archived=True)
+ # Bookmark search filter - off
+ query = queries.query_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_OFF))
+ self.assertCountEqual(list(query), read_bookmarks + unread_bookmarks)
+ # Bookmark search filter - yes
+ query = queries.query_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_YES))
+ self.assertCountEqual(list(query), unread_bookmarks)
+
+ # Bookmark search filter - no
+ query = queries.query_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_NO))
+ self.assertCountEqual(list(query), read_bookmarks)
+
+ def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
+ unread_bookmarks = self.setup_numbered_bookmarks(5, unread=True, archived=True)
+ read_bookmarks = self.setup_numbered_bookmarks(5, unread=False, archived=True)
+
+ # Legacy query filter
query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
self.assertCountEqual(list(query), unread_bookmarks)
+ # Bookmark search filter - off
+ query = queries.query_archived_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_OFF))
+ self.assertCountEqual(list(query), read_bookmarks + unread_bookmarks)
+
+ # Bookmark search filter - yes
+ query = queries.query_archived_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_YES))
+ self.assertCountEqual(list(query), unread_bookmarks)
+
+ # Bookmark search filter - no
+ query = queries.query_archived_bookmarks(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_NO))
+ self.assertCountEqual(list(query), read_bookmarks)
+
def test_query_bookmarks_filter_shared(self):
unshared_bookmarks = self.setup_numbered_bookmarks(5)
shared_bookmarks = self.setup_numbered_bookmarks(5, shared=True)
@@ -681,6 +701,31 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
BookmarkSearch(query=f'!untagged #{tag.name}'))
self.assertCountEqual(list(query), [])
+ def test_query_bookmark_tags_filter_unread(self):
+ unread_bookmarks = self.setup_numbered_bookmarks(5, unread=True, with_tags=True)
+ read_bookmarks = self.setup_numbered_bookmarks(5, unread=False, with_tags=True)
+ unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
+ read_tags = self.get_tags_from_bookmarks(read_bookmarks)
+
+ # Legacy query filter
+ query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!unread'))
+ self.assertCountEqual(list(query), unread_tags)
+
+ # Bookmark search filter - off
+ query = queries.query_bookmark_tags(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_OFF))
+ self.assertCountEqual(list(query), read_tags + unread_tags)
+
+ # Bookmark search filter - yes
+ query = queries.query_bookmark_tags(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_YES))
+ self.assertCountEqual(list(query), unread_tags)
+
+ # Bookmark search filter - no
+ query = queries.query_bookmark_tags(self.user, self.profile,
+ BookmarkSearch(unread=BookmarkSearch.FILTER_UNREAD_NO))
+ self.assertCountEqual(list(query), read_tags)
+
def test_query_bookmark_tags_filter_shared(self):
unshared_bookmarks = self.setup_numbered_bookmarks(5, with_tags=True)
shared_bookmarks = self.setup_numbered_bookmarks(5, with_tags=True, shared=True)
diff --git a/bookmarks/tests/test_user_select_tag.py b/bookmarks/tests/test_user_select_tag.py
index f2bb1fa..9707509 100644
--- a/bookmarks/tests/test_user_select_tag.py
+++ b/bookmarks/tests/test_user_select_tag.py
@@ -79,12 +79,14 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')
+ self.assertNoHiddenInput(rendered_template, 'unread')
# With params
- url = '/test?q=foo&user=john&sort=title_asc&shared=shared'
+ url = '/test?q=foo&user=john&sort=title_asc&shared=shared&unread=yes'
rendered_template = self.render_template(url)
self.assertNoHiddenInput(rendered_template, 'user')
self.assertHiddenInput(rendered_template, 'q', 'foo')
self.assertHiddenInput(rendered_template, 'sort', 'title_asc')
self.assertHiddenInput(rendered_template, 'shared', 'shared')
+ self.assertHiddenInput(rendered_template, 'unread', 'yes')