mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-13 13:39:27 +02:00
Add bulk and single bookmark metadata refresh (#999)
* Add url create/edit query paramter to clear cache * Add refresh bookmark metadata button in create/edit bookmark page * Fix refresh bookmark metadata when editing existing bookmark * Add bulk refresh metadata functionality * Fix test cases for bulk view dropdown selection list * Allow bulk metadata refresh when background tasks are disabled * Move load preview image call on refresh metadata * Update bookmark modified time on metadata refresh * Rename function to align with convention * Add tests for refresh task * Add tests for bookmarks service refresh metadata * Add tests for bookmarks api disable cache on check * Remove bulk refresh metadata when background tasks disabled * Refactor refresh metadata task * Remove unnecessary call * Fix testing mock name * Abstract clearing metadata cache * Add test to check if load page is called twice when cache disabled * Remove refresh button for new bookmarks * Remove strict disable cache is true check * Refactor refresh metadata form logic into its own function * move button and highlight changes * polish and update tests --------- Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
This commit is contained in:
@@ -278,6 +278,7 @@ class BookmarkArchivedViewTestCase(
|
||||
<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>
|
||||
</select>
|
||||
""",
|
||||
html,
|
||||
@@ -303,6 +304,7 @@ class BookmarkArchivedViewTestCase(
|
||||
<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>
|
||||
</select>
|
||||
""",
|
||||
html,
|
||||
|
@@ -259,6 +259,7 @@ class BookmarkIndexViewTestCase(
|
||||
<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>
|
||||
</select>
|
||||
""",
|
||||
html,
|
||||
@@ -284,6 +285,7 @@ class BookmarkIndexViewTestCase(
|
||||
<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>
|
||||
</select>
|
||||
""",
|
||||
html,
|
||||
|
@@ -1047,6 +1047,43 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertCountEqual(auto_tags, ["tag1", "tag2"])
|
||||
|
||||
def test_check_ignore_cache(self):
|
||||
self.authenticate()
|
||||
|
||||
with patch.object(
|
||||
website_loader, "load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
expected_metadata = WebsiteMetadata(
|
||||
"https://example.com",
|
||||
"Scraped metadata",
|
||||
"Scraped description",
|
||||
"https://example.com/preview.png",
|
||||
)
|
||||
mock_load_website_metadata.return_value = expected_metadata
|
||||
|
||||
# Does not ignore cache by default
|
||||
url = reverse("linkding:bookmark-check")
|
||||
check_url = urllib.parse.quote_plus("https://example.com")
|
||||
self.get(
|
||||
f"{url}?url={check_url}",
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
mock_load_website_metadata.assert_called_once_with(
|
||||
"https://example.com", ignore_cache=False
|
||||
)
|
||||
mock_load_website_metadata.reset_mock()
|
||||
|
||||
# Ignores cache based on query param
|
||||
self.get(
|
||||
f"{url}?url={check_url}&ignore_cache=true",
|
||||
expected_status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
mock_load_website_metadata.assert_called_once_with(
|
||||
"https://example.com", ignore_cache=True
|
||||
)
|
||||
|
||||
def test_can_only_access_own_bookmarks(self):
|
||||
self.authenticate()
|
||||
self.setup_bookmark()
|
||||
|
@@ -21,6 +21,7 @@ from bookmarks.services.bookmarks import (
|
||||
share_bookmarks,
|
||||
unshare_bookmarks,
|
||||
enhance_with_website_metadata,
|
||||
refresh_bookmarks_metadata,
|
||||
)
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
@@ -30,6 +31,21 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self) -> None:
|
||||
self.get_or_create_test_user()
|
||||
|
||||
self.mock_schedule_refresh_metadata_patcher = patch(
|
||||
"bookmarks.services.bookmarks.tasks.refresh_metadata"
|
||||
)
|
||||
self.mock_schedule_refresh_metadata = (
|
||||
self.mock_schedule_refresh_metadata_patcher.start()
|
||||
)
|
||||
self.mock_load_preview_image_patcher = patch(
|
||||
"bookmarks.services.bookmarks.tasks.load_preview_image"
|
||||
)
|
||||
self.mock_load_preview_image = self.mock_load_preview_image_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.mock_schedule_refresh_metadata_patcher.stop()
|
||||
self.mock_load_preview_image_patcher.stop()
|
||||
|
||||
def test_create_should_not_update_website_metadata(self):
|
||||
with patch.object(
|
||||
website_loader, "load_website_metadata"
|
||||
@@ -891,3 +907,70 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertEqual("", bookmark.title)
|
||||
self.assertEqual("", bookmark.description)
|
||||
|
||||
def test_refresh_bookmarks_metadata(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
refresh_bookmarks_metadata(
|
||||
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
|
||||
self.assertEqual(self.mock_load_preview_image.call_count, 3)
|
||||
|
||||
def test_refresh_bookmarks_metadata_should_only_refresh_specified_bookmarks(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
refresh_bookmarks_metadata(
|
||||
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
|
||||
)
|
||||
|
||||
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
|
||||
self.assertEqual(self.mock_load_preview_image.call_count, 2)
|
||||
|
||||
for call_args in self.mock_schedule_refresh_metadata.call_args_list:
|
||||
args, kwargs = call_args
|
||||
self.assertNotIn(bookmark2.id, args)
|
||||
|
||||
for call_args in self.mock_load_preview_image.call_args_list:
|
||||
args, kwargs = call_args
|
||||
self.assertNotIn(bookmark2.id, args)
|
||||
|
||||
def test_refresh_bookmarks_metadata_should_only_refresh_user_owned_bookmarks(self):
|
||||
other_user = self.setup_user()
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||
|
||||
refresh_bookmarks_metadata(
|
||||
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
|
||||
self.assertEqual(self.mock_load_preview_image.call_count, 2)
|
||||
|
||||
for call_args in self.mock_schedule_refresh_metadata.call_args_list:
|
||||
args, kwargs = call_args
|
||||
self.assertNotIn(inaccessible_bookmark.id, args)
|
||||
|
||||
for call_args in self.mock_load_preview_image.call_args_list:
|
||||
args, kwargs = call_args
|
||||
self.assertNotIn(inaccessible_bookmark.id, args)
|
||||
|
||||
def test_refresh_bookmarks_metadata_should_accept_mix_of_int_and_string_ids(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
refresh_bookmarks_metadata(
|
||||
[str(bookmark1.id), str(bookmark2.id), bookmark3.id],
|
||||
self.get_or_create_test_user(),
|
||||
)
|
||||
|
||||
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
|
||||
self.assertEqual(self.mock_load_preview_image.call_count, 3)
|
||||
|
@@ -8,6 +8,7 @@ from waybackpy.exceptions import WaybackError
|
||||
|
||||
from bookmarks.models import BookmarkAsset, UserProfile
|
||||
from bookmarks.services import tasks
|
||||
from bookmarks.services.website_loader import WebsiteMetadata
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
@@ -615,3 +616,52 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(BookmarkAsset.objects.count(), count)
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_refresh_metadata_task_not_called_when_background_tasks_disabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
with mock.patch(
|
||||
"bookmarks.services.tasks._refresh_metadata_task"
|
||||
) as mock_refresh_metadata_task:
|
||||
tasks.refresh_metadata(bookmark)
|
||||
mock_refresh_metadata_task.assert_not_called()
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=False)
|
||||
def test_refresh_metadata_task_called_when_background_tasks_enabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
with mock.patch(
|
||||
"bookmarks.services.tasks._refresh_metadata_task"
|
||||
) as mock_refresh_metadata_task:
|
||||
tasks.refresh_metadata(bookmark)
|
||||
mock_refresh_metadata_task.assert_called_once()
|
||||
|
||||
def test_refresh_metadata_task_should_handle_missing_bookmark(self):
|
||||
with mock.patch(
|
||||
"bookmarks.services.website_loader.load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
tasks._refresh_metadata_task(123)
|
||||
|
||||
mock_load_website_metadata.assert_not_called()
|
||||
|
||||
def test_refresh_metadata_updates_title_description(self):
|
||||
bookmark = self.setup_bookmark(
|
||||
title="Initial title",
|
||||
description="Initial description",
|
||||
)
|
||||
mock_website_metadata = WebsiteMetadata(
|
||||
url=bookmark.url,
|
||||
title="New title",
|
||||
description="New description",
|
||||
preview_image=None,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"bookmarks.services.tasks.load_website_metadata"
|
||||
) as mock_load_website_metadata:
|
||||
mock_load_website_metadata.return_value = mock_website_metadata
|
||||
|
||||
tasks.refresh_metadata(bookmark)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
self.assertEqual(bookmark.title, "New title")
|
||||
self.assertEqual(bookmark.description, "New description")
|
||||
|
@@ -27,7 +27,7 @@ class MockStreamingResponse:
|
||||
class WebsiteLoaderTestCase(TestCase):
|
||||
def setUp(self):
|
||||
# clear cached metadata before test run
|
||||
website_loader.load_website_metadata.cache_clear()
|
||||
website_loader._load_website_metadata_cached.cache_clear()
|
||||
|
||||
def render_html_document(
|
||||
self, title, description="", og_description="", og_image=""
|
||||
@@ -183,3 +183,20 @@ class WebsiteLoaderTestCase(TestCase):
|
||||
metadata = website_loader.load_website_metadata("https://example.com")
|
||||
self.assertEqual("test title", metadata.title)
|
||||
self.assertEqual("test description", metadata.description)
|
||||
|
||||
def test_website_metadata_ignore_cache(self):
|
||||
expected_html = '<html><head><title>Test Title</title><meta name="description" content="Test Description"><meta property="og:image" content="/images/test.jpg"></head></html>'
|
||||
|
||||
with mock.patch.object(
|
||||
website_loader, "load_page", return_value=expected_html
|
||||
) as mock_load_page:
|
||||
website_loader.load_website_metadata("https://example.com")
|
||||
mock_load_page.assert_called_once()
|
||||
|
||||
website_loader.load_website_metadata("https://example.com")
|
||||
mock_load_page.assert_called_once()
|
||||
|
||||
website_loader.load_website_metadata(
|
||||
"https://example.com", ignore_cache=True
|
||||
)
|
||||
self.assertEqual(mock_load_page.call_count, 2)
|
||||
|
Reference in New Issue
Block a user