Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include archived bookmarks in export #579

Merged
merged 1 commit into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bookmarks/services/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
10 changes: 5 additions & 5 deletions bookmarks/services/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand All @@ -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
15 changes: 13 additions & 2 deletions bookmarks/services/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from html.parser import HTMLParser
from typing import Dict, List

from bookmarks.models import parse_tag_string


@dataclass
class NetscapeBookmark:
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions bookmarks/tests/test_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -35,6 +38,8 @@ def test_export_bookmarks(self):
'<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>',
'<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)

Expand Down
21 changes: 21 additions & 0 deletions bookmarks/tests/test_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='''
<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):
# initial notes
test_html = self.render_html(tags_html='''
Expand Down
3 changes: 2 additions & 1 deletion bookmarks/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
33 changes: 33 additions & 0 deletions bookmarks/tests/test_settings_export_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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'),
Expand All @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions bookmarks/views/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down