Add LAST_MODIFIED attribute when exporting (#860)

* add LAST_MODIFIED attribute when exporting

* complement test_exporter for LAST_MODIFIED attribute

* parse LAST_MODIFIED attribute when importing

* use bookmark date_added when no modified date is parsed, otherwise use parsed datetime.

* complement test_parser and test_importer for LAST_MODIFIED attribute

* cleanup tests a bit

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
This commit is contained in:
ixzhao 2024-10-03 04:21:08 +08:00 committed by GitHub
parent 0dd05b9269
commit 1f2cf21585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 101 additions and 28 deletions

View file

@ -40,9 +40,10 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
toread = "1" if bookmark.unread else "0" toread = "1" if bookmark.unread else "0"
private = "0" if bookmark.shared else "1" private = "0" if bookmark.shared else "1"
added = int(bookmark.date_added.timestamp()) added = int(bookmark.date_added.timestamp())
modified = int(bookmark.date_modified.timestamp())
doc.append( doc.append(
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>' f'<DT><A HREF="{url}" ADD_DATE="{added}" LAST_MODIFIED="{modified}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
) )
if desc: if desc:

View file

@ -231,7 +231,10 @@ def _copy_bookmark_data(
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added) bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)
else: else:
bookmark.date_added = timezone.now() bookmark.date_added = timezone.now()
bookmark.date_modified = bookmark.date_added if netscape_bookmark.date_modified:
bookmark.date_modified = parse_timestamp(netscape_bookmark.date_modified)
else:
bookmark.date_modified = bookmark.date_added
bookmark.unread = netscape_bookmark.to_read bookmark.unread = netscape_bookmark.to_read
if netscape_bookmark.title: if netscape_bookmark.title:
bookmark.title = netscape_bookmark.title bookmark.title = netscape_bookmark.title

View file

@ -12,6 +12,7 @@ class NetscapeBookmark:
description: str description: str
notes: str notes: str
date_added: str date_added: str
date_modified: str
tag_names: List[str] tag_names: List[str]
to_read: bool to_read: bool
private: bool private: bool
@ -27,6 +28,7 @@ class BookmarkParser(HTMLParser):
self.bookmark = None self.bookmark = None
self.href = "" self.href = ""
self.add_date = "" self.add_date = ""
self.last_modified = ""
self.tags = "" self.tags = ""
self.title = "" self.title = ""
self.description = "" self.description = ""
@ -72,6 +74,7 @@ class BookmarkParser(HTMLParser):
description="", description="",
notes="", notes="",
date_added=self.add_date, date_added=self.add_date,
date_modified=self.last_modified,
tag_names=tag_names, tag_names=tag_names,
to_read=self.toread == "1", to_read=self.toread == "1",
# Mark as private by default, also when attribute is not specified # Mark as private by default, also when attribute is not specified
@ -97,6 +100,7 @@ class BookmarkParser(HTMLParser):
self.bookmark = None self.bookmark = None
self.href = "" self.href = ""
self.add_date = "" self.add_date = ""
self.last_modified = ""
self.tags = "" self.tags = ""
self.title = "" self.title = ""
self.description = "" self.description = ""

View file

