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 %}
@@ -9,7 +10,26 @@
-{% 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 %}
- {% include "shelf/form.html" with editable=shelf.editable form=create_form %} + {% include "shelf/form.html" with editable=True form=create_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