Merge pull request #594 from mouse-reeve/broadcast-fixes

Refactors broadcasting
This commit is contained in:
Mouse Reeve 2021-02-10 08:52:15 -08:00 committed by GitHub
commit 486b660351
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1806 additions and 1340 deletions

View file

@ -93,7 +93,10 @@ class ActivityObject:
with transaction.atomic():
# we can't set many to many and reverse fields on an unsaved object
try:
instance.save()
try:
instance.save(broadcast=False)
except TypeError:
instance.save()
except IntegrityError as e:
raise ActivitySerializerError(e)

View file

@ -18,7 +18,7 @@ class Note(ActivityObject):
''' Note activity '''
published: str
attributedTo: str
content: str
content: str = ''
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
replies: Dict = field(default_factory=lambda: {})

View file

@ -1,87 +0,0 @@
''' send out activitypub messages '''
import json
from django.utils.http import http_date
import requests
from bookwyrm import models, settings
from bookwyrm.activitypub import ActivityEncoder
from bookwyrm.tasks import app
from bookwyrm.signatures import make_signature, make_digest
def get_public_recipients(user, software=None):
''' everybody and their public inboxes '''
followers = user.followers.filter(local=False)
if software:
followers = followers.filter(bookwyrm_user=(software == 'bookwyrm'))
# we want shared inboxes when available
shared = followers.filter(
shared_inbox__isnull=False
).values_list('shared_inbox', flat=True).distinct()
# if a user doesn't have a shared inbox, we need their personal inbox
# iirc pixelfed doesn't have shared inboxes
inboxes = followers.filter(
shared_inbox__isnull=True
).values_list('inbox', flat=True)
return list(shared) + list(inboxes)
def broadcast(sender, activity, software=None, \
privacy='public', direct_recipients=None):
''' send out an event '''
# start with parsing the direct recipients
recipients = [u.inbox for u in direct_recipients or []]
# and then add any other recipients
if privacy == 'public':
recipients += get_public_recipients(sender, software=software)
broadcast_task.delay(
sender.id,
json.dumps(activity, cls=ActivityEncoder),
recipients
)
@app.task
def broadcast_task(sender_id, activity, recipients):
''' the celery task for broadcast '''
sender = models.User.objects.get(id=sender_id)
errors = []
for recipient in recipients:
try:
sign_and_send(sender, activity, recipient)
except requests.exceptions.HTTPError as e:
errors.append({
'error': str(e),
'recipient': recipient,
'activity': activity,
})
return errors
def sign_and_send(sender, data, destination):
''' crpyto whatever and http junk '''
now = http_date()
if not sender.key_pair.private_key:
# this shouldn't happen. it would be bad if it happened.
raise ValueError('No private key found for sender')
digest = make_digest(data)
response = requests.post(
destination,
data=data,
headers={
'Date': now,
'Digest': digest,
'Signature': make_signature(sender, destination, now, digest),
'Content-Type': 'application/activity+json; charset=utf-8',
'User-Agent': settings.USER_AGENT,
},
)
if not response.ok:
response.raise_for_status()
return response

View file

@ -3,7 +3,6 @@ import csv
import logging
from bookwyrm import models
from bookwyrm.broadcast import broadcast
from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.status import create_notification
from bookwyrm.tasks import app
@ -82,7 +81,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
return
existing_shelf = models.ShelfBook.objects.filter(
book=item.book, added_by=user).exists()
book=item.book, user=user).exists()
# shelve the book if it hasn't been shelved already
if item.shelf and not existing_shelf:
@ -90,9 +89,8 @@ def handle_imported_book(user, item, include_reviews, privacy):
identifier=item.shelf,
user=user
)
shelf_book = models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, added_by=user)
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, user=user)
for read in item.reads:
# check for an existing readthrough with the same dates
@ -114,7 +112,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
# we don't know the publication date of the review,
# but "now" is a bad guess
published_date_guess = item.date_read or item.date_added
review = models.Review.objects.create(
models.Review.objects.create(
user=user,
book=item.book,
name=review_title,
@ -123,6 +121,3 @@ def handle_imported_book(user, item, include_reviews, privacy):
published_date=published_date_guess,
privacy=privacy,
)
# we don't need to send out pure activities because non-bookwyrm
# instances don't need this data
broadcast(user, review.to_create_activity(user), privacy=privacy)

View file

@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import requests
from bookwyrm import activitypub, models, views
from bookwyrm import activitypub, models
from bookwyrm import status as status_builder
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
@ -144,7 +144,7 @@ def handle_follow(activity):
related_user=relationship.user_subject
)
if not manually_approves:
views.handle_accept(relationship)
relationship.accept()
@app.task

View file

@ -1,6 +1,6 @@
# Generated by Django 3.0.7 on 2020-11-30 18:19
import bookwyrm.models.base_model
import bookwyrm.models.activitypub_mixin
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
options={
'abstract': False,
},
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
),
migrations.AddField(
model_name='user',

View file

@ -1,6 +1,6 @@
# Generated by Django 3.0.7 on 2021-01-31 16:14
import bookwyrm.models.base_model
import bookwyrm.models.activitypub_mixin
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
@ -29,7 +29,7 @@ class Migration(migrations.Migration):
options={
'abstract': False,
},
bases=(bookwyrm.models.base_model.OrderedCollectionMixin, models.Model),
bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model),
),
migrations.CreateModel(
name='ListItem',
@ -50,7 +50,7 @@ class Migration(migrations.Migration):
'ordering': ('-created_date',),
'unique_together': {('book', 'book_list')},
},
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
),
migrations.AddField(
model_name='list',

View file

@ -0,0 +1,23 @@
# Generated by Django 3.0.7 on 2021-02-04 22:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0042_auto_20210201_2108'),
]
operations = [
migrations.RenameField(
model_name='listitem',
old_name='added_by',
new_name='user',
),
migrations.RenameField(
model_name='shelfbook',
old_name='added_by',
new_name='user',
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 3.0.7 on 2021-02-07 19:24
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
def set_user(app_registry, schema_editor):
db_alias = schema_editor.connection.alias
shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook')
for item in shelfbook.objects.using(db_alias).filter(user__isnull=True):
item.user = item.shelf.user
item.save(broadcast=False)
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0043_auto_20210204_2223'),
]
operations = [
migrations.RunPython(set_user, lambda x, y: None),
migrations.AlterField(
model_name='shelfbook',
name='user',
field=bookwyrm.models.fields.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View file

@ -0,0 +1,497 @@
''' activitypub model functionality '''
from base64 import b64encode
from functools import reduce
import json
import operator
import logging
from uuid import uuid4
import requests
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from django.apps import apps
from django.core.paginator import Paginator
from django.db.models import Q
from django.utils.http import http_date
from bookwyrm import activitypub
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
from bookwyrm.signatures import make_signature, make_digest
from bookwyrm.tasks import app
from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__)
# I tried to separate these classes into mutliple files but I kept getting
# circular import errors so I gave up. I'm sure it could be done though!
class ActivitypubMixin:
''' add this mixin for models that are AP serializable '''
activity_serializer = lambda: {}
reverse_unfurl = False
def __init__(self, *args, **kwargs):
''' collect some info on model fields '''
self.image_fields = []
self.many_to_many_fields = []
self.simple_fields = [] # "simple"
# sort model fields by type
for field in self._meta.get_fields():
if not hasattr(field, 'field_to_activity'):
continue
if isinstance(field, ImageField):
self.image_fields.append(field)
elif isinstance(field, ManyToManyField):
self.many_to_many_fields.append(field)
else:
self.simple_fields.append(field)
# a list of allll the serializable fields
self.activity_fields = self.image_fields + \
self.many_to_many_fields + self.simple_fields
# these are separate to avoid infinite recursion issues
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
if hasattr(self, 'deserialize_reverse_fields') else []
self.serialize_reverse_fields = self.serialize_reverse_fields \
if hasattr(self, 'serialize_reverse_fields') else []
super().__init__(*args, **kwargs)
@classmethod
def find_existing_by_remote_id(cls, remote_id):
''' look up a remote id in the db '''
return cls.find_existing({'id': remote_id})
@classmethod
def find_existing(cls, data):
''' compare data to fields that can be used for deduplation.
This always includes remote_id, but can also be unique identifiers
like an isbn for an edition '''
filters = []
# grabs all the data from the model to create django queryset filters
for field in cls._meta.get_fields():
if not hasattr(field, 'deduplication_field') or \
not field.deduplication_field:
continue
value = data.get(field.get_activitypub_field())
if not value:
continue
filters.append({field.name: value})
if hasattr(cls, 'origin_id') and 'id' in data:
# kinda janky, but this handles special case for books
filters.append({'origin_id': data['id']})
if not filters:
# if there are no deduplication fields, it will match the first
# item no matter what. this shouldn't happen but just in case.
return None
objects = cls.objects
if hasattr(objects, 'select_subclasses'):
objects = objects.select_subclasses()
# an OR operation on all the match fields, sorry for the dense syntax
match = objects.filter(
reduce(operator.or_, (Q(**f) for f in filters))
)
# there OUGHT to be only one match
return match.first()
def broadcast(self, activity, sender, software=None):
''' send out an activity '''
broadcast_task.delay(
sender.id,
json.dumps(activity, cls=activitypub.ActivityEncoder),
self.get_recipients(software=software)
)
def get_recipients(self, software=None):
''' figure out which inbox urls to post to '''
# first we have to figure out who should receive this activity
privacy = self.privacy if hasattr(self, 'privacy') else 'public'
# is this activity owned by a user (statuses, lists, shelves), or is it
# general to the instance (like books)
user = self.user if hasattr(self, 'user') else None
user_model = apps.get_model('bookwyrm.User', require_ready=True)
if not user and isinstance(self, user_model):
# or maybe the thing itself is a user
user = self
# find anyone who's tagged in a status, for example
mentions = self.recipients if hasattr(self, 'recipients') else []
# we always send activities to explicitly mentioned users' inboxes
recipients = [u.inbox for u in mentions or []]
# unless it's a dm, all the followers should receive the activity
if privacy != 'direct':
# we will send this out to a subset of all remote users
queryset = user_model.objects.filter(
local=False,
)
# filter users first by whether they're using the desired software
# this lets us send book updates only to other bw servers
if software:
queryset = queryset.filter(
bookwyrm_user=(software == 'bookwyrm')
)
# if there's a user, we only want to send to the user's followers
if user:
queryset = queryset.filter(following=user)
# ideally, we will send to shared inboxes for efficiency
shared_inboxes = queryset.filter(
shared_inbox__isnull=False
).values_list('shared_inbox', flat=True).distinct()
# but not everyone has a shared inbox
inboxes = queryset.filter(
shared_inbox__isnull=True
).values_list('inbox', flat=True)
recipients += list(shared_inboxes) + list(inboxes)
return recipients
def to_activity(self):
''' convert from a model to an activity '''
activity = generate_activity(self)
return self.activity_serializer(**activity).serialize()
class ObjectMixin(ActivitypubMixin):
''' add this mixin for object models that are AP serializable '''
def save(self, *args, created=None, **kwargs):
''' broadcast created/updated/deleted objects as appropriate '''
broadcast = kwargs.get('broadcast', True)
# this bonus kwarg woul cause an error in the base save method
if 'broadcast' in kwargs:
del kwargs['broadcast']
created = created or not bool(self.id)
# first off, we want to save normally no matter what
super().save(*args, **kwargs)
if not broadcast:
return
# this will work for objects owned by a user (lists, shelves)
user = self.user if hasattr(self, 'user') else None
if created:
# broadcast Create activities for objects owned by a local user
if not user or not user.local:
return
try:
software = None
# do we have a "pure" activitypub version of this for mastodon?
if hasattr(self, 'pure_content'):
pure_activity = self.to_create_activity(user, pure=True)
self.broadcast(pure_activity, user, software='other')
software = 'bookwyrm'
# sends to BW only if we just did a pure version for masto
activity = self.to_create_activity(user)
self.broadcast(activity, user, software=software)
except KeyError:
# janky as heck, this catches the mutliple inheritence chain
# for boosts and ignores this auxilliary broadcast
return
return
# --- updating an existing object
if not user:
# users don't have associated users, they ARE users
user_model = apps.get_model('bookwyrm.User', require_ready=True)
if isinstance(self, user_model):
user = self
# book data tracks last editor
elif hasattr(self, 'last_edited_by'):
user = self.last_edited_by
# again, if we don't know the user or they're remote, don't bother
if not user or not user.local:
return
# is this a deletion?
if hasattr(self, 'deleted') and self.deleted:
activity = self.to_delete_activity(user)
else:
activity = self.to_update_activity(user)
self.broadcast(activity, user)
def to_create_activity(self, user, **kwargs):
''' returns the object wrapped in a Create activity '''
activity_object = self.to_activity(**kwargs)
signature = None
create_id = self.remote_id + '/activity'
if 'content' in activity_object and activity_object['content']:
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
content = activity_object['content']
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
signature = activitypub.Signature(
creator='%s#main-key' % user.remote_id,
created=activity_object['published'],
signatureValue=b64encode(signed_message).decode('utf8')
)
return activitypub.Create(
id=create_id,
actor=user.remote_id,
to=activity_object['to'],
cc=activity_object['cc'],
object=activity_object,
signature=signature,
).serialize()
def to_delete_activity(self, user):
''' notice of deletion '''
return activitypub.Delete(
id=self.remote_id + '/activity',
actor=user.remote_id,
to=['%s/followers' % user.remote_id],
cc=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity(),
).serialize()
def to_update_activity(self, user):
''' wrapper for Updates to an activity '''
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
return activitypub.Update(
id=activity_id,
actor=user.remote_id,
to=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity()
).serialize()
class OrderedCollectionPageMixin(ObjectMixin):
''' just the paginator utilities, so you don't HAVE to
override ActivitypubMixin's to_activity (ie, for outbox) '''
@property
def collection_remote_id(self):
''' this can be overriden if there's a special remote id, ie outbox '''
return self.remote_id
def to_ordered_collection(self, queryset, \
remote_id=None, page=False, collection_only=False, **kwargs):
''' an ordered collection of whatevers '''
if not queryset.ordered:
raise RuntimeError('queryset must be ordered')
remote_id = remote_id or self.remote_id
if page:
return to_ordered_collection_page(
queryset, remote_id, **kwargs)
if collection_only or not hasattr(self, 'activity_serializer'):
serializer = activitypub.OrderedCollection
activity = {}
else:
serializer = self.activity_serializer
# a dict from the model fields
activity = generate_activity(self)
if remote_id:
activity['id'] = remote_id
paginated = Paginator(queryset, PAGE_LENGTH)
# add computed fields specific to orderd collections
activity['totalItems'] = paginated.count
activity['first'] = '%s?page=1' % remote_id
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
return serializer(**activity).serialize()
class OrderedCollectionMixin(OrderedCollectionPageMixin):
''' extends activitypub models to work as ordered collections '''
@property
def collection_queryset(self):
''' usually an ordered collection model aggregates a different model '''
raise NotImplementedError('Model must define collection_queryset')
activity_serializer = activitypub.OrderedCollection
def to_activity(self, **kwargs):
''' an ordered collection of the specified model queryset '''
return self.to_ordered_collection(self.collection_queryset, **kwargs)
class CollectionItemMixin(ActivitypubMixin):
''' for items that are part of an (Ordered)Collection '''
activity_serializer = activitypub.Add
object_field = collection_field = None
def save(self, *args, broadcast=True, **kwargs):
''' broadcast updated '''
created = not bool(self.id)
# first off, we want to save normally no matter what
super().save(*args, **kwargs)
# these shouldn't be edited, only created and deleted
if not broadcast or not created or not self.user.local:
return
# adding an obj to the collection
activity = self.to_add_activity()
self.broadcast(activity, self.user)
def delete(self, *args, **kwargs):
''' broadcast a remove activity '''
activity = self.to_remove_activity()
super().delete(*args, **kwargs)
self.broadcast(activity, self.user)
def to_add_activity(self):
''' AP for shelving a book'''
object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field)
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=self.user.remote_id,
object=object_field.to_activity(),
target=collection_field.remote_id
).serialize()
def to_remove_activity(self):
''' AP for un-shelving a book'''
object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field)
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=self.user.remote_id,
object=object_field.to_activity(),
target=collection_field.remote_id
).serialize()
class ActivityMixin(ActivitypubMixin):
''' add this mixin for models that are AP serializable '''
def save(self, *args, broadcast=True, **kwargs):
''' broadcast activity '''
super().save(*args, **kwargs)
user = self.user if hasattr(self, 'user') else self.user_subject
if broadcast and user.local:
self.broadcast(self.to_activity(), user)
def delete(self, *args, broadcast=True, **kwargs):
''' nevermind, undo that activity '''
user = self.user if hasattr(self, 'user') else self.user_subject
if broadcast and user.local:
self.broadcast(self.to_undo_activity(), user)
super().delete(*args, **kwargs)
def to_undo_activity(self):
''' undo an action '''
user = self.user if hasattr(self, 'user') else self.user_subject
return activitypub.Undo(
id='%s#undo' % self.remote_id,
actor=user.remote_id,
object=self.to_activity()
).serialize()
def generate_activity(obj):
''' go through the fields on an object '''
activity = {}
for field in obj.activity_fields:
field.set_activity_from_field(activity, obj)
if hasattr(obj, 'serialize_reverse_fields'):
# for example, editions of a work
for model_field_name, activity_field_name, sort_field in \
obj.serialize_reverse_fields:
related_field = getattr(obj, model_field_name)
activity[activity_field_name] = \
unfurl_related_field(related_field, sort_field)
if not activity.get('id'):
activity['id'] = obj.get_remote_id()
return activity
def unfurl_related_field(related_field, sort_field=None):
''' load reverse lookups (like public key owner or Status attachment '''
if hasattr(related_field, 'all'):
return [unfurl_related_field(i) for i in related_field.order_by(
sort_field).all()]
if related_field.reverse_unfurl:
return related_field.field_to_activity()
return related_field.remote_id
@app.task
def broadcast_task(sender_id, activity, recipients):
''' the celery task for broadcast '''
user_model = apps.get_model('bookwyrm.User', require_ready=True)
sender = user_model.objects.get(id=sender_id)
for recipient in recipients:
try:
sign_and_send(sender, activity, recipient)
except requests.exceptions.HTTPError as e:
logger.exception(e)
def sign_and_send(sender, data, destination):
''' crpyto whatever and http junk '''
now = http_date()
if not sender.key_pair.private_key:
# this shouldn't happen. it would be bad if it happened.
raise ValueError('No private key found for sender')
digest = make_digest(data)
response = requests.post(
destination,
data=data,
headers={
'Date': now,
'Digest': digest,
'Signature': make_signature(sender, destination, now, digest),
'Content-Type': 'application/activity+json; charset=utf-8',
'User-Agent': USER_AGENT,
},
)
if not response.ok:
response.raise_for_status()
return response
# pylint: disable=unused-argument
def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, **kwargs):
''' serialize and pagiante a queryset '''
paginated = Paginator(queryset, PAGE_LENGTH)
activity_page = paginated.page(page)
if id_only:
items = [s.remote_id for s in activity_page.object_list]
else:
items = [s.to_activity() for s in activity_page.object_list]
prev_page = next_page = None
if activity_page.has_next():
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
if activity_page.has_previous():
prev_page = '%s?page=%d' % \
(remote_id, activity_page.previous_page_number())
return activitypub.OrderedCollectionPage(
id='%s?page=%s' % (remote_id, page),
partOf=remote_id,
orderedItems=items,
next=next_page,
prev=prev_page
).serialize()

