From 4f26c3483bb61d1f443b145bfec193ed2d125e17 Mon Sep 17 00:00:00 2001 From: Justin Mason Date: Mon, 5 Jan 2026 18:12:14 +0000 Subject: [PATCH] Allow setting date_added and date_modified for new bookmarks through REST API (#1063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make date_added and date_modified optionally writable fields for the POST /api/bookmarks/ API * Update as per PR feedback to avoid double-save; add test coverage * Remove blank line * improve tests --------- Co-authored-by: Justin.Mason Co-authored-by: Sascha Ißbrücker --- bookmarks/api/serializers.py | 5 +-- bookmarks/services/bookmarks.py | 9 +++-- bookmarks/tests/test_bookmarks_api.py | 31 ++++++++++++++++ bookmarks/tests/test_bookmarks_service.py | 43 +++++++++++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/bookmarks/api/serializers.py b/bookmarks/api/serializers.py index 4542abb..6d521fe 100644 --- a/bookmarks/api/serializers.py +++ b/bookmarks/api/serializers.py @@ -86,8 +86,6 @@ class BookmarkSerializer(serializers.ModelSerializer): "favicon_url", "preview_image_url", "tag_names", - "date_added", - "date_modified", "website_title", "website_description", ] @@ -102,6 +100,9 @@ class BookmarkSerializer(serializers.ModelSerializer): # Add dummy website title and description fields for backwards compatibility but keep them empty website_title = EmtpyField() website_description = EmtpyField() + # these are optional + date_added = serializers.DateTimeField(required=False) + date_modified = serializers.DateTimeField(required=False) def get_favicon_url(self, obj: Bookmark): if not obj.favicon_file: diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py index c01b99a..bcf0389 100644 --- a/bookmarks/services/bookmarks.py +++ b/bookmarks/services/bookmarks.py @@ -26,9 +26,12 @@ def create_bookmark( # Set currently logged in user as owner bookmark.owner = current_user - # Set dates - bookmark.date_added = timezone.now() - bookmark.date_modified = timezone.now() + # Set dates only if not already provided + # This allows to sync existing dates through the REST API for example + if not bookmark.date_added: + bookmark.date_added = timezone.now() + if not bookmark.date_modified: + bookmark.date_modified = timezone.now() bookmark.save() # Update tag list _update_bookmark_tags(bookmark, tag_string, current_user) diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py index 79c4b12..86f7a51 100644 --- a/bookmarks/tests/test_bookmarks_api.py +++ b/bookmarks/tests/test_bookmarks_api.py @@ -649,6 +649,37 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): bookmark = Bookmark.objects.get(url=data["url"]) self.assertCountEqual(bookmark.tags.all(), [tag1, tag2]) + def test_create_bookmark_should_set_default_dates(self): + self.authenticate() + + with patch("bookmarks.services.bookmarks.timezone.now") as mock_now: + fixed_time = timezone.make_aware(datetime.datetime(2024, 1, 15, 12, 0, 0)) + mock_now.return_value = fixed_time + + data = {"url": "https://example.com/"} + self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED) + bookmark = Bookmark.objects.get(url=data["url"]) + self.assertEqual(bookmark.date_added, fixed_time) + self.assertEqual(bookmark.date_modified, fixed_time) + + def test_create_bookmark_with_date_added(self): + self.authenticate() + + date1 = timezone.now() - datetime.timedelta(days=30) + data = {"url": "https://example.com/", "date_added": date1.isoformat()} + self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED) + bookmark = Bookmark.objects.get(url=data["url"]) + self.assertEqual(bookmark.date_added.isoformat(), date1.isoformat()) + + def test_create_bookmark_with_date_modified(self): + self.authenticate() + + date1 = timezone.now() - datetime.timedelta(days=15) + data = {"url": "https://example.com/", "date_modified": date1.isoformat()} + self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED) + bookmark = Bookmark.objects.get(url=data["url"]) + self.assertEqual(bookmark.date_modified.isoformat(), date1.isoformat()) + def test_get_bookmark(self): self.authenticate() bookmark = self.setup_bookmark() diff --git a/bookmarks/tests/test_bookmarks_service.py b/bookmarks/tests/test_bookmarks_service.py index 47878c8..86ff6e5 100644 --- a/bookmarks/tests/test_bookmarks_service.py +++ b/bookmarks/tests/test_bookmarks_service.py @@ -1,3 +1,4 @@ +import datetime from unittest.mock import patch from django.test import TestCase @@ -226,6 +227,48 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin): self.assertCountEqual(bookmark.tags.all(), [tag1, tag2]) + def test_create_should_set_default_dates(self): + with patch("bookmarks.services.bookmarks.timezone.now") as mock_now: + fixed_time = timezone.make_aware(datetime.datetime(2024, 1, 15, 12, 0, 0)) + mock_now.return_value = fixed_time + + bookmark_data = Bookmark(url="https://example.com") + bookmark = create_bookmark(bookmark_data, "", self.user) + + bookmark.refresh_from_db() + self.assertEqual(bookmark.date_added, fixed_time) + self.assertEqual(bookmark.date_modified, fixed_time) + + def test_create_should_use_provided_date_added(self): + custom_date = timezone.now() - datetime.timedelta(days=30) + bookmark_data = Bookmark(url="https://example.com", date_added=custom_date) + bookmark = create_bookmark(bookmark_data, "", self.user) + + bookmark.refresh_from_db() + self.assertEqual(bookmark.date_added, custom_date) + + def test_create_should_use_provided_date_modified(self): + custom_date = timezone.now() - datetime.timedelta(days=15) + bookmark_data = Bookmark(url="https://example.com", date_modified=custom_date) + bookmark = create_bookmark(bookmark_data, "", self.user) + + bookmark.refresh_from_db() + self.assertEqual(bookmark.date_modified, custom_date) + + def test_create_should_use_provided_dates(self): + custom_date_added = timezone.now() - datetime.timedelta(days=30) + custom_date_modified = timezone.now() - datetime.timedelta(days=15) + bookmark_data = Bookmark( + url="https://example.com", + date_added=custom_date_added, + date_modified=custom_date_modified, + ) + bookmark = create_bookmark(bookmark_data, "", self.user) + + bookmark.refresh_from_db() + self.assertEqual(bookmark.date_added, custom_date_added) + self.assertEqual(bookmark.date_modified, custom_date_modified) + def test_update_should_create_web_archive_snapshot_if_url_did_change(self): with patch.object( tasks, "create_web_archive_snapshot"