Merge pull request #784 from mouse-reeve/redis-activity-stream

Uses redis to manage activity streams
This commit is contained in:
Mouse Reeve 2021-03-23 18:22:21 -07:00 committed by GitHub
commit ac0e13d6e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 935 additions and 437 deletions

View file

@ -22,8 +22,13 @@ POSTGRES_USER=fedireads
POSTGRES_DB=fedireads
POSTGRES_HOST=db
CELERY_BROKER=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# Redis activity stream manager
REDIS_ACTIVITY_HOST=redis_activity
REDIS_ACTIVITY_PORT=6379
# Celery config with redis broker
CELERY_BROKER=redis://redis_broker:6379/0
CELERY_RESULT_BACKEND=redis://redis_broker:6379/0
EMAIL_HOST="smtp.mailgun.org"
EMAIL_PORT=587

View file

@ -76,6 +76,7 @@ Web backend
- [ActivityPub](http://activitypub.rocks/) federation
- [Celery](http://celeryproject.org/) task queuing
- [Redis](https://redis.io/) task backend
- [Redis (again)](https://redis.io/) activity stream manager
Front end
- Django templates
@ -236,6 +237,11 @@ When there are changes available in the production branch, you can install and g
- `docker-compose exec web python manage.py collectstatic --no-input` loads any updated static files (such as the JavaScript and CSS)
- `docker-compose restart` reloads the docker containers
### Re-building activity streams
If something goes awry with user timelines, and you want to re-create them en mass, there's a management command for that:
`docker-compose run --rm web python manage.py rebuild_feeds`
### Port Conflicts
BookWyrm has multiple services that run on their default ports.

278
bookwyrm/activitystreams.py Normal file
View file

@ -0,0 +1,278 @@
""" access the activity streams stored in redis """
from abc import ABC
from django.dispatch import receiver
from django.db.models import signals, Q
import redis
from bookwyrm import models, settings
from bookwyrm.views.helpers import privacy_filter
r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
)
class ActivityStream(ABC):
""" a category of activity stream (like home, local, federated) """
def stream_id(self, user):
""" the redis key for this user's instance of this stream """
return "{}-{}".format(user.id, self.key)
def unread_id(self, user):
""" the redis key for this user's unread count for this stream """
return "{}-unread".format(self.stream_id(user))
def add_status(self, status):
""" add a status to users' feeds """
# we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline()
for user in self.stream_users(status):
# add the status to the feed
pipeline.lpush(self.stream_id(user), status.id)
pipeline.ltrim(self.stream_id(user), 0, settings.MAX_STREAM_LENGTH)
# add to the unread status count
pipeline.incr(self.unread_id(user))
# and go!
pipeline.execute()
def remove_status(self, status):
""" remove a status from all feeds """
pipeline = r.pipeline()
for user in self.stream_users(status):
pipeline.lrem(self.stream_id(user), -1, status.id)
pipeline.execute()
def add_user_statuses(self, viewer, user):
""" add a user's statuses to another user's feed """
pipeline = r.pipeline()
for status in user.status_set.all()[: settings.MAX_STREAM_LENGTH]:
pipeline.lpush(self.stream_id(viewer), status.id)
pipeline.execute()
def remove_user_statuses(self, viewer, user):
""" remove a user's status from another user's feed """
pipeline = r.pipeline()
for status in user.status_set.all()[: settings.MAX_STREAM_LENGTH]:
pipeline.lrem(self.stream_id(viewer), -1, status.id)
pipeline.execute()
def get_activity_stream(self, user):
""" load the ids for statuses to be displayed """
# clear unreads for this feed
r.set(self.unread_id(user), 0)
statuses = r.lrange(self.stream_id(user), 0, -1)
return (
models.Status.objects.select_subclasses()
.filter(id__in=statuses)
.order_by("-published_date")
)
def get_unread_count(self, user):
""" get the unread status count for this user's feed """
return int(r.get(self.unread_id(user)))
def populate_stream(self, user):
""" go from zero to a timeline """
pipeline = r.pipeline()
statuses = self.stream_statuses(user)
stream_id = self.stream_id(user)
for status in statuses.all()[: settings.MAX_STREAM_LENGTH]:
pipeline.lpush(stream_id, status.id)
pipeline.execute()
def stream_users(self, status): # pylint: disable=no-self-use
""" given a status, what users should see it """
# direct messages don't appeard in feeds, direct comments/reviews/etc do
if status.privacy == "direct" and status.status_type == "Note":
return None
# everybody who could plausibly see this status
audience = models.User.objects.filter(
is_active=True,
local=True, # we only create feeds for users of this instance
).exclude(
Q(id__in=status.user.blocks.all()) | Q(blocks=status.user) # not blocked
)
# only visible to the poster and mentioned users
if status.privacy == "direct":
audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author
| Q(id__in=status.mention_users.all()) # if the user is mentioned
)
# only visible to the poster's followers and tagged users
elif status.privacy == "followers":
audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author
| Q(following=status.user) # if the user is following the author
)
return audience
def stream_statuses(self, user): # pylint: disable=no-self-use
""" given a user, what statuses should they see on this stream """
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
privacy_levels=["public", "unlisted", "followers"],
)
class HomeStream(ActivityStream):
""" users you follow """
key = "home"
def stream_users(self, status):
audience = super().stream_users(status)
return audience.filter(
Q(id=status.user.id) # if the user is the post's author
| Q(following=status.user) # if the user is following the author
)
def stream_statuses(self, user):
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
privacy_levels=["public", "unlisted", "followers"],
following_only=True,
)
class LocalStream(ActivityStream):
""" users you follow """
key = "local"
def stream_users(self, status):
# this stream wants no part in non-public statuses
if status.privacy != "public" or not status.user.local:
return None
return super().stream_users(status)
def stream_statuses(self, user):
# all public statuses by a local user
return privacy_filter(
user,
models.Status.objects.select_subclasses().filter(user__local=True),
privacy_levels=["public"],
)
class FederatedStream(ActivityStream):
""" users you follow """
key = "federated"
def stream_users(self, status):
# this stream wants no part in non-public statuses
if status.privacy != "public":
return None
return super().stream_users(status)
def stream_statuses(self, user):
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
privacy_levels=["public"],
)
streams = {
"home": HomeStream(),
"local": LocalStream(),
"federated": FederatedStream(),
}
@receiver(signals.post_save)
# pylint: disable=unused-argument
def add_status_on_create(sender, instance, created, *args, **kwargs):
""" add newly created statuses to activity feeds """
# we're only interested in new statuses
if not issubclass(sender, models.Status):
return
if instance.deleted:
for stream in streams.values():
stream.remove_status(instance)
return
if not created:
return
# iterates through Home, Local, Federated
for stream in streams.values():
stream.add_status(instance)
@receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument
def remove_boost_on_delete(sender, instance, *args, **kwargs):
""" boosts are deleted """
# we're only interested in new statuses
for stream in streams.values():
stream.remove_status(instance)
@receiver(signals.post_save, sender=models.UserFollows)
# pylint: disable=unused-argument
def add_statuses_on_follow(sender, instance, created, *args, **kwargs):
""" add a newly followed user's statuses to feeds """
if not created or not instance.user_subject.local:
return
HomeStream().add_user_statuses(instance.user_subject, instance.user_object)
@receiver(signals.post_delete, sender=models.UserFollows)
# pylint: disable=unused-argument
def remove_statuses_on_unfollow(sender, instance, *args, **kwargs):
""" remove statuses from a feed on unfollow """
if not instance.user_subject.local:
return
HomeStream().remove_user_statuses(instance.user_subject, instance.user_object)
@receiver(signals.post_save, sender=models.UserBlocks)
# pylint: disable=unused-argument
def remove_statuses_on_block(sender, instance, *args, **kwargs):
""" remove statuses from all feeds on block """
# blocks apply ot all feeds
if instance.user_subject.local:
for stream in streams.values():
stream.remove_user_statuses(instance.user_subject, instance.user_object)
# and in both directions
if instance.user_object.local:
for stream in streams.values():
stream.remove_user_statuses(instance.user_object, instance.user_subject)
@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 = [LocalStream(), FederatedStream()]
# add statuses back to streams with statuses from anyone
if instance.user_subject.local:
for stream in public_streams:
stream.add_user_statuses(instance.user_subject, instance.user_object)
# add statuses back to streams with statuses from anyone
if instance.user_object.local:
for stream in public_streams:
stream.add_user_statuses(instance.user_object, instance.user_subject)
@receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument
def populate_feed_on_account_create(sender, instance, created, *args, **kwargs):
""" build a user's feeds when they join """
if not created or not instance.local:
return
for stream in streams.values():
stream.populate_stream(instance)