View file

@ -2,7 +2,7 @@
from django.db import models
from bookwyrm import activitypub
from .base_model import ActivitypubMixin
from .activitypub_mixin import ActivitypubMixin
from .base_model import BookWyrmModel
from . import fields

View file

@ -1,20 +1,9 @@
''' base model with default fields '''
from base64 import b64encode
from functools import reduce
import operator
from uuid import uuid4
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from django.core.paginator import Paginator
from django.db import models
from django.db.models import Q
from django.dispatch import receiver
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
from .fields import ImageField, ManyToManyField, RemoteIdField
from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField
class BookWyrmModel(models.Model):
@ -49,254 +38,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
return
if not instance.remote_id:
instance.remote_id = instance.get_remote_id()
instance.save()
def unfurl_related_field(related_field, sort_field=None):
''' load reverse lookups (like public key owner or Status attachment '''
if hasattr(related_field, 'all'):
return [unfurl_related_field(i) for i in related_field.order_by(
sort_field).all()]
if related_field.reverse_unfurl:
return related_field.field_to_activity()
return related_field.remote_id
class ActivitypubMixin:
''' add this mixin for models that are AP serializable '''
activity_serializer = lambda: {}
reverse_unfurl = False
def __init__(self, *args, **kwargs):
''' collect some info on model fields '''
self.image_fields = []
self.many_to_many_fields = []
self.simple_fields = [] # "simple"
for field in self._meta.get_fields():
if not hasattr(field, 'field_to_activity'):
continue
if isinstance(field, ImageField):
self.image_fields.append(field)
elif isinstance(field, ManyToManyField):
self.many_to_many_fields.append(field)
else:
self.simple_fields.append(field)
self.activity_fields = self.image_fields + \
self.many_to_many_fields + self.simple_fields
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
if hasattr(self, 'deserialize_reverse_fields') else []
self.serialize_reverse_fields = self.serialize_reverse_fields \
if hasattr(self, 'serialize_reverse_fields') else []
super().__init__(*args, **kwargs)
@classmethod
def find_existing_by_remote_id(cls, remote_id):
''' look up a remote id in the db '''
return cls.find_existing({'id': remote_id})
@classmethod
def find_existing(cls, data):
''' compare data to fields that can be used for deduplation.
This always includes remote_id, but can also be unique identifiers
like an isbn for an edition '''
filters = []
for field in cls._meta.get_fields():
if not hasattr(field, 'deduplication_field') or \
not field.deduplication_field:
continue
value = data.get(field.get_activitypub_field())
if not value:
continue
filters.append({field.name: value})
if hasattr(cls, 'origin_id') and 'id' in data:
# kinda janky, but this handles special case for books
filters.append({'origin_id': data['id']})
if not filters:
# if there are no deduplication fields, it will match the first
# item no matter what. this shouldn't happen but just in case.
return None
objects = cls.objects
if hasattr(objects, 'select_subclasses'):
objects = objects.select_subclasses()
# an OR operation on all the match fields
match = objects.filter(
reduce(
operator.or_, (Q(**f) for f in filters)
)
)
# there OUGHT to be only one match
return match.first()
def to_activity(self):
''' convert from a model to an activity '''
activity = generate_activity(self)
return self.activity_serializer(**activity).serialize()
def to_create_activity(self, user, **kwargs):
''' returns the object wrapped in a Create activity '''
activity_object = self.to_activity(**kwargs)
signature = None
create_id = self.remote_id + '/activity'
if 'content' in activity_object:
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
content = activity_object['content']
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
signature = activitypub.Signature(
creator='%s#main-key' % user.remote_id,
created=activity_object['published'],
signatureValue=b64encode(signed_message).decode('utf8')
)
return activitypub.Create(
id=create_id,
actor=user.remote_id,
to=activity_object['to'],
cc=activity_object['cc'],
object=activity_object,
signature=signature,
).serialize()
def to_delete_activity(self, user):
''' notice of deletion '''
return activitypub.Delete(
id=self.remote_id + '/activity',
actor=user.remote_id,
to=['%s/followers' % user.remote_id],
cc=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity(),
).serialize()
def to_update_activity(self, user):
''' wrapper for Updates to an activity '''
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
return activitypub.Update(
id=activity_id,
actor=user.remote_id,
to=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity()
).serialize()
def to_undo_activity(self, user):
''' undo an action '''
return activitypub.Undo(
id='%s#undo' % self.remote_id,
actor=user.remote_id,
object=self.to_activity()
).serialize()
class OrderedCollectionPageMixin(ActivitypubMixin):
''' just the paginator utilities, so you don't HAVE to
override ActivitypubMixin's to_activity (ie, for outbox '''
@property
def collection_remote_id(self):
''' this can be overriden if there's a special remote id, ie outbox '''
return self.remote_id
def to_ordered_collection(self, queryset, \
remote_id=None, page=False, collection_only=False, **kwargs):
''' an ordered collection of whatevers '''
if not queryset.ordered:
raise RuntimeError('queryset must be ordered')
remote_id = remote_id or self.remote_id
if page:
return to_ordered_collection_page(
queryset, remote_id, **kwargs)
if collection_only or not hasattr(self, 'activity_serializer'):
serializer = activitypub.OrderedCollection
activity = {}
else:
serializer = self.activity_serializer
# a dict from the model fields
activity = generate_activity(self)
if remote_id:
activity['id'] = remote_id
paginated = Paginator(queryset, PAGE_LENGTH)
# add computed fields specific to orderd collections
activity['totalItems'] = paginated.count
activity['first'] = '%s?page=1' % remote_id
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
return serializer(**activity).serialize()
# pylint: disable=unused-argument
def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, **kwargs):
''' serialize and pagiante a queryset '''
paginated = Paginator(queryset, PAGE_LENGTH)
activity_page = paginated.page(page)
if id_only:
items = [s.remote_id for s in activity_page.object_list]
else:
items = [s.to_activity() for s in activity_page.object_list]
prev_page = next_page = None
if activity_page.has_next():
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
if activity_page.has_previous():
prev_page = '%s?page=%d' % \
(remote_id, activity_page.previous_page_number())
return activitypub.OrderedCollectionPage(
id='%s?page=%s' % (remote_id, page),
partOf=remote_id,
orderedItems=items,
next=next_page,
prev=prev_page
).serialize()
class OrderedCollectionMixin(OrderedCollectionPageMixin):
''' extends activitypub models to work as ordered collections '''
@property
def collection_queryset(self):
''' usually an ordered collection model aggregates a different model '''
raise NotImplementedError('Model must define collection_queryset')
activity_serializer = activitypub.OrderedCollection
def to_activity(self, **kwargs):
''' an ordered collection of the specified model queryset '''
return self.to_ordered_collection(self.collection_queryset, **kwargs)
def generate_activity(obj):
''' go through the fields on an object '''
activity = {}
for field in obj.activity_fields:
field.set_activity_from_field(activity, obj)
if hasattr(obj, 'serialize_reverse_fields'):
# for example, editions of a work
for model_field_name, activity_field_name, sort_field in \
obj.serialize_reverse_fields:
related_field = getattr(obj, model_field_name)
activity[activity_field_name] = \
unfurl_related_field(related_field, sort_field)
if not activity.get('id'):
activity['id'] = obj.get_remote_id()
return activity
try:
instance.save(broadcast=False)
except TypeError:
instance.save()

View file

@ -7,11 +7,11 @@ from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from . import fields
class BookDataModel(ActivitypubMixin, BookWyrmModel):
class BookDataModel(ObjectMixin, BookWyrmModel):
''' fields shared between editable book data (books, works, authors) '''
origin_id = models.CharField(max_length=255, null=True, blank=True)
openlibrary_key = fields.CharField(
@ -74,6 +74,7 @@ class Book(BookDataModel):
@property
def latest_readthrough(self):
''' most recent readthrough activity '''
return self.readthrough_set.order_by('-updated_date').first()
@property

View file

@ -3,10 +3,11 @@ from django.db import models
from django.utils import timezone
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, BookWyrmModel
from .activitypub_mixin import ActivityMixin
from .base_model import BookWyrmModel
from . import fields
class Favorite(ActivitypubMixin, BookWyrmModel):
class Favorite(ActivityMixin, BookWyrmModel):
''' fav'ing a post '''
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor')
@ -18,7 +19,7 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
def save(self, *args, **kwargs):
''' update user active time '''
self.user.last_active_date = timezone.now()
self.user.save()
self.user.save(broadcast=False)
super().save(*args, **kwargs)
class Meta:

View file

@ -3,8 +3,8 @@ from django.db import models
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .base_model import ActivitypubMixin, BookWyrmModel
from .base_model import OrderedCollectionMixin
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
from . import fields
@ -51,13 +51,13 @@ class List(OrderedCollectionMixin, BookWyrmModel):
ordering = ('-updated_date',)
class ListItem(ActivitypubMixin, BookWyrmModel):
class ListItem(CollectionItemMixin, BookWyrmModel):
''' ok '''
book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='object')
book_list = fields.ForeignKey(
'List', on_delete=models.CASCADE, activitypub_field='target')
added_by = fields.ForeignKey(
user = fields.ForeignKey(
'User',
on_delete=models.PROTECT,
activitypub_field='actor'
@ -68,24 +68,8 @@ class ListItem(ActivitypubMixin, BookWyrmModel):
endorsement = models.ManyToManyField('User', related_name='endorsers')
activity_serializer = activitypub.AddBook
def to_add_activity(self, user):
''' AP for shelving a book'''
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.book_list.remote_id,
).serialize()
def to_remove_activity(self, user):
''' AP for un-shelving a book'''
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.book_list.remote_id
).serialize()
object_field = 'book'
collection_field = 'book_list'
class Meta:
''' an opinionated constraint! you can't put a book on a list twice '''

