From c746afcf76b4944138c28aa07a2aa7a091b6aa5a Mon Sep 17 00:00:00 2001 From: thR CIrcU5 <141405263+Tql-ws1@users.noreply.github.com> Date: Wed, 13 Aug 2025 05:06:23 +0800 Subject: [PATCH] Bulk create HTML snapshots (#1132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add option to create HTML snapshot for bulk edit * Add the prerequisite for displaying the "Create HTML Snapshot" bulk action option * Add test case This test case covers the scenario where the bulk actions panel displays the corresponding options when the HTML snapshot feature is enabled. * Use the existing `tasks.create_html_snapshots()` instead of the for loop * Fix the exposure of `settings.LD_ENABLE_SNAPSHOTS` within `BookmarkListContext` * add service tests * cleanup context --------- Co-authored-by: Sascha Ißbrücker --- bookmarks/services/bookmarks.py | 9 +++ .../templates/bookmarks/bulk_edit/bar.html | 3 + .../tests/test_bookmark_archived_view.py | 52 +++++++++++++- bookmarks/tests/test_bookmark_index_view.py | 52 +++++++++++++- bookmarks/tests/test_bookmarks_service.py | 71 +++++++++++++++++++ bookmarks/views/bookmarks.py | 3 + bookmarks/views/contexts.py | 1 + 7 files changed, 189 insertions(+), 2 deletions(-) diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py index f2b8a81..82b1fb2 100644 --- a/bookmarks/services/bookmarks.py +++ b/bookmarks/services/bookmarks.py @@ -208,6 +208,15 @@ def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: Us tasks.load_preview_image(current_user, bookmark) +def create_html_snapshots(bookmark_ids: list[Union[int, str]], current_user: User): + sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) + owned_bookmarks = Bookmark.objects.filter( + owner=current_user, id__in=sanitized_bookmark_ids + ) + + tasks.create_html_snapshots(owned_bookmarks) + + def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark): to_bookmark.title = from_bookmark.title to_bookmark.description = from_bookmark.description diff --git a/bookmarks/templates/bookmarks/bulk_edit/bar.html b/bookmarks/templates/bookmarks/bulk_edit/bar.html index 3648483..4f0f372 100644 --- a/bookmarks/templates/bookmarks/bulk_edit/bar.html +++ b/bookmarks/templates/bookmarks/bulk_edit/bar.html @@ -23,6 +23,9 @@ {% endif %} + {% if bookmark_list.snapshot_feature_enabled %} + + {% endif %}
diff --git a/bookmarks/tests/test_bookmark_archived_view.py b/bookmarks/tests/test_bookmark_archived_view.py index 00b9c7b..771c7fa 100644 --- a/bookmarks/tests/test_bookmark_archived_view.py +++ b/bookmarks/tests/test_bookmark_archived_view.py @@ -1,7 +1,7 @@ import urllib.parse from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TestCase, override_settings from django.urls import reverse from bookmarks.models import BookmarkSearch, UserProfile @@ -319,6 +319,28 @@ class BookmarkArchivedViewTestCase( html, ) + @override_settings(LD_ENABLE_SNAPSHOTS=True) + def test_allowed_bulk_actions_with_html_snapshot_enabled(self): + url = reverse("linkding:bookmarks.archived") + response = self.client.get(url) + html = response.content.decode() + + self.assertInHTML( + f""" + + """, + html, + ) + def test_allowed_bulk_actions_with_sharing_enabled(self): user_profile = self.user.profile user_profile.enable_sharing = True @@ -345,6 +367,34 @@ class BookmarkArchivedViewTestCase( html, ) + @override_settings(LD_ENABLE_SNAPSHOTS=True) + def test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled(self): + user_profile = self.user.profile + user_profile.enable_sharing = True + user_profile.save() + + url = reverse("linkding:bookmarks.archived") + response = self.client.get(url) + html = response.content.decode() + + self.assertInHTML( + f""" + + """, + html, + ) + def test_apply_search_preferences(self): # no params response = self.client.post(reverse("linkding:bookmarks.archived")) diff --git a/bookmarks/tests/test_bookmark_index_view.py b/bookmarks/tests/test_bookmark_index_view.py index a82d014..b44155e 100644 --- a/bookmarks/tests/test_bookmark_index_view.py +++ b/bookmarks/tests/test_bookmark_index_view.py @@ -1,7 +1,7 @@ import urllib.parse from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TestCase, override_settings from django.urls import reverse from bookmarks.models import BookmarkSearch, UserProfile @@ -313,6 +313,28 @@ class BookmarkIndexViewTestCase( html, ) + @override_settings(LD_ENABLE_SNAPSHOTS=True) + def test_allowed_bulk_actions_with_html_snapshot_enabled(self): + url = reverse("linkding:bookmarks.index") + response = self.client.get(url) + html = response.content.decode() + + self.assertInHTML( + f""" + + """, + html, + ) + def test_allowed_bulk_actions_with_sharing_enabled(self): user_profile = self.user.profile user_profile.enable_sharing = True @@ -339,6 +361,34 @@ class BookmarkIndexViewTestCase( html, ) + @override_settings(LD_ENABLE_SNAPSHOTS=True) + def test_allowed_bulk_actions_with_sharing_and_html_snapshot_enabled(self): + user_profile = self.user.profile + user_profile.enable_sharing = True + user_profile.save() + + url = reverse("linkding:bookmarks.index") + response = self.client.get(url) + html = response.content.decode() + + self.assertInHTML( + f""" + + """, + html, + ) + def test_apply_search_preferences(self): # no params response = self.client.post(reverse("linkding:bookmarks.index")) diff --git a/bookmarks/tests/test_bookmarks_service.py b/bookmarks/tests/test_bookmarks_service.py index db2c34a..42cb78d 100644 --- a/bookmarks/tests/test_bookmarks_service.py +++ b/bookmarks/tests/test_bookmarks_service.py @@ -22,6 +22,7 @@ from bookmarks.services.bookmarks import ( unshare_bookmarks, enhance_with_website_metadata, refresh_bookmarks_metadata, + create_html_snapshots, ) from bookmarks.tests.helpers import BookmarkFactoryMixin @@ -974,3 +975,73 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin): self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3) self.assertEqual(self.mock_load_preview_image.call_count, 3) + + def test_create_html_snapshots(self): + with patch.object(tasks, "create_html_snapshots") as mock_create_html_snapshots: + bookmark1 = self.setup_bookmark() + bookmark2 = self.setup_bookmark() + bookmark3 = self.setup_bookmark() + + create_html_snapshots( + [bookmark1.id, bookmark2.id, bookmark3.id], + self.get_or_create_test_user(), + ) + + mock_create_html_snapshots.assert_called_once() + call_args = mock_create_html_snapshots.call_args[0][0] + bookmark_ids = list(call_args.values_list("id", flat=True)) + self.assertCountEqual( + bookmark_ids, [bookmark1.id, bookmark2.id, bookmark3.id] + ) + + def test_create_html_snapshots_should_only_create_for_specified_bookmarks(self): + with patch.object(tasks, "create_html_snapshots") as mock_create_html_snapshots: + bookmark1 = self.setup_bookmark() + bookmark2 = self.setup_bookmark() + bookmark3 = self.setup_bookmark() + + create_html_snapshots( + [bookmark1.id, bookmark3.id], self.get_or_create_test_user() + ) + + mock_create_html_snapshots.assert_called_once() + call_args = mock_create_html_snapshots.call_args[0][0] + bookmark_ids = list(call_args.values_list("id", flat=True)) + self.assertCountEqual(bookmark_ids, [bookmark1.id, bookmark3.id]) + self.assertNotIn(bookmark2.id, bookmark_ids) + + def test_create_html_snapshots_should_only_create_for_user_owned_bookmarks(self): + with patch.object(tasks, "create_html_snapshots") as mock_create_html_snapshots: + other_user = self.setup_user() + bookmark1 = self.setup_bookmark() + bookmark2 = self.setup_bookmark() + inaccessible_bookmark = self.setup_bookmark(user=other_user) + + create_html_snapshots( + [bookmark1.id, bookmark2.id, inaccessible_bookmark.id], + self.get_or_create_test_user(), + ) + + mock_create_html_snapshots.assert_called_once() + call_args = mock_create_html_snapshots.call_args[0][0] + bookmark_ids = list(call_args.values_list("id", flat=True)) + self.assertCountEqual(bookmark_ids, [bookmark1.id, bookmark2.id]) + self.assertNotIn(inaccessible_bookmark.id, bookmark_ids) + + def test_create_html_snapshots_should_accept_mix_of_int_and_string_ids(self): + with patch.object(tasks, "create_html_snapshots") as mock_create_html_snapshots: + bookmark1 = self.setup_bookmark() + bookmark2 = self.setup_bookmark() + bookmark3 = self.setup_bookmark() + + create_html_snapshots( + [str(bookmark1.id), bookmark2.id, str(bookmark3.id)], + self.get_or_create_test_user(), + ) + + mock_create_html_snapshots.assert_called_once() + call_args = mock_create_html_snapshots.call_args[0][0] + bookmark_ids = list(call_args.values_list("id", flat=True)) + self.assertCountEqual( + bookmark_ids, [bookmark1.id, bookmark2.id, bookmark3.id] + ) diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py index 7b4556c..6ed3932 100644 --- a/bookmarks/views/bookmarks.py +++ b/bookmarks/views/bookmarks.py @@ -31,6 +31,7 @@ from bookmarks.services.bookmarks import ( share_bookmarks, unshare_bookmarks, refresh_bookmarks_metadata, + create_html_snapshots, ) from bookmarks.type_defs import HttpRequest from bookmarks.utils import get_safe_return_url @@ -368,6 +369,8 @@ def handle_action(request: HttpRequest, query: QuerySet[Bookmark] = None): return unshare_bookmarks(bookmark_ids, request.user) if "bulk_refresh" == bulk_action: return refresh_bookmarks_metadata(bookmark_ids, request.user) + if "bulk_snapshot" == bulk_action: + return create_html_snapshots(bookmark_ids, request.user) @login_required diff --git a/bookmarks/views/contexts.py b/bookmarks/views/contexts.py index f0e6cd4..065b1fe 100644 --- a/bookmarks/views/contexts.py +++ b/bookmarks/views/contexts.py @@ -219,6 +219,7 @@ class BookmarkListContext: self.show_notes = user_profile.permanent_notes self.collapse_side_panel = user_profile.collapse_side_panel self.is_preview = False + self.snapshot_feature_enabled = settings.LD_ENABLE_SNAPSHOTS @staticmethod def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):