From 3e36f90b38fd4614bae270a5c50242094b8cf46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sat, 16 Sep 2023 10:39:27 +0200 Subject: [PATCH] Add filter for unread state (#535) --- bookmarks/models.py | 17 +++- bookmarks/queries.py | 8 +- bookmarks/styles/bookmark-page.scss | 1 + bookmarks/templates/bookmarks/nav_menu.html | 4 +- bookmarks/templates/bookmarks/search.html | 11 +++ bookmarks/templatetags/bookmarks.py | 2 +- bookmarks/tests/helpers.py | 8 +- bookmarks/tests/test_bookmark_search_form.py | 11 ++- bookmarks/tests/test_bookmark_search_model.py | 12 ++- bookmarks/tests/test_bookmark_search_tag.py | 4 +- bookmarks/tests/test_bookmarks_api.py | 19 +++++ bookmarks/tests/test_queries.py | 79 +++++++++++++++---- bookmarks/tests/test_user_select_tag.py | 4 +- 13 files changed, 149 insertions(+), 31 deletions(-) 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 %} +
    +
    Unread filter
    + {% for radio in form.unread %} + + {% 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')