View file

@ -31,7 +31,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs):
''' update user active time '''
self.user.last_active_date = timezone.now()
self.user.save()
self.user.save(broadcast=False)
super().save(*args, **kwargs)
def create_update(self):
@ -54,5 +54,5 @@ class ProgressUpdate(BookWyrmModel):
def save(self, *args, **kwargs):
''' update user active time '''
self.user.last_active_date = timezone.now()
self.user.save()
self.user.save(broadcast=False)
super().save(*args, **kwargs)

View file

@ -1,14 +1,15 @@
''' defines relationships between users '''
from django.db import models
from django.db import models, transaction
from django.db.models import Q
from django.dispatch import receiver
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, BookWyrmModel
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .base_model import BookWyrmModel
from . import fields
class UserRelationship(ActivitypubMixin, BookWyrmModel):
class UserRelationship(BookWyrmModel):
''' many-to-many through table for followers '''
user_subject = fields.ForeignKey(
'User',
@ -23,6 +24,16 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
activitypub_field='object',
)
@property
def privacy(self):
''' all relationships are handled directly with the participants '''
return 'direct'
@property
def recipients(self):
''' the remote user needs to recieve direct broadcasts '''
return [u for u in [self.user_subject, self.user_object] if not u.local]
class Meta:
''' relationships should be unique '''
abstract = True
@ -37,8 +48,6 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
)
]
activity_serializer = activitypub.Follow
def get_remote_id(self, status=None):# pylint: disable=arguments-differ
''' use shelf identifier in remote_id '''
status = status or 'follows'
@ -46,55 +55,73 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
return '%s#%s/%d' % (base_path, status, self.id)
def to_accept_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Accept(
id=self.get_remote_id(status='accepts'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
def to_reject_activity(self):
''' generate a Reject for this follow request '''
return activitypub.Reject(
id=self.get_remote_id(status='rejects'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
class UserFollows(UserRelationship):
class UserFollows(ActivitypubMixin, UserRelationship):
''' Following a user '''
status = 'follows'
activity_serializer = activitypub.Follow
@classmethod
def from_request(cls, follow_request):
''' converts a follow request into a follow relationship '''
return cls(
return cls.objects.create(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
remote_id=follow_request.remote_id,
)
class UserFollowRequest(UserRelationship):
class UserFollowRequest(ActivitypubMixin, UserRelationship):
''' following a user requires manual or automatic confirmation '''
status = 'follow_request'
activity_serializer = activitypub.Follow
def save(self, *args, **kwargs):
''' make sure the follow relationship doesn't already exist '''
def save(self, *args, broadcast=True, **kwargs):
''' make sure the follow or block relationship doesn't already exist '''
try:
UserFollows.objects.get(
user_subject=self.user_subject,
user_object=self.user_object
)
UserBlocks.objects.get(
user_subject=self.user_subject,
user_object=self.user_object
)
return None
except UserFollows.DoesNotExist:
return super().save(*args, **kwargs)
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
super().save(*args, **kwargs)
if broadcast and self.user_subject.local and not self.user_object.local:
self.broadcast(self.to_activity(), self.user_subject)
class UserBlocks(UserRelationship):
def accept(self):
''' turn this request into the real deal'''
user = self.user_object
activity = activitypub.Accept(
id=self.get_remote_id(status='accepts'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
with transaction.atomic():
UserFollows.from_request(self)
self.delete()
self.broadcast(activity, user)
def reject(self):
''' generate a Reject for this follow request '''
user = self.user_object
activity = activitypub.Reject(
id=self.get_remote_id(status='rejects'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
self.delete()
self.broadcast(activity, user)
class UserBlocks(ActivityMixin, UserRelationship):
''' prevent another user from following you and seeing your posts '''
status = 'blocks'
activity_serializer = activitypub.Block

View file

@ -3,8 +3,8 @@ import re
from django.db import models
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, BookWyrmModel
from .base_model import OrderedCollectionMixin
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
from . import fields
@ -27,12 +27,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
def save(self, *args, **kwargs):
''' set the identifier '''
saved = super().save(*args, **kwargs)
super().save(*args, **kwargs)
if not self.identifier:
slug = re.sub(r'[^\w]', '', self.name).lower()
self.identifier = '%s-%d' % (slug, self.id)
return super().save(*args, **kwargs)
return saved
super().save(*args, **kwargs)
@property
def collection_queryset(self):
@ -49,39 +48,18 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
unique_together = ('user', 'identifier')
class ShelfBook(ActivitypubMixin, BookWyrmModel):
class ShelfBook(CollectionItemMixin, BookWyrmModel):
''' many to many join table for books and shelves '''
book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='object')
shelf = fields.ForeignKey(
'Shelf', on_delete=models.PROTECT, activitypub_field='target')
added_by = fields.ForeignKey(
'User',
blank=True,
null=True,
on_delete=models.PROTECT,
activitypub_field='actor'
)
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor')
activity_serializer = activitypub.AddBook
def to_add_activity(self, user):
''' AP for shelving a book'''
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.shelf.remote_id,
).serialize()
def to_remove_activity(self, user):
''' AP for un-shelving a book'''
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.shelf.to_activity()
).serialize()
object_field = 'book'
collection_field = 'shelf'
class Meta:

View file

@ -9,10 +9,12 @@ from django.utils import timezone
from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import OrderedCollectionPageMixin
from .base_model import BookWyrmModel
from . import fields
from .fields import image_serializer
from . import fields
class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc '''
@ -50,6 +52,15 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
serialize_reverse_fields = [('attachments', 'attachment', 'id')]
deserialize_reverse_fields = [('attachments', 'attachment')]
@property
def recipients(self):
''' tagged users who definitely need to get this status in broadcast '''
mentions = [u for u in self.mention_users.all() if not u.local]
if hasattr(self, 'reply_parent') and self.reply_parent \
and not self.reply_parent.user.local:
mentions.append(self.reply_parent.user)
return list(set(mentions))
@classmethod
def ignore_activity(cls, activity):
''' keep notes if they are replies to existing statuses '''
@ -126,14 +137,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return activity
def save(self, *args, **kwargs):
''' update user active time '''
if self.user.local:
self.user.last_active_date = timezone.now()
self.user.save()
return super().save(*args, **kwargs)
class GeneratedNote(Status):
''' these are app-generated messages about user activity '''
@property
@ -223,7 +226,7 @@ class Review(Status):
pure_type = 'Article'
class Boost(Status):
class Boost(ActivityMixin, Status):
''' boost'ing a post '''
boosted_status = fields.ForeignKey(
'Status',
@ -231,6 +234,8 @@ class Boost(Status):
related_name='boosters',
activitypub_field='object',
)
activity_serializer = activitypub.Boost
def __init__(self, *args, **kwargs):
''' the user field is "actor" here instead of "attributedTo" '''
@ -244,8 +249,6 @@ class Boost(Status):
self.image_fields = []
self.deserialize_reverse_fields = []
activity_serializer = activitypub.Boost
# This constraint can't work as it would cross tables.
# class Meta:
# unique_together = ('user', 'boosted_status')

View file

@ -5,7 +5,8 @@ from django.db import models
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .base_model import OrderedCollectionMixin, BookWyrmModel
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
from . import fields
@ -40,7 +41,7 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
super().save(*args, **kwargs)
class UserTag(BookWyrmModel):
class UserTag(CollectionItemMixin, BookWyrmModel):
''' an instance of a tag on a book by a user '''
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor')
@ -50,25 +51,8 @@ class UserTag(BookWyrmModel):
'Tag', on_delete=models.PROTECT, activitypub_field='target')
activity_serializer = activitypub.AddBook
def to_add_activity(self, user):
''' AP for shelving a book'''
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.remote_id,
).serialize()
def to_remove_activity(self, user):
''' AP for un-shelving a book'''
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.remote_id,
).serialize()
object_field = 'book'
collection_field = 'tag'
class Meta:
''' unqiueness constraint '''

View file

@ -17,8 +17,8 @@ from bookwyrm.settings import DOMAIN
from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app
from bookwyrm.utils import regex
from .base_model import OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, BookWyrmModel
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
from .base_model import BookWyrmModel
from .federated_server import FederatedServer
from . import fields, Review
@ -211,6 +211,9 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
def save(self, *args, **kwargs):
''' create a key pair '''
# no broadcasting happening here
if 'broadcast' in kwargs:
del kwargs['broadcast']
if not self.public_key:
self.private_key, self.public_key = create_key_pair()
return super().save(*args, **kwargs)
@ -291,7 +294,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
instance.key_pair = KeyPair.objects.create(
remote_id='%s/#main-key' % instance.remote_id)
instance.save()
instance.save(broadcast=False)
shelves = [{
'name': 'To Read',
@ -310,7 +313,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
identifier=shelf['identifier'],
user=instance,
editable=False
).save()
).save(broadcast=False)
@app.task

View file

@ -23,7 +23,7 @@
{% include 'snippets/book_titleby.html' with book=item.book %}
</td>
<td>
{% include 'snippets/username.html' with user=item.added_by %}
{% include 'snippets/username.html' with user=item.user %}
</td>
<td>
<div class="field has-addons">

View file

@ -31,9 +31,9 @@
</div>
<div class="card-footer has-background-white-bis">
<div class="card-footer-item">
<p>Added by {% include 'snippets/username.html' with user=item.added_by %}</p>
<p>Added by {% include 'snippets/username.html' with user=item.user %}</p>
</div>
{% if list.user == request.user or list.curation == 'open' and item.added_by == request.user %}
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">

View file

@ -23,7 +23,7 @@ class BaseActivity(TestCase):
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.user.remote_id = 'http://example.com/a/b'
self.user.save()
self.user.save(broadcast=False)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
@ -167,16 +167,19 @@ class BaseActivity(TestCase):
with self.assertRaises(ValueError):
self.user.avatar.file #pylint: disable=pointless-statement
activity.to_model(models.User, self.user)
# this would trigger a broadcast because it's a local user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
activity.to_model(models.User, self.user)
self.assertIsNotNone(self.user.avatar.name)
self.assertIsNotNone(self.user.avatar.file)
def test_to_model_many_to_many(self):
''' annoying that these all need special handling '''
status = models.Status.objects.create(
content='test status',
user=self.user,
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
content='test status',
user=self.user,
)
book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book')
update_data = activitypub.Note(
@ -208,10 +211,11 @@ class BaseActivity(TestCase):
def test_to_model_one_to_many(self):
''' these are reversed relationships, where the secondary object
keys the primary object but not vice versa '''
status = models.Status.objects.create(
content='test status',
user=self.user,
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
content='test status',
user=self.user,
)
update_data = activitypub.Note(
id=status.remote_id,
content=status.content,
@ -242,10 +246,11 @@ class BaseActivity(TestCase):
@responses.activate
def test_set_related_field(self):
''' celery task to add back-references to created objects '''
status = models.Status.objects.create(
content='test status',
user=self.user,
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
content='test status',
user=self.user,
)
data = {
'url': 'http://www.example.com/image.jpg',
'name': 'alt text',

View file

@ -0,0 +1,383 @@
''' testing model activitypub utilities '''
from unittest.mock import patch
from collections import namedtuple
from dataclasses import dataclass
import re
from django import db
from django.test import TestCase
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm import models
from bookwyrm.models import base_model
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin
class ActivitypubMixins(TestCase):
''' functionality shared across models '''
def setUp(self):
''' shared data '''
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'http://example.com/a/b'
self.local_user.save(broadcast=False)
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',
)
# ActivitypubMixin
def test_to_activity(self):
''' model to ActivityPub json '''
@dataclass(init=False)
class TestActivity(ActivityObject):
''' real simple mock '''
type: str = 'Test'
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
instance = TestModel()
instance.remote_id = 'https://www.example.com/test'
instance.activity_serializer = TestActivity
activity = instance.to_activity()
self.assertIsInstance(activity, dict)
self.assertEqual(activity['id'], 'https://www.example.com/test')
self.assertEqual(activity['type'], 'Test')
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
book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book')
self.assertEqual(book.origin_id, 'http://book.com/book')
self.assertNotEqual(book.remote_id, 'http://book.com/book')
# uses subclasses
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Comment.objects.create(
user=self.local_user, content='test status', book=book, \
remote_id='https://comment.net')
result = models.User.find_existing_by_remote_id('hi')
self.assertIsNone(result)
result = models.User.find_existing_by_remote_id(
'http://example.com/a/b')
self.assertEqual(result, self.local_user)
# test using origin id
result = models.Edition.find_existing_by_remote_id(
'http://book.com/book')
self.assertEqual(result, book)
# test subclass match
result = models.Status.find_existing_by_remote_id(
'https://comment.net')
def test_find_existing(self):
''' match a blob of data to a model '''
book = models.Edition.objects.create(
title='Test edition',
openlibrary_key='OL1234',
)
result = models.Edition.find_existing(
{'openlibraryKey': 'OL1234'})
self.assertEqual(result, book)
def test_get_recipients_public_object(self):
''' determines the recipients for an object's broadcast '''
MockSelf = namedtuple('Self', ('privacy'))
mock_self = MockSelf('public')
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox)
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)
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 0)
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)
self.local_user.followers.add(self.remote_user)
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox)
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)
self.local_user.followers.add(self.remote_user)
with patch('bookwyrm.models.user.set_remote_server.delay'):
another_remote_user = models.User.objects.create_user(
'nutria', 'nutria@nutria.com', 'nutriaword',
local=False,
remote_id='https://example.com/users/nutria',
inbox='https://example.com/users/nutria/inbox',
outbox='https://example.com/users/nutria/outbox',
)
MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients'))
mock_self = MockSelf('public', self.local_user, [another_remote_user])
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 2)
self.assertEqual(recipients[0], another_remote_user.inbox)
self.assertEqual(recipients[1], self.remote_user.inbox)
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)
self.local_user.followers.add(self.remote_user)
with patch('bookwyrm.models.user.set_remote_server.delay'):
another_remote_user = models.User.objects.create_user(
'nutria', 'nutria@nutria.com', 'nutriaword',
local=False,
remote_id='https://example.com/users/nutria',
inbox='https://example.com/users/nutria/inbox',
outbox='https://example.com/users/nutria/outbox',
)
MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients'))
mock_self = MockSelf('direct', self.local_user, [another_remote_user])
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], another_remote_user.inbox)
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)
with patch('bookwyrm.models.user.set_remote_server.delay'):
another_remote_user = models.User.objects.create_user(
'nutria', 'nutria@nutria.com', 'nutriaword',
local=False,
remote_id='https://example.com/users/nutria',
inbox='https://example.com/users/nutria/inbox',
shared_inbox='http://example.com/inbox',
outbox='https://example.com/users/nutria/outbox',
)
MockSelf = namedtuple('Self', ('privacy', 'user'))
mock_self = MockSelf('public', self.local_user)
self.local_user.followers.add(self.remote_user)
self.local_user.followers.add(another_remote_user)
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], 'http://example.com/inbox')
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(
'nutria', 'nutria@nutria.com', 'nutriaword',
local=False,
remote_id='https://example.com/users/nutria',
inbox='https://example.com/users/nutria/inbox',
outbox='https://example.com/users/nutria/outbox',
bookwyrm_user=False,
)
MockSelf = namedtuple('Self', ('privacy', 'user'))
mock_self = MockSelf('public', self.local_user)
self.local_user.followers.add(self.remote_user)
self.local_user.followers.add(another_remote_user)
recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 2)
recipients = ActivitypubMixin.get_recipients(
mock_self, software='bookwyrm')
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox)
recipients = ActivitypubMixin.get_recipients(
mock_self, software='other')
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], another_remote_user.inbox)
# ObjectMixin
def test_object_save_create(self):
''' should save uneventufully when broadcast is disabled '''
class Success(Exception):
''' this means we got to the right method '''
class ObjectModel(ObjectMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
def save(self, *args, **kwargs):
with patch('django.db.models.Model.save'):
super().save(*args, **kwargs)
def broadcast(self, activity, sender, **kwargs):#pylint: disable=arguments-differ
''' do something '''
raise Success()
def to_create_activity(self, user):#pylint: disable=arguments-differ
return {}
with self.assertRaises(Success):
ObjectModel(user=self.local_user).save()
ObjectModel(user=self.remote_user).save()
ObjectModel(user=self.local_user).save(broadcast=False)
ObjectModel(user=None).save()
def test_object_save_update(self):
''' should save uneventufully when broadcast is disabled '''
class Success(Exception):
''' this means we got to the right method '''
class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
last_edited_by = models.fields.ForeignKey(
'User', on_delete=db.models.CASCADE)
def save(self, *args, **kwargs):
with patch('django.db.models.Model.save'):
super().save(*args, **kwargs)
def to_update_activity(self, user):
raise Success()
with self.assertRaises(Success):
UpdateObjectModel(id=1, user=self.local_user).save()
with self.assertRaises(Success):
UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
def test_object_save_delete(self):
''' should create delete activities when objects are deleted by flag '''
class ActivitySuccess(Exception):
''' this means we got to the right method '''
class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
deleted = models.fields.BooleanField()
def save(self, *args, **kwargs):
with patch('django.db.models.Model.save'):
super().save(*args, **kwargs)
def to_delete_activity(self, user):
raise ActivitySuccess()
with self.assertRaises(ActivitySuccess):
DeletableObjectModel(
id=1, user=self.local_user, deleted=True).save()
def test_to_create_activity(self):
''' wrapper for ActivityPub "create" action '''
object_activity = {
'to': 'to field', 'cc': 'cc field',
'content': 'hi',
'published': '2020-12-04T17:52:22.623807+00:00',
}
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: object_activity
)
activity = ObjectMixin.to_create_activity(
mock_self, self.local_user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['type'], 'Create')
self.assertEqual(activity['to'], 'to field')
self.assertEqual(activity['cc'], 'cc field')
self.assertEqual(activity['object'], object_activity)
self.assertEqual(
activity['signature'].creator,
'%s#main-key' % self.local_user.remote_id
)
def test_to_delete_activity(self):
''' wrapper for Delete activity '''
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ObjectMixin.to_delete_activity(
mock_self, self.local_user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['type'], 'Delete')
self.assertEqual(
activity['to'],
['%s/followers' % self.local_user.remote_id])
self.assertEqual(
activity['cc'],
['https://www.w3.org/ns/activitystreams#Public'])
def test_to_update_activity(self):
''' ditto above but for Update '''
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ObjectMixin.to_update_activity(
mock_self, self.local_user)
self.assertIsNotNone(
re.match(
r'^https:\/\/example\.com\/status\/1#update\/.*',
activity['id']
)
)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['type'], 'Update')
self.assertEqual(
activity['to'],
['https://www.w3.org/ns/activitystreams#Public'])
self.assertEqual(activity['object'], {})
# Activity mixin
def test_to_undo_activity(self):
''' and again, for Undo '''
MockSelf = namedtuple('Self', ('remote_id', 'to_activity', 'user'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {},
self.local_user,
)
activity = ActivityMixin.to_undo_activity(mock_self)
self.assertEqual(
activity['id'],
'https://example.com/status/1#undo'
)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['type'], 'Undo')
self.assertEqual(activity['object'], {})