@ -45,6 +45,7 @@ class BookmarkFactoryMixin:
favicon_file: str = "", favicon_file: str = "",
preview_image_file: str = "", preview_image_file: str = "",
added: datetime = None, added: datetime = None,
modified: datetime = None,
): ):
if title is None: if title is None:
title = get_random_string(length=32) title = get_random_string(length=32)
@ -57,13 +58,15 @@ class BookmarkFactoryMixin:
url = "https://example.com/" + unique_id url = "https://example.com/" + unique_id
if added is None: if added is None:
added = timezone.now() added = timezone.now()
if modified is None:
modified = timezone.now()
bookmark = Bookmark( bookmark = Bookmark(
url=url, url=url,
title=title, title=title,
description=description, description=description,
notes=notes, notes=notes,
date_added=added, date_added=added,
date_modified=timezone.now(), date_modified=modified,
owner=user, owner=user,
is_archived=is_archived, is_archived=is_archived,
unread=unread, unread=unread,
@ -320,6 +323,7 @@ class BookmarkHtmlTag:
title: str = "", title: str = "",
description: str = "", description: str = "",
add_date: str = "", add_date: str = "",
last_modified: str = "",
tags: str = "", tags: str = "",
to_read: bool = False, to_read: bool = False,
private: bool = True, private: bool = True,
@ -328,6 +332,7 @@ class BookmarkHtmlTag:
self.title = title self.title = title
self.description = description self.description = description
self.add_date = add_date self.add_date = add_date
self.last_modified = last_modified
self.tags = tags self.tags = tags
self.to_read = to_read self.to_read = to_read
self.private = private self.private = private
@ -339,6 +344,7 @@ class ImportTestMixin:
<DT> <DT>
<A {f'HREF="{tag.href}"' if tag.href else ''} <A {f'HREF="{tag.href}"' if tag.href else ''}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''} {f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
{f'LAST_MODIFIED="{tag.last_modified}"' if tag.last_modified else ''}
{f'TAGS="{tag.tags}"' if tag.tags else ''} {f'TAGS="{tag.tags}"' if tag.tags else ''}
TOREAD="{1 if tag.to_read else 0}" TOREAD="{1 if tag.to_read else 0}"
PRIVATE="{1 if tag.private else 0}"> PRIVATE="{1 if tag.private else 0}">

View file

@ -1,5 +1,6 @@
from datetime import datetime, timezone
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from bookmarks.services import exporter from bookmarks.services import exporter
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -7,20 +8,19 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class ExporterTestCase(TestCase, BookmarkFactoryMixin): class ExporterTestCase(TestCase, BookmarkFactoryMixin):
def test_export_bookmarks(self): def test_export_bookmarks(self):
added = timezone.now()
timestamp = int(added.timestamp())
bookmarks = [ bookmarks = [
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/1", url="https://example.com/1",
title="Title 1", title="Title 1",
added=added, added=datetime.fromtimestamp(1, timezone.utc),
modified=datetime.fromtimestamp(11, timezone.utc),
description="Example description", description="Example description",
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/2", url="https://example.com/2",
title="Title 2", title="Title 2",
added=added, added=datetime.fromtimestamp(2, timezone.utc),
modified=datetime.fromtimestamp(22, timezone.utc),
tags=[ tags=[
self.setup_tag(name="tag1"), self.setup_tag(name="tag1"),
self.setup_tag(name="tag2"), self.setup_tag(name="tag2"),
@ -28,15 +28,24 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
], ],
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/3", title="Title 3", added=added, unread=True url="https://example.com/3",
title="Title 3",
added=datetime.fromtimestamp(3, timezone.utc),
modified=datetime.fromtimestamp(33, timezone.utc),
unread=True,
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/4", title="Title 4", added=added, shared=True url="https://example.com/4",
title="Title 4",
added=datetime.fromtimestamp(4, timezone.utc),
modified=datetime.fromtimestamp(44, timezone.utc),
shared=True,
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/5", url="https://example.com/5",
title="Title 5", title="Title 5",
added=added, added=datetime.fromtimestamp(5, timezone.utc),
modified=datetime.fromtimestamp(55, timezone.utc),
shared=True, shared=True,
description="Example description", description="Example description",
notes="Example notes", notes="Example notes",
@ -44,20 +53,23 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/6", url="https://example.com/6",
title="Title 6", title="Title 6",
added=added, added=datetime.fromtimestamp(6, timezone.utc),
modified=datetime.fromtimestamp(66, timezone.utc),
shared=True, shared=True,
notes="Example notes", notes="Example notes",
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/7", url="https://example.com/7",
title="Title 7", title="Title 7",
added=added, added=datetime.fromtimestamp(7, timezone.utc),
modified=datetime.fromtimestamp(77, timezone.utc),
is_archived=True, is_archived=True,
), ),
self.setup_bookmark( self.setup_bookmark(
url="https://example.com/8", url="https://example.com/8",
title="Title 8", title="Title 8",
added=added, added=datetime.fromtimestamp(8, timezone.utc),
modified=datetime.fromtimestamp(88, timezone.utc),
tags=[self.setup_tag(name="tag4"), self.setup_tag(name="tag5")], tags=[self.setup_tag(name="tag4"), self.setup_tag(name="tag5")],
is_archived=True, is_archived=True,
), ),
@ -65,17 +77,17 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
html = exporter.export_netscape_html(bookmarks) html = exporter.export_netscape_html(bookmarks)
lines = [ lines = [
f'<DT><A HREF="https://example.com/1" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>', '<DT><A HREF="https://example.com/1" ADD_DATE="1" LAST_MODIFIED="11" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>',
"<DD>Example description", "<DD>Example description",
f'<DT><A HREF="https://example.com/2" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>', '<DT><A HREF="https://example.com/2" ADD_DATE="2" LAST_MODIFIED="22" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>',
f'<DT><A HREF="https://example.com/3" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>', '<DT><A HREF="https://example.com/3" ADD_DATE="3" LAST_MODIFIED="33" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>',
f'<DT><A HREF="https://example.com/4" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>', '<DT><A HREF="https://example.com/4" ADD_DATE="4" LAST_MODIFIED="44" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>',
f'<DT><A HREF="https://example.com/5" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>', '<DT><A HREF="https://example.com/5" ADD_DATE="5" LAST_MODIFIED="55" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>',
"<DD>Example description[linkding-notes]Example notes[/linkding-notes]", "<DD>Example description[linkding-notes]Example notes[/linkding-notes]",
f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>', '<DT><A HREF="https://example.com/6" ADD_DATE="6" LAST_MODIFIED="66" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
"<DD>[linkding-notes]Example notes[/linkding-notes]", "<DD>[linkding-notes]Example notes[/linkding-notes]",
f'<DT><A HREF="https://example.com/7" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>', '<DT><A HREF="https://example.com/7" ADD_DATE="7" LAST_MODIFIED="77" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>', '<DT><A HREF="https://example.com/8" ADD_DATE="8" LAST_MODIFIED="88" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
] ]
self.assertIn("\n\r".join(lines), html) self.assertIn("\n\r".join(lines), html)

View file

@ -26,6 +26,9 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
self.assertEqual(bookmark.title, html_tag.title) self.assertEqual(bookmark.title, html_tag.title)
self.assertEqual(bookmark.description, html_tag.description) self.assertEqual(bookmark.description, html_tag.description)
self.assertEqual(bookmark.date_added, parse_timestamp(html_tag.add_date)) self.assertEqual(bookmark.date_added, parse_timestamp(html_tag.add_date))
self.assertEqual(
bookmark.date_modified, parse_timestamp(html_tag.last_modified)
)
self.assertEqual(bookmark.unread, html_tag.to_read) self.assertEqual(bookmark.unread, html_tag.to_read)
self.assertEqual(bookmark.shared, not html_tag.private) self.assertEqual(bookmark.shared, not html_tag.private)
@ -45,6 +48,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Example title", title="Example title",
description="Example description", description="Example description",
add_date="1", add_date="1",
last_modified="11",
tags="example-tag", tags="example-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -52,6 +56,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Foo title", title="Foo title",
description="", description="",
add_date="2", add_date="2",
last_modified="22",
tags="", tags="",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -59,6 +64,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Bar title", title="Bar title",
description="Bar description", description="Bar description",
add_date="3", add_date="3",
last_modified="33",
tags="bar-tag, other-tag", tags="bar-tag, other-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -66,6 +72,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Baz title", title="Baz title",
description="Baz description", description="Baz description",
add_date="4", add_date="4",
last_modified="44",
to_read=True, to_read=True,
), ),
] ]
@ -90,6 +97,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Example title", title="Example title",
description="Example description", description="Example description",
add_date="1", add_date="1",
last_modified="11",
tags="example-tag", tags="example-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -97,6 +105,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Foo title", title="Foo title",
description="", description="",
add_date="2", add_date="2",
last_modified="22",
tags="", tags="",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -104,20 +113,23 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Bar title", title="Bar title",
description="Bar description", description="Bar description",
add_date="3", add_date="3",
last_modified="33",
tags="bar-tag, other-tag", tags="bar-tag, other-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
href="https://example.com/unread", href="https://example.com/unread",
title="Unread title", title="Unread title",
description="Unread description", description="Unread description",
add_date="3", add_date="4",
last_modified="44",
to_read=True, to_read=True,
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
href="https://example.com/private", href="https://example.com/private",
title="Private title", title="Private title",
description="Private description", description="Private description",
add_date="4", add_date="5",
last_modified="55",
private=True, private=True,
), ),
] ]
@ -136,6 +148,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Updated Example title", title="Updated Example title",
description="Updated Example description", description="Updated Example description",
add_date="111", add_date="111",
last_modified="1111",
tags="updated-example-tag", tags="updated-example-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -143,6 +156,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Updated Foo title", title="Updated Foo title",
description="Updated Foo description", description="Updated Foo description",
add_date="222", add_date="222",
last_modified="2222",
tags="new-tag", tags="new-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -150,6 +164,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Updated Bar title", title="Updated Bar title",
description="Updated Bar description", description="Updated Bar description",
add_date="333", add_date="333",
last_modified="3333",
tags="updated-bar-tag, updated-other-tag", tags="updated-bar-tag, updated-other-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -157,6 +172,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Unread title", title="Unread title",
description="Unread description", description="Unread description",
add_date="3", add_date="3",
last_modified="3",
to_read=False, to_read=False,
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -164,9 +180,15 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
title="Private title", title="Private title",
description="Private description", description="Private description",
add_date="4", add_date="4",
last_modified="4",
private=False, private=False,
), ),
BookmarkHtmlTag(href="https://baz.com", add_date="444", tags="baz-tag"), BookmarkHtmlTag(
href="https://baz.com",
add_date="444",
last_modified="4444",
tags="baz-tag",
),
] ]
# Import updated data # Import updated data
@ -291,6 +313,19 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
Bookmark.objects.all()[0].date_added, timezone.datetime(2021, 1, 1) Bookmark.objects.all()[0].date_added, timezone.datetime(2021, 1, 1)
) )
def test_use_add_date_when_no_last_modified(self):
test_html = self.render_html(
tags_html=f"""
<DT><A HREF="https://example.com" ADD_DATE="1">Example.com</A>
<DD>Example.com
"""
)
import_netscape_html(test_html, self.get_or_create_test_user())
self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].date_modified, parse_timestamp("1"))
def test_keep_title_if_imported_bookmark_has_empty_title(self): def test_keep_title_if_imported_bookmark_has_empty_title(self):
test_html = self.render_html( test_html = self.render_html(
tags=[BookmarkHtmlTag(href="https://example.com", title="Example.com")] tags=[BookmarkHtmlTag(href="https://example.com", title="Example.com")]

View file

@ -18,6 +18,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
self.assertEqual(bookmark.href, html_tag.href) self.assertEqual(bookmark.href, html_tag.href)
self.assertEqual(bookmark.title, html_tag.title) self.assertEqual(bookmark.title, html_tag.title)
self.assertEqual(bookmark.date_added, html_tag.add_date) self.assertEqual(bookmark.date_added, html_tag.add_date)
self.assertEqual(bookmark.date_modified, html_tag.last_modified)
self.assertEqual(bookmark.description, html_tag.description) self.assertEqual(bookmark.description, html_tag.description)
self.assertEqual(bookmark.tag_names, parse_tag_string(html_tag.tags)) self.assertEqual(bookmark.tag_names, parse_tag_string(html_tag.tags))
self.assertEqual(bookmark.to_read, html_tag.to_read) self.assertEqual(bookmark.to_read, html_tag.to_read)
@ -30,6 +31,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
title="Example title", title="Example title",
description="Example description", description="Example description",
add_date="1", add_date="1",
last_modified="11",
tags="example-tag", tags="example-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -37,6 +39,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
title="Foo title", title="Foo title",
description="", description="",
add_date="2", add_date="2",
last_modified="22",
tags="", tags="",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
@ -44,13 +47,14 @@ class ParserTestCase(TestCase, ImportTestMixin):
title="Bar title", title="Bar title",
description="Bar description", description="Bar description",
add_date="3", add_date="3",
last_modified="33",
tags="bar-tag, other-tag", tags="bar-tag, other-tag",
), ),
BookmarkHtmlTag( BookmarkHtmlTag(
href="https://example.com/baz", href="https://example.com/baz",
title="Baz title", title="Baz title",
description="Baz description", description="Baz description",
add_date="3", add_date="4",
to_read=True, to_read=True,
), ),
] ]
@ -72,9 +76,17 @@ class ParserTestCase(TestCase, ImportTestMixin):
title="Example title", title="Example title",
description="Example description", description="Example description",
add_date="1", add_date="1",
last_modified="1",
tags="example-tag", tags="example-tag",
), ),
BookmarkHtmlTag(href="", title="", description="", add_date="", tags=""), BookmarkHtmlTag(
href="",
title="",
description="",
add_date="",
last_modified="",
tags="",
),
] ]
html = self.render_html(html_tags) html = self.render_html(html_tags)
bookmarks = parse(html) bookmarks = parse(html)