View file

@ -0,0 +1,36 @@
""" Delete and re-create user feeds """
from django.core.management.base import BaseCommand
import redis
from bookwyrm import activitystreams, models, settings
r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
)
def erase_feeds():
""" throw the whole redis away """
r.flushall()
def create_feeds():
""" build all the fields for all the users """
users = models.User.objects.filter(
local=True,
is_active=True,
)
for user in users:
for stream in activitystreams.streams.values():
stream.populate_stream(user)
class Command(BaseCommand):
""" start all over with user feeds """
help = "Delete and re-create all the user feeds"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
""" run feed builder """
erase_feeds()
create_feeds()

View file

@ -34,7 +34,7 @@ class BookWyrmModel(models.Model):
@receiver(models.signals.post_save)
# pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):
def set_remote_id(sender, instance, created, *args, **kwargs):
""" set the remote_id after save (when the id is available) """
if not created or not hasattr(instance, "get_remote_id"):
return

View file

@ -62,7 +62,7 @@ class UserFollows(ActivityMixin, UserRelationship):
status = "follows"
def to_activity(self):
def to_activity(self): # pylint: disable=arguments-differ
""" overrides default to manually set serializer """
return activitypub.Follow(**generate_activity(self))

View file

@ -114,7 +114,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return list(set(mentions))
@classmethod
def ignore_activity(cls, activity):
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
""" keep notes if they are replies to existing statuses """
if activity.type == "Announce":
try:

View file

@ -92,6 +92,12 @@ TEMPLATES = [
WSGI_APPLICATION = "bookwyrm.wsgi.application"
# redis/activity streams settings
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
MAX_STREAM_LENGTH = env("MAX_STREAM_LENGTH", 200)
STREAMS = ["home", "local", "federated"]
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

View file

@ -61,9 +61,9 @@ function polling(el, delay) {
function updateCountElement(el, data) {
const currentCount = el.innerText;
const count = data[el.getAttribute('data-poll')];
const count = data.count;
if (count != currentCount) {
addRemoveClass(el, 'hidden', count < 1);
addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1);
el.innerText = count;
}
}

View file

@ -3,7 +3,15 @@
{% load bookwyrm_tags %}
{% block panel %}
<h1 class="title">{% blocktrans %}{{ tab_title }} Timeline{% endblocktrans %}</h1>
<h1 class="title">
{% if tab == 'home' %}
{% trans "Home Timeline" %}
{% elif tab == 'local' %}
{% trans "Local Timeline" %}
{% else %}
{% trans "Federated Timeline" %}
{% endif %}
</h1>
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}"{% if tab == 'home' %} aria-current="page"{% endif %}>
@ -19,7 +27,12 @@
</div>
{# announcements and system messages #}
{% if request.user.show_goal and not goal and tab == 'home' and not activities.number > 1 %}
{% if not activities.number > 1 %}
<a href="{{ request.path }}" class="hidden notification is-primary is-block" data-poll-wrapper>
{% blocktrans %}load <span data-poll="stream/{{ tab }}">0</span> unread status(es){% endblocktrans %}
</a>
{% if request.user.show_goal and not goal and tab == 'home' %}
{% now 'Y' as year %}
<section class="block">
{% include 'snippets/goal_card.html' with year=year %}
@ -27,6 +40,8 @@
</section>
{% endif %}
{% endif %}
{# activity feed #}
{% if not activities %}
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>

View file

@ -139,8 +139,8 @@
<span class="is-sr-only">{% trans "Notifications" %}</span>
</span>
</span>
<span class="{% if not request.user|notification_count %}hidden {% endif %}tag is-danger is-medium" data-poll="notifications">
{{ request.user | notification_count }}
<span class="{% if not request.user|notification_count %}hidden {% endif %}tag is-danger is-medium" data-poll-wrapper>
<span data-poll="notifications">{{ request.user | notification_count }}</span>
</span>
</a>
</div>

View file

@ -19,6 +19,7 @@ from bookwyrm.activitypub import ActivitySerializerError
from bookwyrm import models
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class BaseActivity(TestCase):
""" the super class for model-linked activitypub dataclasses """
@ -43,24 +44,24 @@ class BaseActivity(TestCase):
image.save(output, format=image.format)
self.image_data = output.getvalue()
def test_init(self):
def test_init(self, _):
""" simple successfuly init """
instance = ActivityObject(id="a", type="b")
self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type"))
def test_init_missing(self):
def test_init_missing(self, _):
""" init with missing required params """
with self.assertRaises(ActivitySerializerError):
ActivityObject()
def test_init_extra_fields(self):
def test_init_extra_fields(self, _):
""" init ignoring additional fields """
instance = ActivityObject(id="a", type="b", fish="c")
self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type"))
def test_init_default_field(self):
def test_init_default_field(self, _):
""" replace an existing required field with a default field """
@dataclass(init=False)
@ -73,7 +74,7 @@ class BaseActivity(TestCase):
self.assertEqual(instance.id, "a")
self.assertEqual(instance.type, "TestObject")
def test_serialize(self):
def test_serialize(self, _):
""" simple function for converting dataclass to dict """
instance = ActivityObject(id="a", type="b")
serialized = instance.serialize()
@ -82,7 +83,7 @@ class BaseActivity(TestCase):
self.assertEqual(serialized["type"], "b")
@responses.activate
def test_resolve_remote_id(self):
def test_resolve_remote_id(self, _):
""" look up or load remote data """
# existing item
result = resolve_remote_id("http://example.com/a/b", model=models.User)
@ -104,14 +105,14 @@ class BaseActivity(TestCase):
self.assertEqual(result.remote_id, "https://example.com/user/mouse")
self.assertEqual(result.name, "MOUSE?? MOUSE!!")
def test_to_model_invalid_model(self):
def test_to_model_invalid_model(self, _):
""" catch mismatch between activity type and model type """
instance = ActivityObject(id="a", type="b")
with self.assertRaises(ActivitySerializerError):
instance.to_model(model=models.User)
@responses.activate
def test_to_model_image(self):
def test_to_model_image(self, _):
""" update an image field """
activity = activitypub.Person(
id=self.user.remote_id,
@ -144,7 +145,7 @@ class BaseActivity(TestCase):
self.assertEqual(self.user.name, "New Name")
self.assertEqual(self.user.key_pair.public_key, "hi")
def test_to_model_many_to_many(self):
def test_to_model_many_to_many(self, _):
""" annoying that these all need special handling """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(
@ -175,7 +176,7 @@ class BaseActivity(TestCase):
self.assertEqual(status.mention_books.first(), book)
@responses.activate
def test_to_model_one_to_many(self):
def test_to_model_one_to_many(self, _):
"""these are reversed relationships, where the secondary object
keys the primary object but not vice versa"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -214,7 +215,7 @@ class BaseActivity(TestCase):
self.assertIsNone(status.attachments.first())
@responses.activate
def test_set_related_field(self):
def test_set_related_field(self, _):
""" celery task to add back-references to created objects """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(

View file

@ -13,6 +13,7 @@ from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class ActivitypubMixins(TestCase):
""" functionality shared across models """
@ -44,7 +45,7 @@ class ActivitypubMixins(TestCase):
}
# ActivitypubMixin
def test_to_activity(self):
def test_to_activity(self, _):
""" model to ActivityPub json """
@dataclass(init=False)
@ -65,7 +66,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(activity["id"], "https://www.example.com/test")
self.assertEqual(activity["type"], "Test")
def test_find_existing_by_remote_id(self):
def test_find_existing_by_remote_id(self, _):
""" attempt to match a remote id to an object in the db """
# uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state
@ -98,7 +99,7 @@ class ActivitypubMixins(TestCase):
# test subclass match
result = models.Status.find_existing_by_remote_id("https://comment.net")
def test_find_existing(self):
def test_find_existing(self, _):
""" match a blob of data to a model """
book = models.Edition.objects.create(
title="Test edition",
@ -108,7 +109,7 @@ class ActivitypubMixins(TestCase):
result = models.Edition.find_existing({"openlibraryKey": "OL1234"})
self.assertEqual(result, book)
def test_get_recipients_public_object(self):
def test_get_recipients_public_object(self, _):
""" determines the recipients for an object's broadcast """
MockSelf = namedtuple("Self", ("privacy"))
mock_self = MockSelf("public")
@ -116,7 +117,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_no_followers(self):
def test_get_recipients_public_user_object_no_followers(self, _):
""" determines the recipients for a user's object broadcast """
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
@ -124,7 +125,7 @@ class ActivitypubMixins(TestCase):
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 0)
def test_get_recipients_public_user_object(self):
def test_get_recipients_public_user_object(self, _):
""" determines the recipients for a user's object broadcast """
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
@ -134,7 +135,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_with_mention(self):
def test_get_recipients_public_user_object_with_mention(self, _):
""" determines the recipients for a user's object broadcast """
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
@ -157,7 +158,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], another_remote_user.inbox)
self.assertEqual(recipients[1], self.remote_user.inbox)
def test_get_recipients_direct(self):
def test_get_recipients_direct(self, _):
""" determines the recipients for a user's object broadcast """
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
@ -179,7 +180,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], another_remote_user.inbox)
def test_get_recipients_combine_inboxes(self):
def test_get_recipients_combine_inboxes(self, _):
""" should combine users with the same shared_inbox """
self.remote_user.shared_inbox = "http://example.com/inbox"
self.remote_user.save(broadcast=False)
@ -203,7 +204,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], "http://example.com/inbox")
def test_get_recipients_software(self):
def test_get_recipients_software(self, _):
""" should differentiate between bookwyrm and other remote users """
with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user(
@ -233,7 +234,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], another_remote_user.inbox)
# ObjectMixin
def test_object_save_create(self):
def test_object_save_create(self, _):
""" should save uneventufully when broadcast is disabled """
class Success(Exception):
@ -264,7 +265,7 @@ class ActivitypubMixins(TestCase):
ObjectModel(user=self.local_user).save(broadcast=False)
ObjectModel(user=None).save()
def test_object_save_update(self):
def test_object_save_update(self, _):
""" should save uneventufully when broadcast is disabled """
class Success(Exception):
@ -290,7 +291,7 @@ class ActivitypubMixins(TestCase):
with self.assertRaises(Success):
UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
def test_object_save_delete(self):
def test_object_save_delete(self, _):
""" should create delete activities when objects are deleted by flag """
class ActivitySuccess(Exception):
@ -312,7 +313,7 @@ class ActivitypubMixins(TestCase):
with self.assertRaises(ActivitySuccess):
DeletableObjectModel(id=1, user=self.local_user, deleted=True).save()
def test_to_delete_activity(self):
def test_to_delete_activity(self, _):
""" wrapper for Delete activity """
MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf(
@ -327,7 +328,7 @@ class ActivitypubMixins(TestCase):
activity["cc"], ["https://www.w3.org/ns/activitystreams#Public"]
)
def test_to_update_activity(self):
def test_to_update_activity(self, _):
""" ditto above but for Update """
MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf(
@ -345,7 +346,7 @@ class ActivitypubMixins(TestCase):
self.assertIsInstance(activity["object"], dict)
# Activity mixin
def test_to_undo_activity(self):
def test_to_undo_activity(self, _):
""" and again, for Undo """
MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user"))
mock_self = MockSelf(

View file

@ -27,18 +27,18 @@ class BaseModel(TestCase):
expected = instance.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN)
def test_execute_after_save(self):
def test_set_remote_id(self):
""" this function sets remote ids after creation """
# using Work because it BookWrymModel is abstract and this requires save
# Work is a relatively not-fancy model.
instance = models.Work.objects.create(title="work title")
instance.remote_id = None
base_model.execute_after_save(None, instance, True)
base_model.set_remote_id(None, instance, True)
self.assertEqual(
instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id)
)
# shouldn't set remote_id if it's not created
instance.remote_id = None
base_model.execute_after_save(None, instance, False)
base_model.set_remote_id(None, instance, False)
self.assertIsNone(instance.remote_id)