View file

@ -1,13 +1,8 @@
''' testing models '''
from collections import namedtuple
from dataclasses import dataclass
import re
from django.test import TestCase
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm import models
from bookwyrm.models import base_model
from bookwyrm.models.base_model import ActivitypubMixin
from bookwyrm.settings import DOMAIN
class BaseModel(TestCase):
@ -48,173 +43,3 @@ class BaseModel(TestCase):
instance.remote_id = None
base_model.execute_after_save(None, instance, False)
self.assertIsNone(instance.remote_id)
def test_to_create_activity(self):
''' wrapper for ActivityPub "create" action '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
object_activity = {
'to': 'to field', 'cc': 'cc field',
'content': 'hi',
'published': '2020-12-04T17:52:22.623807+00:00',
}
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: object_activity
)
activity = ActivitypubMixin.to_create_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Create')
self.assertEqual(activity['to'], 'to field')
self.assertEqual(activity['cc'], 'cc field')
self.assertEqual(activity['object'], object_activity)
self.assertEqual(
activity['signature'].creator,
'%s#main-key' % user.remote_id
)
def test_to_delete_activity(self):
''' wrapper for Delete activity '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_delete_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Delete')
self.assertEqual(
activity['to'],
['%s/followers' % user.remote_id])
self.assertEqual(
activity['cc'],
['https://www.w3.org/ns/activitystreams#Public'])
def test_to_update_activity(self):
''' ditto above but for Update '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_update_activity(mock_self, user)
self.assertIsNotNone(
re.match(
r'^https:\/\/example\.com\/status\/1#update\/.*',
activity['id']
)
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Update')
self.assertEqual(
activity['to'],
['https://www.w3.org/ns/activitystreams#Public'])
self.assertEqual(activity['object'], {})
def test_to_undo_activity(self):
''' and again, for Undo '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_undo_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1#undo'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Undo')
self.assertEqual(activity['object'], {})
def test_to_activity(self):
''' model to ActivityPub json '''
@dataclass(init=False)
class TestActivity(ActivityObject):
''' real simple mock '''
type: str = 'Test'
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
instance = TestModel()
instance.remote_id = 'https://www.example.com/test'
instance.activity_serializer = TestActivity
activity = instance.to_activity()
self.assertIsInstance(activity, dict)
self.assertEqual(activity['id'], 'https://www.example.com/test')
self.assertEqual(activity['type'], 'Test')
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
book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book')
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'http://example.com/a/b'
user.save()
self.assertEqual(book.origin_id, 'http://book.com/book')
self.assertNotEqual(book.remote_id, 'http://book.com/book')
# uses subclasses
models.Comment.objects.create(
user=user, content='test status', book=book, \
remote_id='https://comment.net')
result = models.User.find_existing_by_remote_id('hi')
self.assertIsNone(result)
result = models.User.find_existing_by_remote_id(
'http://example.com/a/b')
self.assertEqual(result, user)
# test using origin id
result = models.Edition.find_existing_by_remote_id(
'http://book.com/book')
self.assertEqual(result, book)
# test subclass match
result = models.Status.find_existing_by_remote_id(
'https://comment.net')
def test_find_existing(self):
''' match a blob of data to a model '''
book = models.Edition.objects.create(
title='Test edition',
openlibrary_key='OL1234',
)
result = models.Edition.find_existing(
{'openlibraryKey': 'OL1234'})
self.assertEqual(result, book)

View file

@ -19,7 +19,8 @@ from django.utils import timezone
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
#pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase):
@ -177,7 +178,8 @@ class ActivitypubFields(TestCase):
self.assertEqual(model_instance.privacy_field, 'unlisted')
def test_privacy_field_set_activity_from_field(self):
@patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast')
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',
@ -194,13 +196,15 @@ class ActivitypubFields(TestCase):
self.assertEqual(activity['to'], [public])
self.assertEqual(activity['cc'], [followers])
model_instance = Status.objects.create(user=user, privacy='unlisted')
model_instance = Status.objects.create(
user=user, content='hi', privacy='unlisted')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
self.assertEqual(activity['cc'], [public])
model_instance = Status.objects.create(user=user, privacy='followers')
model_instance = Status.objects.create(
user=user, content='hi', privacy='followers')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
@ -208,6 +212,7 @@ class ActivitypubFields(TestCase):
model_instance = Status.objects.create(
user=user,
content='hi',
privacy='direct',
)
model_instance.mention_users.set([user])
@ -289,12 +294,13 @@ class ActivitypubFields(TestCase):
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'https://example.com/user/mouse'
user.save()
user.save(broadcast=False)
User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
value = instance.field_from_activity(userdata)
with patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast'):
value = instance.field_from_activity(userdata)
self.assertEqual(value, user)
@ -393,7 +399,8 @@ class ActivitypubFields(TestCase):
@responses.activate
def test_image_field(self):
@patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast')
def test_image_field(self, _):
''' storing images '''
user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',

View file

@ -1,9 +1,11 @@
''' testing models '''
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, settings
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
class List(TestCase):
''' some activitypub oddness ahead '''
def setUp(self):
@ -11,17 +13,18 @@ class List(TestCase):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.list = models.List.objects.create(
name='Test List', user=self.user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.list = models.List.objects.create(
name='Test List', user=self.user)
def test_remote_id(self):
def test_remote_id(self, _):
''' shelves use custom remote ids '''
expected_id = 'https://%s/list/%d' % \
(settings.DOMAIN, self.list.id)
self.assertEqual(self.list.get_remote_id(), expected_id)
def test_to_activity(self):
def test_to_activity(self, _):
''' jsonify it '''
activity_json = self.list.to_activity()
self.assertIsInstance(activity_json, dict)
@ -31,24 +34,24 @@ class List(TestCase):
self.assertEqual(activity_json['name'], 'Test List')
self.assertEqual(activity_json['owner'], self.user.remote_id)
def test_list_item(self):
def test_list_item(self, _):
''' a list entry '''
work = models.Work.objects.create(title='hello')
book = models.Edition.objects.create(title='hi', parent_work=work)
item = models.ListItem.objects.create(
book_list=self.list,
book=book,
added_by=self.user,
user=self.user,
)
self.assertTrue(item.approved)
add_activity = item.to_add_activity(self.user)
add_activity = item.to_add_activity()
self.assertEqual(add_activity['actor'], self.user.remote_id)
self.assertEqual(add_activity['object']['id'], book.remote_id)
self.assertEqual(add_activity['target'], self.list.remote_id)
remove_activity = item.to_remove_activity(self.user)
remove_activity = item.to_remove_activity()
self.assertEqual(remove_activity['actor'], self.user.remote_id)
self.assertEqual(remove_activity['object']['id'], book.remote_id)
self.assertEqual(remove_activity['target'], self.list.remote_id)

View file

@ -6,7 +6,9 @@ from bookwyrm import models
class Relationship(TestCase):
''' following, blocking, stuff like that '''
def setUp(self):
''' we need some users for this '''
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
@ -19,68 +21,31 @@ class Relationship(TestCase):
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save()
self.local_user.save(broadcast=False)
def test_user_follows(self):
rel = models.UserFollows.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(
rel.remote_id,
'http://local.com/user/mouse#follows/%d' % rel.id
)
''' create a follow relationship '''
with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'):
rel = models.UserFollows.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
activity = rel.to_activity()
self.assertEqual(activity['id'], rel.remote_id)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object'], self.remote_user.remote_id)
def test_user_follow_accept_serialization(self):
rel = models.UserFollows.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(
rel.remote_id,
'http://local.com/user/mouse#follows/%d' % rel.id
)
accept = rel.to_accept_activity()
self.assertEqual(accept['type'], 'Accept')
self.assertEqual(
accept['id'],
'http://local.com/user/mouse#accepts/%d' % rel.id
)
self.assertEqual(accept['actor'], self.remote_user.remote_id)
self.assertEqual(accept['object']['id'], rel.remote_id)
self.assertEqual(accept['object']['actor'], self.local_user.remote_id)
self.assertEqual(accept['object']['object'], self.remote_user.remote_id)
def test_user_follow_reject_serialization(self):
rel = models.UserFollows.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(
rel.remote_id,
'http://local.com/user/mouse#follows/%d' % rel.id
)
reject = rel.to_reject_activity()
self.assertEqual(reject['type'], 'Reject')
self.assertEqual(
reject['id'],
'http://local.com/user/mouse#rejects/%d' % rel.id
)
self.assertEqual(reject['actor'], self.remote_user.remote_id)
self.assertEqual(reject['object']['id'], rel.remote_id)
self.assertEqual(reject['object']['actor'], self.local_user.remote_id)
self.assertEqual(reject['object']['object'], self.remote_user.remote_id)
def test_user_follows_from_request(self):
''' convert a follow request into a follow '''
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
''' introspect what's being sent out '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Follow')
models.UserFollowRequest.broadcast = mock_broadcast
request = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
@ -99,14 +64,18 @@ class Relationship(TestCase):
self.assertEqual(rel.status, 'follows')
self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user)
models.UserFollowRequest.broadcast = real_broadcast
def test_user_follows_from_request_custom_remote_id(self):
request = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user,
remote_id='http://antoher.server/sdkfhskdjf/23'
)
''' store a specific remote id for a relationship provided by remote '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
request = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user,
remote_id='http://antoher.server/sdkfhskdjf/23'
)
self.assertEqual(
request.remote_id,
'http://antoher.server/sdkfhskdjf/23'
@ -121,3 +90,67 @@ class Relationship(TestCase):
self.assertEqual(rel.status, 'follows')
self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user)
def test_follow_request_activity(self):
''' accept a request and make it a relationship '''
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object'], self.remote_user.remote_id)
self.assertEqual(activity['type'], 'Follow')
models.UserFollowRequest.broadcast = mock_broadcast
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user,
)
models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_accept(self):
''' accept a request and make it a relationship '''
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Accept')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(
activity['object']['id'], request.remote_id)
models.UserFollowRequest.broadcast = mock_broadcast
request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user,
user_object=self.local_user,
)
request.accept()
self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertTrue(models.UserFollows.objects.exists())
rel = models.UserFollows.objects.get()
self.assertEqual(rel.user_subject, self.remote_user)
self.assertEqual(rel.user_object, self.local_user)
models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_reject(self):
''' accept a request and make it a relationship '''
real_broadcast = models.UserFollowRequest.broadcast
def mock_reject(_, activity, user):
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Reject')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(
activity['object']['id'], request.remote_id)
models.UserFollowRequest.broadcast = mock_reject
request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user,
user_object=self.local_user,
)
request.reject()
self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertFalse(models.UserFollows.objects.exists())
models.UserFollowRequest.broadcast = real_broadcast

