diff --git a/bookmarks/services/exporter.py b/bookmarks/services/exporter.py index 1bdff35d..dc0717fb 100644 --- a/bookmarks/services/exporter.py +++ b/bookmarks/services/exporter.py @@ -33,7 +33,10 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark): desc = html.escape(bookmark.resolved_description or '') if bookmark.notes: desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]' - tags = ','.join(bookmark.tag_names) + tag_names = bookmark.tag_names + if bookmark.is_archived: + tag_names.append('linkding:archived') + tags = ','.join(tag_names) toread = '1' if bookmark.unread else '0' private = '0' if bookmark.shared else '1' added = int(bookmark.date_added.timestamp()) diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py index abffd824..96f8fd0e 100644 --- a/bookmarks/services/importer.py +++ b/bookmarks/services/importer.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from django.utils import timezone -from bookmarks.models import Bookmark, Tag, parse_tag_string +from bookmarks.models import Bookmark, Tag from bookmarks.services import tasks from bookmarks.services.parser import parse, NetscapeBookmark from bookmarks.utils import parse_timestamp @@ -93,8 +93,7 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User) tags_to_create = [] for netscape_bookmark in netscape_bookmarks: - tag_names = parse_tag_string(netscape_bookmark.tag_string) - for tag_name in tag_names: + for tag_name in netscape_bookmark.tag_names: tag = tag_cache.get(tag_name) if not tag: tag = Tag(name=tag_name, owner=user) @@ -194,8 +193,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark], continue # Get tag models by string, schedule inserts for bookmark -> tag associations - tag_names = parse_tag_string(netscape_bookmark.tag_string) - tags = tag_cache.get_all(tag_names) + tags = tag_cache.get_all(netscape_bookmark.tag_names) for tag in tags: relationships.append(BookmarkToTagRelationShip(bookmark=bookmark, tag=tag)) @@ -219,3 +217,5 @@ def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark, bookmark.notes = netscape_bookmark.notes if options.map_private_flag and not netscape_bookmark.private: bookmark.shared = True + if netscape_bookmark.archived: + bookmark.is_archived = True diff --git a/bookmarks/services/parser.py b/bookmarks/services/parser.py index 61bafc47..0004dbc5 100644 --- a/bookmarks/services/parser.py +++ b/bookmarks/services/parser.py @@ -2,6 +2,8 @@ from html.parser import HTMLParser from typing import Dict, List +from bookmarks.models import parse_tag_string + @dataclass class NetscapeBookmark: @@ -10,9 +12,10 @@ class NetscapeBookmark: description: str notes: str date_added: str - tag_string: str + tag_names: List[str] to_read: bool private: bool + archived: bool class BookmarkParser(HTMLParser): @@ -56,16 +59,24 @@ def handle_start_dt(self, attrs: Dict[str, str]): def handle_start_a(self, attrs: Dict[str, str]): vars(self).update(attrs) + tag_names = parse_tag_string(self.tags) + archived = 'linkding:archived' in self.tags + try: + tag_names.remove('linkding:archived') + except ValueError: + pass + self.bookmark = NetscapeBookmark( href=self.href, title='', description='', notes='', date_added=self.add_date, - tag_string=self.tags, + tag_names=tag_names, to_read=self.toread == '1', # Mark as private by default, also when attribute is not specified private=self.private != '0', + archived=archived, ) def handle_a_data(self, data): diff --git a/bookmarks/tests/test_exporter.py b/bookmarks/tests/test_exporter.py index 8e6b7aad..033246c8 100644 --- a/bookmarks/tests/test_exporter.py +++ b/bookmarks/tests/test_exporter.py @@ -22,6 +22,9 @@ def test_export_bookmarks(self): description='Example description', notes='Example notes'), self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True, notes='Example notes'), + self.setup_bookmark(url='https://example.com/7', title='Title 7', added=added, is_archived=True), + self.setup_bookmark(url='https://example.com/8', title='Title 8', added=added, + tags=[self.setup_tag(name='tag4'), self.setup_tag(name='tag5')], is_archived=True), ] html = exporter.export_netscape_html(bookmarks) @@ -35,6 +38,8 @@ def test_export_bookmarks(self): '
Example description[linkding-notes]Example notes[/linkding-notes]', f'
Title 6', '
[linkding-notes]Example notes[/linkding-notes]', + f'
Title 7', + f'
Title 8', ] self.assertIn('\n\r'.join(lines), html) diff --git a/bookmarks/tests/test_importer.py b/bookmarks/tests/test_importer.py index 9c2675c4..9fc0d5da 100644 --- a/bookmarks/tests/test_importer.py +++ b/bookmarks/tests/test_importer.py @@ -295,6 +295,27 @@ def test_private_flag(self): self.assertEqual(bookmark2.shared, False) self.assertEqual(bookmark3.shared, True) + def test_archived_state(self): + test_html = self.render_html(tags_html=''' +
Example title 1 +
Example description 1
+
Example title 2 +
Example description 2
+
Example title 3 +
Example description 3
+ ''') + import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) + + self.assertEqual(Bookmark.objects.count(), 3) + self.assertEqual(Bookmark.objects.all()[0].is_archived, True) + self.assertEqual(Bookmark.objects.all()[1].is_archived, False) + self.assertEqual(Bookmark.objects.all()[2].is_archived, False) + + tags = Tag.objects.all() + self.assertEqual(len(tags), 2) + self.assertEqual(tags[0].name, 'tag1') + self.assertEqual(tags[1].name, 'tag2') + def test_notes(self): # initial notes test_html = self.render_html(tags_html=''' diff --git a/bookmarks/tests/test_parser.py b/bookmarks/tests/test_parser.py index 1b2403b2..7d8ddc67 100644 --- a/bookmarks/tests/test_parser.py +++ b/bookmarks/tests/test_parser.py @@ -2,6 +2,7 @@ from django.test import TestCase +from bookmarks.models import parse_tag_string from bookmarks.services.parser import NetscapeBookmark from bookmarks.services.parser import parse from bookmarks.tests.helpers import ImportTestMixin, BookmarkHtmlTag @@ -16,7 +17,7 @@ def assertTagsEqual(self, bookmarks: List[NetscapeBookmark], html_tags: List[Boo self.assertEqual(bookmark.title, html_tag.title) self.assertEqual(bookmark.date_added, html_tag.add_date) self.assertEqual(bookmark.description, html_tag.description) - self.assertEqual(bookmark.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.private, html_tag.private) diff --git a/bookmarks/tests/test_settings_export_view.py b/bookmarks/tests/test_settings_export_view.py index e1d56b18..8f72e3d8 100644 --- a/bookmarks/tests/test_settings_export_view.py +++ b/bookmarks/tests/test_settings_export_view.py @@ -3,6 +3,7 @@ from django.test import TestCase from django.urls import reverse +from bookmarks.models import Bookmark from bookmarks.tests.helpers import BookmarkFactoryMixin @@ -20,6 +21,9 @@ def test_should_export_successfully(self): self.setup_bookmark(tags=[self.setup_tag()]) self.setup_bookmark(tags=[self.setup_tag()]) self.setup_bookmark(tags=[self.setup_tag()]) + self.setup_bookmark(tags=[self.setup_tag()], is_archived=True) + self.setup_bookmark(tags=[self.setup_tag()], is_archived=True) + self.setup_bookmark(tags=[self.setup_tag()], is_archived=True) response = self.client.get( reverse('bookmarks:settings.export'), @@ -30,6 +34,35 @@ def test_should_export_successfully(self): self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8') self.assertEqual(response['Content-Disposition'], 'attachment; filename="bookmarks.html"') + for bookmark in Bookmark.objects.all(): + self.assertContains(response, bookmark.url) + + def test_should_only_export_user_bookmarks(self): + other_user = self.setup_user() + owned_bookmarks = [ + self.setup_bookmark(tags=[self.setup_tag()]), + self.setup_bookmark(tags=[self.setup_tag()]), + self.setup_bookmark(tags=[self.setup_tag()]), + ] + non_owned_bookmarks = [ + self.setup_bookmark(tags=[self.setup_tag()], user=other_user), + self.setup_bookmark(tags=[self.setup_tag()], user=other_user), + self.setup_bookmark(tags=[self.setup_tag()], user=other_user), + ] + + response = self.client.get( + reverse('bookmarks:settings.export'), + follow=True + ) + + text = response.content.decode('utf-8') + + for bookmark in owned_bookmarks: + self.assertIn(bookmark.url, text) + + for bookmark in non_owned_bookmarks: + self.assertNotIn(bookmark.url, text) + def test_should_check_authentication(self): self.client.logout() response = self.client.get(reverse('bookmarks:settings.export'), follow=True) diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py index d174805f..768a8c30 100644 --- a/bookmarks/views/settings.py +++ b/bookmarks/views/settings.py @@ -12,8 +12,7 @@ from django.urls import reverse from rest_framework.authtoken.models import Token -from bookmarks.models import BookmarkSearch, UserProfileForm, FeedToken -from bookmarks.queries import query_bookmarks +from bookmarks.models import Bookmark, BookmarkSearch, UserProfileForm, FeedToken from bookmarks.services import exporter, tasks from bookmarks.services import importer from bookmarks.utils import app_version @@ -136,7 +135,7 @@ def bookmark_import(request): def bookmark_export(request): # noinspection PyBroadException try: - bookmarks = list(query_bookmarks(request.user, request.user_profile, BookmarkSearch())) + bookmarks = Bookmark.objects.filter(owner=request.user) # Prefetch tags to prevent n+1 queries prefetch_related_objects(bookmarks, 'tags') file_content = exporter.export_netscape_html(bookmarks)