From 7caaddbb222d6bab129a23786b25e6b804cb5f0b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 15:20:35 -0700 Subject: [PATCH 01/20] Get thread children with depth first recusive search --- bookwyrm/templates/feed/status.html | 14 +++++++++++++- bookwyrm/templates/feed/thread.html | 25 ------------------------- bookwyrm/views/feed.py | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 26 deletions(-) delete mode 100644 bookwyrm/templates/feed/thread.html diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html index 903ca790..429c657d 100644 --- a/bookwyrm/templates/feed/status.html +++ b/bookwyrm/templates/feed/status.html @@ -9,7 +9,19 @@ -{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} +
+
+
+ {% 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 c1b624e3..00000000 --- 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/views/feed.py b/bookwyrm/views/feed.py index 0c2647ae..f79b1515 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -109,10 +109,30 @@ class Status(View): status.to_activity(pure=not is_bookwyrm_request(request)) ) + 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' + + 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 + + ) + + SELECT * FROM get_thread ORDER BY path; + """, params=[status.id]) + data = { **feed_page_data(request.user), **{ "status": status, + "children": children, }, } return TemplateResponse(request, "feed/status.html", data) From cd571614898af7d825a73bfa9abd04bdc26d3b75 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 16:55:05 -0700 Subject: [PATCH 02/20] Privacy filter for thread --- bookwyrm/views/feed.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index f79b1515..92953057 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -109,24 +109,31 @@ 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) + 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' + 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 + 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]) + """, params=[status.id, visible_thread, visible_thread]) + data = { **feed_page_data(request.user), From 43f0440505e1163ca35eb59de2f8b6b42b1a9a56 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 16:55:15 -0700 Subject: [PATCH 03/20] Improved privacy query --- bookwyrm/views/helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index bd31fbbc..7e469f7f 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 ), From 14ac8bb1b5a36aef2cb87134a143896dde5024b6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 16:56:23 -0700 Subject: [PATCH 04/20] Python formatting --- bookwyrm/views/feed.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index 92953057..b873224b 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -110,12 +110,12 @@ class Status(View): ) visible_thread = privacy_filter( - request.user, - models.Status.objects.filter(thread_id=status.thread_id) + request.user, models.Status.objects.filter(thread_id=status.thread_id) ).values_list("id", flat=True) visible_thread = list(visible_thread) - children = models.Status.objects.select_subclasses().raw(""" + children = models.Status.objects.select_subclasses().raw( + """ WITH RECURSIVE get_thread(depth, id, path) AS ( SELECT 1, st.id, ARRAY[st.id] @@ -132,8 +132,9 @@ class Status(View): ) SELECT * FROM get_thread ORDER BY path; - """, params=[status.id, visible_thread, visible_thread]) - + """, + params=[status.id, visible_thread, visible_thread], + ) data = { **feed_page_data(request.user), From e1271dd07906df63e33f468f4b9b3900878b5c38 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 19:04:01 -0700 Subject: [PATCH 05/20] Less costly migration --- bookwyrm/migrations/0101_auto_20210929_1847.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/migrations/0101_auto_20210929_1847.py b/bookwyrm/migrations/0101_auto_20210929_1847.py index 3fca28ea..bdda8484 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): From c821aaa18e3b231c81327fc2c621dd2e4e235575 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 18:24:54 -0700 Subject: [PATCH 06/20] Load status ancestors --- bookwyrm/templates/feed/status.html | 7 +++++++ bookwyrm/views/feed.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html index 429c657d..8f354fb8 100644 --- a/bookwyrm/templates/feed/status.html +++ b/bookwyrm/templates/feed/status.html @@ -11,6 +11,13 @@
+ {% for parent in ancestors %} + {% if parent %} +
+ {% include 'snippets/status/status.html' with status=parent %} +
+ {% endif %} + {% endfor %}
{% include 'snippets/status/status.html' with status=status main=True %}
diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index b873224b..7f1bc22c 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -114,6 +114,27 @@ class Status(View): ).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 ( @@ -141,6 +162,7 @@ class Status(View): **{ "status": status, "children": children, + "ancestors": ancestors, }, } return TemplateResponse(request, "feed/status.html", data) From 3c82230eedab24a6619e85032b096bd1a8f16618 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 2 Oct 2021 19:22:21 -0700 Subject: [PATCH 07/20] Load subclasses --- bookwyrm/templates/feed/status.html | 7 ++++--- bookwyrm/templatetags/bookwyrm_tags.py | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html index 8f354fb8..5febf4e2 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 %}
@@ -12,13 +13,13 @@
{% for parent in ancestors %} - {% if parent %} + {% if parent.id %}
- {% include 'snippets/status/status.html' with status=parent %} + {% include 'snippets/status/status.html' with status=parent|load_subclass %}
{% endif %} {% endfor %} -
+
{% include 'snippets/status/status.html' with status=status main=True %}
diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index e683f9c2..2e03c13b 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) From 9509c5e2884fb8be40981b8b69939115bcadc792 Mon Sep 17 00:00:00 2001 From: Hugh Rundle Date: Sun, 3 Oct 2021 19:41:38 +1100 Subject: [PATCH 08/20] new shelves can be given names always Previously new shelves created when a default shelf was selected did not provide the option to create a new unique name. Now they do. fixes #1491 --- bookwyrm/templates/shelf/create_shelf_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/templates/shelf/create_shelf_form.html b/bookwyrm/templates/shelf/create_shelf_form.html index e15e1cc1..c3d2b5fa 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 %} From 9a5003f92a151f5936c787b7d772fc2e196ce716 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 09:18:17 -0700 Subject: [PATCH 09/20] Don't let anonymous users search remote data --- bookwyrm/views/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index df891266..4c19a193 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 and not search_remote): return results, False # if there were no local results, or the request was for remote, search all sources From 4787d854b836ed30003a9fafa8e3a89057e1b9ac Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 09:19:19 -0700 Subject: [PATCH 10/20] require auth on resolve book endpoint --- bookwyrm/views/books/books.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/views/books/books.py b/bookwyrm/views/books/books.py index 9de647a2..298ba5a3 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""" From ca7967a3a376d2b71b7d4d8b0f4b6ae6c4687ec6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 09:29:24 -0700 Subject: [PATCH 11/20] Adds test for remote search for anonymous user --- bookwyrm/tests/views/test_search.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index da35f557..3299249a 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() From 7d8cd999263ba66efa34298be95796e9364f34e0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 09:38:41 -0700 Subject: [PATCH 12/20] Remove hard limit on search endpoints --- bookwyrm/views/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 4c19a193..f03206e3 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -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 From 9059b78b57601c0dcdf79732515ab0150d59f2dc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 09:44:27 -0700 Subject: [PATCH 13/20] Fixes testing if endpoint got results --- bookwyrm/views/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 4c19a193..33ce3706 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -71,7 +71,7 @@ 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 not user.is_authenticated or (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 From 668f71f96c28141c37ede383a3b73b878245dfac Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 11:19:03 -0700 Subject: [PATCH 14/20] Tests block and unblock activitystream signals --- .../tests/activitystreams/test_signals.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/bookwyrm/tests/activitystreams/test_signals.py b/bookwyrm/tests/activitystreams/test_signals.py index 1c94cc9f..eb70d28e 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) From 0798ba028f5d52f0e9691befb80600ef498eb1ad Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 11:46:26 -0700 Subject: [PATCH 15/20] Fixes unblock signal --- bookwyrm/activitystreams.py | 11 +++++++++-- bookwyrm/views/preferences/block.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index 5e0969e5..1feb495b 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/views/preferences/block.py b/bookwyrm/views/preferences/block.py index 90b3be90..1eccf461 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): From 889930aa69bfe07523aa9dbb63fcce130df76785 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 11:55:16 -0700 Subject: [PATCH 16/20] Fixes create book flow for search refactor --- bookwyrm/views/books/edit_book.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py index 94bd1415..1445dc01 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 From 5cd8109820f903a76589203a0ed2e65a74d83a95 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 12:15:44 -0700 Subject: [PATCH 17/20] Adds missing connector migration --- .../0105_alter_connector_connector_file.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 bookwyrm/migrations/0105_alter_connector_connector_file.py diff --git a/bookwyrm/migrations/0105_alter_connector_connector_file.py b/bookwyrm/migrations/0105_alter_connector_connector_file.py new file mode 100644 index 00000000..a6f08f77 --- /dev/null +++ b/bookwyrm/migrations/0105_alter_connector_connector_file.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2021-10-03 19:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0104_auto_20211001_2012"), + ] + + operations = [ + migrations.AlterField( + model_name="connector", + name="connector_file", + field=models.CharField( + choices=[ + ("openlibrary", "Openlibrary"), + ("inventaire", "Inventaire"), + ("bookwyrm_connector", "Bookwyrm Connector"), + ], + max_length=255, + ), + ), + ] From 23b021e87d10d5f835cbaa4a4a3644312fc6539b Mon Sep 17 00:00:00 2001 From: "OragePika, aka \"FANS DON'T CARE" <78921858+oragegu@users.noreply.github.com> Date: Sun, 3 Oct 2021 21:48:36 +0200 Subject: [PATCH 18/20] Update django.po --- locale/de_DE/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po index 569330fa..4ee3b9f7 100644 --- a/locale/de_DE/LC_MESSAGES/django.po +++ b/locale/de_DE/LC_MESSAGES/django.po @@ -2118,7 +2118,7 @@ msgstr "Beziehungen" #, fuzzy, python-format #| msgid "Finish \"%(book_title)s\"" msgid "Finish \"%(book_title)s\"" -msgstr "\"%(book_title)s\" abschließen" +msgstr "\"%(book_title)s\"zu Ende gelesen" #: bookwyrm/templates/reading_progress/start.html:5 #, fuzzy, python-format From ea182525491fed697592801a5fcde5a6e85ed619 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 3 Oct 2021 13:26:02 -0700 Subject: [PATCH 19/20] Fixes broken migration --- bookwyrm/migrations/0101_auto_20210929_1847.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/migrations/0101_auto_20210929_1847.py b/bookwyrm/migrations/0101_auto_20210929_1847.py index bdda8484..3fca28ea 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(broadcast=False, update_fields=["physical_format"]) + edition.save() def reverse(app_registry, schema_editor): From 1aa3d67d6406c8bbb46ab77eb1e479e61c64a840 Mon Sep 17 00:00:00 2001 From: oragegu Date: Mon, 4 Oct 2021 02:44:37 +0200 Subject: [PATCH 20/20] localisation compiled --- locale/fr_FR/LC_MESSAGES/django.mo | Bin 46063 -> 45713 bytes locale/fr_FR/LC_MESSAGES/django.po | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo index 641b48438b36f55f2cfc193d1cebb82ec9c568f2..fef5a0bafdeb1d25cc2cf11890f510326708073f 100644 GIT binary patch delta 13605 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 ZmYFXnQC8U{d3>@utVs9II delta 13879 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 d8Q>ETmzqj-{H+