View file

@ -185,7 +185,8 @@ class ActivitypubFields(TestCase):
self.assertEqual(model_instance.privacy_field, "unlisted")
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
def test_privacy_field_set_activity_from_field(self, _):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_):
""" translate between to/cc fields and privacy """
user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"

View file

@ -15,6 +15,7 @@ from bookwyrm import activitypub, models, settings
# pylint: disable=too-many-public-methods
@patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Status(TestCase):
""" lotta types of statuses """
@ -44,14 +45,14 @@ class Status(TestCase):
image.save(output, format=image.format)
self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
def test_status_generated_fields(self, _):
def test_status_generated_fields(self, *_):
""" setting remote id """
status = models.Status.objects.create(content="bleh", user=self.local_user)
expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id)
self.assertEqual(status.remote_id, expected_id)
self.assertEqual(status.privacy, "public")
def test_replies(self, _):
def test_replies(self, *_):
""" get a list of replies """
parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create(
@ -70,7 +71,7 @@ class Status(TestCase):
# should select subclasses
self.assertIsInstance(replies.last(), models.Review)
def test_status_type(self, _):
def test_status_type(self, *_):
""" class name """
self.assertEqual(models.Status().status_type, "Note")
self.assertEqual(models.Review().status_type, "Review")
@ -78,14 +79,14 @@ class Status(TestCase):
self.assertEqual(models.Comment().status_type, "Comment")
self.assertEqual(models.Boost().status_type, "Announce")
def test_boostable(self, _):
def test_boostable(self, *_):
""" can a status be boosted, based on privacy """
self.assertTrue(models.Status(privacy="public").boostable)
self.assertTrue(models.Status(privacy="unlisted").boostable)
self.assertFalse(models.Status(privacy="followers").boostable)
self.assertFalse(models.Status(privacy="direct").boostable)
def test_to_replies(self, _):
def test_to_replies(self, *_):
""" activitypub replies collection """
parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create(
@ -102,7 +103,7 @@ class Status(TestCase):
self.assertEqual(replies["id"], "%s/replies" % parent.remote_id)
self.assertEqual(replies["totalItems"], 2)
def test_status_to_activity(self, _):
def test_status_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Status.objects.create(
content="test content", user=self.local_user
@ -113,20 +114,21 @@ class Status(TestCase):
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["sensitive"], False)
def test_status_to_activity_tombstone(self, _):
def test_status_to_activity_tombstone(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Status.objects.create(
content="test content",
user=self.local_user,
deleted=True,
deleted_date=timezone.now(),
)
with patch("bookwyrm.activitystreams.ActivityStream.remove_status"):
status = models.Status.objects.create(
content="test content",
user=self.local_user,
deleted=True,
deleted_date=timezone.now(),
)
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Tombstone")
self.assertFalse(hasattr(activity, "content"))
def test_status_to_pure_activity(self, _):
def test_status_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Status.objects.create(
content="test content", user=self.local_user
@ -138,7 +140,7 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False)
self.assertEqual(activity["attachment"], [])
def test_generated_note_to_activity(self, _):
def test_generated_note_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
@ -152,7 +154,7 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False)
self.assertEqual(len(activity["tag"]), 2)
def test_generated_note_to_pure_activity(self, _):
def test_generated_note_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
@ -176,7 +178,7 @@ class Status(TestCase):
)
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_comment_to_activity(self, _):
def test_comment_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book
@ -187,7 +189,7 @@ class Status(TestCase):
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_comment_to_pure_activity(self, _):
def test_comment_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book
@ -207,7 +209,7 @@ class Status(TestCase):
)
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_quotation_to_activity(self, _):
def test_quotation_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Quotation.objects.create(
quote="a sickening sense",
@ -222,7 +224,7 @@ class Status(TestCase):
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_quotation_to_pure_activity(self, _):
def test_quotation_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Quotation.objects.create(
quote="a sickening sense",
@ -245,7 +247,7 @@ class Status(TestCase):
)
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_review_to_activity(self, _):
def test_review_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Review.objects.create(
name="Review name",
@ -262,7 +264,7 @@ class Status(TestCase):
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_review_to_pure_activity(self, _):
def test_review_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
status = models.Review.objects.create(
name="Review name",
@ -285,7 +287,7 @@ class Status(TestCase):
)
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_favorite(self, _):
def test_favorite(self, *_):
""" fav a status """
real_broadcast = models.Favorite.broadcast
@ -311,7 +313,7 @@ class Status(TestCase):
self.assertEqual(activity["object"], status.remote_id)
models.Favorite.broadcast = real_broadcast
def test_boost(self, _):
def test_boost(self, *_):
""" boosting, this one's a bit fussy """
status = models.Status.objects.create(
content="test content", user=self.local_user
@ -323,7 +325,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Announce")
self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self, _):
def test_notification(self, *_):
""" a simple model """
notification = models.Notification.objects.create(
user=self.local_user, notification_type="FAVORITE"
@ -335,7 +337,7 @@ class Status(TestCase):
user=self.local_user, notification_type="GLORB"
)
def test_create_broadcast(self, broadcast_mock):
def test_create_broadcast(self, _, broadcast_mock):
""" should send out two verions of a status on create """
models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book
@ -355,7 +357,7 @@ class Status(TestCase):
self.assertEqual(args["type"], "Create")
self.assertEqual(args["object"]["type"], "Comment")
def test_recipients_with_mentions(self, _):
def test_recipients_with_mentions(self, *_):
""" get recipients to broadcast a status """
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
@ -364,7 +366,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent(self, _):
def test_recipients_with_reply_parent(self, *_):
""" get recipients to broadcast a status """
parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user
@ -375,7 +377,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent_and_mentions(self, _):
def test_recipients_with_reply_parent_and_mentions(self, *_):
""" get recipients to broadcast a status """
parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user
@ -388,7 +390,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user])
@responses.activate
def test_ignore_activity_boost(self, _):
def test_ignore_activity_boost(self, *_):
""" don't bother with most remote statuses """
activity = activitypub.Announce(
id="http://www.faraway.com/boost/12",

View file

@ -0,0 +1,204 @@
""" testing activitystreams """
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Activitystreams(TestCase):
""" using redis to build activity streams """
def setUp(self):
""" use a test csv """
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(
"nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria"
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
self.book = models.Edition.objects.create(title="test book")
class TestStream(activitystreams.ActivityStream):
""" test stream, don't have to do anything here """
key = "test"
self.test_stream = TestStream()
def test_activitystream_class_ids(self, *_):
""" the abstract base class for stream objects """
self.assertEqual(
self.test_stream.stream_id(self.local_user),
"{}-test".format(self.local_user.id),
)
self.assertEqual(
self.test_stream.unread_id(self.local_user),
"{}-test-unread".format(self.local_user.id),
)
def test_abstractstream_stream_users(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
users = self.test_stream.stream_users(status)
# remote users don't have feeds
self.assertFalse(self.remote_user in users)
self.assertTrue(self.local_user in users)
self.assertTrue(self.another_user in users)
def test_abstractstream_stream_users_direct(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
)
status.mention_users.add(self.local_user)
users = self.test_stream.stream_users(status)
self.assertIsNone(users)
status = models.Comment.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
book=self.book,
)
status.mention_users.add(self.local_user)
users = self.test_stream.stream_users(status)
self.assertTrue(self.local_user in users)
self.assertFalse(self.another_user in users)
self.assertFalse(self.remote_user in users)
def test_abstractstream_stream_users_followers_remote_user(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
privacy="followers",
)
users = self.test_stream.stream_users(status)
self.assertFalse(users.exists())
def test_abstractstream_stream_users_followers_self(self, *_):
""" get a list of users that should see a status """
status = models.Comment.objects.create(
user=self.local_user,
content="hi",
privacy="direct",
book=self.book,
)
users = self.test_stream.stream_users(status)
self.assertTrue(self.local_user in users)
self.assertFalse(self.another_user in users)
self.assertFalse(self.remote_user in users)
def test_abstractstream_stream_users_followers_with_mention(self, *_):
""" get a list of users that should see a status """
status = models.Comment.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
book=self.book,
)
status.mention_users.add(self.local_user)
users = self.test_stream.stream_users(status)
self.assertTrue(self.local_user in users)
self.assertFalse(self.another_user in users)
self.assertFalse(self.remote_user in users)
def test_abstractstream_stream_users_followers_with_relationship(self, *_):
""" get a list of users that should see a status """
self.remote_user.followers.add(self.local_user)
status = models.Comment.objects.create(
user=self.remote_user,
content="hi",
privacy="direct",
book=self.book,
)
users = self.test_stream.stream_users(status)
self.assertFalse(self.local_user in users)
self.assertFalse(self.another_user in users)
self.assertFalse(self.remote_user in users)
def test_homestream_stream_users(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
users = activitystreams.HomeStream().stream_users(status)
self.assertFalse(users.exists())
def test_homestream_stream_users_with_mentions(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
status.mention_users.add(self.local_user)
users = activitystreams.HomeStream().stream_users(status)
self.assertFalse(self.local_user in users)
self.assertFalse(self.another_user in users)
def test_homestream_stream_users_with_relationship(self, *_):
""" get a list of users that should see a status """
self.remote_user.followers.add(self.local_user)
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
users = activitystreams.HomeStream().stream_users(status)
self.assertTrue(self.local_user in users)
self.assertFalse(self.another_user in users)
def test_localstream_stream_users_remote_status(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
users = activitystreams.LocalStream().stream_users(status)
self.assertIsNone(users)
def test_localstream_stream_users_local_status(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="public"
)
users = activitystreams.LocalStream().stream_users(status)
self.assertTrue(self.local_user in users)
self.assertTrue(self.another_user in users)
def test_localstream_stream_users_unlisted(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="unlisted"
)
users = activitystreams.LocalStream().stream_users(status)
self.assertIsNone(users)
def test_federatedstream_stream_users(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
users = activitystreams.FederatedStream().stream_users(status)
self.assertTrue(self.local_user in users)
self.assertTrue(self.another_user in users)
def test_federatedstream_stream_users_unlisted(self, *_):
""" get a list of users that should see a status """
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="unlisted"
)
users = activitystreams.FederatedStream().stream_users(status)
self.assertIsNone(users)

View file

@ -203,7 +203,8 @@ class GoodreadsImport(TestCase):
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_review(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _):
""" goodreads review import """
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("data/goodreads.csv")

View file

@ -1,5 +1,4 @@
""" testing import """
from collections import namedtuple
import csv
import pathlib
from unittest.mock import patch
@ -16,8 +15,8 @@ class LibrarythingImport(TestCase):
""" importing from librarything tsv """
def setUp(self):
self.importer = LibrarythingImporter()
""" use a test tsv """
self.importer = LibrarythingImporter()
datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv")
# Librarything generates latin encoded exports...
@ -200,7 +199,8 @@ class LibrarythingImport(TestCase):
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_review(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _):
""" librarything review import """
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("data/librarything.tsv")

View file

@ -10,6 +10,7 @@ from bookwyrm import models
from bookwyrm.templatetags import bookwyrm_tags
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class TemplateTags(TestCase):
""" lotta different things here """
@ -32,34 +33,34 @@ class TemplateTags(TestCase):
)
self.book = models.Edition.objects.create(title="Test Book")
def test_dict_key(self):
def test_dict_key(self, _):
""" just getting a value out of a dict """
test_dict = {"a": 1, "b": 3}
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1)
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0)
def test_get_user_rating(self):
def test_get_user_rating(self, _):
""" get a user's most recent rating of a book """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Review.objects.create(user=self.user, book=self.book, rating=3)
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3)
def test_get_user_rating_doesnt_exist(self):
def test_get_user_rating_doesnt_exist(self, _):
""" there is no rating available """
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0)
def test_get_user_identifer_local(self):
def test_get_user_identifer_local(self, _):
""" fall back to the simplest uid available """
self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse")
def test_get_user_identifer_remote(self):
def test_get_user_identifer_remote(self, _):
""" for a remote user, should be their full username """
self.assertEqual(
bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com"
)
def test_get_notification_count(self):
def test_get_notification_count(self, _):
""" just countin' """
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
@ -72,7 +73,7 @@ class TemplateTags(TestCase):
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2)
def test_get_replies(self):
def test_get_replies(self, _):
""" direct replies to a status """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
@ -84,12 +85,13 @@ class TemplateTags(TestCase):
second_child = models.Status.objects.create(
reply_parent=parent, user=self.user, content="hi"
)
third_child = models.Status.objects.create(
reply_parent=parent,
user=self.user,
deleted=True,
deleted_date=timezone.now(),
)
with patch("bookwyrm.activitystreams.ActivityStream.remove_status"):
third_child = models.Status.objects.create(
reply_parent=parent,
user=self.user,
deleted=True,
deleted_date=timezone.now(),
)
replies = bookwyrm_tags.get_replies(parent)
self.assertEqual(len(replies), 2)
@ -97,7 +99,7 @@ class TemplateTags(TestCase):
self.assertTrue(second_child in replies)
self.assertFalse(third_child in replies)
def test_get_parent(self):
def test_get_parent(self, _):
""" get the reply parent of a status """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
@ -111,7 +113,7 @@ class TemplateTags(TestCase):
self.assertEqual(result, parent)
self.assertIsInstance(result, models.Review)
def test_get_user_liked(self):
def test_get_user_liked(self, _):
""" did a user like a status """
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -120,7 +122,7 @@ class TemplateTags(TestCase):
models.Favorite.objects.create(user=self.user, status=status)
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
def test_get_user_boosted(self):
def test_get_user_boosted(self, _):
""" did a user boost a status """
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -129,7 +131,7 @@ class TemplateTags(TestCase):
models.Boost.objects.create(user=self.user, boosted_status=status)
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
def test_follow_request_exists(self):
def test_follow_request_exists(self, _):
""" does a user want to follow """
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
@ -147,7 +149,7 @@ class TemplateTags(TestCase):
bookwyrm_tags.follow_request_exists(self.remote_user, self.user)
)
def test_get_boosted(self):
def test_get_boosted(self, _):
""" load a boosted status """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(user=self.remote_user, book=self.book)
@ -156,7 +158,7 @@ class TemplateTags(TestCase):
self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status)
def test_get_book_description(self):
def test_get_book_description(self, _):
""" grab it from the edition or the parent """
work = models.Work.objects.create(title="Test Work")
self.book.parent_work = work
@ -172,12 +174,12 @@ class TemplateTags(TestCase):
self.book.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello")
def test_get_uuid(self):
def test_get_uuid(self, _):
""" uuid functionality """
uuid = bookwyrm_tags.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_time_since(self):
def test_time_since(self, _):
""" ultraconcise timestamps """
self.assertEqual(bookwyrm_tags.time_since("bleh"), "")
@ -207,7 +209,7 @@ class TemplateTags(TestCase):
re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago))
)
def test_get_markdown(self):
def test_get_markdown(self, _):
""" mardown format data """
result = bookwyrm_tags.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>")
@ -215,13 +217,13 @@ class TemplateTags(TestCase):
result = bookwyrm_tags.get_markdown("<marquee>_hi_</marquee>")
self.assertEqual(result, "<p><em>hi</em></p>")
def test_get_mentions(self):
def test_get_mentions(self, _):
""" list of people mentioned """
status = models.Status.objects.create(content="hi", user=self.remote_user)
result = bookwyrm_tags.get_mentions(status, self.user)
self.assertEqual(result, "@rat@example.com ")
def test_get_status_preview_name(self):
def test_get_status_preview_name(self, _):
""" status context string """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)
@ -246,7 +248,7 @@ class TemplateTags(TestCase):
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, "quotation from <em>Test Book</em>")
def test_related_status(self):
def test_related_status(self, _):
""" gets the subclass model for a notification status """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)

