mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
e9f60f93b2
14 changed files with 170 additions and 50 deletions
|
@ -331,8 +331,15 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
|
||||||
@receiver(signals.post_delete, sender=models.UserBlocks)
|
@receiver(signals.post_delete, sender=models.UserBlocks)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def add_statuses_on_unblock(sender, instance, *args, **kwargs):
|
def add_statuses_on_unblock(sender, instance, *args, **kwargs):
|
||||||
"""remove statuses from all feeds on block"""
|
"""add statuses back to all feeds on unblock"""
|
||||||
public_streams = [v for (k, v) in streams.items() if k != "home"]
|
# 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
|
# add statuses back to streams with statuses from anyone
|
||||||
if instance.user_subject.local:
|
if instance.user_subject.local:
|
||||||
|
|
|
@ -17,7 +17,7 @@ def infer_format(app_registry, schema_editor):
|
||||||
for edition in editions:
|
for edition in editions:
|
||||||
free_format = edition.physical_format_detail.lower()
|
free_format = edition.physical_format_detail.lower()
|
||||||
edition.physical_format = infer_physical_format(free_format)
|
edition.physical_format = infer_physical_format(free_format)
|
||||||
edition.save()
|
edition.save(broadcast=False, update_fields=["physical_format"])
|
||||||
|
|
||||||
|
|
||||||
def reverse(app_registry, schema_editor):
|
def reverse(app_registry, schema_editor):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'feed/layout.html' %}
|
{% extends 'feed/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<header class="block">
|
<header class="block">
|
||||||
|
@ -9,7 +10,26 @@
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
|
<div class="thread-parent is-relative block">
|
||||||
|
<div class="thread">
|
||||||
|
{% for parent in ancestors %}
|
||||||
|
{% if parent.id %}
|
||||||
|
<div class="block">
|
||||||
|
{% include 'snippets/status/status.html' with status=parent|load_subclass %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="is-main block" id="anchor-{{ status.id }}">
|
||||||
|
{% include 'snippets/status/status.html' with status=status main=True %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for child in children %}
|
||||||
|
<div class="block">
|
||||||
|
{% include 'snippets/status/status.html' with status=child %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
{% load status_display %}
|
|
||||||
|
|
||||||
<div class="thread-parent is-relative block">
|
|
||||||
<div class="thread">
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
<div{% if is_root %} class="block mt-5 is-main"{% endif %}>
|
|
||||||
{% include 'snippets/status/status.html' with status=status main=is_root %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<form name="create-shelf" action="{% url 'shelf-create' %}" method="post">
|
<form name="create-shelf" action="{% url 'shelf-create' %}" method="post">
|
||||||
{% include "shelf/form.html" with editable=shelf.editable form=create_form %}
|
{% include "shelf/form.html" with editable=True form=create_form %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -53,18 +53,24 @@ def get_next_shelf(current_shelf):
|
||||||
return "to-read"
|
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)
|
@register.simple_tag(takes_context=False)
|
||||||
def related_status(notification):
|
def related_status(notification):
|
||||||
"""for notifications"""
|
"""for notifications"""
|
||||||
if not notification.related_status:
|
if not notification.related_status:
|
||||||
return None
|
return None
|
||||||
if hasattr(notification.related_status, "quotation"):
|
return load_subclass(notification.related_status)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
|
|
|
@ -16,6 +16,9 @@ class ActivitystreamsSignals(TestCase):
|
||||||
self.local_user = models.User.objects.create_user(
|
self.local_user = models.User.objects.create_user(
|
||||||
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
|
"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"):
|
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
||||||
self.remote_user = models.User.objects.create_user(
|
self.remote_user = models.User.objects.create_user(
|
||||||
"rat",
|
"rat",
|
||||||
|
@ -66,3 +69,49 @@ class ActivitystreamsSignals(TestCase):
|
||||||
args = mock.call_args[0]
|
args = mock.call_args[0]
|
||||||
self.assertEqual(args[0], "books")
|
self.assertEqual(args[0], "books")
|
||||||
self.assertEqual(args[1], self.local_user.id)
|
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)
|
||||||
|
|
|
@ -51,7 +51,7 @@ class Views(TestCase):
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertEqual(len(data), 1)
|
self.assertEqual(len(data), 1)
|
||||||
self.assertEqual(data[0]["title"], "Test Book")
|
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):
|
def test_search_no_query(self):
|
||||||
"""just the search page"""
|
"""just the search page"""
|
||||||
|
@ -91,12 +91,27 @@ class Views(TestCase):
|
||||||
self.assertIsInstance(response, TemplateResponse)
|
self.assertIsInstance(response, TemplateResponse)
|
||||||
response.render()
|
response.render()
|
||||||
connector_results = response.context_data["results"]
|
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[0]["results"][0].title, "Test Book")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
connector_results[1]["results"][0].title,
|
connector_results[1]["results"][0].title,
|
||||||
"This Is How You Lose the Time War",
|
"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):
|
def test_search_users(self):
|
||||||
"""searches remote connectors"""
|
"""searches remote connectors"""
|
||||||
view = views.Search.as_view()
|
view = views.Search.as_view()
|
||||||
|
|
|
@ -172,6 +172,7 @@ def add_description(request, book_id):
|
||||||
return redirect("book", book.id)
|
return redirect("book", book.id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def resolve_book(request):
|
def resolve_book(request):
|
||||||
"""figure out the local path to a book from a remote_id"""
|
"""figure out the local path to a book from a remote_id"""
|
||||||
|
|
|
@ -10,8 +10,7 @@ from django.utils.datastructures import MultiValueDictKeyError
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import book_search, forms, models
|
||||||
from bookwyrm.connectors import connector_manager
|
|
||||||
from bookwyrm.views.helpers import get_edition
|
from bookwyrm.views.helpers import get_edition
|
||||||
from .books import set_cover_from_url
|
from .books import set_cover_from_url
|
||||||
|
|
||||||
|
@ -73,10 +72,9 @@ class EditBook(View):
|
||||||
if not book:
|
if not book:
|
||||||
# check if this is an edition of an existing work
|
# check if this is an edition of an existing work
|
||||||
author_text = book.author_text if book else add_author
|
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}',
|
f'{form.cleaned_data.get("title")} {author_text}',
|
||||||
min_confidence=0.5,
|
min_confidence=0.5,
|
||||||
raw=True,
|
|
||||||
)[:5]
|
)[:5]
|
||||||
|
|
||||||
# either of the above cases requires additional confirmation
|
# either of the above cases requires additional confirmation
|
||||||
|
|
|
@ -109,10 +109,60 @@ class Status(View):
|
||||||
status.to_activity(pure=not is_bookwyrm_request(request))
|
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 = {
|
data = {
|
||||||
**feed_page_data(request.user),
|
**feed_page_data(request.user),
|
||||||
**{
|
**{
|
||||||
"status": status,
|
"status": status,
|
||||||
|
"children": children,
|
||||||
|
"ancestors": ancestors,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "feed/status.html", data)
|
return TemplateResponse(request, "feed/status.html", data)
|
||||||
|
|
|
@ -61,8 +61,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
|
||||||
|
|
||||||
# exclude blocks from both directions
|
# exclude blocks from both directions
|
||||||
if not viewer.is_anonymous:
|
if not viewer.is_anonymous:
|
||||||
blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
|
queryset = queryset.exclude(Q(user__blocked_by=viewer) | Q(user__blocks=viewer))
|
||||||
queryset = queryset.exclude(Q(user__in=blocked) | Q(user__blocks=viewer))
|
|
||||||
|
|
||||||
# you can't see followers only or direct messages if you're not logged in
|
# you can't see followers only or direct messages if you're not logged in
|
||||||
if viewer.is_anonymous:
|
if viewer.is_anonymous:
|
||||||
|
@ -75,7 +74,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
|
||||||
if following_only:
|
if following_only:
|
||||||
queryset = queryset.exclude(
|
queryset = queryset.exclude(
|
||||||
~Q( # remove everythign except
|
~Q( # remove everythign except
|
||||||
Q(user__in=viewer.following.all())
|
Q(user__followers=viewer)
|
||||||
| Q(user=viewer) # user following
|
| Q(user=viewer) # user following
|
||||||
| Q(mention_users=viewer) # is self # mentions user
|
| Q(mention_users=viewer) # is self # mentions user
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Block(View):
|
||||||
"""blocking users"""
|
"""blocking users"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""list of blocked users?"""
|
"""list of blocked users"""
|
||||||
return TemplateResponse(request, "preferences/blocks.html")
|
return TemplateResponse(request, "preferences/blocks.html")
|
||||||
|
|
||||||
def post(self, request, user_id):
|
def post(self, request, user_id):
|
||||||
|
|
|
@ -67,11 +67,11 @@ class Search(View):
|
||||||
return TemplateResponse(request, f"search/{search_type}.html", data)
|
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"""
|
"""the real business is elsewhere"""
|
||||||
# try a local-only search
|
# try a local-only search
|
||||||
results = [{"results": search(query, min_confidence=min_confidence)}]
|
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
|
return results, False
|
||||||
|
|
||||||
# if there were no local results, or the request was for remote, search all sources
|
# 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(
|
.filter(
|
||||||
similarity__gt=0.5,
|
similarity__gt=0.5,
|
||||||
)
|
)
|
||||||
.order_by("-similarity")[:10]
|
.order_by("-similarity")
|
||||||
), None
|
), None
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,5 +122,5 @@ def list_search(query, viewer, *_):
|
||||||
.filter(
|
.filter(
|
||||||
similarity__gt=0.1,
|
similarity__gt=0.1,
|
||||||
)
|
)
|
||||||
.order_by("-similarity")[:10]
|
.order_by("-similarity")
|
||||||
), None
|
), None
|
||||||
|
|
Loading…
Reference in a new issue