View file

@ -8,24 +8,113 @@ class Shelf(TestCase):
''' some activitypub oddness ahead '''
def setUp(self):
''' look, a shelf '''
self.user = models.User.objects.create_user(
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=self.user)
work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='test book',
parent_work=work)
def test_remote_id(self):
''' shelves use custom remote ids '''
real_broadcast = models.Shelf.broadcast
def broadcast_mock(_, activity, user, **kwargs):
''' nah '''
models.Shelf.broadcast = broadcast_mock
shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf',
user=self.local_user)
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
self.assertEqual(self.shelf.get_remote_id(), expected_id)
self.assertEqual(shelf.get_remote_id(), expected_id)
models.Shelf.broadcast = real_broadcast
def test_to_activity(self):
''' jsonify it '''
activity_json = self.shelf.to_activity()
real_broadcast = models.Shelf.broadcast
def empty_mock(_, activity, user, **kwargs):
''' nah '''
models.Shelf.broadcast = empty_mock
shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf',
user=self.local_user)
activity_json = shelf.to_activity()
self.assertIsInstance(activity_json, dict)
self.assertEqual(activity_json['id'], self.shelf.remote_id)
self.assertEqual(activity_json['id'], shelf.remote_id)
self.assertEqual(activity_json['totalItems'], 0)
self.assertEqual(activity_json['type'], 'Shelf')
self.assertEqual(activity_json['name'], 'Test Shelf')
self.assertEqual(activity_json['owner'], self.user.remote_id)
self.assertEqual(activity_json['owner'], self.local_user.remote_id)
models.Shelf.broadcast = real_broadcast
def test_create_update_shelf(self):
''' create and broadcast shelf creation '''
real_broadcast = models.Shelf.broadcast
def create_mock(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Create')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object']['name'], 'Test Shelf')
models.Shelf.broadcast = create_mock
shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=self.local_user)
def update_mock(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Update')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object']['name'], 'arthur russel')
models.Shelf.broadcast = update_mock
shelf.name = 'arthur russel'
shelf.save()
self.assertEqual(shelf.name, 'arthur russel')
models.Shelf.broadcast = real_broadcast
def test_shelve(self):
''' create and broadcast shelf creation '''
real_broadcast = models.Shelf.broadcast
real_shelfbook_broadcast = models.ShelfBook.broadcast
def add_mock(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object']['id'], self.book.remote_id)
self.assertEqual(activity['target'], shelf.remote_id)
def remove_mock(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Remove')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object']['id'], self.book.remote_id)
self.assertEqual(activity['target'], shelf.remote_id)
def empty_mock(_, activity, user, **kwargs):
''' nah '''
models.Shelf.broadcast = empty_mock
shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=self.local_user)
models.ShelfBook.broadcast = add_mock
shelf_book = models.ShelfBook.objects.create(
shelf=shelf,
user=self.local_user,
book=self.book)
self.assertEqual(shelf.books.first(), self.book)
models.ShelfBook.broadcast = remove_mock
shelf_book.delete()
self.assertFalse(shelf.books.exists())
models.ShelfBook.broadcast = real_shelfbook_broadcast
models.Shelf.broadcast = real_broadcast

View file

@ -1,4 +1,5 @@
''' testing models '''
from unittest.mock import patch
from io import BytesIO
import pathlib
@ -11,6 +12,7 @@ from django.utils import timezone
from bookwyrm import models, settings
@patch('bookwyrm.models.Status.broadcast')
class Status(TestCase):
''' lotta types of statuses '''
def setUp(self):
@ -24,13 +26,14 @@ class Status(TestCase):
'../../static/images/default_avi.jpg')
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
self.book.cover.save(
'test.jpg',
ContentFile(output.getvalue())
)
with patch('bookwyrm.models.Status.broadcast'):
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.user)
expected_id = 'https://%s/user/mouse/status/%d' % \
@ -38,7 +41,7 @@ class Status(TestCase):
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.user)
child = models.Status.objects.create(
@ -54,7 +57,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')
@ -62,14 +65,14 @@ class Status(TestCase):
self.assertEqual(models.Comment().status_type, 'Comment')
self.assertEqual(models.Boost().status_type, 'Boost')
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.user)
child = models.Status.objects.create(
@ -83,7 +86,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.user)
@ -93,7 +96,7 @@ 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.user,
@ -103,7 +106,7 @@ class Status(TestCase):
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.user)
@ -114,7 +117,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.user)
@ -127,7 +130,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.user)
@ -149,7 +152,7 @@ class Status(TestCase):
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
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.user, book=self.book)
@ -159,7 +162,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.user, book=self.book)
@ -176,7 +179,7 @@ class Status(TestCase):
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
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', content='test content',
@ -188,7 +191,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', content='test content',
@ -206,7 +209,7 @@ class Status(TestCase):
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
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', content='test content', rating=3,
@ -219,7 +222,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', content='test content', rating=3,
@ -237,8 +240,15 @@ class Status(TestCase):
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
def test_favorite(self):
def test_favorite(self, _):
''' fav a status '''
real_broadcast = models.Favorite.broadcast
def fav_broadcast_mock(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.user.remote_id)
self.assertEqual(activity['type'], 'Like')
models.Favorite.broadcast = fav_broadcast_mock
status = models.Status.objects.create(
content='test content', user=self.user)
fav = models.Favorite.objects.create(status=status, user=self.user)
@ -251,8 +261,9 @@ class Status(TestCase):
self.assertEqual(activity['type'], 'Like')
self.assertEqual(activity['actor'], self.user.remote_id)
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.user)
@ -264,7 +275,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.user, notification_type='FAVORITE')

View file

@ -1,80 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, broadcast
class Book(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
local_follower = models.User.objects.create_user(
'joe', 'joe@mouse.mouse', 'jeoword',
local=True, localname='joe')
self.user.followers.add(local_follower)
with patch('bookwyrm.models.user.set_remote_server.delay'):
follower = models.User.objects.create_user(
'rat', 'rat@mouse.mouse', 'ratword', local=False,
remote_id='http://example.com/u/1',
outbox='http://example.com/u/1/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/1/inbox')
self.user.followers.add(follower)
no_inbox_follower = models.User.objects.create_user(
'hamster', 'hamster@mouse.mouse', 'hamword',
shared_inbox=None, local=False,
remote_id='http://example.com/u/2',
outbox='http://example.com/u/2/o',
inbox='http://example.com/u/2/inbox')
self.user.followers.add(no_inbox_follower)
non_bw_follower = models.User.objects.create_user(
'gerbil', 'gerb@mouse.mouse', 'gerbword',
remote_id='http://example.com/u/3',
outbox='http://example2.com/u/3/o',
inbox='http://example2.com/u/3/inbox',
shared_inbox='http://example2.com/inbox',
bookwyrm_user=False, local=False)
self.user.followers.add(non_bw_follower)
models.User.objects.create_user(
'nutria', 'nutria@mouse.mouse', 'nuword',
remote_id='http://example.com/u/4',
outbox='http://example.com/u/4/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/4/inbox',
local=False)
def test_get_public_recipients(self):
expected = [
'http://example2.com/inbox',
'http://example.com/inbox',
'http://example.com/u/2/inbox',
]
recipients = broadcast.get_public_recipients(self.user)
self.assertEqual(recipients, expected)
def test_get_public_recipients_software(self):
expected = [
'http://example.com/inbox',
'http://example.com/u/2/inbox',
]
recipients = broadcast.get_public_recipients(self.user, software='bookwyrm')
self.assertEqual(recipients, expected)
def test_get_public_recipients_software_other(self):
expected = [
'http://example2.com/inbox',
]
recipients = broadcast.get_public_recipients(self.user, software='mastodon')
self.assertEqual(recipients, expected)

View file

@ -124,7 +124,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
@ -144,9 +144,10 @@ class GoodreadsImport(TestCase):
def test_handle_imported_book_already_shelved(self):
''' goodreads import added a book, this adds related connections '''
shelf = self.user.shelf_set.filter(identifier='to-read').first()
models.ShelfBook.objects.create(
shelf=shelf, added_by=self.user, book=self.book)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
shelf = self.user.shelf_set.filter(identifier='to-read').first()
models.ShelfBook.objects.create(
shelf=shelf, user=self.user, book=self.book)
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
@ -156,7 +157,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
@ -185,7 +186,7 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
goodreads_import.handle_imported_book(
@ -214,7 +215,7 @@ class GoodreadsImport(TestCase):
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, True, 'unlisted')
review = models.Review.objects.get(book=self.book, user=self.user)
@ -235,7 +236,7 @@ class GoodreadsImport(TestCase):
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'unlisted')
self.assertFalse(models.Review.objects.filter(

View file

@ -22,7 +22,7 @@ class Incoming(TestCase):
'mouse@example.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save()
self.local_user.save(broadcast=False)
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
@ -31,11 +31,12 @@ class Incoming(TestCase):
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.status = models.Status.objects.create(
user=self.local_user,
content='Test status',
remote_id='https://example.com/status/1',
)
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',
)
self.factory = RequestFactory()
@ -117,7 +118,7 @@ class Incoming(TestCase):
"object": "https://example.com/user/mouse"
}
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
incoming.handle_follow(activity)
# notification created
@ -145,9 +146,9 @@ class Incoming(TestCase):
}
self.local_user.manually_approves_followers = True
self.local_user.save()
self.local_user.save(broadcast=False)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
incoming.handle_follow(activity)
# notification created
@ -177,8 +178,9 @@ class Incoming(TestCase):
"object": "https://example.com/user/mouse"
}
}
models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user)
self.assertEqual(self.remote_user, self.local_user.followers.first())
incoming.handle_unfollow(activity)
@ -200,10 +202,11 @@ class Incoming(TestCase):
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=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
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_accept(activity)
@ -232,10 +235,11 @@ class Incoming(TestCase):
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=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
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_reject(activity)
@ -280,9 +284,10 @@ class Incoming(TestCase):
def test_handle_update_list(self):
''' a new list '''
book_list = models.List.objects.create(
name='hi', remote_id='https://example.com/list/22',
user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
book_list = models.List.objects.create(
name='hi', remote_id='https://example.com/list/22',
user=self.local_user)
activity = {
'object': {
"id": "https://example.com/list/22",
@ -387,11 +392,14 @@ class Incoming(TestCase):
def test_handle_delete_status(self):
''' remove a status '''
self.status.user = self.remote_user
self.status.save(broadcast=False)
self.assertFalse(self.status.deleted)
activity = {
'type': 'Delete',
'id': '%s/activity' % self.status.remote_id,
'actor': self.local_user.remote_id,
'actor': self.remote_user.remote_id,
'object': {'id': self.status.remote_id},
}
incoming.handle_delete_status(activity)
@ -403,6 +411,8 @@ class Incoming(TestCase):
def test_handle_delete_status_notifications(self):
''' remove a status with related notifications '''
self.status.user = self.remote_user
self.status.save(broadcast=False)
models.Notification.objects.create(
related_status=self.status,
user=self.local_user,
@ -418,7 +428,7 @@ class Incoming(TestCase):
activity = {
'type': 'Delete',
'id': '%s/activity' % self.status.remote_id,
'actor': self.local_user.remote_id,
'actor': self.remote_user.remote_id,
'object': {'id': self.status.remote_id},
}
incoming.handle_delete_status(activity)
@ -510,7 +520,6 @@ class Incoming(TestCase):
self.assertEqual(models.Boost.objects.count(), 0)
def test_handle_unboost(self):
''' undo a boost '''
activity = {
@ -539,7 +548,7 @@ class Incoming(TestCase):
activity = {
"id": "https://bookwyrm.social/shelfbook/6189#add",
"type": "Add",
"actor": "hhttps://example.com/users/rat",
"actor": "https://example.com/users/rat",
"object": "https://bookwyrm.social/book/37292",
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams"
@ -608,9 +617,10 @@ class Incoming(TestCase):
def test_handle_blocks(self):
''' create a "block" database entry from an activity '''
self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=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)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())

View file

@ -35,8 +35,9 @@ class TemplateTags(TestCase):
def test_get_user_rating(self):
''' get a user's most recent rating of a book '''
models.Review.objects.create(
user=self.user, book=self.book, rating=3)
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)
@ -76,14 +77,16 @@ class TemplateTags(TestCase):
def test_get_replies(self):
''' direct replies to a status '''
parent = models.Review.objects.create(
user=self.user, book=self.book)
first_child = models.Status.objects.create(
reply_parent=parent, user=self.user)
second_child = models.Status.objects.create(
reply_parent=parent, user=self.user)
third_child = models.Status.objects.create(
reply_parent=parent, user=self.user, deleted=True)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
parent = models.Review.objects.create(
user=self.user, book=self.book, content='hi')
first_child = models.Status.objects.create(
reply_parent=parent, user=self.user, content='hi')
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())
replies = bookwyrm_tags.get_replies(parent)
self.assertEqual(len(replies), 2)
@ -94,10 +97,11 @@ class TemplateTags(TestCase):
def test_get_parent(self):
''' get the reply parent of a status '''
parent = models.Review.objects.create(
user=self.user, book=self.book)
child = models.Status.objects.create(
reply_parent=parent, user=self.user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
parent = models.Review.objects.create(
user=self.user, book=self.book, content='hi')
child = models.Status.objects.create(
reply_parent=parent, user=self.user, content='hi')
result = bookwyrm_tags.get_parent(child)
self.assertEqual(result, parent)
@ -110,10 +114,11 @@ class TemplateTags(TestCase):
user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
models.Favorite.objects.create(
user=self.user,
status=status
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Favorite.objects.create(
user=self.user,
status=status
)
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
@ -123,10 +128,11 @@ class TemplateTags(TestCase):
user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
models.Boost.objects.create(
user=self.user,
boosted_status=status
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Boost.objects.create(
user=self.user,
boosted_status=status
)
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
@ -135,9 +141,10 @@ class TemplateTags(TestCase):
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
models.UserFollowRequest.objects.create(
user_subject=self.user,
user_object=self.remote_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.UserFollowRequest.objects.create(
user_subject=self.user,
user_object=self.remote_user)
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
@ -147,12 +154,13 @@ class TemplateTags(TestCase):
def test_get_boosted(self):
''' load a boosted status '''
status = models.Review.objects.create(
user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(
user=self.user,
boosted_status=status
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Review.objects.create(
user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(
user=self.user,
boosted_status=status
)
boosted = bookwyrm_tags.get_boosted(boost)
self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status)
@ -233,29 +241,31 @@ class TemplateTags(TestCase):
def test_get_status_preview_name(self):
''' status context string '''
status = models.Status.objects.create(content='hi', user=self.user)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'status')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(content='hi', user=self.user)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'status')
status = models.Review.objects.create(
content='hi', user=self.user, book=self.book)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'review of <em>Test Book</em>')
status = models.Review.objects.create(
content='hi', user=self.user, book=self.book)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'review of <em>Test Book</em>')
status = models.Comment.objects.create(
content='hi', user=self.user, book=self.book)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'comment on <em>Test Book</em>')
status = models.Comment.objects.create(
content='hi', user=self.user, book=self.book)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'comment on <em>Test Book</em>')
status = models.Quotation.objects.create(
content='hi', user=self.user, book=self.book)
status = models.Quotation.objects.create(
content='hi', user=self.user, book=self.book)
result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'quotation from <em>Test Book</em>')
def test_related_status(self):
''' gets the subclass model for a notification status '''
status = models.Status.objects.create(content='hi', user=self.user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(content='hi', user=self.user)
notification = models.Notification.objects.create(
user=self.user, notification_type='MENTION',
related_status=status)

View file

@ -84,7 +84,7 @@ class AuthorViews(TestCase):
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, author.id)
author.refresh_from_db()
self.assertEqual(author.name, 'New Name')

View file

@ -40,15 +40,16 @@ class BlockViews(TestCase):
''' create a "block" database entry from an activity '''
view = views.Block.as_view()
self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=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)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
request = self.factory.post('')
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, self.remote_user.id)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.local_user)
@ -63,7 +64,7 @@ class BlockViews(TestCase):
request = self.factory.post('')
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.block.unblock(request, self.remote_user.id)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -77,7 +77,7 @@ class BookViews(TestCase):
form.data['last_edited_by'] = self.local_user.id
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, self.book.id)
self.book.refresh_from_db()
self.assertEqual(self.book.title, 'New Title')
@ -90,9 +90,14 @@ class BookViews(TestCase):
title='first ed', parent_work=work)
edition2 = models.Edition.objects.create(
title='second ed', parent_work=work)
shelf = models.Shelf.objects.create(
name='Test Shelf', user=self.local_user)
shelf.books.add(edition1)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
shelf = models.Shelf.objects.create(
name='Test Shelf', user=self.local_user)
models.ShelfBook.objects.create(
book=edition1,
user=self.local_user,
shelf=shelf,
)
models.ReadThrough.objects.create(
user=self.local_user, book=edition1)
@ -102,7 +107,7 @@ class BookViews(TestCase):
'edition': edition2.id
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2)