View file

@ -7,6 +7,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class BlockViews(TestCase):
""" view user and edit profile """
@ -32,7 +33,7 @@ class BlockViews(TestCase):
)
models.SiteSettings.objects.create()
def test_block_get(self):
def test_block_get(self, _):
""" there are so many views, this just makes sure it LOADS """
view = views.Block.as_view()
request = self.factory.get("")
@ -42,20 +43,19 @@ class BlockViews(TestCase):
result.render()
self.assertEqual(result.status_code, 200)
def test_block_post(self):
def test_block_post(self, _):
""" create a "block" database entry from an activity """
view = views.Block.as_view()
self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.remove_user_statuses"):
view(request, self.remote_user.id)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.local_user)
@ -64,13 +64,13 @@ class BlockViews(TestCase):
self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_unblock(self):
def test_unblock(self, _):
""" undo a block """
self.local_user.blocks.add(self.remote_user)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_user_statuses"):
views.block.unblock(request, self.remote_user.id)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -14,6 +14,8 @@ from bookwyrm import views
from bookwyrm.activitypub import ActivitypubResponse
@patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class FeedViews(TestCase):
""" activity feed, statuses, dms """
@ -34,7 +36,7 @@ class FeedViews(TestCase):
)
models.SiteSettings.objects.create()
def test_feed(self):
def test_feed(self, *_):
""" there are so many views, this just makes sure it LOADS """
view = views.Feed.as_view()
request = self.factory.get("")
@ -44,7 +46,7 @@ class FeedViews(TestCase):
result.render()
self.assertEqual(result.status_code, 200)
def test_status_page(self):
def test_status_page(self, *_):
""" there are so many views, this just makes sure it LOADS """
view = views.Status.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -64,7 +66,7 @@ class FeedViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_status_page_with_image(self):
def test_status_page_with_image(self, *_):
""" there are so many views, this just makes sure it LOADS """
view = views.Status.as_view()
@ -100,7 +102,7 @@ class FeedViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_replies_page(self):
def test_replies_page(self, *_):
""" there are so many views, this just makes sure it LOADS """
view = views.Replies.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -120,7 +122,7 @@ class FeedViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_direct_messages_page(self):
def test_direct_messages_page(self, *_):
""" there are so many views, this just makes sure it LOADS """
view = views.DirectMessage.as_view()
request = self.factory.get("")
@ -130,7 +132,7 @@ class FeedViews(TestCase):
result.render()
self.assertEqual(result.status_code, 200)
def test_get_suggested_book(self):
def test_get_suggested_book(self, *_):
""" gets books the ~*~ algorithm ~*~ thinks you want to post about """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ShelfBook.objects.create(

View file

@ -102,7 +102,8 @@ class GoalViews(TestCase):
result = view(request, self.local_user.localname, 2020)
self.assertEqual(result.status_code, 404)
def test_create_goal(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_create_goal(self, _):
""" create a new goal """
view = views.Goal.as_view()
request = self.factory.post(

View file

@ -80,113 +80,6 @@ class ViewsHelpers(TestCase):
request.headers = {"Accept": "Praise"}
self.assertFalse(views.helpers.is_api_request(request))
def test_get_activity_feed(self):
""" loads statuses """
rat = models.User.objects.create_user(
"rat", "rat@rat.rat", "password", local=True
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
public_status = models.Comment.objects.create(
content="public status", book=self.book, user=self.local_user
)
direct_status = models.Status.objects.create(
content="direct", user=self.local_user, privacy="direct"
)
rat_public = models.Status.objects.create(content="blah blah", user=rat)
rat_unlisted = models.Status.objects.create(
content="blah blah", user=rat, privacy="unlisted"
)
remote_status = models.Status.objects.create(
content="blah blah", user=self.remote_user
)
followers_status = models.Status.objects.create(
content="blah", user=rat, privacy="followers"
)
rat_mention = models.Status.objects.create(
content="blah blah blah", user=rat, privacy="followers"
)
rat_mention.mention_users.set([self.local_user])
statuses = views.helpers.get_activity_feed(
self.local_user,
privacy=["public", "unlisted", "followers"],
following_only=True,
queryset=models.Comment.objects,
)
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], public_status)
statuses = views.helpers.get_activity_feed(
self.local_user, privacy=["public", "followers"], local_only=True
)
self.assertEqual(len(statuses), 2)
self.assertEqual(statuses[1], public_status)
self.assertEqual(statuses[0], rat_public)
statuses = views.helpers.get_activity_feed(self.local_user, privacy=["direct"])
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], direct_status)
statuses = views.helpers.get_activity_feed(
self.local_user,
privacy=["public", "followers"],
)
self.assertEqual(len(statuses), 3)
self.assertEqual(statuses[2], public_status)
self.assertEqual(statuses[1], rat_public)
self.assertEqual(statuses[0], remote_status)
statuses = views.helpers.get_activity_feed(
self.local_user,
privacy=["public", "unlisted", "followers"],
following_only=True,
)
self.assertEqual(len(statuses), 2)
self.assertEqual(statuses[1], public_status)
self.assertEqual(statuses[0], rat_mention)
rat.followers.add(self.local_user)
statuses = views.helpers.get_activity_feed(
self.local_user,
privacy=["public", "unlisted", "followers"],
following_only=True,
)
self.assertEqual(len(statuses), 5)
self.assertEqual(statuses[4], public_status)
self.assertEqual(statuses[3], rat_public)
self.assertEqual(statuses[2], rat_unlisted)
self.assertEqual(statuses[1], followers_status)
self.assertEqual(statuses[0], rat_mention)
def test_get_activity_feed_blocks(self):
""" feed generation with blocked users """
rat = models.User.objects.create_user(
"rat", "rat@rat.rat", "password", local=True
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
public_status = models.Comment.objects.create(
content="public status", book=self.book, user=self.local_user
)
rat_public = models.Status.objects.create(content="blah blah", user=rat)
statuses = views.helpers.get_activity_feed(
self.local_user, privacy=["public"]
)
self.assertEqual(len(statuses), 2)
# block relationship
rat.blocks.add(self.local_user)
statuses = views.helpers.get_activity_feed(self.local_user, privacy=["public"])
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], public_status)
statuses = views.helpers.get_activity_feed(rat, privacy=["public"])
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], rat_public)
def test_is_bookwyrm_request(self):
""" checks if a request came from a bookwyrm instance """
request = self.factory.get("", {"q": "Test Book"})
@ -241,7 +134,8 @@ class ViewsHelpers(TestCase):
self.assertIsInstance(result, models.User)
self.assertEqual(result.username, "mouse@example.com")
def test_handle_reading_status_to_read(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_reading_status_to_read(self, _):
""" posts shelve activities """
shelf = self.local_user.shelf_set.get(identifier="to-read")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -253,7 +147,8 @@ class ViewsHelpers(TestCase):
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, "wants to read")
def test_handle_reading_status_reading(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_reading_status_reading(self, _):
""" posts shelve activities """
shelf = self.local_user.shelf_set.get(identifier="reading")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -265,7 +160,8 @@ class ViewsHelpers(TestCase):
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, "started reading")
def test_handle_reading_status_read(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_reading_status_read(self, _):
""" posts shelve activities """
shelf = self.local_user.shelf_set.get(identifier="read")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -277,7 +173,8 @@ class ViewsHelpers(TestCase):
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, "finished reading")
def test_handle_reading_status_other(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_reading_status_other(self, _):
""" posts shelve activities """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.helpers.handle_reading_status(
@ -285,7 +182,8 @@ class ViewsHelpers(TestCase):
)
self.assertFalse(models.GeneratedNote.objects.exists())
def test_object_visible_to_user(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user(self, _):
""" does a user have permission to view an object """
obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public"
@ -313,7 +211,8 @@ class ViewsHelpers(TestCase):
obj.mention_users.add(self.local_user)
self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj))
def test_object_visible_to_user_follower(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_follower(self, _):
""" what you can see if you follow a user """
self.remote_user.followers.add(self.local_user)
obj = models.Status.objects.create(
@ -332,7 +231,8 @@ class ViewsHelpers(TestCase):
obj.mention_users.add(self.local_user)
self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj))
def test_object_visible_to_user_blocked(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_blocked(self, _):
""" you can't see it if they block you """
self.remote_user.blocks.add(self.local_user)
obj = models.Status.objects.create(

View file

@ -38,11 +38,12 @@ class Inbox(TestCase):
outbox="https://example.com/users/rat/outbox",
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = {
"id": "hi",
@ -139,7 +140,9 @@ class Inbox(TestCase):
activity = self.create_json
activity["object"] = status_data
views.inbox.activity_task(activity)
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
status = models.Quotation.objects.get()
self.assertEqual(
@ -166,7 +169,9 @@ class Inbox(TestCase):
activity = self.create_json
activity["object"] = status_data
views.inbox.activity_task(activity)
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
status = models.Status.objects.last()
self.assertEqual(status.content, "test content in note")
self.assertEqual(status.mention_users.first(), self.local_user)
@ -187,7 +192,9 @@ class Inbox(TestCase):
activity = self.create_json
activity["object"] = status_data
views.inbox.activity_task(activity)
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
status = models.Status.objects.last()
self.assertEqual(status.content, "test content in note")
self.assertEqual(status.reply_parent, self.status)
@ -218,7 +225,7 @@ class Inbox(TestCase):
self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_handle_follow_x(self):
def test_handle_follow(self):
""" remote user wants to follow local user """
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
@ -436,7 +443,11 @@ class Inbox(TestCase):
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
views.inbox.activity_task(activity)
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# deletion doens't remove the status, it turns it into a tombstone
status = models.Status.objects.get()
self.assertTrue(status.deleted)
@ -465,7 +476,11 @@ class Inbox(TestCase):
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
views.inbox.activity_task(activity)
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# deletion doens't remove the status, it turns it into a tombstone
status = models.Status.objects.get()
self.assertTrue(status.deleted)
@ -535,7 +550,8 @@ class Inbox(TestCase):
views.inbox.activity_task(activity)
self.assertEqual(models.Favorite.objects.count(), 0)
def test_handle_boost(self):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_boost(self, _):
""" boost a status """
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
@ -560,7 +576,8 @@ class Inbox(TestCase):
content="hi",
user=self.remote_user,
)
status.save(broadcast=False)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status.save(broadcast=False)
activity = {
"type": "Announce",
"id": "http://www.faraway.com/boost/12",
@ -575,9 +592,10 @@ class Inbox(TestCase):
def test_handle_unboost(self):
""" undo a boost """
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
)
activity = {
"type": "Undo",
"actor": "hi",
@ -591,7 +609,11 @@ class Inbox(TestCase):
"object": self.status.remote_id,
},
}
views.inbox.activity_task(activity)
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
def test_handle_unboost_unknown_boost(self):
""" undo a boost """
@ -863,6 +885,11 @@ class Inbox(TestCase):
"object": "https://example.com/user/mouse",
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
views.inbox.activity_task(activity)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.remote_user)
@ -896,5 +923,9 @@ class Inbox(TestCase):
"object": "https://example.com/user/mouse",
},
}
views.inbox.activity_task(activity)
with patch(
"bookwyrm.activitystreams.ActivityStream.add_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -6,6 +6,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class InteractionViews(TestCase):
""" viewing and creating statuses """
@ -38,12 +39,12 @@ class InteractionViews(TestCase):
parent_work=work,
)
def test_handle_favorite(self):
def test_handle_favorite(self, _):
""" create and broadcast faving a status """
view = views.Favorite.as_view()
request = self.factory.post("")
request.user = self.remote_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
view(request, status.id)
@ -56,12 +57,12 @@ class InteractionViews(TestCase):
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
def test_handle_unfavorite(self):
def test_handle_unfavorite(self, _):
""" unfav a status """
view = views.Unfavorite.as_view()
request = self.factory.post("")
request.user = self.remote_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
views.Favorite.as_view()(request, status.id)
@ -73,12 +74,12 @@ class InteractionViews(TestCase):
self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)
def test_handle_boost(self):
def test_handle_boost(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
request.user = self.remote_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
view(request, status.id)
@ -94,12 +95,12 @@ class InteractionViews(TestCase):
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status)
def test_handle_boost_unlisted(self):
def test_handle_boost_unlisted(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="unlisted"
)
@ -109,12 +110,12 @@ class InteractionViews(TestCase):
boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, "unlisted")
def test_handle_boost_private(self):
def test_handle_boost_private(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="followers"
)
@ -122,31 +123,35 @@ class InteractionViews(TestCase):
view(request, status.id)
self.assertFalse(models.Boost.objects.exists())
def test_handle_boost_twice(self):
def test_handle_boost_twice(self, _):
""" boost a status """
view = views.Boost.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
view(request, status.id)
view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)
def test_handle_unboost(self):
def test_handle_unboost(self, broadcast_mock):
""" undo a boost """
view = views.Unboost.as_view()
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
views.Boost.as_view()(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)
self.assertEqual(models.Notification.objects.count(), 1)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
broadcast_mock.call_count = 0
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
view(request, status.id)
self.assertEqual(mock.call_count, 1)
self.assertEqual(broadcast_mock.call_count, 1)
self.assertTrue(redis_mock.called)
self.assertEqual(models.Boost.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)

