diff --git a/bookmarks/tests/test_bookmark_action_view.py b/bookmarks/tests/test_bookmark_action_view.py
index 3ddf167..58d0ba4 100644
--- a/bookmarks/tests/test_bookmark_action_view.py
+++ b/bookmarks/tests/test_bookmark_action_view.py
@@ -94,6 +94,30 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(bookmark.unread)
+ def test_unshare_should_unshare_bookmark(self):
+ bookmark = self.setup_bookmark(shared=True)
+
+ self.client.post(reverse('bookmarks:action'), {
+ 'unshare': [bookmark.id],
+ })
+
+ bookmark.refresh_from_db()
+
+ self.assertFalse(bookmark.shared)
+
+ def test_can_only_unshare_own_bookmarks(self):
+ other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
+ bookmark = self.setup_bookmark(user=other_user, shared=True)
+
+ response = self.client.post(reverse('bookmarks:action'), {
+ 'unshare': [bookmark.id],
+ })
+
+ bookmark.refresh_from_db()
+
+ self.assertEqual(response.status_code, 404)
+ self.assertTrue(bookmark.shared)
+
def test_bulk_archive(self):
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
diff --git a/bookmarks/tests/test_bookmark_archived_view.py b/bookmarks/tests/test_bookmark_archived_view.py
index b293c2d..c272f70 100644
--- a/bookmarks/tests/test_bookmark_archived_view.py
+++ b/bookmarks/tests/test_bookmark_archived_view.py
@@ -20,7 +20,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
for bookmark in bookmarks:
self.assertInHTML(
- f'
{bookmark.resolved_title}',
+ f'
{bookmark.resolved_title}',
html
)
@@ -29,7 +29,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
for bookmark in bookmarks:
self.assertInHTML(
- f'
{bookmark.resolved_title}',
+ f'
{bookmark.resolved_title}',
html,
count=0
)
diff --git a/bookmarks/tests/test_bookmark_index_view.py b/bookmarks/tests/test_bookmark_index_view.py
index c92c5a1..e6f952a 100644
--- a/bookmarks/tests/test_bookmark_index_view.py
+++ b/bookmarks/tests/test_bookmark_index_view.py
@@ -21,7 +21,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
for bookmark in bookmarks:
self.assertInHTML(
- f'
{bookmark.resolved_title}',
+ f'
{bookmark.resolved_title}',
html
)
@@ -30,7 +30,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
for bookmark in bookmarks:
self.assertInHTML(
- f'
{bookmark.resolved_title}',
+ f'
{bookmark.resolved_title}',
html,
count=0
)
diff --git a/bookmarks/tests/test_bookmark_shared_view.py b/bookmarks/tests/test_bookmark_shared_view.py
index eb393e5..3536e23 100644
--- a/bookmarks/tests/test_bookmark_shared_view.py
+++ b/bookmarks/tests/test_bookmark_shared_view.py
@@ -16,13 +16,13 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
def assertBookmarkCount(self, html: str, bookmark: Bookmark, count: int, link_target: str = '_blank'):
self.assertInHTML(
- f'
{bookmark.resolved_title}',
+ f'
{bookmark.resolved_title}',
html, count=count
)
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
html = response.content.decode()
- self.assertContains(response, '
', count=len(bookmarks))
+ self.assertContains(response, '', count=len(bookmarks))
for bookmark in bookmarks:
self.assertBookmarkCount(html, bookmark, 1, link_target)
diff --git a/bookmarks/tests/test_bookmark_shared_view_performance.py b/bookmarks/tests/test_bookmark_shared_view_performance.py
index 588442c..eeeded6 100644
--- a/bookmarks/tests/test_bookmark_shared_view_performance.py
+++ b/bookmarks/tests/test_bookmark_shared_view_performance.py
@@ -28,7 +28,7 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
context = CaptureQueriesContext(self.get_connection())
with context:
response = self.client.get(reverse('bookmarks:shared'))
- self.assertContains(response, '', num_initial_bookmarks)
+ self.assertContains(response, '', num_initial_bookmarks)
number_of_queries = context.final_queries
@@ -41,4 +41,4 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
# assert num queries doesn't increase
with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse('bookmarks:shared'))
- self.assertContains(response, '', num_initial_bookmarks + num_additional_bookmarks)
+ self.assertContains(response, '', num_initial_bookmarks + num_additional_bookmarks)
diff --git a/bookmarks/tests/test_bookmarks_list_template.py b/bookmarks/tests/test_bookmarks_list_template.py
index 39b2f8f..f8ba05d 100644
--- a/bookmarks/tests/test_bookmarks_list_template.py
+++ b/bookmarks/tests/test_bookmarks_list_template.py
@@ -17,14 +17,12 @@ from bookmarks.views.partials import contexts
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
- unread = bookmark.unread
favicon_img = f'
' if bookmark.favicon_file else ''
self.assertInHTML(
f'''
+ rel="noopener">
{favicon_img}
{bookmark.resolved_title}
@@ -34,21 +32,16 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
def assertDateLabel(self, html: str, label_content: str):
self.assertInHTML(f'''
-
- {label_content}
-
+ {label_content}
|
''', html)
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
self.assertInHTML(f'''
-
-
- {label_content}
- ∞
-
-
+
+ {label_content} ∞
+
|
''', html)
@@ -126,18 +119,36 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
def assertNotesToggle(self, html: str, count=1):
self.assertInHTML(f'''
-
+
+ ''', html, count=count)
+
+ def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
+ self.assertInHTML(f'''
+
+ ''', html, count=count)
+
+ def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1):
+ self.assertInHTML(f'''
+
''', html, count=count)
def render_template(self,
@@ -236,11 +247,31 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_self')
- def test_should_respect_unread_flag(self):
- bookmark = self.setup_bookmark(unread=True)
+ def test_should_reflect_unread_state_as_css_class(self):
+ self.setup_bookmark(unread=True)
html = self.render_template()
- self.assertBookmarksLink(html, bookmark)
+ self.assertIn('', html)
+
+ def test_should_reflect_shared_state_as_css_class(self):
+ profile = self.get_or_create_test_user().profile
+ profile.enable_sharing = True
+ profile.save()
+
+ self.setup_bookmark(shared=True)
+ html = self.render_template()
+
+ self.assertIn('', html)
+
+ def test_should_reflect_both_unread_and_shared_state_as_css_class(self):
+ profile = self.get_or_create_test_user().profile
+ profile.enable_sharing = True
+ profile.save()
+
+ self.setup_bookmark(unread=True, shared=True)
+ html = self.render_template()
+
+ self.assertIn('', html)
def test_show_bookmark_actions_for_owned_bookmarks(self):
bookmark = self.setup_bookmark()
@@ -333,6 +364,66 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertBookmarkURLHidden(html, bookmark)
+ def test_show_mark_as_read_when_unread(self):
+ bookmark = self.setup_bookmark(unread=True)
+ html = self.render_template()
+
+ self.assertMarkAsReadButton(html, bookmark)
+
+ def test_hide_mark_as_read_when_read(self):
+ bookmark = self.setup_bookmark(unread=False)
+ html = self.render_template()
+
+ self.assertMarkAsReadButton(html, bookmark, count=0)
+
+ def test_hide_mark_as_read_for_non_owned_bookmarks(self):
+ other_user = self.setup_user(enable_sharing=True)
+
+ bookmark = self.setup_bookmark(user=other_user, shared=True, unread=True)
+ html = self.render_template(context_type=contexts.SharedBookmarkListContext)
+
+ self.assertBookmarksLink(html, bookmark)
+ self.assertMarkAsReadButton(html, bookmark, count=0)
+
+ def test_show_unshare_button_when_shared(self):
+ profile = self.get_or_create_test_user().profile
+ profile.enable_sharing = True
+ profile.save()
+
+ bookmark = self.setup_bookmark(shared=True)
+ html = self.render_template()
+
+ self.assertUnshareButton(html, bookmark)
+
+ def test_hide_unshare_button_when_not_shared(self):
+ profile = self.get_or_create_test_user().profile
+ profile.enable_sharing = True
+ profile.save()
+
+ bookmark = self.setup_bookmark(shared=False)
+ html = self.render_template()
+
+ self.assertUnshareButton(html, bookmark, count=0)
+
+ def test_hide_unshare_button_when_sharing_is_disabled(self):
+ profile = self.get_or_create_test_user().profile
+ profile.enable_sharing = False
+ profile.save()
+
+ bookmark = self.setup_bookmark(shared=True)
+ html = self.render_template()
+
+ self.assertUnshareButton(html, bookmark, count=0)
+
+ def test_hide_unshare_for_non_owned_bookmarks(self):
+ other_user = self.setup_user(enable_sharing=True)
+
+ bookmark = self.setup_bookmark(user=other_user, shared=True)
+ html = self.render_template(context_type=contexts.SharedBookmarkListContext)
+
+ self.assertBookmarksLink(html, bookmark)
+ self.assertUnshareButton(html, bookmark, count=0)
+
def test_without_notes(self):
self.setup_bookmark()
html = self.render_template()
@@ -425,6 +516,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
bookmark.notes = '**Example:** `print("Hello world!")`'
bookmark.favicon_file = 'https_example_com.png'
bookmark.shared = True
+ bookmark.unread = True
bookmark.save()
html = self.render_template(context_type=contexts.SharedBookmarkListContext, user=AnonymousUser())
@@ -432,6 +524,8 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
self.assertNoBookmarkActions(html, bookmark)
self.assertShareInfo(html, bookmark)
+ self.assertMarkAsReadButton(html, bookmark, count=0)
+ self.assertUnshareButton(html, bookmark, count=0)
note_html = 'Example: print("Hello world!")
'
self.assertNotes(html, note_html, 1)
self.assertFaviconVisible(html, bookmark)
diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py
index 64aa152..8e46803 100644
--- a/bookmarks/views/bookmarks.py
+++ b/bookmarks/views/bookmarks.py
@@ -145,6 +145,16 @@ def unarchive(request, bookmark_id: int):
unarchive_bookmark(bookmark)
+def unshare(request, bookmark_id: int):
+ try:
+ bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
+ except Bookmark.DoesNotExist:
+ raise Http404('Bookmark does not exist')
+
+ bookmark.shared = False
+ bookmark.save()
+
+
def mark_as_read(request, bookmark_id: int):
try:
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
@@ -166,6 +176,8 @@ def action(request):
remove(request, request.POST['remove'])
if 'mark_as_read' in request.POST:
mark_as_read(request, request.POST['mark_as_read'])
+ if 'unshare' in request.POST:
+ unshare(request, request.POST['unshare'])
if 'bulk_archive' in request.POST:
bookmark_ids = request.POST.getlist('bookmark_id')
archive_bookmarks(bookmark_ids, request.user)
diff --git a/bookmarks/views/partials/contexts.py b/bookmarks/views/partials/contexts.py
index b2ddd15..d48075b 100644
--- a/bookmarks/views/partials/contexts.py
+++ b/bookmarks/views/partials/contexts.py
@@ -7,17 +7,58 @@ from django.db import models
from django.urls import reverse
from bookmarks import queries
-from bookmarks.models import BookmarkFilters, User, UserProfile, Tag
-from bookmarks.utils import unique
+from bookmarks.models import Bookmark, BookmarkFilters, User, UserProfile, Tag
+from bookmarks import utils
DEFAULT_PAGE_SIZE = 30
+class BookmarkItem:
+ def __init__(self, bookmark: Bookmark, user: User, profile: UserProfile) -> None:
+ self.bookmark = bookmark
+
+ is_editable = bookmark.owner == user
+ self.is_editable = is_editable
+
+ self.id = bookmark.id
+ self.url = bookmark.url
+ self.title = bookmark.resolved_title
+ self.description = bookmark.resolved_description
+ self.notes = bookmark.notes
+ self.tag_names = bookmark.tag_names
+ self.web_archive_snapshot_url = bookmark.web_archive_snapshot_url
+ self.favicon_file = bookmark.favicon_file
+ self.is_archived = bookmark.is_archived
+ self.unread = bookmark.unread
+ self.owner = bookmark.owner
+
+ css_classes = []
+ if bookmark.unread:
+ css_classes.append('unread')
+ if bookmark.shared:
+ css_classes.append('shared')
+
+ self.css_classes = ' '.join(css_classes)
+
+ if profile.bookmark_date_display == UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE:
+ self.display_date = utils.humanize_relative_date(bookmark.date_added)
+ elif profile.bookmark_date_display == UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE:
+ self.display_date = utils.humanize_absolute_date(bookmark.date_added)
+
+ self.show_notes_button = bookmark.notes and not profile.permanent_notes
+ self.show_mark_as_read = is_editable and bookmark.unread
+ self.show_unshare = is_editable and bookmark.shared and profile.enable_sharing
+
+ self.has_extra_actions = self.show_notes_button or self.show_mark_as_read or self.show_unshare
+
+
class BookmarkListContext:
def __init__(self, request: WSGIRequest) -> None:
self.request = request
self.filters = BookmarkFilters(self.request)
+ user = request.user
+ user_profile = request.user_profile
query_set = self.get_bookmark_query_set()
page_number = request.GET.get('page')
paginator = Paginator(query_set, DEFAULT_PAGE_SIZE)
@@ -25,14 +66,16 @@ class BookmarkListContext:
# Prefetch related objects, this avoids n+1 queries when accessing fields in templates
models.prefetch_related_objects(bookmarks_page.object_list, 'owner', 'tags')
+ self.items = [BookmarkItem(bookmark, user, user_profile) for bookmark in bookmarks_page]
+
self.is_empty = paginator.count == 0
self.bookmarks_page = bookmarks_page
self.return_url = self.generate_return_url(page_number)
- self.link_target = request.user_profile.bookmark_link_target
- self.date_display = request.user_profile.bookmark_date_display
- self.show_url = request.user_profile.display_url
- self.show_favicons = request.user_profile.enable_favicons
- self.show_notes = request.user_profile.permanent_notes
+ self.link_target = user_profile.bookmark_link_target
+ self.date_display = user_profile.bookmark_date_display
+ self.show_url = user_profile.display_url
+ self.show_favicons = user_profile.enable_favicons
+ self.show_notes = user_profile.permanent_notes
def generate_return_url(self, page: int):
base_url = self.get_base_url()
@@ -120,8 +163,8 @@ class TagCloudContext:
query_set = self.get_tag_query_set()
tags = list(query_set)
selected_tags = self.get_selected_tags(tags)
- unique_tags = unique(tags, key=lambda x: str.lower(x.name))
- unique_selected_tags = unique(selected_tags, key=lambda x: str.lower(x.name))
+ unique_tags = utils.unique(tags, key=lambda x: str.lower(x.name))
+ unique_selected_tags = utils.unique(selected_tags, key=lambda x: str.lower(x.name))
has_selected_tags = len(unique_selected_tags) > 0
unselected_tags = set(unique_tags).symmetric_difference(unique_selected_tags)
groups = TagGroup.create_tag_groups(unselected_tags)