View file

@ -18,6 +18,7 @@ class FeedMessageViews(TestCase):
'mouse@local.com', 'mouse@mouse.mouse', 'password',
local=True, localname='mouse')
self.book = models.Edition.objects.create(
parent_work=models.Work.objects.create(title='hi'),
title='Example Edition',
remote_id='https://example.com/book/1',
)
@ -38,8 +39,9 @@ class FeedMessageViews(TestCase):
def test_status_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Status.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.feed.is_api_request') as is_api:
@ -59,8 +61,9 @@ class FeedMessageViews(TestCase):
def test_replies_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Replies.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.feed.is_api_request') as is_api:
@ -90,11 +93,12 @@ class FeedMessageViews(TestCase):
def test_get_suggested_book(self):
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
models.ShelfBook.objects.create(
book=self.book,
added_by=self.local_user,
shelf=self.local_user.shelf_set.get(identifier='reading')
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=self.local_user.shelf_set.get(identifier='reading')
)
suggestions = views.feed.get_suggested_books(self.local_user)
self.assertEqual(suggestions[0]['name'], 'Currently Reading')
self.assertEqual(suggestions[0]['books'][0], self.book)

View file

@ -46,7 +46,7 @@ class BookViews(TestCase):
request.user = self.local_user
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.follow(request)
rel = models.UserFollowRequest.objects.get()
@ -62,7 +62,7 @@ class BookViews(TestCase):
request.user = self.local_user
self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.unfollow(request)
self.assertEqual(self.remote_user.followers.count(), 0)
@ -77,7 +77,7 @@ class BookViews(TestCase):
user_object=self.local_user
)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.accept_follow_request(request)
# request should be deleted
self.assertEqual(
@ -96,7 +96,7 @@ class BookViews(TestCase):
user_object=self.local_user
)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.delete_follow_request(request)
# request should be deleted
self.assertEqual(

View file

@ -100,7 +100,7 @@ class GoalViews(TestCase):
'post-status': True
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, self.local_user.localname, 2020)
goal = models.AnnualGoal.objects.get()

View file

@ -38,11 +38,12 @@ class ViewsHelpers(TestCase):
)
self.userdata = json.loads(datafile.read_bytes())
del self.userdata['icon']
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.local_user
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.local_user
)
def test_get_edition(self):
@ -83,22 +84,23 @@ class ViewsHelpers(TestCase):
rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True)
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')
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])
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,
@ -159,14 +161,15 @@ class ViewsHelpers(TestCase):
rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True)
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)
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, ['public'])
self.assertEqual(len(statuses), 2)
statuses = views.helpers.get_activity_feed(
self.local_user, ['public'])
self.assertEqual(len(statuses), 2)
# block relationship
rat.blocks.add(self.local_user)
@ -240,7 +243,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_to_read(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='to-read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
@ -251,7 +254,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_reading(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='reading')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
@ -262,7 +265,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_read(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
@ -272,7 +275,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_other(self):
''' posts shelve activities '''
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.helpers.handle_reading_status(
self.local_user, self.shelf, self.book, 'public')
self.assertFalse(models.GeneratedNote.objects.exists())

View file

@ -38,10 +38,10 @@ class InteractionViews(TestCase):
view = views.Favorite.as_view()
request = self.factory.post('')
request.user = self.remote_user
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id)
fav = models.Favorite.objects.get()
self.assertEqual(fav.status, status)
@ -58,15 +58,15 @@ class InteractionViews(TestCase):
view = views.Unfavorite.as_view()
request = self.factory.post('')
request.user = self.remote_user
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi')
views.Favorite.as_view()(request, status.id)
self.assertEqual(models.Favorite.objects.count(), 1)
self.assertEqual(models.Notification.objects.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, status.id)
self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)
@ -77,10 +77,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view()
request = self.factory.post('')
request.user = self.remote_user
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id)
boost = models.Boost.objects.get()
@ -99,10 +99,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view()
request = self.factory.post('')
request.user = self.local_user
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='unlisted')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='unlisted')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id)
boost = models.Boost.objects.get()
@ -113,10 +113,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view()
request = self.factory.post('')
request.user = self.local_user
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='followers')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='followers')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id)
self.assertFalse(models.Boost.objects.exists())
@ -125,10 +125,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view()
request = self.factory.post('')
request.user = self.local_user
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id)
view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1)
@ -139,14 +139,14 @@ class InteractionViews(TestCase):
view = views.Unboost.as_view()
request = self.factory.post('')
request.user = self.remote_user
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
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.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)

View file