View file

@ -3,12 +3,10 @@ import json
from unittest.mock import patch
from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.connectors import abstract_connector
from bookwyrm.settings import DOMAIN

View file

@ -1,4 +1,5 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse
from django.test import TestCase
@ -30,7 +31,8 @@ class LandingViews(TestCase):
view = views.Home.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
with patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream"):
result = view(request)
self.assertEqual(result.status_code, 200)
result.render()

View file

@ -11,6 +11,7 @@ from bookwyrm.settings import USER_AGENT
# pylint: disable=too-many-public-methods
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class OutboxView(TestCase):
""" sends out activities """
@ -32,19 +33,19 @@ class OutboxView(TestCase):
parent_work=work,
)
def test_outbox(self):
def test_outbox(self, _):
""" returns user's statuses """
request = self.factory.get("")
result = views.Outbox.as_view()(request, "mouse")
self.assertIsInstance(result, JsonResponse)
def test_outbox_bad_method(self):
def test_outbox_bad_method(self, _):
""" can't POST to outbox """
request = self.factory.post("")
result = views.Outbox.as_view()(request, "mouse")
self.assertEqual(result.status_code, 405)
def test_outbox_unknown_user(self):
def test_outbox_unknown_user(self, _):
""" should 404 for unknown and remote users """
request = self.factory.post("")
result = views.Outbox.as_view()(request, "beepboop")
@ -52,9 +53,9 @@ class OutboxView(TestCase):
result = views.Outbox.as_view()(request, "rat")
self.assertEqual(result.status_code, 405)
def test_outbox_privacy(self):
def test_outbox_privacy(self, _):
""" don't show dms et cetera in outbox """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Status.objects.create(
content="PRIVATE!!", user=self.local_user, privacy="direct"
)
@ -75,9 +76,9 @@ class OutboxView(TestCase):
self.assertEqual(data["type"], "OrderedCollection")
self.assertEqual(data["totalItems"], 2)
def test_outbox_filter(self):
def test_outbox_filter(self, _):
""" if we only care about reviews, only get reviews """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Review.objects.create(
content="look at this",
name="hi",
@ -101,9 +102,9 @@ class OutboxView(TestCase):
self.assertEqual(data["type"], "OrderedCollection")
self.assertEqual(data["totalItems"], 1)
def test_outbox_bookwyrm_request_true(self):
def test_outbox_bookwyrm_request_true(self, _):
""" should differentiate between bookwyrm and outside requests """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Review.objects.create(
name="hi",
content="look at this",
@ -119,9 +120,9 @@ class OutboxView(TestCase):
self.assertEqual(len(data["orderedItems"]), 1)
self.assertEqual(data["orderedItems"][0]["type"], "Review")
def test_outbox_bookwyrm_request_false(self):
def test_outbox_bookwyrm_request_false(self, _):
""" should differentiate between bookwyrm and outside requests """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Review.objects.create(
name="hi",
content="look at this",

View file

@ -8,6 +8,7 @@ from django.utils import timezone
from bookwyrm import models, views
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class ReadingViews(TestCase):
""" viewing and creating statuses """
@ -39,7 +40,7 @@ class ReadingViews(TestCase):
outbox="https://example.com/users/rat/outbox",
)
def test_start_reading(self):
def test_start_reading(self, _):
""" begin a book """
shelf = self.local_user.shelf_set.get(identifier="reading")
self.assertFalse(shelf.books.exists())
@ -70,7 +71,7 @@ class ReadingViews(TestCase):
self.assertEqual(readthrough.user, self.local_user)
self.assertEqual(readthrough.book, self.book)
def test_start_reading_reshelf(self):
def test_start_reading_reshelf(self, _):
""" begin a book """
to_read_shelf = self.local_user.shelf_set.get(identifier="to-read")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -90,7 +91,7 @@ class ReadingViews(TestCase):
self.assertFalse(to_read_shelf.books.exists())
self.assertEqual(shelf.books.get(), self.book)
def test_finish_reading(self):
def test_finish_reading(self, _):
""" begin a book """
shelf = self.local_user.shelf_set.get(identifier="read")
self.assertFalse(shelf.books.exists())
@ -126,7 +127,7 @@ class ReadingViews(TestCase):
self.assertEqual(readthrough.user, self.local_user)
self.assertEqual(readthrough.book, self.book)
def test_edit_readthrough(self):
def test_edit_readthrough(self, _):
""" adding dates to an ongoing readthrough """
start = timezone.make_aware(dateutil.parser.parse("2021-01-03"))
readthrough = models.ReadThrough.objects.create(
@ -153,7 +154,7 @@ class ReadingViews(TestCase):
self.assertEqual(readthrough.finish_date.day, 7)
self.assertEqual(readthrough.book, self.book)
def test_delete_readthrough(self):
def test_delete_readthrough(self, _):
""" remove a readthrough """
readthrough = models.ReadThrough.objects.create(
book=self.book, user=self.local_user
@ -170,7 +171,7 @@ class ReadingViews(TestCase):
views.delete_readthrough(request)
self.assertFalse(models.ReadThrough.objects.filter(id=readthrough.id).exists())
def test_create_readthrough(self):
def test_create_readthrough(self, _):
""" adding new read dates """
request = self.factory.post(
"",

View file

@ -26,28 +26,30 @@ class RssFeedView(TestCase):
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.review = models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.user,
book=self.book,
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.review = models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.user,
book=self.book,
)
self.quote = models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.user,
book=self.book,
)
self.quote = models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.user,
book=self.book,
)
self.generatednote = models.GeneratedNote.objects.create(
content="test content", user=self.user
)
self.generatednote = models.GeneratedNote.objects.create(
content="test content", user=self.user
)
self.factory = RequestFactory()
def test_rss_feed(self):
@patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream")
def test_rss_feed(self, _):
""" load an rss feed """
view = rss_feed.RssFeed()
request = self.factory.get("/user/rss_user/rss")

View file

@ -8,6 +8,7 @@ from bookwyrm import forms, models, views
from bookwyrm.settings import DOMAIN
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class StatusViews(TestCase):
""" viewing and creating statuses """
@ -40,7 +41,7 @@ class StatusViews(TestCase):
parent_work=work,
)
def test_handle_status(self):
def test_handle_status(self, _):
""" create a status """
view = views.CreateStatus.as_view()
form = forms.CommentForm(
@ -53,20 +54,23 @@ class StatusViews(TestCase):
)
request = self.factory.post("", form.data)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
view(request, "comment")
self.assertTrue(redis_mock.called)
status = models.Comment.objects.get()
self.assertEqual(status.content, "<p>hi</p>")
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.book, self.book)
def test_handle_status_reply(self):
def test_handle_status_reply(self, _):
""" create a status in reply to an existing status """
view = views.CreateStatus.as_view()
user = models.User.objects.create_user(
"rat", "rat@rat.com", "password", local=True
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
parent = models.Status.objects.create(
content="parent status", user=self.local_user
)
@ -80,14 +84,17 @@ class StatusViews(TestCase):
)
request = self.factory.post("", form.data)
request.user = user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
view(request, "reply")
self.assertTrue(redis_mock.called)
status = models.Status.objects.get(user=user)
self.assertEqual(status.content, "<p>hi</p>")
self.assertEqual(status.user, user)
self.assertEqual(models.Notification.objects.get().user, self.local_user)
def test_handle_status_mentions(self):
def test_handle_status_mentions(self, _):
""" @mention a user in a post """
view = views.CreateStatus.as_view()
user = models.User.objects.create_user(
@ -104,8 +111,10 @@ class StatusViews(TestCase):
request = self.factory.post("", form.data)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
view(request, "comment")
self.assertTrue(redis_mock.called)
status = models.Status.objects.get()
self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user)
@ -113,7 +122,7 @@ class StatusViews(TestCase):
status.content, '<p>hi <a href="%s">@rat</a></p>' % user.remote_id
)
def test_handle_status_reply_with_mentions(self):
def test_handle_status_reply_with_mentions(self, _):
""" reply to a post with an @mention'ed user """
view = views.CreateStatus.as_view()
user = models.User.objects.create_user(
@ -130,8 +139,9 @@ class StatusViews(TestCase):
request = self.factory.post("", form.data)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
view(request, "comment")
self.assertTrue(redis_mock.called)
status = models.Status.objects.get()
form = forms.ReplyForm(
@ -144,8 +154,10 @@ class StatusViews(TestCase):
)
request = self.factory.post("", form.data)
request.user = user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
view(request, "reply")
self.assertTrue(redis_mock.called)
reply = models.Status.replies(status).first()
self.assertEqual(reply.content, "<p>right</p>")
@ -154,7 +166,7 @@ class StatusViews(TestCase):
self.assertFalse(self.remote_user in reply.mention_users.all())
self.assertTrue(self.local_user in reply.mention_users.all())
def test_find_mentions(self):
def test_find_mentions(self, _):
""" detect and look up @ mentions of users """
user = models.User.objects.create_user(
"nutria@%s" % DOMAIN,
@ -200,7 +212,7 @@ class StatusViews(TestCase):
("@nutria@%s" % DOMAIN, user),
)
def test_format_links(self):
def test_format_links(self, _):
""" find and format urls into a tags """
url = "http://www.fish.com/"
self.assertEqual(
@ -223,7 +235,7 @@ class StatusViews(TestCase):
"?q=arkady+strugatsky&mode=everything</a>" % url,
)
def test_to_markdown(self):
def test_to_markdown(self, _):
""" this is mostly handled in other places, but nonetheless """
text = "_hi_ and http://fish.com is <marquee>rad</marquee>"
result = views.status.to_markdown(text)
@ -232,32 +244,36 @@ class StatusViews(TestCase):
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' "is rad</p>",
)
def test_to_markdown_link(self):
def test_to_markdown_link(self, _):
""" this is mostly handled in other places, but nonetheless """
text = "[hi](http://fish.com) is <marquee>rad</marquee>"
result = views.status.to_markdown(text)
self.assertEqual(result, '<p><a href="http://fish.com">hi</a> ' "is rad</p>")
def test_handle_delete_status(self):
def test_handle_delete_status(self, mock):
""" marks a status as deleted """
view = views.DeleteStatus.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
self.assertFalse(status.deleted)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
view(request, status.id)
activity = json.loads(mock.call_args_list[0][0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"]["type"], "Tombstone")
self.assertTrue(redis_mock.called)
activity = json.loads(mock.call_args_list[1][0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"]["type"], "Tombstone")
status.refresh_from_db()
self.assertTrue(status.deleted)
def test_handle_delete_status_permission_denied(self):
def test_handle_delete_status_permission_denied(self, _):
""" marks a status as deleted """
view = views.DeleteStatus.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
self.assertFalse(status.deleted)
request = self.factory.post("")
@ -268,20 +284,23 @@ class StatusViews(TestCase):
status.refresh_from_db()
self.assertFalse(status.deleted)
def test_handle_delete_status_moderator(self):
def test_handle_delete_status_moderator(self, mock):
""" marks a status as deleted """
view = views.DeleteStatus.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status = models.Status.objects.create(user=self.local_user, content="hi")
self.assertFalse(status.deleted)
request = self.factory.post("")
request.user = self.remote_user
request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_status"
) as redis_mock:
view(request, status.id)
activity = json.loads(mock.call_args_list[0][0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"]["type"], "Tombstone")
self.assertTrue(redis_mock.called)
activity = json.loads(mock.call_args_list[1][0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"]["type"], "Tombstone")
status.refresh_from_db()
self.assertTrue(status.deleted)

View file

@ -1,5 +1,7 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from django.http import JsonResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -22,21 +24,33 @@ class UpdateViews(TestCase):
)
models.SiteSettings.objects.create()
def test_get_updates(self):
def test_get_notification_count(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Updates.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
result = views.get_notification_count(request)
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.getvalue())
self.assertEqual(data["notifications"], 0)
self.assertEqual(data["count"], 0)
models.Notification.objects.create(
notification_type="BOOST", user=self.local_user
)
result = view(request)
result = views.get_notification_count(request)
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.getvalue())
self.assertEqual(data["notifications"], 1)
self.assertEqual(data["count"], 1)
def test_get_unread_status_count(self):
""" there are so many views, this just makes sure it LOADS """
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.activitystreams.ActivityStream.get_unread_count") as mock:
mock.return_value = 3
result = views.get_unread_status_count(request, "home")
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.getvalue())
self.assertEqual(data["count"], 3)

View file

@ -37,7 +37,8 @@ urlpatterns = [
re_path(r"^api/v1/instance/?$", views.instance_info),
re_path(r"^api/v1/instance/peers/?$", views.peers),
# polling updates
re_path("^api/updates/notifications/?$", views.Updates.as_view()),
re_path("^api/updates/notifications/?$", views.get_notification_count),
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
# authentication
re_path(r"^login/?$", views.Login.as_view()),
re_path(r"^register/?$", views.Register.as_view()),

View file

@ -33,6 +33,6 @@ from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus
from .tag import Tag, AddTag, RemoveTag
from .updates import Updates
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following
from .wellknown import webfinger, nodeinfo_pointer, nodeinfo, instance_info, peers

View file

@ -19,8 +19,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_activity_feed, get_edition
from .helpers import privacy_filter
from .helpers import is_api_request, get_edition, privacy_filter
# pylint: disable= no-self-use
@ -53,7 +52,7 @@ class Book(View):
# all reviews for the book
reviews = models.Review.objects.filter(book__in=work.editions.all())
reviews = get_activity_feed(request.user, queryset=reviews)
reviews = privacy_filter(request.user, reviews)
# the reviews to show
paginated = Paginator(

View file

@ -6,13 +6,12 @@ from django.http import HttpResponseNotFound
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views import View
from bookwyrm import forms, models
from bookwyrm import activitystreams, forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username
from bookwyrm.settings import PAGE_LENGTH, STREAMS
from .helpers import get_user_from_username, privacy_filter
from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user
@ -28,19 +27,11 @@ class Feed(View):
except ValueError:
page = 1
if tab == "home":
activities = get_activity_feed(request.user, following_only=True)
tab_title = _("Home")
elif tab == "local":
activities = get_activity_feed(
request.user, privacy=["public", "followers"], local_only=True
)
tab_title = _("Local")
else:
activities = get_activity_feed(
request.user, privacy=["public", "followers"]
)
tab_title = _("Federated")
if not tab in STREAMS:
tab = "home"
activities = activitystreams.streams[tab].get_activity_stream(request.user)
paginated = Paginator(activities, PAGE_LENGTH)
data = {
@ -49,7 +40,6 @@ class Feed(View):
"user": request.user,
"activities": paginated.page(page),
"tab": tab,
"tab_title": tab_title,
"goal_form": forms.GoalForm(),
"path": "/%s" % tab,
},
@ -68,7 +58,13 @@ class DirectMessage(View):
except ValueError:
page = 1
queryset = models.Status.objects
# remove fancy subclasses of status, keep just good ol' notes
queryset = models.Status.objects.filter(
review__isnull=True,
comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
)
user = None
if username:
@ -79,9 +75,7 @@ class DirectMessage(View):
if user:
queryset = queryset.filter(Q(user=user) | Q(mention_users=user))
activities = get_activity_feed(
request.user, privacy=["direct"], queryset=queryset
)
activities = privacy_filter(request.user, queryset, privacy_levels=["direct"])
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)

View file

@ -59,6 +59,11 @@ def object_visible_to_user(viewer, obj):
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
""" filter objects that have "user" and "privacy" fields """
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]
# if there'd a deleted field, exclude deleted items
try:
queryset = queryset.filter(deleted=False)
except FieldError:
pass
# exclude blocks from both directions
if not viewer.is_anonymous:
@ -102,54 +107,6 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
return queryset
def get_activity_feed(
user, privacy=None, local_only=False, following_only=False, queryset=None
):
""" get a filtered queryset of statuses """
if queryset is None:
queryset = models.Status.objects.select_subclasses()
# exclude deleted
queryset = queryset.exclude(deleted=True).order_by("-published_date")
# apply privacy filters
queryset = privacy_filter(user, queryset, privacy, following_only=following_only)
# only show dms if we only want dms
if privacy == ["direct"]:
# dms are direct statuses not related to books
queryset = queryset.filter(
review__isnull=True,
comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
)
else:
try:
queryset = queryset.exclude(
review__isnull=True,
comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
privacy="direct",
)
except FieldError:
# if we're looking at a subtype of Status (like Review)
pass
# filter for only local status
if local_only:
queryset = queryset.filter(user__local=True)
# remove statuses that have boosts in the same queryset
try:
queryset = queryset.filter(~Q(boosters__in=queryset))
except ValueError:
pass
return queryset
def handle_remote_webfinger(query):
""" webfingerin' other servers """
user = None

View file

@ -1,7 +1,7 @@
""" serialize user's posts in rss feed """
from django.contrib.syndication.views import Feed
from .helpers import get_activity_feed, get_user_from_username
from .helpers import get_user_from_username, privacy_filter
# pylint: disable=no-self-use, unused-argument
class RssFeed(Feed):
@ -24,10 +24,10 @@ class RssFeed(Feed):
def items(self, obj):
""" the user's activity feed """
return get_activity_feed(
return privacy_filter(
obj,
privacy=["public", "unlisted"],
queryset=obj.status_set.select_subclasses(),
obj.status_set.select_subclasses(),
privacy_levels=["public", "unlisted"],
)
def item_link(self, item):

View file

@ -1,20 +1,24 @@
""" endpoints for getting updates about activity """
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
class Updates(View):
""" so the app can poll """
from bookwyrm import activitystreams
def get(self, request):
""" any notifications waiting? """
return JsonResponse(
{
"notifications": request.user.notification_set.filter(
read=False
).count(),
}
)
@login_required
def get_notification_count(request):
""" any notifications waiting? """
return JsonResponse(
{
"count": request.user.notification_set.filter(read=False).count(),
}
)
@login_required
def get_unread_status_count(request, stream="home"):
""" any unread statuses for this feed? """
stream = activitystreams.streams.get(stream)
if not stream:
return JsonResponse({})
return JsonResponse({"count": stream.get_unread_count(request.user)})

View file

@ -16,8 +16,8 @@ from django.views import View
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username, is_api_request
from .helpers import is_blocked, object_visible_to_user
from .helpers import get_user_from_username, is_api_request
from .helpers import is_blocked, privacy_filter, object_visible_to_user
# pylint: disable= no-self-use
@ -72,9 +72,9 @@ class User(View):
break
# user's posts
activities = get_activity_feed(
activities = privacy_filter(
request.user,
queryset=user.status_set.select_subclasses(),
user.status_set.select_subclasses(),
)
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(

View file

@ -31,11 +31,20 @@ services:
depends_on:
- db
- celery_worker
- redis_activity
networks:
- main
ports:
- 8000:8000
redis:
redis_activity:
image: redis
env_file: .env
ports:
- 6378:6378
networks:
- main
restart: on-failure
redis_broker:
image: redis
env_file: .env
ports:
@ -55,7 +64,7 @@ services:
- media_volume:/app/images
depends_on:
- db
- redis
- redis_broker
restart: on-failure
flower:
build: .
@ -67,7 +76,7 @@ services:
- main
depends_on:
- db
- redis
- redis_broker
restart: on-failure
ports:
- 8888:8888