diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py
index 5e0969e56..1feb495b7 100644
--- a/bookwyrm/activitystreams.py
+++ b/bookwyrm/activitystreams.py
@@ -331,8 +331,15 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
@receiver(signals.post_delete, sender=models.UserBlocks)
# pylint: disable=unused-argument
def add_statuses_on_unblock(sender, instance, *args, **kwargs):
- """remove statuses from all feeds on block"""
- public_streams = [v for (k, v) in streams.items() if k != "home"]
+ """add statuses back to all feeds on unblock"""
+ # make sure there isn't a block in the other direction
+ if models.UserBlocks.objects.filter(
+ user_subject=instance.user_object,
+ user_object=instance.user_subject,
+ ).exists():
+ return
+
+ public_streams = [k for (k, v) in streams.items() if k != "home"]
# add statuses back to streams with statuses from anyone
if instance.user_subject.local:
diff --git a/bookwyrm/migrations/0101_auto_20210929_1847.py b/bookwyrm/migrations/0101_auto_20210929_1847.py
index 3fca28eac..bdda8484f 100644
--- a/bookwyrm/migrations/0101_auto_20210929_1847.py
+++ b/bookwyrm/migrations/0101_auto_20210929_1847.py
@@ -17,7 +17,7 @@ def infer_format(app_registry, schema_editor):
for edition in editions:
free_format = edition.physical_format_detail.lower()
edition.physical_format = infer_physical_format(free_format)
- edition.save()
+ edition.save(broadcast=False, update_fields=["physical_format"])
def reverse(app_registry, schema_editor):
diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html
index 903ca7907..5febf4e22 100644
--- a/bookwyrm/templates/feed/status.html
+++ b/bookwyrm/templates/feed/status.html
@@ -1,5 +1,6 @@
{% extends 'feed/layout.html' %}
{% load i18n %}
+{% load bookwyrm_tags %}
{% block panel %}
-{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
+
+
+ {% for parent in ancestors %}
+ {% if parent.id %}
+
+ {% include 'snippets/status/status.html' with status=parent|load_subclass %}
+
+ {% endif %}
+ {% endfor %}
+
+ {% include 'snippets/status/status.html' with status=status main=True %}
+
+
+ {% for child in children %}
+
+ {% include 'snippets/status/status.html' with status=child %}
+
+ {% endfor %}
+
+
{% endblock %}
diff --git a/bookwyrm/templates/feed/thread.html b/bookwyrm/templates/feed/thread.html
deleted file mode 100644
index c1b624e3c..000000000
--- a/bookwyrm/templates/feed/thread.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% load status_display %}
-
-
-
-{% with depth=depth|add:1 %}
- {% if depth <= max_depth and status.reply_parent and direction <= 0 %}
- {% with direction=-1 %}
- {% include 'feed/thread.html' with status=status|parent is_root=False %}
- {% endwith %}
- {% endif %}
-
-
- {% include 'snippets/status/status.html' with status=status main=is_root %}
-
-
-{% if depth <= max_depth and direction >= 0 %}
- {% for reply in status|replies %}
- {% with direction=1 %}
- {% include 'feed/thread.html' with status=reply is_root=False %}
- {% endwith %}
- {% endfor %}
-{% endif %}
-{% endwith %}
-
-
diff --git a/bookwyrm/templates/shelf/create_shelf_form.html b/bookwyrm/templates/shelf/create_shelf_form.html
index e15e1cc1d..c3d2b5faa 100644
--- a/bookwyrm/templates/shelf/create_shelf_form.html
+++ b/bookwyrm/templates/shelf/create_shelf_form.html
@@ -7,7 +7,7 @@
{% block form %}
{% endblock %}
diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py
index e683f9c24..2e03c13b4 100644
--- a/bookwyrm/templatetags/bookwyrm_tags.py
+++ b/bookwyrm/templatetags/bookwyrm_tags.py
@@ -53,18 +53,24 @@ def get_next_shelf(current_shelf):
return "to-read"
+@register.filter(name="load_subclass")
+def load_subclass(status):
+ """sometimes you didn't select_subclass"""
+ if hasattr(status, "quotation"):
+ return status.quotation
+ if hasattr(status, "review"):
+ return status.review
+ if hasattr(status, "comment"):
+ return status.comment
+ return status
+
+
@register.simple_tag(takes_context=False)
def related_status(notification):
"""for notifications"""
if not notification.related_status:
return None
- if hasattr(notification.related_status, "quotation"):
- return notification.related_status.quotation
- if hasattr(notification.related_status, "review"):
- return notification.related_status.review
- if hasattr(notification.related_status, "comment"):
- return notification.related_status.comment
- return notification.related_status
+ return load_subclass(notification.related_status)
@register.simple_tag(takes_context=True)
diff --git a/bookwyrm/tests/activitystreams/test_signals.py b/bookwyrm/tests/activitystreams/test_signals.py
index 1c94cc9f5..eb70d28e4 100644
--- a/bookwyrm/tests/activitystreams/test_signals.py
+++ b/bookwyrm/tests/activitystreams/test_signals.py
@@ -16,6 +16,9 @@ class ActivitystreamsSignals(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
)
+ self.another_user = models.User.objects.create_user(
+ "fish", "fish@fish.fish", "password", local=True, localname="fish"
+ )
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
@@ -66,3 +69,49 @@ class ActivitystreamsSignals(TestCase):
args = mock.call_args[0]
self.assertEqual(args[0], "books")
self.assertEqual(args[1], self.local_user.id)
+
+ def test_remove_statuses_on_block(self, _):
+ """don't show statuses from blocked users"""
+ with patch("bookwyrm.activitystreams.remove_user_statuses_task.delay") as mock:
+ models.UserBlocks.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user,
+ )
+
+ args = mock.call_args[0]
+ self.assertEqual(args[0], self.local_user.id)
+ self.assertEqual(args[1], self.remote_user.id)
+
+ def test_add_statuses_on_unblock(self, _):
+ """re-add statuses on unblock"""
+ with patch("bookwyrm.activitystreams.remove_user_statuses_task.delay"):
+ block = models.UserBlocks.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user,
+ )
+
+ with patch("bookwyrm.activitystreams.add_user_statuses_task.delay") as mock:
+ block.delete()
+
+ args = mock.call_args[0]
+ kwargs = mock.call_args.kwargs
+ self.assertEqual(args[0], self.local_user.id)
+ self.assertEqual(args[1], self.remote_user.id)
+ self.assertEqual(kwargs["stream_list"], ["local", "books"])
+
+ def test_add_statuses_on_unblock_reciprocal_block(self, _):
+ """re-add statuses on unblock"""
+ with patch("bookwyrm.activitystreams.remove_user_statuses_task.delay"):
+ block = models.UserBlocks.objects.create(
+ user_subject=self.local_user,
+ user_object=self.remote_user,
+ )
+ block = models.UserBlocks.objects.create(
+ user_subject=self.remote_user,
+ user_object=self.local_user,
+ )
+
+ with patch("bookwyrm.activitystreams.add_user_statuses_task.delay") as mock:
+ block.delete()
+
+ self.assertEqual(mock.call_count, 0)
diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py
index da35f5571..3299249a0 100644
--- a/bookwyrm/tests/views/test_search.py
+++ b/bookwyrm/tests/views/test_search.py
@@ -51,7 +51,7 @@ class Views(TestCase):
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book")
- self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id))
+ self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
def test_search_no_query(self):
"""just the search page"""
@@ -91,12 +91,27 @@ class Views(TestCase):
self.assertIsInstance(response, TemplateResponse)
response.render()
connector_results = response.context_data["results"]
+ self.assertEqual(len(connector_results), 2)
self.assertEqual(connector_results[0]["results"][0].title, "Test Book")
self.assertEqual(
connector_results[1]["results"][0].title,
"This Is How You Lose the Time War",
)
+ # don't search remote
+ request = self.factory.get("", {"q": "Test Book", "remote": True})
+ anonymous_user = AnonymousUser
+ anonymous_user.is_authenticated = False
+ request.user = anonymous_user
+ with patch("bookwyrm.views.search.is_api_request") as is_api:
+ is_api.return_value = False
+ response = view(request)
+ self.assertIsInstance(response, TemplateResponse)
+ response.render()
+ connector_results = response.context_data["results"]
+ self.assertEqual(len(connector_results), 1)
+ self.assertEqual(connector_results[0]["results"][0].title, "Test Book")
+
def test_search_users(self):
"""searches remote connectors"""
view = views.Search.as_view()
diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py
index 9de647a24..298ba5a30 100644
--- a/bookwyrm/views/books/books.py
+++ b/bookwyrm/views/books/books.py
@@ -172,6 +172,7 @@ def add_description(request, book_id):
return redirect("book", book.id)
+@login_required
@require_POST
def resolve_book(request):
"""figure out the local path to a book from a remote_id"""
diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py
index 94bd14155..1445dc011 100644
--- a/bookwyrm/views/books/edit_book.py
+++ b/bookwyrm/views/books/edit_book.py
@@ -10,8 +10,7 @@ from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator
from django.views import View
-from bookwyrm import forms, models
-from bookwyrm.connectors import connector_manager
+from bookwyrm import book_search, forms, models
from bookwyrm.views.helpers import get_edition
from .books import set_cover_from_url
@@ -73,10 +72,9 @@ class EditBook(View):
if not book:
# check if this is an edition of an existing work
author_text = book.author_text if book else add_author
- data["book_matches"] = connector_manager.local_search(
+ data["book_matches"] = book_search.search(
f'{form.cleaned_data.get("title")} {author_text}',
min_confidence=0.5,
- raw=True,
)[:5]
# either of the above cases requires additional confirmation
diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py
index 0c2647aea..7f1bc22c2 100644
--- a/bookwyrm/views/feed.py
+++ b/bookwyrm/views/feed.py
@@ -109,10 +109,60 @@ class Status(View):
status.to_activity(pure=not is_bookwyrm_request(request))
)
+ visible_thread = privacy_filter(
+ request.user, models.Status.objects.filter(thread_id=status.thread_id)
+ ).values_list("id", flat=True)
+ visible_thread = list(visible_thread)
+
+ ancestors = models.Status.objects.select_subclasses().raw(
+ """
+ WITH RECURSIVE get_thread(depth, id, path) AS (
+
+ SELECT 1, st.id, ARRAY[st.id]
+ FROM bookwyrm_status st
+ WHERE id = '%s' AND id = ANY(%s)
+
+ UNION
+
+ SELECT (gt.depth + 1), st.reply_parent_id, path || st.id
+ FROM get_thread gt, bookwyrm_status st
+
+ WHERE st.id = gt.id AND depth < 5 AND st.id = ANY(%s)
+
+ )
+
+ SELECT * FROM get_thread ORDER BY path DESC;
+ """,
+ params=[status.reply_parent_id or 0, visible_thread, visible_thread],
+ )
+ children = models.Status.objects.select_subclasses().raw(
+ """
+ WITH RECURSIVE get_thread(depth, id, path) AS (
+
+ SELECT 1, st.id, ARRAY[st.id]
+ FROM bookwyrm_status st
+ WHERE reply_parent_id = '%s' AND id = ANY(%s)
+
+ UNION
+
+ SELECT (gt.depth + 1), st.id, path || st.id
+ FROM get_thread gt, bookwyrm_status st
+
+ WHERE st.reply_parent_id = gt.id AND depth < 5 AND st.id = ANY(%s)
+
+ )
+
+ SELECT * FROM get_thread ORDER BY path;
+ """,
+ params=[status.id, visible_thread, visible_thread],
+ )
+
data = {
**feed_page_data(request.user),
**{
"status": status,
+ "children": children,
+ "ancestors": ancestors,
},
}
return TemplateResponse(request, "feed/status.html", data)
diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py
index bd31fbbc8..7e469f7f0 100644
--- a/bookwyrm/views/helpers.py
+++ b/bookwyrm/views/helpers.py
@@ -61,8 +61,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
# exclude blocks from both directions
if not viewer.is_anonymous:
- blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
- queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))
+ queryset = queryset.exclude(Q(user__blocked_by=viewer) | Q(user__blocks=viewer))
# you can't see followers only or direct messages if you're not logged in
if viewer.is_anonymous:
@@ -75,7 +74,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
if following_only:
queryset = queryset.exclude(
~Q( # remove everythign except
- Q(user__in=viewer.following.all())
+ Q(user__followers=viewer)
| Q(user=viewer) # user following
| Q(mention_users=viewer) # is self # mentions user
),
diff --git a/bookwyrm/views/preferences/block.py b/bookwyrm/views/preferences/block.py
index 90b3be90c..1eccf4612 100644
--- a/bookwyrm/views/preferences/block.py
+++ b/bookwyrm/views/preferences/block.py
@@ -14,7 +14,7 @@ class Block(View):
"""blocking users"""
def get(self, request):
- """list of blocked users?"""
+ """list of blocked users"""
return TemplateResponse(request, "preferences/blocks.html")
def post(self, request, user_id):
diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py
index df891266d..d131b399f 100644
--- a/bookwyrm/views/search.py
+++ b/bookwyrm/views/search.py
@@ -67,11 +67,11 @@ class Search(View):
return TemplateResponse(request, f"search/{search_type}.html", data)
-def book_search(query, _, min_confidence, search_remote=False):
+def book_search(query, user, min_confidence, search_remote=False):
"""the real business is elsewhere"""
# try a local-only search
results = [{"results": search(query, min_confidence=min_confidence)}]
- if results and not search_remote:
+ if not user.is_authenticated or (results[0]["results"] and not search_remote):
return results, False
# if there were no local results, or the request was for remote, search all sources
@@ -101,7 +101,7 @@ def user_search(query, viewer, *_):
.filter(
similarity__gt=0.5,
)
- .order_by("-similarity")[:10]
+ .order_by("-similarity")
), None
@@ -122,5 +122,5 @@ def list_search(query, viewer, *_):
.filter(
similarity__gt=0.1,
)
- .order_by("-similarity")[:10]
+ .order_by("-similarity")
), None