Merge branch 'main' into activitystreams-celery
This commit is contained in:
@ -6,11 +6,12 @@ from django.db.models import signals, Q
|
||||
from bookwyrm import models
|
||||
from bookwyrm.redis_store import RedisStore, r
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.settings import STREAMS
|
||||
from bookwyrm.views.helpers import privacy_filter
|
||||
|
||||
|
||||
class ActivityStream(RedisStore):
|
||||
"""a category of activity stream (like home, local, federated)"""
|
||||
"""a category of activity stream (like home, local, books)"""
|
||||
|
||||
def stream_id(self, user):
|
||||
"""the redis key for this user's instance of this stream"""
|
||||
@ -157,29 +158,60 @@ class LocalStream(ActivityStream):
|
||||
)
|
||||
|
||||
|
||||
class FederatedStream(ActivityStream):
|
||||
"""users you follow"""
|
||||
class BooksStream(ActivityStream):
|
||||
"""books on your shelves"""
|
||||
|
||||
key = "federated"
|
||||
key = "books"
|
||||
|
||||
def get_audience(self, status):
|
||||
# this stream wants no part in non-public statuses
|
||||
if status.privacy != "public":
|
||||
"""anyone with the mentioned book on their shelves"""
|
||||
# only show public statuses on the books feed,
|
||||
# and only statuses that mention books
|
||||
if status.privacy != "public" or not (
|
||||
status.mention_books.exists() or hasattr(status, "book")
|
||||
):
|
||||
return []
|
||||
return super().get_audience(status)
|
||||
|
||||
work = (
|
||||
status.book.parent_work
|
||||
if hasattr(status, "book")
|
||||
else status.mention_books.first().parent_work
|
||||
)
|
||||
|
||||
audience = super().get_audience(status)
|
||||
if not audience:
|
||||
return []
|
||||
return audience.filter(shelfbook__book__parent_work=work).distinct()
|
||||
|
||||
def get_statuses_for_user(self, user):
|
||||
"""any public status that mentions the user's books"""
|
||||
books = user.shelfbook_set.values_list(
|
||||
"book__parent_work__id", flat=True
|
||||
).distinct()
|
||||
return privacy_filter(
|
||||
user,
|
||||
models.Status.objects.select_subclasses(),
|
||||
models.Status.objects.select_subclasses()
|
||||
.filter(
|
||||
Q(comment__book__parent_work__id__in=books)
|
||||
| Q(quotation__book__parent_work__id__in=books)
|
||||
| Q(review__book__parent_work__id__in=books)
|
||||
| Q(mention_books__parent_work__id__in=books)
|
||||
)
|
||||
.distinct(),
|
||||
privacy_levels=["public"],
|
||||
)
|
||||
|
||||
|
||||
# determine which streams are enabled in settings.py
|
||||
available_streams = [s["key"] for s in STREAMS]
|
||||
streams = {
|
||||
"home": HomeStream(),
|
||||
"local": LocalStream(),
|
||||
"federated": FederatedStream(),
|
||||
k: v
|
||||
for (k, v) in {
|
||||
"home": HomeStream(),
|
||||
"local": LocalStream(),
|
||||
"books": BooksStream(),
|
||||
}.items()
|
||||
if k in available_streams
|
||||
}
|
||||
|
||||
|
||||
@ -203,8 +235,6 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
|
||||
|
||||
def add_status_on_create_command(sender, instance):
|
||||
"""runs this code only after the database commit completes"""
|
||||
|
||||
# iterates through Home, Local, Federated
|
||||
add_status_task.delay(instance.id)
|
||||
|
||||
if sender != models.Boost:
|
||||
@ -272,12 +302,14 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
|
||||
# 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 streams with statuses from anyone
|
||||
if instance.user_subject.local:
|
||||
add_user_statuses_task.delay(
|
||||
instance.user_subject.id,
|
||||
instance.user_object.id,
|
||||
stream_list=["local", "federated"],
|
||||
stream_list=public_streams,
|
||||
)
|
||||
|
||||
# add statuses back to streams with statuses from anyone
|
||||
@ -285,7 +317,7 @@ def add_statuses_on_unblock(sender, instance, *args, **kwargs):
|
||||
add_user_statuses_task.delay(
|
||||
instance.user_object.id,
|
||||
instance.user_subject.id,
|
||||
stream_list=["local", "federated"],
|
||||
stream_list=public_streams,
|
||||
)
|
||||
|
||||
|
||||
|
17
bookwyrm/migrations/0080_alter_shelfbook_options.py
Normal file
17
bookwyrm/migrations/0080_alter_shelfbook_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.4 on 2021-08-05 00:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0079_merge_20210804_1746"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="shelfbook",
|
||||
options={"ordering": ("-shelved_date", "-created_date", "-updated_date")},
|
||||
),
|
||||
]
|
@ -118,7 +118,11 @@ REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
|
||||
REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None)
|
||||
|
||||
MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200))
|
||||
STREAMS = ["home", "local", "federated"]
|
||||
|
||||
STREAMS = [
|
||||
{"key": "home", "name": _("Home Timeline"), "shortname": _("Home")},
|
||||
{"key": "books", "name": _("Books Timeline"), "shortname": _("Books")},
|
||||
]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
@ -4,35 +4,25 @@
|
||||
{% block panel %}
|
||||
|
||||
<h1 class="title">
|
||||
{% if tab == 'home' %}
|
||||
{% trans "Home Timeline" %}
|
||||
{% elif tab == 'local' %}
|
||||
{% trans "Local Timeline" %}
|
||||
{% else %}
|
||||
{% trans "Federated Timeline" %}
|
||||
{% endif %}
|
||||
{{ tab.name }}
|
||||
</h1>
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
<li class="{% if tab == 'home' %}is-active{% endif %}"{% if tab == 'home' %} aria-current="page"{% endif %}>
|
||||
<a href="/#feed">{% trans "Home" %}</a>
|
||||
</li>
|
||||
<li class="{% if tab == 'local' %}is-active{% endif %}"{% if tab == 'local' %} aria-current="page"{% endif %}>
|
||||
<a href="/local#feed">{% trans "Local" %}</a>
|
||||
</li>
|
||||
<li class="{% if tab == 'federated' %}is-active{% endif %}"{% if tab == 'federated' %} aria-current="page"{% endif %}>
|
||||
<a href="/federated#feed">{% trans "Federated" %}</a>
|
||||
{% for stream in streams %}
|
||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# announcements and system messages #}
|
||||
{% if not activities.number > 1 %}
|
||||
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
||||
{% blocktrans %}load <span data-poll="stream/{{ tab }}">0</span> unread status(es){% endblocktrans %}
|
||||
{% blocktrans %}load <span data-poll="stream/{{ tab.key }}">0</span> unread status(es){% endblocktrans %}
|
||||
</a>
|
||||
|
||||
{% if request.user.show_goal and not goal and tab == 'home' %}
|
||||
{% if request.user.show_goal and not goal and tab.key == streams.first.key %}
|
||||
{% now 'Y' as year %}
|
||||
<section class="block">
|
||||
{% include 'snippets/goal_card.html' with year=year %}
|
||||
|
@ -46,4 +46,4 @@ class Activitystreams(TestCase):
|
||||
"bookwyrm.activitystreams.ActivityStream.populate_store"
|
||||
) as redis_mock:
|
||||
populate_streams()
|
||||
self.assertEqual(redis_mock.call_count, 6) # 2 users x 3 streams
|
||||
self.assertEqual(redis_mock.call_count, 4) # 2 users x 2 streams
|
||||
|
@ -6,6 +6,7 @@ from bookwyrm import activitystreams, models
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
|
||||
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
class Activitystreams(TestCase):
|
||||
"""using redis to build activity streams"""
|
||||
|
||||
@ -32,7 +33,8 @@ class Activitystreams(TestCase):
|
||||
inbox="https://example.com/users/rat/inbox",
|
||||
outbox="https://example.com/users/rat/outbox",
|
||||
)
|
||||
self.book = models.Edition.objects.create(title="test book")
|
||||
work = models.Work.objects.create(title="test work")
|
||||
self.book = models.Edition.objects.create(title="test book", parent_work=work)
|
||||
|
||||
class TestStream(activitystreams.ActivityStream):
|
||||
"""test stream, don't have to do anything here"""
|
||||
@ -191,19 +193,95 @@ class Activitystreams(TestCase):
|
||||
users = activitystreams.LocalStream().get_audience(status)
|
||||
self.assertEqual(users, [])
|
||||
|
||||
def test_federatedstream_get_audience(self, *_):
|
||||
def test_localstream_get_audience_books_no_book(self, *_):
|
||||
"""get a list of users that should see a status"""
|
||||
status = models.Status.objects.create(
|
||||
user=self.remote_user, content="hi", privacy="public"
|
||||
user=self.local_user, content="hi", privacy="public"
|
||||
)
|
||||
users = activitystreams.FederatedStream().get_audience(status)
|
||||
self.assertTrue(self.local_user in users)
|
||||
self.assertTrue(self.another_user in users)
|
||||
audience = activitystreams.BooksStream().get_audience(status)
|
||||
# no books, no audience
|
||||
self.assertEqual(audience, [])
|
||||
|
||||
def test_federatedstream_get_audience_unlisted(self, *_):
|
||||
def test_localstream_get_audience_books_mention_books(self, *_):
|
||||
"""get a list of users that should see a status"""
|
||||
status = models.Status.objects.create(
|
||||
user=self.remote_user, content="hi", privacy="unlisted"
|
||||
user=self.local_user, content="hi", privacy="public"
|
||||
)
|
||||
users = activitystreams.FederatedStream().get_audience(status)
|
||||
self.assertEqual(users, [])
|
||||
status.mention_books.add(self.book)
|
||||
status.save(broadcast=False)
|
||||
models.ShelfBook.objects.create(
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
book=self.book,
|
||||
)
|
||||
# yes book, yes audience
|
||||
audience = activitystreams.BooksStream().get_audience(status)
|
||||
self.assertTrue(self.local_user in audience)
|
||||
|
||||
def test_localstream_get_audience_books_book_field(self, *_):
|
||||
"""get a list of users that should see a status"""
|
||||
status = models.Comment.objects.create(
|
||||
user=self.local_user, content="hi", privacy="public", book=self.book
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
book=self.book,
|
||||
)
|
||||
# yes book, yes audience
|
||||
audience = activitystreams.BooksStream().get_audience(status)
|
||||
self.assertTrue(self.local_user in audience)
|
||||
|
||||
def test_localstream_get_audience_books_alternate_edition(self, *_):
|
||||
"""get a list of users that should see a status"""
|
||||
alt_book = models.Edition.objects.create(
|
||||
title="hi", parent_work=self.book.parent_work
|
||||
)
|
||||
status = models.Comment.objects.create(
|
||||
user=self.remote_user, content="hi", privacy="public", book=alt_book
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
book=self.book,
|
||||
)
|
||||
# yes book, yes audience
|
||||
audience = activitystreams.BooksStream().get_audience(status)
|
||||
self.assertTrue(self.local_user in audience)
|
||||
|
||||
def test_localstream_get_audience_books_non_public(self, *_):
|
||||
"""get a list of users that should see a status"""
|
||||
alt_book = models.Edition.objects.create(
|
||||
title="hi", parent_work=self.book.parent_work
|
||||
)
|
||||
status = models.Comment.objects.create(
|
||||
user=self.remote_user, content="hi", privacy="unlisted", book=alt_book
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
book=self.book,
|
||||
)
|
||||
# yes book, yes audience
|
||||
audience = activitystreams.BooksStream().get_audience(status)
|
||||
self.assertEqual(audience, [])
|
||||
|
||||
def test_get_statuses_for_user_books(self, *_):
|
||||
"""create a stream for a user"""
|
||||
alt_book = models.Edition.objects.create(
|
||||
title="hi", parent_work=self.book.parent_work
|
||||
)
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content="hi", privacy="public"
|
||||
)
|
||||
status = models.Comment.objects.create(
|
||||
user=self.remote_user, content="hi", privacy="public", book=alt_book
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
book=self.book,
|
||||
)
|
||||
# yes book, yes audience
|
||||
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
|
||||
self.assertEqual(list(result), [status])
|
||||
|
@ -45,7 +45,7 @@ class FeedViews(TestCase):
|
||||
view = views.Feed.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request, "local")
|
||||
result = view(request, "home")
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
result.render()
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
@ -23,6 +23,8 @@ STATUS_PATH = r"%s/(%s)/(?P<status_id>\d+)" % (USER_PATH, "|".join(status_types)
|
||||
|
||||
BOOK_PATH = r"^book/(?P<book_id>\d+)"
|
||||
|
||||
STREAMS = "|".join(s["key"] for s in settings.STREAMS)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
@ -177,7 +179,7 @@ urlpatterns = [
|
||||
name="get-started-users",
|
||||
),
|
||||
# feeds
|
||||
re_path(r"^(?P<tab>home|local|federated)/?$", views.Feed.as_view()),
|
||||
re_path(r"^(?P<tab>{:s})/?$".format(STREAMS), views.Feed.as_view()),
|
||||
re_path(
|
||||
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
|
||||
),
|
||||
|
@ -23,10 +23,12 @@ class Feed(View):
|
||||
|
||||
def get(self, request, tab):
|
||||
"""user's homepage with activity feed"""
|
||||
if not tab in STREAMS:
|
||||
tab = "home"
|
||||
tab = [s for s in STREAMS if s["key"] == tab]
|
||||
tab = tab[0] if tab else STREAMS[0]
|
||||
|
||||
activities = activitystreams.streams[tab].get_activity_stream(request.user)
|
||||
activities = activitystreams.streams[tab["key"]].get_activity_stream(
|
||||
request.user
|
||||
)
|
||||
paginated = Paginator(activities, PAGE_LENGTH)
|
||||
|
||||
suggestions = suggested_users.get_suggestions(request.user)
|
||||
@ -38,8 +40,9 @@ class Feed(View):
|
||||
"activities": paginated.get_page(request.GET.get("page")),
|
||||
"suggested_users": suggestions,
|
||||
"tab": tab,
|
||||
"streams": STREAMS,
|
||||
"goal_form": forms.GoalForm(),
|
||||
"path": "/%s" % tab,
|
||||
"path": "/%s" % tab["key"],
|
||||
},
|
||||
}
|
||||
return TemplateResponse(request, "feed/feed.html", data)
|
||||
|
Reference in New Issue
Block a user