mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 14:09:26 +02:00
Bulk create HTML snapshots (#1132)
* 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 <sascha.issbruecker@gmail.com>
This commit is contained in:
@@ -208,6 +208,15 @@ def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: Us
|
|||||||
tasks.load_preview_image(current_user, bookmark)
|
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):
|
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||||
to_bookmark.title = from_bookmark.title
|
to_bookmark.title = from_bookmark.title
|
||||||
to_bookmark.description = from_bookmark.description
|
to_bookmark.description = from_bookmark.description
|
||||||
|
@@ -23,6 +23,9 @@
|
|||||||
<option value="bulk_unshare">Unshare</option>
|
<option value="bulk_unshare">Unshare</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<option value="bulk_refresh">Refresh from website</option>
|
<option value="bulk_refresh">Refresh from website</option>
|
||||||
|
{% if bookmark_list.snapshot_feature_enabled %}
|
||||||
|
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||||
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
<div class="tag-autocomplete d-none" ld-tag-autocomplete>
|
<div class="tag-autocomplete d-none" ld-tag-autocomplete>
|
||||||
<input name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names..." variant="small">
|
<input name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names..." variant="small">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
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 django.urls import reverse
|
||||||
|
|
||||||
from bookmarks.models import BookmarkSearch, UserProfile
|
from bookmarks.models import BookmarkSearch, UserProfile
|
||||||
@@ -319,6 +319,28 @@ class BookmarkArchivedViewTestCase(
|
|||||||
html,
|
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"""
|
||||||
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
|
<option value="bulk_unarchive">Unarchive</option>
|
||||||
|
<option value="bulk_delete">Delete</option>
|
||||||
|
<option value="bulk_tag">Add tags</option>
|
||||||
|
<option value="bulk_untag">Remove tags</option>
|
||||||
|
<option value="bulk_read">Mark as read</option>
|
||||||
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
|
<option value="bulk_refresh">Refresh from website</option>
|
||||||
|
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||||
|
</select>
|
||||||
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.enable_sharing = True
|
user_profile.enable_sharing = True
|
||||||
@@ -345,6 +367,34 @@ class BookmarkArchivedViewTestCase(
|
|||||||
html,
|
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"""
|
||||||
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
|
<option value="bulk_unarchive">Unarchive</option>
|
||||||
|
<option value="bulk_delete">Delete</option>
|
||||||
|
<option value="bulk_tag">Add tags</option>
|
||||||
|
<option value="bulk_untag">Remove tags</option>
|
||||||
|
<option value="bulk_read">Mark as read</option>
|
||||||
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
|
<option value="bulk_share">Share</option>
|
||||||
|
<option value="bulk_unshare">Unshare</option>
|
||||||
|
<option value="bulk_refresh">Refresh from website</option>
|
||||||
|
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||||
|
</select>
|
||||||
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_apply_search_preferences(self):
|
def test_apply_search_preferences(self):
|
||||||
# no params
|
# no params
|
||||||
response = self.client.post(reverse("linkding:bookmarks.archived"))
|
response = self.client.post(reverse("linkding:bookmarks.archived"))
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
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 django.urls import reverse
|
||||||
|
|
||||||
from bookmarks.models import BookmarkSearch, UserProfile
|
from bookmarks.models import BookmarkSearch, UserProfile
|
||||||
@@ -313,6 +313,28 @@ class BookmarkIndexViewTestCase(
|
|||||||
html,
|
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"""
|
||||||
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
|
<option value="bulk_archive">Archive</option>
|
||||||
|
<option value="bulk_delete">Delete</option>
|
||||||
|
<option value="bulk_tag">Add tags</option>
|
||||||
|
<option value="bulk_untag">Remove tags</option>
|
||||||
|
<option value="bulk_read">Mark as read</option>
|
||||||
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
|
<option value="bulk_refresh">Refresh from website</option>
|
||||||
|
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||||
|
</select>
|
||||||
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
def test_allowed_bulk_actions_with_sharing_enabled(self):
|
||||||
user_profile = self.user.profile
|
user_profile = self.user.profile
|
||||||
user_profile.enable_sharing = True
|
user_profile.enable_sharing = True
|
||||||
@@ -339,6 +361,34 @@ class BookmarkIndexViewTestCase(
|
|||||||
html,
|
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"""
|
||||||
|
<select name="bulk_action" class="form-select select-sm">
|
||||||
|
<option value="bulk_archive">Archive</option>
|
||||||
|
<option value="bulk_delete">Delete</option>
|
||||||
|
<option value="bulk_tag">Add tags</option>
|
||||||
|
<option value="bulk_untag">Remove tags</option>
|
||||||
|
<option value="bulk_read">Mark as read</option>
|
||||||
|
<option value="bulk_unread">Mark as unread</option>
|
||||||
|
<option value="bulk_share">Share</option>
|
||||||
|
<option value="bulk_unshare">Unshare</option>
|
||||||
|
<option value="bulk_refresh">Refresh from website</option>
|
||||||
|
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||||
|
</select>
|
||||||
|
""",
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
def test_apply_search_preferences(self):
|
def test_apply_search_preferences(self):
|
||||||
# no params
|
# no params
|
||||||
response = self.client.post(reverse("linkding:bookmarks.index"))
|
response = self.client.post(reverse("linkding:bookmarks.index"))
|
||||||
|
@@ -22,6 +22,7 @@ from bookmarks.services.bookmarks import (
|
|||||||
unshare_bookmarks,
|
unshare_bookmarks,
|
||||||
enhance_with_website_metadata,
|
enhance_with_website_metadata,
|
||||||
refresh_bookmarks_metadata,
|
refresh_bookmarks_metadata,
|
||||||
|
create_html_snapshots,
|
||||||
)
|
)
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
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_schedule_refresh_metadata.call_count, 3)
|
||||||
self.assertEqual(self.mock_load_preview_image.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]
|
||||||
|
)
|
||||||
|
@@ -31,6 +31,7 @@ from bookmarks.services.bookmarks import (
|
|||||||
share_bookmarks,
|
share_bookmarks,
|
||||||
unshare_bookmarks,
|
unshare_bookmarks,
|
||||||
refresh_bookmarks_metadata,
|
refresh_bookmarks_metadata,
|
||||||
|
create_html_snapshots,
|
||||||
)
|
)
|
||||||
from bookmarks.type_defs import HttpRequest
|
from bookmarks.type_defs import HttpRequest
|
||||||
from bookmarks.utils import get_safe_return_url
|
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)
|
return unshare_bookmarks(bookmark_ids, request.user)
|
||||||
if "bulk_refresh" == bulk_action:
|
if "bulk_refresh" == bulk_action:
|
||||||
return refresh_bookmarks_metadata(bookmark_ids, request.user)
|
return refresh_bookmarks_metadata(bookmark_ids, request.user)
|
||||||
|
if "bulk_snapshot" == bulk_action:
|
||||||
|
return create_html_snapshots(bookmark_ids, request.user)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@@ -219,6 +219,7 @@ class BookmarkListContext:
|
|||||||
self.show_notes = user_profile.permanent_notes
|
self.show_notes = user_profile.permanent_notes
|
||||||
self.collapse_side_panel = user_profile.collapse_side_panel
|
self.collapse_side_panel = user_profile.collapse_side_panel
|
||||||
self.is_preview = False
|
self.is_preview = False
|
||||||
|
self.snapshot_feature_enabled = settings.LD_ENABLE_SNAPSHOTS
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):
|
def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):
|
||||||
|
Reference in New Issue
Block a user