@ -10,7 +10,6 @@ from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
@patch('bookwyrm.broadcast.broadcast_task.delay')
class ListViews(TestCase):
''' tag views'''
def setUp(self):
@ -32,19 +31,21 @@ class ListViews(TestCase):
remote_id='https://example.com/book/1',
parent_work=work,
)
self.list = models.List.objects.create(
name='Test List', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.list = models.List.objects.create(
name='Test List', user=self.local_user)
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
models.SiteSettings.objects.create()
def test_lists_page(self, _):
def test_lists_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Lists.as_view()
models.List.objects.create(name='Public list', user=self.local_user)
models.List.objects.create(
name='Private list', privacy='private', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.List.objects.create(name='Public list', user=self.local_user)
models.List.objects.create(
name='Private list', privacy='private', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
@ -61,8 +62,16 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_lists_create(self, _):
def test_lists_create(self):
''' create list view '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Create')
self.assertEqual(activity['actor'], self.local_user.remote_id)
models.List.broadcast = mock_broadcast
view = views.Lists.as_view()
request = self.factory.post('', {
'name': 'A list',
@ -78,9 +87,10 @@ class ListViews(TestCase):
self.assertEqual(new_list.description, 'wow')
self.assertEqual(new_list.privacy, 'unlisted')
self.assertEqual(new_list.curation, 'open')
models.List.broadcast = real_broadcast
def test_list_page(self, _):
def test_list_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.List.as_view()
request = self.factory.get('')
@ -116,8 +126,17 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_list_edit(self, _):
def test_list_edit(self):
''' edit a list '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Update')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['object']['id'], self.list.remote_id)
models.List.broadcast = mock_broadcast
view = views.List.as_view()
request = self.factory.post('', {
'name': 'New Name',
@ -136,14 +155,16 @@ class ListViews(TestCase):
self.assertEqual(self.list.description, 'wow')
self.assertEqual(self.list.privacy, 'direct')
self.assertEqual(self.list.curation, 'curated')
models.List.broadcast = real_broadcast
def test_curate_page(self, _):
def test_curate_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Curate.as_view()
models.List.objects.create(name='Public list', user=self.local_user)
models.List.objects.create(
name='Private list', privacy='private', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.List.objects.create(name='Public list', user=self.local_user)
models.List.objects.create(
name='Private list', privacy='private', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
@ -157,15 +178,25 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 302)
def test_curate_approve(self, _):
def test_curate_approve(self):
''' approve a pending item '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user, **kwargs):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
models.ListItem.broadcast = mock_broadcast
view = views.Curate.as_view()
pending = models.ListItem.objects.create(
book_list=self.list,
added_by=self.local_user,
book=self.book,
approved=False
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
pending = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=False
)
request = self.factory.post('', {
'item': pending.id,
@ -178,17 +209,19 @@ class ListViews(TestCase):
self.assertEqual(self.list.books.count(), 1)
self.assertEqual(self.list.listitem_set.first(), pending)
self.assertTrue(pending.approved)
models.ListItem.broadcast = real_broadcast
def test_curate_reject(self, _):
def test_curate_reject(self):
''' approve a pending item '''
view = views.Curate.as_view()
pending = models.ListItem.objects.create(
book_list=self.list,
added_by=self.local_user,
book=self.book,
approved=False
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
pending = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=False
)
request = self.factory.post('', {
'item': pending.id,
@ -196,13 +229,22 @@ class ListViews(TestCase):
})
request.user = self.local_user
view(request, self.list.id)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, self.list.id)
self.assertFalse(self.list.books.exists())
self.assertFalse(models.ListItem.objects.exists())
def test_add_book(self, _):
def test_add_book(self):
''' put a book on a list '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
models.ListItem.broadcast = mock_broadcast
request = self.factory.post('', {
'book': self.book.id,
})
@ -211,14 +253,23 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.local_user)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
models.ListItem.broadcast = real_broadcast
def test_add_book_outsider(self, _):
def test_add_book_outsider(self):
''' put a book on a list '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.rat.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.rat.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
models.ListItem.broadcast = mock_broadcast
self.list.curation = 'open'
self.list.save()
self.list.save(broadcast=False)
request = self.factory.post('', {
'book': self.book.id,
})
@ -227,14 +278,24 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.rat)
self.assertEqual(item.user, self.rat)
self.assertTrue(item.approved)
models.ListItem.broadcast = real_broadcast
def test_add_book_pending(self, _):
''' put a book on a list '''
def test_add_book_pending(self):
''' put a book on a list awaiting approval '''
real_broadcast = models.List.broadcast
def mock_broadcast(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.rat.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.rat.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
self.assertEqual(activity['object']['id'], self.book.remote_id)
models.ListItem.broadcast = mock_broadcast
self.list.curation = 'curated'
self.list.save()
self.list.save(broadcast=False)
request = self.factory.post('', {
'book': self.book.id,
})
@ -243,14 +304,24 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.rat)
self.assertEqual(item.user, self.rat)
self.assertFalse(item.approved)
models.ListItem.broadcast = real_broadcast
def test_add_book_self_curated(self, _):
''' put a book on a list '''
def test_add_book_self_curated(self):
''' put a book on a list automatically approved '''
real_broadcast = models.ListItem.broadcast
def mock_broadcast(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Add')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
models.ListItem.broadcast = mock_broadcast
self.list.curation = 'curated'
self.list.save()
self.list.save(broadcast=False)
request = self.factory.post('', {
'book': self.book.id,
})
@ -259,18 +330,30 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.local_user)
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
models.ListItem.broadcast = real_broadcast
def test_remove_book(self, _):
def test_remove_book(self):
''' take an item off a list '''
item = models.ListItem.objects.create(
book_list=self.list,
added_by=self.local_user,
book=self.book,
)
real_broadcast = models.ListItem.broadcast
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
item = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
)
self.assertTrue(self.list.listitem_set.exists())
def mock_broadcast(_, activity, user):
''' ok '''
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity['type'], 'Remove')
self.assertEqual(activity['actor'], self.local_user.remote_id)
self.assertEqual(activity['target'], self.list.remote_id)
models.ListItem.broadcast = mock_broadcast
request = self.factory.post('', {
'item': item.id,
})
@ -279,15 +362,17 @@ class ListViews(TestCase):
views.list.remove_book(request, self.list.id)
self.assertFalse(self.list.listitem_set.exists())
models.ListItem.broadcast = real_broadcast
def test_remove_book_unauthorized(self, _):
def test_remove_book_unauthorized(self):
''' take an item off a list '''
item = models.ListItem.objects.create(
book_list=self.list,
added_by=self.local_user,
book=self.book,
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
item = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post('', {
'item': item.id,

View file

@ -1,4 +1,5 @@
''' sending out activities '''
from unittest.mock import patch
import json
from django.http import JsonResponse
@ -49,14 +50,16 @@ class OutboxView(TestCase):
def test_outbox_privacy(self):
''' don't show dms et cetera in outbox '''
models.Status.objects.create(
content='PRIVATE!!', user=self.local_user, privacy='direct')
models.Status.objects.create(
content='bffs ONLY', user=self.local_user, privacy='followers')
models.Status.objects.create(
content='unlisted status', user=self.local_user, privacy='unlisted')
models.Status.objects.create(
content='look at this', user=self.local_user, privacy='public')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Status.objects.create(
content='PRIVATE!!', user=self.local_user, privacy='direct')
models.Status.objects.create(
content='bffs ONLY', user=self.local_user, privacy='followers')
models.Status.objects.create(
content='unlisted status', user=self.local_user,
privacy='unlisted')
models.Status.objects.create(
content='look at this', user=self.local_user, privacy='public')
request = self.factory.get('')
result = views.Outbox.as_view()(request, 'mouse')
@ -67,11 +70,12 @@ class OutboxView(TestCase):
def test_outbox_filter(self):
''' if we only care about reviews, only get reviews '''
models.Review.objects.create(
content='look at this', name='hi', rating=1,
book=self.book, user=self.local_user)
models.Status.objects.create(
content='look at this', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Review.objects.create(
content='look at this', name='hi', rating=1,
book=self.book, user=self.local_user)
models.Status.objects.create(
content='look at this', user=self.local_user)
request = self.factory.get('', {'type': 'bleh'})
result = views.Outbox.as_view()(request, 'mouse')

View file

@ -45,7 +45,7 @@ class ReadingViews(TestCase):
'start_date': '2020-01-05',
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.start_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book)
@ -65,8 +65,9 @@ class ReadingViews(TestCase):
def test_start_reading_reshelf(self):
''' begin a book '''
to_read_shelf = self.local_user.shelf_set.get(identifier='to-read')
models.ShelfBook.objects.create(
shelf=to_read_shelf, book=self.book, added_by=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.ShelfBook.objects.create(
shelf=to_read_shelf, book=self.book, user=self.local_user)
shelf = self.local_user.shelf_set.get(identifier='reading')
self.assertEqual(to_read_shelf.books.get(), self.book)
self.assertFalse(shelf.books.exists())
@ -74,7 +75,7 @@ class ReadingViews(TestCase):
request = self.factory.post('')
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.start_reading(request, self.book.id)
self.assertFalse(to_read_shelf.books.exists())
@ -98,7 +99,7 @@ class ReadingViews(TestCase):
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.finish_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book)

View file

@ -1,13 +1,16 @@
''' tests updating reading progress '''
from datetime import datetime
from unittest.mock import patch
from django.test import TestCase, Client
from django.utils import timezone
from datetime import datetime
from bookwyrm import models
@patch('bookwyrm.broadcast.broadcast_task.delay')
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
class ReadThrough(TestCase):
''' readthrough tests '''
def setUp(self):
''' basic user and book data '''
self.client = Client()
self.work = models.Work.objects.create(
@ -25,7 +28,8 @@ class ReadThrough(TestCase):
'cinco', 'cinco@example.com', 'seissiete',
local=True, localname='cinco')
self.client.force_login(self.user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.client.force_login(self.user)
def test_create_basic_readthrough(self, delay_mock):
"""A basic readthrough doesn't create a progress update"""
@ -38,13 +42,15 @@ class ReadThrough(TestCase):
readthroughs = self.edition.readthrough_set.all()
self.assertEqual(len(readthroughs), 1)
self.assertEqual(readthroughs[0].progressupdate_set.count(), 0)
self.assertEqual(readthroughs[0].start_date,
self.assertEqual(
readthroughs[0].start_date,
datetime(2020, 11, 27, tzinfo=timezone.utc))
self.assertEqual(readthroughs[0].progress, None)
self.assertEqual(readthroughs[0].finish_date, None)
self.assertEqual(delay_mock.call_count, 1)
def test_create_progress_readthrough(self, delay_mock):
''' a readthrough with progress '''
self.assertEqual(self.edition.readthrough_set.count(), 0)
self.client.post('/start-reading/{}'.format(self.edition.id), {
@ -54,7 +60,8 @@ class ReadThrough(TestCase):
readthroughs = self.edition.readthrough_set.all()
self.assertEqual(len(readthroughs), 1)
self.assertEqual(readthroughs[0].start_date,
self.assertEqual(
readthroughs[0].start_date,
datetime(2020, 11, 27, tzinfo=timezone.utc))
self.assertEqual(readthroughs[0].progress, 50)
self.assertEqual(readthroughs[0].finish_date, None)
@ -76,7 +83,9 @@ class ReadThrough(TestCase):
self.assertEqual(len(progress_updates), 2)
self.assertEqual(progress_updates[1].mode, models.ProgressMode.PAGE)
self.assertEqual(progress_updates[1].progress, 100)
self.assertEqual(delay_mock.call_count, 1) # Edit doesn't publish anything
# Edit doesn't publish anything
self.assertEqual(delay_mock.call_count, 1)
self.client.post('/delete-readthrough', {
'id': readthroughs[0].id,

View file

@ -2,16 +2,14 @@
from unittest.mock import patch
from django.test import RequestFactory, TestCase
import responses
from bookwyrm import models
from bookwyrm.views import rss_feed
from bookwyrm.settings import DOMAIN
class RssFeedView(TestCase):
''' rss feed behaves as expected '''
def setUp(self):
''' test data '''
self.site = models.SiteSettings.objects.create()
self.user = models.User.objects.create_user(
@ -24,21 +22,23 @@ class RssFeedView(TestCase):
parent_work=work
)
self.review = models.Review.objects.create(
name='Review name', content='test content', rating=3,
user=self.user, book=self.book)
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)
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):
''' load an rss feed '''
view = rss_feed.RssFeed()
request = self.factory.get('/user/rss_user/rss')
with patch("bookwyrm.models.SiteSettings.objects.get") as site:
@ -47,6 +47,5 @@ class RssFeedView(TestCase):
self.assertEqual(result.status_code, 200)
self.assertIn(b"Status updates from rss_user", result.content)
self.assertIn( b"a sickening sense", result.content)
self.assertIn(b"a sickening sense", result.content)
self.assertIn(b"Example Edition", result.content)

View file

@ -8,7 +8,7 @@ from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
@patch('bookwyrm.broadcast.broadcast_task.delay')
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
class ShelfViews(TestCase):
''' tag views'''
def setUp(self):
@ -25,11 +25,12 @@ class ShelfViews(TestCase):
remote_id='https://example.com/book/1',
parent_work=self.work
)
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.local_user
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.local_user
)
models.SiteSettings.objects.create()
@ -96,7 +97,8 @@ class ShelfViews(TestCase):
'name': 'cool name'
})
request.user = self.local_user
view(request, request.user.username, shelf.identifier)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, request.user.username, shelf.identifier)
shelf.refresh_from_db()
self.assertEqual(shelf.name, 'cool name')
@ -116,7 +118,8 @@ class ShelfViews(TestCase):
'name': 'cool name'
})
request.user = self.local_user
view(request, request.user.username, shelf.identifier)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, request.user.username, shelf.identifier)
self.assertEqual(shelf.name, 'To Read')
@ -128,7 +131,8 @@ class ShelfViews(TestCase):
'shelf': self.shelf.identifier
})
request.user = self.local_user
views.shelve(request)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.shelve(request)
# make sure the book is on the shelf
self.assertEqual(self.shelf.books.get(), self.book)
@ -142,7 +146,8 @@ class ShelfViews(TestCase):
})
request.user = self.local_user
views.shelve(request)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.shelve(request)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
@ -156,7 +161,8 @@ class ShelfViews(TestCase):
})
request.user = self.local_user
views.shelve(request)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.shelve(request)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
@ -170,7 +176,7 @@ class ShelfViews(TestCase):
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.shelve(request)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
@ -178,7 +184,12 @@ class ShelfViews(TestCase):
def test_handle_unshelve(self, _):
''' remove a book from a shelf '''
self.shelf.books.add(self.book)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=self.shelf
)
self.shelf.save()
self.assertEqual(self.shelf.books.count(), 1)
request = self.factory.post('', {
@ -186,6 +197,6 @@ class ShelfViews(TestCase):
'shelf': self.shelf.id
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.unshelve(request)
self.assertEqual(self.shelf.books.count(), 0)

View file

@ -1,11 +1,9 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import DOMAIN
@ -47,7 +45,7 @@ class StatusViews(TestCase):
})
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, 'comment')
status = models.Comment.objects.get()
self.assertEqual(status.content, '<p>hi</p>')
@ -59,8 +57,9 @@ class StatusViews(TestCase):
view = views.CreateStatus.as_view()
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True)
parent = models.Status.objects.create(
content='parent status', user=self.local_user)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
parent = models.Status.objects.create(
content='parent status', user=self.local_user)
form = forms.ReplyForm({
'content': 'hi',
'user': user.id,
@ -69,7 +68,7 @@ class StatusViews(TestCase):
})
request = self.factory.post('', form.data)
request.user = user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, 'reply')
status = models.Status.objects.get(user=user)
self.assertEqual(status.content, '<p>hi</p>')
@ -92,7 +91,7 @@ class StatusViews(TestCase):
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, 'comment')
status = models.Status.objects.get()
self.assertEqual(list(status.mention_users.all()), [user])
@ -116,7 +115,7 @@ class StatusViews(TestCase):
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, 'comment')
status = models.Status.objects.get()
@ -128,7 +127,7 @@ class StatusViews(TestCase):
})
request = self.factory.post('', form.data)
request.user = user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, 'reply')
reply = models.Status.replies(status).first()
@ -221,12 +220,13 @@ class StatusViews(TestCase):
def test_handle_delete_status(self):
''' marks a status as deleted '''
view = views.DeleteStatus.as_view()
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
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.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, status.id)
status.refresh_from_db()
self.assertTrue(status.deleted)

View file

@ -39,9 +39,10 @@ class TagViews(TestCase):
def test_tag_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Tag.as_view()
tag = models.Tag.objects.create(name='hi there')
models.UserTag.objects.create(
tag=tag, user=self.local_user, book=self.book)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
tag = models.Tag.objects.create(name='hi there')
models.UserTag.objects.create(
tag=tag, user=self.local_user, book=self.book)
request = self.factory.get('')
with patch('bookwyrm.views.tag.is_api_request') as is_api:
is_api.return_value = False
@ -68,7 +69,7 @@ class TagViews(TestCase):
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request)
tag = models.Tag.objects.get()
@ -82,9 +83,10 @@ class TagViews(TestCase):
def test_untag(self):
''' remove a tag from a book '''
view = views.RemoveTag.as_view()
tag = models.Tag.objects.create(name='A Tag!?')
models.UserTag.objects.create(
user=self.local_user, book=self.book, tag=tag)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
tag = models.Tag.objects.create(name='A Tag!?')
models.UserTag.objects.create(
user=self.local_user, book=self.book, tag=tag)
request = self.factory.post(
'', {
'user': self.local_user.id,
@ -93,7 +95,7 @@ class TagViews(TestCase):
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request)
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists())

View file

@ -138,7 +138,7 @@ class UserViews(TestCase):
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request)
self.assertEqual(self.local_user.name, 'New Name')

View file

@ -8,7 +8,7 @@ from .error import not_found_page, server_error_page
from .federation import Federation
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request, handle_accept
from .follow import accept_follow_request, delete_follow_request
from .goal import Goal
from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost

View file

@ -46,6 +46,7 @@ class Login(View):
# successful login
login(request, user)
user.last_active_date = timezone.now()
user.save(broadcast=False)
return redirect(request.GET.get('next', '/'))
# login errors

View file

