Include archived bookmarks in export (#579)

This commit is contained in:
Sascha Ißbrücker
2023-11-24 09:21:23 +01:00
committed by GitHub
parent 47e944e6c5
commit a9512b2333
8 changed files with 85 additions and 12 deletions

View File

@@ -33,7 +33,10 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
desc = html.escape(bookmark.resolved_description or '') desc = html.escape(bookmark.resolved_description or '')
if bookmark.notes: if bookmark.notes:
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-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' 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())

View File

@@ -5,7 +5,7 @@ from typing import List
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone 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 import tasks
from bookmarks.services.parser import parse, NetscapeBookmark from bookmarks.services.parser import parse, NetscapeBookmark
from bookmarks.utils import parse_timestamp from bookmarks.utils import parse_timestamp
@@ -93,8 +93,7 @@ def _create_missing_tags(netscape_bookmarks: List[NetscapeBookmark], user: User)
tags_to_create = [] tags_to_create = []
for netscape_bookmark in netscape_bookmarks: for netscape_bookmark in netscape_bookmarks:
tag_names = parse_tag_string(netscape_bookmark.tag_string) for tag_name in netscape_bookmark.tag_names:
for tag_name in tag_names:
tag = tag_cache.get(tag_name) tag = tag_cache.get(tag_name)
if not tag: if not tag:
tag = Tag(name=tag_name, owner=user) tag = Tag(name=tag_name, owner=user)
@@ -194,8 +193,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
continue continue
# Get tag models by string, schedule inserts for bookmark -> tag associations # 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(netscape_bookmark.tag_names)
tags = tag_cache.get_all(tag_names)
for tag in tags: for tag in tags:
relationships.append(BookmarkToTagRelationShip(bookmark=bookmark, tag=tag)) 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 bookmark.notes = netscape_bookmark.notes
if options.map_private_flag and not netscape_bookmark.private: if options.map_private_flag and not netscape_bookmark.private:
bookmark.shared = True bookmark.shared = True
if netscape_bookmark.archived:
bookmark.is_archived = True

View File

@@ -2,6 +2,8 @@ from dataclasses import dataclass
from html.parser import HTMLParser from html.parser import HTMLParser
from typing import Dict, List from typing import Dict, List
from bookmarks.models import parse_tag_string
@dataclass @dataclass
class NetscapeBookmark: class NetscapeBookmark:
@@ -10,9 +12,10 @@ class NetscapeBookmark:
description: str description: str
notes: str notes: str
date_added: str date_added: str
tag_string: str tag_names: List[str]
to_read: bool to_read: bool
private: bool private: bool
archived: bool
class BookmarkParser(HTMLParser): class BookmarkParser(HTMLParser):
@@ -56,16 +59,24 @@ class BookmarkParser(HTMLParser):
def handle_start_a(self, attrs: Dict[str, str]): def handle_start_a(self, attrs: Dict[str, str]):
vars(self).update(attrs) 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( self.bookmark = NetscapeBookmark(
href=self.href, href=self.href,
title='', title='',
description='', description='',
notes='', notes='',
date_added=self.add_date, date_added=self.add_date,
tag_string=self.tags, 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
private=self.private != '0', private=self.private != '0',
archived=archived,
) )
def handle_a_data(self, data): def handle_a_data(self, data):

View File

@@ -22,6 +22,9 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
description='Example description', notes='Example notes'), description='Example description', notes='Example notes'),
self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True, self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True,
notes='Example notes'), 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) html = exporter.export_netscape_html(bookmarks)
@@ -35,6 +38,8 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
'<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>', f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" 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>',
f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" 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

@@ -295,6 +295,27 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
self.assertEqual(bookmark2.shared, False) self.assertEqual(bookmark2.shared, False)
self.assertEqual(bookmark3.shared, True) self.assertEqual(bookmark3.shared, True)
def test_archived_state(self):
test_html = self.render_html(tags_html='''
<DT><A HREF="https://example.com/1" ADD_DATE="1" TAGS="tag1,tag2,linkding:archived">Example title 1</A>
<DD>Example description 1</DD>
<DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1" TAGS="tag1,tag2">Example title 2</A>
<DD>Example description 2</DD>
<DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A>
<DD>Example description 3</DD>
''')
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): def test_notes(self):
# initial notes # initial notes
test_html = self.render_html(tags_html=''' test_html = self.render_html(tags_html='''

View File

@@ -2,6 +2,7 @@ from typing import List
from django.test import TestCase from django.test import TestCase
from bookmarks.models import parse_tag_string
from bookmarks.services.parser import NetscapeBookmark from bookmarks.services.parser import NetscapeBookmark
from bookmarks.services.parser import parse from bookmarks.services.parser import parse
from bookmarks.tests.helpers import ImportTestMixin, BookmarkHtmlTag from bookmarks.tests.helpers import ImportTestMixin, BookmarkHtmlTag
@@ -16,7 +17,7 @@ class ParserTestCase(TestCase, ImportTestMixin):
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.description, html_tag.description) 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.to_read, html_tag.to_read)
self.assertEqual(bookmark.private, html_tag.private) self.assertEqual(bookmark.private, html_tag.private)

View File

@@ -3,6 +3,7 @@ from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from bookmarks.models import Bookmark
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@@ -20,6 +21,9 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
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()])
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( response = self.client.get(
reverse('bookmarks:settings.export'), reverse('bookmarks:settings.export'),
@@ -30,6 +34,35 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8') self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8')
self.assertEqual(response['Content-Disposition'], 'attachment; filename="bookmarks.html"') 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): def test_should_check_authentication(self):
self.client.logout() self.client.logout()
response = self.client.get(reverse('bookmarks:settings.export'), follow=True) response = self.client.get(reverse('bookmarks:settings.export'), follow=True)

View File

@@ -12,8 +12,7 @@ from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from bookmarks.models import BookmarkSearch, UserProfileForm, FeedToken from bookmarks.models import Bookmark, BookmarkSearch, UserProfileForm, FeedToken
from bookmarks.queries import query_bookmarks
from bookmarks.services import exporter, tasks from bookmarks.services import exporter, tasks
from bookmarks.services import importer from bookmarks.services import importer
from bookmarks.utils import app_version from bookmarks.utils import app_version
@@ -136,7 +135,7 @@ def bookmark_import(request):
def bookmark_export(request): def bookmark_export(request):
# noinspection PyBroadException # noinspection PyBroadException
try: 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 tags to prevent n+1 queries
prefetch_related_objects(bookmarks, 'tags') prefetch_related_objects(bookmarks, 'tags')
file_content = exporter.export_netscape_html(bookmarks) file_content = exporter.export_netscape_html(bookmarks)