@ -8,7 +8,6 @@ from django.views import View
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from .helpers import is_api_request
@ -62,5 +61,4 @@ class EditAuthor(View):
return TemplateResponse(request, 'edit_author.html', data)
author = form.save()
broadcast(request.user, author.to_update_activity(request.user))
return redirect('/author/%s' % author.id)

View file

@ -8,7 +8,6 @@ from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.broadcast import broadcast
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@ -22,15 +21,8 @@ class Block(View):
def post(self, request, user_id):
''' block a user '''
to_block = get_object_or_404(models.User, id=user_id)
block = models.UserBlocks.objects.create(
models.UserBlocks.objects.create(
user_subject=request.user, user_object=to_block)
if not to_block.local:
broadcast(
request.user,
block.to_activity(),
privacy='direct',
direct_recipients=[to_block]
)
return redirect('/preferences/block')
@ -46,13 +38,5 @@ def unblock(request, user_id):
)
except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound()
if not to_unblock.local:
broadcast(
request.user,
block.to_undo_activity(request.user),
privacy='direct',
direct_recipients=[to_unblock]
)
block.delete()
return redirect('/preferences/block')

View file

@ -12,7 +12,6 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_activity_feed, get_edition
@ -78,12 +77,12 @@ class Book(View):
.order_by('-updated_date')
user_shelves = models.ShelfBook.objects.filter(
added_by=request.user, book=book
user=request.user, book=book
)
other_edition_shelves = models.ShelfBook.objects.filter(
~Q(book=book),
added_by=request.user,
user=request.user,
book__parent_work=book.parent_work,
)
@ -136,7 +135,6 @@ class EditBook(View):
return TemplateResponse(request, 'edit_book.html', data)
book = form.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id)
@ -170,7 +168,6 @@ def upload_cover(request, book_id):
book.cover = form.files['cover']
book.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id)
@ -189,7 +186,6 @@ def add_description(request, book_id):
book.description = description
book.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id)
@ -215,12 +211,14 @@ def switch_edition(request):
shelf__user=request.user
)
for shelfbook in shelfbooks.all():
broadcast(request.user, shelfbook.to_remove_activity(request.user))
shelfbook.book = new_edition
shelfbook.save()
broadcast(request.user, shelfbook.to_add_activity(request.user))
with transaction.atomic():
models.ShelfBook.objects.create(
created_date=shelfbook.created_date,
user=shelfbook.user,
shelf=shelfbook.shelf,
book=new_edition
)
shelfbook.delete()
readthroughs = models.ReadThrough.objects.filter(
book__parent_work=new_edition.parent_work,

View file

@ -1,12 +1,10 @@
''' views for actions you can take in the application '''
from django.db import transaction
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.broadcast import broadcast
from .helpers import get_user_from_username
@login_required
@ -19,13 +17,10 @@ def follow(request):
except models.User.DoesNotExist:
return HttpResponseBadRequest()
relationship, _ = models.UserFollowRequest.objects.get_or_create(
models.UserFollowRequest.objects.get_or_create(
user_subject=request.user,
user_object=to_follow,
)
activity = relationship.to_activity()
broadcast(
request.user, activity, privacy='direct', direct_recipients=[to_follow])
return redirect(to_follow.local_path)
@ -39,14 +34,10 @@ def unfollow(request):
except models.User.DoesNotExist:
return HttpResponseBadRequest()
relationship = models.UserFollows.objects.get(
models.UserFollows.objects.get(
user_subject=request.user,
user_object=to_unfollow
)
activity = relationship.to_undo_activity(request.user)
broadcast(
request.user, activity,
privacy='direct', direct_recipients=[to_unfollow])
to_unfollow.followers.remove(request.user)
return redirect(to_unfollow.local_path)
@ -70,24 +61,11 @@ def accept_follow_request(request):
except models.UserFollowRequest.DoesNotExist:
# Request already dealt with.
return redirect(request.user.local_path)
handle_accept(follow_request)
follow_request.accept()
return redirect(request.user.local_path)
def handle_accept(follow_request):
''' send an acceptance message to a follow request '''
user = follow_request.user_subject
to_follow = follow_request.user_object
with transaction.atomic():
relationship = models.UserFollows.from_request(follow_request)
follow_request.delete()
relationship.save()
activity = relationship.to_accept_activity()
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
@login_required
@require_POST
def delete_follow_request(request):
@ -106,8 +84,5 @@ def delete_follow_request(request):
except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest()
activity = follow_request.to_reject_activity()
follow_request.delete()
broadcast(
request.user, activity, privacy='direct', direct_recipients=[requester])
return redirect('/user/%s' % request.user.localname)

View file

@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, models
from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_generated_note
from .helpers import get_user_from_username, object_visible_to_user
@ -63,23 +62,10 @@ class Goal(View):
if request.POST.get('post-status'):
# create status, if appropraite
status = create_generated_note(
create_generated_note(
request.user,
'set a goal to read %d books in %d' % (goal.goal, goal.year),
privacy=goal.privacy
)
broadcast(
request.user,
status.to_create_activity(request.user),
privacy=status.privacy,
software='bookwyrm')
# re-format the activity for non-bookwyrm servers
remote_activity = status.to_create_activity(request.user, pure=True)
broadcast(
request.user,
remote_activity,
privacy=status.privacy,
software='other')
return redirect(request.headers.get('Referer', '/'))

View file

@ -4,7 +4,6 @@ from requests import HTTPError
from django.db.models import Q
from bookwyrm import activitypub, models
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.status import create_generated_note
from bookwyrm.utils import regex
@ -199,7 +198,6 @@ def handle_reading_status(user, shelf, book, privacy):
)
status.save()
broadcast(user, status.to_create_activity(user))
def is_blocked(viewer, user):
''' is this viewer blocked by the user? '''

View file

@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_notification
@ -19,7 +18,7 @@ class Favorite(View):
''' create a like '''
status = models.Status.objects.get(id=status_id)
try:
favorite = models.Favorite.objects.create(
models.Favorite.objects.create(
status=status,
user=request.user
)
@ -27,10 +26,6 @@ class Favorite(View):
# you already fav'ed that
return HttpResponseBadRequest()
fav_activity = favorite.to_activity()
broadcast(
request.user, fav_activity, privacy='direct',
direct_recipients=[status.user])
if status.user.local:
create_notification(
status.user,
@ -56,9 +51,7 @@ class Unfavorite(View):
# can't find that status, idk
return HttpResponseNotFound()
fav_activity = favorite.to_undo_activity(request.user)
favorite.delete()
broadcast(request.user, fav_activity, direct_recipients=[status.user])
# check for notification
if status.user.local:
@ -86,15 +79,12 @@ class Boost(View):
# you already boosted that.
return redirect(request.headers.get('Referer', '/'))
boost = models.Boost.objects.create(
models.Boost.objects.create(
boosted_status=status,
privacy=status.privacy,
user=request.user,
)
boost_activity = boost.to_activity()
broadcast(request.user, boost_activity)
if status.user.local:
create_notification(
status.user,
@ -114,10 +104,8 @@ class Unboost(View):
boost = models.Boost.objects.filter(
boosted_status=status, user=request.user
).first()
activity = boost.to_undo_activity(request.user)
boost.delete()
broadcast(request.user, activity)
# delete related notification
if status.user.local:

View file

@ -11,7 +11,6 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import connector_manager
from .helpers import is_api_request, object_visible_to_user, privacy_filter
from .helpers import get_user_from_username
@ -55,13 +54,6 @@ class Lists(View):
return redirect('lists')
book_list = form.save()
# let the world know
broadcast(
request.user,
book_list.to_create_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect(book_list.local_path)
class UserLists(View):
@ -136,19 +128,12 @@ class List(View):
@method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument
def post(self, request, list_id):
''' edit a book_list '''
''' edit a list '''
book_list = get_object_or_404(models.List, id=list_id)
form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid():
return redirect('list', book_list.id)
book_list = form.save()
# let the world know
broadcast(
request.user,
book_list.to_update_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect(book_list.local_path)
@ -182,13 +167,6 @@ class Curate(View):
if approved:
suggestion.approved = True
suggestion.save()
# let the world know
broadcast(
request.user,
suggestion.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
else:
suggestion.delete()
return redirect('list-curate', book_list.id)
@ -205,17 +183,10 @@ def add_book(request, list_id):
# do you have permission to add to the list?
if request.user == book_list.user or book_list.curation == 'open':
# go ahead and add it
item = models.ListItem.objects.create(
models.ListItem.objects.create(
book=book,
book_list=book_list,
added_by=request.user,
)
# let the world know
broadcast(
request.user,
item.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
user=request.user,
)
elif book_list.curation == 'curated':
# make a pending entry
@ -223,7 +194,7 @@ def add_book(request, list_id):
approved=False,
book=book,
book_list=book_list,
added_by=request.user,
user=request.user,
)
else:
# you can't add to this list, what were you THINKING
@ -238,16 +209,8 @@ def remove_book(request, list_id):
book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get('item'))
if not book_list.user == request.user and not item.added_by == request.user:
if not book_list.user == request.user and not item.user == request.user:
return HttpResponseNotFound()
activity = item.to_remove_activity(request.user)
item.delete()
# let the world know
broadcast(
request.user,
activity,
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect('list', list_id)

View file

@ -79,7 +79,7 @@ class PasswordReset(View):
return TemplateResponse(request, 'password_reset.html', data)
user.set_password(new_password)
user.save()
user.save(broadcast=False)
login(request, user)
reset_code.delete()
return redirect('/')
@ -106,6 +106,6 @@ class ChangePassword(View):
return redirect('preferences/password')
request.user.set_password(new_password)
request.user.save()
request.user.save(broadcast=False)
login(request, request.user)
return redirect(request.user.local_path)

View file

@ -9,7 +9,6 @@ from django.utils import timezone
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.broadcast import broadcast
from .helpers import get_edition, handle_reading_status
from .shelf import handle_unshelve
@ -44,9 +43,8 @@ def start_reading(request, book_id):
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
shelfbook = models.ShelfBook.objects.create(
book=book, shelf=shelf, added_by=request.user)
broadcast(request.user, shelfbook.to_add_activity(request.user))
models.ShelfBook.objects.create(
book=book, shelf=shelf, user=request.user)
# post about it (if you want)
if request.POST.get('post-status'):
@ -82,9 +80,8 @@ def finish_reading(request, book_id):
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
shelfbook = models.ShelfBook.objects.create(
book=book, shelf=shelf, added_by=request.user)
broadcast(request.user, shelfbook.to_add_activity(request.user))
models.ShelfBook.objects.create(
book=book, shelf=shelf, user=request.user)
# post about it (if you want)
if request.POST.get('post-status'):

View file

@ -9,7 +9,6 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from .helpers import is_api_request, get_edition, get_user_from_username
from .helpers import handle_reading_status
@ -50,7 +49,7 @@ class Shelf(View):
return ActivitypubResponse(shelf.to_activity(**request.GET))
books = models.ShelfBook.objects.filter(
added_by=user, shelf=shelf
user=user, shelf=shelf
).order_by('-updated_date').all()
data = {
@ -136,14 +135,8 @@ def shelve(request):
except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves
pass
shelfbook = models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, added_by=request.user)
broadcast(
request.user,
shelfbook.to_add_activity(request.user),
privacy=shelfbook.shelf.privacy,
software='bookwyrm'
)
models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, user=request.user)
# post about "want to read" shelves
if desired_shelf.identifier == 'to-read':
@ -168,10 +161,8 @@ def unshelve(request):
return redirect(request.headers.get('Referer', '/'))
#pylint: disable=unused-argument
def handle_unshelve(user, book, shelf):
''' unshelve a book '''
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
activity = row.to_remove_activity(user)
row.delete()
broadcast(user, activity, privacy=shelf.privacy, software='bookwyrm')

View file

@ -8,7 +8,6 @@ from django.views import View
from markdown import markdown
from bookwyrm import forms, models
from bookwyrm.broadcast import broadcast
from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN
from bookwyrm.status import create_notification, delete_status
@ -35,7 +34,7 @@ class CreateStatus(View):
if not status.sensitive and status.content_warning:
# the cw text field remains populated when you click "remove"
status.content_warning = None
status.save()
status.save(broadcast=False)
# inspect the text for user tags
content = status.content
@ -83,16 +82,7 @@ class CreateStatus(View):
if hasattr(status, 'quote'):
status.quote = to_markdown(status.quote)
status.save()
broadcast(
request.user,
status.to_create_activity(request.user),
software='bookwyrm')
# re-format the activity for non-bookwyrm servers
remote_activity = status.to_create_activity(request.user, pure=True)
broadcast(request.user, remote_activity, software='other')
status.save(created=True)
return redirect(request.headers.get('Referer', '/'))
@ -108,7 +98,6 @@ class DeleteStatus(View):
# perform deletion
delete_status(status)
broadcast(request.user, status.to_delete_activity(request.user))
return redirect(request.headers.get('Referer', '/'))
def find_mentions(content):

View file

@ -8,7 +8,6 @@ from django.views import View
from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from .helpers import is_api_request
@ -45,17 +44,15 @@ class AddTag(View):
name = request.POST.get('name')
book_id = request.POST.get('book')
book = get_object_or_404(models.Edition, id=book_id)
tag_obj, created = models.Tag.objects.get_or_create(
tag_obj, _ = models.Tag.objects.get_or_create(
name=name,
)
user_tag, _ = models.UserTag.objects.get_or_create(
models.UserTag.objects.get_or_create(
user=request.user,
book=book,
tag=tag_obj,
)
if created:
broadcast(request.user, user_tag.to_add_activity(request.user))
return redirect('/book/%s' % book_id)
@ -71,8 +68,6 @@ class RemoveTag(View):
user_tag = get_object_or_404(
models.UserTag, tag=tag_obj, book=book, user=request.user)
tag_activity = user_tag.to_remove_activity(request.user)
user_tag.delete()
broadcast(request.user, tag_activity)
return redirect('/book/%s' % book_id)

View file

@ -15,7 +15,6 @@ from django.views import View
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
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
@ -176,7 +175,6 @@ class EditUser(View):
user.avatar.save(filename, image)
user.save()
broadcast(user, user.to_update_activity(user))
return redirect(user.local_path)