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

View file

@ -18,7 +18,7 @@ class Note(ActivityObject):
''' Note activity ''' ''' Note activity '''
published: str published: str
attributedTo: str attributedTo: str
content: str content: str = ''
to: List[str] = field(default_factory=lambda: []) to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: []) cc: List[str] = field(default_factory=lambda: [])
replies: Dict = 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 import logging
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast
from bookwyrm.models import ImportJob, ImportItem from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.status import create_notification from bookwyrm.status import create_notification
from bookwyrm.tasks import app from bookwyrm.tasks import app
@ -82,7 +81,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
return return
existing_shelf = models.ShelfBook.objects.filter( 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 # shelve the book if it hasn't been shelved already
if item.shelf and not existing_shelf: if item.shelf and not existing_shelf:
@ -90,9 +89,8 @@ def handle_imported_book(user, item, include_reviews, privacy):
identifier=item.shelf, identifier=item.shelf,
user=user user=user
) )
shelf_book = models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, added_by=user) book=item.book, shelf=desired_shelf, user=user)
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
for read in item.reads: for read in item.reads:
# check for an existing readthrough with the same dates # 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, # we don't know the publication date of the review,
# but "now" is a bad guess # but "now" is a bad guess
published_date_guess = item.date_read or item.date_added published_date_guess = item.date_read or item.date_added
review = models.Review.objects.create( models.Review.objects.create(
user=user, user=user,
book=item.book, book=item.book,
name=review_title, name=review_title,
@ -123,6 +121,3 @@ def handle_imported_book(user, item, include_reviews, privacy):
published_date=published_date_guess, published_date=published_date_guess,
privacy=privacy, 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 from django.views.decorators.http import require_POST
import requests import requests
from bookwyrm import activitypub, models, views from bookwyrm import activitypub, models
from bookwyrm import status as status_builder from bookwyrm import status as status_builder
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.signatures import Signature from bookwyrm.signatures import Signature
@ -144,7 +144,7 @@ def handle_follow(activity):
related_user=relationship.user_subject related_user=relationship.user_subject
) )
if not manually_approves: if not manually_approves:
views.handle_accept(relationship) relationship.accept()
@app.task @app.task

View file

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

View file

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

View file

@ -1,20 +1,9 @@
''' base model with default fields ''' ''' 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 import models
from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm.settings import DOMAIN
from bookwyrm.settings import DOMAIN, PAGE_LENGTH from .fields import RemoteIdField
from .fields import ImageField, ManyToManyField, RemoteIdField
class BookWyrmModel(models.Model): class BookWyrmModel(models.Model):
@ -49,254 +38,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
return return
if not instance.remote_id: if not instance.remote_id:
instance.remote_id = instance.get_remote_id() instance.remote_id = instance.get_remote_id()
try:
instance.save(broadcast=False)
except TypeError:
instance.save() 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

View file

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

View file

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

View file

@ -3,8 +3,8 @@ from django.db import models
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .base_model import ActivitypubMixin, BookWyrmModel from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import OrderedCollectionMixin from .base_model import BookWyrmModel
from . import fields from . import fields
@ -51,13 +51,13 @@ class List(OrderedCollectionMixin, BookWyrmModel):
ordering = ('-updated_date',) ordering = ('-updated_date',)
class ListItem(ActivitypubMixin, BookWyrmModel): class ListItem(CollectionItemMixin, BookWyrmModel):
''' ok ''' ''' ok '''
book = fields.ForeignKey( book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='object') 'Edition', on_delete=models.PROTECT, activitypub_field='object')
book_list = fields.ForeignKey( book_list = fields.ForeignKey(
'List', on_delete=models.CASCADE, activitypub_field='target') 'List', on_delete=models.CASCADE, activitypub_field='target')
added_by = fields.ForeignKey( user = fields.ForeignKey(
'User', 'User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
activitypub_field='actor' activitypub_field='actor'
@ -68,24 +68,8 @@ class ListItem(ActivitypubMixin, BookWyrmModel):
endorsement = models.ManyToManyField('User', related_name='endorsers') endorsement = models.ManyToManyField('User', related_name='endorsers')
activity_serializer = activitypub.AddBook activity_serializer = activitypub.AddBook
object_field = 'book'
def to_add_activity(self, user): collection_field = 'book_list'
''' 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()
class Meta: class Meta:
''' an opinionated constraint! you can't put a book on a list twice ''' ''' 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): def save(self, *args, **kwargs):
''' update user active time ''' ''' update user active time '''
self.user.last_active_date = timezone.now() self.user.last_active_date = timezone.now()
self.user.save() self.user.save(broadcast=False)
super().save(*args, **kwargs) super().save(*args, **kwargs)
def create_update(self): def create_update(self):
@ -54,5 +54,5 @@ class ProgressUpdate(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' update user active time ''' ''' update user active time '''
self.user.last_active_date = timezone.now() self.user.last_active_date = timezone.now()
self.user.save() self.user.save(broadcast=False)
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -1,14 +1,15 @@
''' defines relationships between users ''' ''' defines relationships between users '''
from django.db import models from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm import activitypub
from .base_model import ActivitypubMixin, BookWyrmModel from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .base_model import BookWyrmModel
from . import fields from . import fields
class UserRelationship(ActivitypubMixin, BookWyrmModel): class UserRelationship(BookWyrmModel):
''' many-to-many through table for followers ''' ''' many-to-many through table for followers '''
user_subject = fields.ForeignKey( user_subject = fields.ForeignKey(
'User', 'User',
@ -23,6 +24,16 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
activitypub_field='object', 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: class Meta:
''' relationships should be unique ''' ''' relationships should be unique '''
abstract = True 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 def get_remote_id(self, status=None):# pylint: disable=arguments-differ
''' use shelf identifier in remote_id ''' ''' use shelf identifier in remote_id '''
status = status or 'follows' status = status or 'follows'
@ -46,55 +55,73 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
return '%s#%s/%d' % (base_path, status, self.id) return '%s#%s/%d' % (base_path, status, self.id)
def to_accept_activity(self): class UserFollows(ActivitypubMixin, UserRelationship):
''' 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):
''' Following a user ''' ''' Following a user '''
status = 'follows' status = 'follows'
activity_serializer = activitypub.Follow
@classmethod @classmethod
def from_request(cls, follow_request): def from_request(cls, follow_request):
''' converts a follow request into a follow relationship ''' ''' converts a follow request into a follow relationship '''
return cls( return cls.objects.create(
user_subject=follow_request.user_subject, user_subject=follow_request.user_subject,
user_object=follow_request.user_object, user_object=follow_request.user_object,
remote_id=follow_request.remote_id, remote_id=follow_request.remote_id,
) )
class UserFollowRequest(UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship):
''' following a user requires manual or automatic confirmation ''' ''' following a user requires manual or automatic confirmation '''
status = 'follow_request' status = 'follow_request'
activity_serializer = activitypub.Follow
def save(self, *args, **kwargs): def save(self, *args, broadcast=True, **kwargs):
''' make sure the follow relationship doesn't already exist ''' ''' make sure the follow or block relationship doesn't already exist '''
try: try:
UserFollows.objects.get( UserFollows.objects.get(
user_subject=self.user_subject, user_subject=self.user_subject,
user_object=self.user_object user_object=self.user_object
) )
UserBlocks.objects.get(
user_subject=self.user_subject,
user_object=self.user_object
)
return None return None
except UserFollows.DoesNotExist: except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
return super().save(*args, **kwargs) 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 ''' ''' prevent another user from following you and seeing your posts '''
status = 'blocks' status = 'blocks'
activity_serializer = activitypub.Block activity_serializer = activitypub.Block

View file

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

View file

@ -9,10 +9,12 @@ from django.utils import timezone
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub 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 .base_model import BookWyrmModel
from . import fields
from .fields import image_serializer from .fields import image_serializer
from . import fields
class Status(OrderedCollectionPageMixin, BookWyrmModel): class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc ''' ''' any post, like a reply to a review, etc '''
@ -50,6 +52,15 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
serialize_reverse_fields = [('attachments', 'attachment', 'id')] serialize_reverse_fields = [('attachments', 'attachment', 'id')]
deserialize_reverse_fields = [('attachments', 'attachment')] 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 @classmethod
def ignore_activity(cls, activity): def ignore_activity(cls, activity):
''' keep notes if they are replies to existing statuses ''' ''' keep notes if they are replies to existing statuses '''
@ -126,14 +137,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return activity 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): class GeneratedNote(Status):
''' these are app-generated messages about user activity ''' ''' these are app-generated messages about user activity '''
@property @property
@ -223,7 +226,7 @@ class Review(Status):
pure_type = 'Article' pure_type = 'Article'
class Boost(Status): class Boost(ActivityMixin, Status):
''' boost'ing a post ''' ''' boost'ing a post '''
boosted_status = fields.ForeignKey( boosted_status = fields.ForeignKey(
'Status', 'Status',
@ -231,6 +234,8 @@ class Boost(Status):
related_name='boosters', related_name='boosters',
activitypub_field='object', activitypub_field='object',
) )
activity_serializer = activitypub.Boost
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
''' the user field is "actor" here instead of "attributedTo" ''' ''' the user field is "actor" here instead of "attributedTo" '''
@ -244,8 +249,6 @@ class Boost(Status):
self.image_fields = [] self.image_fields = []
self.deserialize_reverse_fields = [] self.deserialize_reverse_fields = []
activity_serializer = activitypub.Boost
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.
# class Meta: # class Meta:
# unique_together = ('user', 'boosted_status') # unique_together = ('user', 'boosted_status')

View file

@ -5,7 +5,8 @@ from django.db import models
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN 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 from . import fields
@ -40,7 +41,7 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class UserTag(BookWyrmModel): class UserTag(CollectionItemMixin, BookWyrmModel):
''' an instance of a tag on a book by a user ''' ''' an instance of a tag on a book by a user '''
user = fields.ForeignKey( user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor') 'User', on_delete=models.PROTECT, activitypub_field='actor')
@ -50,25 +51,8 @@ class UserTag(BookWyrmModel):
'Tag', on_delete=models.PROTECT, activitypub_field='target') 'Tag', on_delete=models.PROTECT, activitypub_field='target')
activity_serializer = activitypub.AddBook activity_serializer = activitypub.AddBook
object_field = 'book'
def to_add_activity(self, user): collection_field = 'tag'
''' 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()
class Meta: class Meta:
''' unqiueness constraint ''' ''' unqiueness constraint '''

View file

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

View file

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

View file

@ -31,9 +31,9 @@
</div> </div>
<div class="card-footer has-background-white-bis"> <div class="card-footer has-background-white-bis">
<div class="card-footer-item"> <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> </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"> <form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}"> <input type="hidden" name="item" value="{{ item.id }}">

View file

@ -23,7 +23,7 @@ class BaseActivity(TestCase):
'mouse', 'mouse@mouse.mouse', 'mouseword', 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse') local=True, localname='mouse')
self.user.remote_id = 'http://example.com/a/b' self.user.remote_id = 'http://example.com/a/b'
self.user.save() self.user.save(broadcast=False)
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json' '../data/ap_user.json'
@ -167,12 +167,15 @@ class BaseActivity(TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.user.avatar.file #pylint: disable=pointless-statement self.user.avatar.file #pylint: disable=pointless-statement
# 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) activity.to_model(models.User, self.user)
self.assertIsNotNone(self.user.avatar.name) self.assertIsNotNone(self.user.avatar.name)
self.assertIsNotNone(self.user.avatar.file) self.assertIsNotNone(self.user.avatar.file)
def test_to_model_many_to_many(self): def test_to_model_many_to_many(self):
''' annoying that these all need special handling ''' ''' annoying that these all need special handling '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
content='test status', content='test status',
user=self.user, user=self.user,
@ -208,6 +211,7 @@ class BaseActivity(TestCase):
def test_to_model_one_to_many(self): def test_to_model_one_to_many(self):
''' these are reversed relationships, where the secondary object ''' these are reversed relationships, where the secondary object
keys the primary object but not vice versa ''' keys the primary object but not vice versa '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
content='test status', content='test status',
user=self.user, user=self.user,
@ -242,6 +246,7 @@ class BaseActivity(TestCase):
@responses.activate @responses.activate
def test_set_related_field(self): def test_set_related_field(self):
''' celery task to add back-references to created objects ''' ''' celery task to add back-references to created objects '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
content='test status', content='test status',
user=self.user, user=self.user,

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

View file

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

View file

@ -6,7 +6,9 @@ from bookwyrm import models
class Relationship(TestCase): class Relationship(TestCase):
''' following, blocking, stuff like that '''
def setUp(self): def setUp(self):
''' we need some users for this '''
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword', 'rat', 'rat@rat.com', 'ratword',
@ -19,68 +21,31 @@ class Relationship(TestCase):
'mouse', 'mouse@mouse.com', 'mouseword', 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse') local=True, localname='mouse')
self.local_user.remote_id = 'http://local.com/user/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): def test_user_follows(self):
''' create a follow relationship '''
with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'):
rel = models.UserFollows.objects.create( rel = models.UserFollows.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user user_object=self.remote_user
) )
self.assertEqual(
rel.remote_id,
'http://local.com/user/mouse#follows/%d' % rel.id
)
activity = rel.to_activity() activity = rel.to_activity()
self.assertEqual(activity['id'], rel.remote_id) self.assertEqual(activity['id'], rel.remote_id)
self.assertEqual(activity['actor'], 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['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): 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( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user user_object=self.remote_user
@ -99,9 +64,13 @@ class Relationship(TestCase):
self.assertEqual(rel.status, 'follows') self.assertEqual(rel.status, 'follows')
self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user) self.assertEqual(rel.user_object, self.remote_user)
models.UserFollowRequest.broadcast = real_broadcast
def test_user_follows_from_request_custom_remote_id(self): def test_user_follows_from_request_custom_remote_id(self):
''' 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( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user, user_object=self.remote_user,
@ -121,3 +90,67 @@ class Relationship(TestCase):
self.assertEqual(rel.status, 'follows') self.assertEqual(rel.status, 'follows')
self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_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 ''' ''' some activitypub oddness ahead '''
def setUp(self): def setUp(self):
''' look, a shelf ''' ''' look, a shelf '''
self.user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse') local=True, localname='mouse')
self.shelf = models.Shelf.objects.create( work = models.Work.objects.create(title='Test Work')
name='Test Shelf', identifier='test-shelf', user=self.user) self.book = models.Edition.objects.create(
title='test book',
parent_work=work)
def test_remote_id(self): def test_remote_id(self):
''' shelves use custom remote ids ''' ''' 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 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): def test_to_activity(self):
''' jsonify it ''' ''' 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.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['totalItems'], 0)
self.assertEqual(activity_json['type'], 'Shelf') self.assertEqual(activity_json['type'], 'Shelf')
self.assertEqual(activity_json['name'], 'Test 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 ''' ''' testing models '''
from unittest.mock import patch
from io import BytesIO from io import BytesIO
import pathlib import pathlib
@ -11,6 +12,7 @@ from django.utils import timezone
from bookwyrm import models, settings from bookwyrm import models, settings
@patch('bookwyrm.models.Status.broadcast')
class Status(TestCase): class Status(TestCase):
''' lotta types of statuses ''' ''' lotta types of statuses '''
def setUp(self): def setUp(self):
@ -24,13 +26,14 @@ class Status(TestCase):
'../../static/images/default_avi.jpg') '../../static/images/default_avi.jpg')
image = Image.open(image_file) image = Image.open(image_file)
output = BytesIO() output = BytesIO()
with patch('bookwyrm.models.Status.broadcast'):
image.save(output, format=image.format) image.save(output, format=image.format)
self.book.cover.save( self.book.cover.save(
'test.jpg', 'test.jpg',
ContentFile(output.getvalue()) ContentFile(output.getvalue())
) )
def test_status_generated_fields(self): def test_status_generated_fields(self, _):
''' setting remote id ''' ''' setting remote id '''
status = models.Status.objects.create(content='bleh', user=self.user) status = models.Status.objects.create(content='bleh', user=self.user)
expected_id = 'https://%s/user/mouse/status/%d' % \ 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.remote_id, expected_id)
self.assertEqual(status.privacy, 'public') self.assertEqual(status.privacy, 'public')
def test_replies(self): def test_replies(self, _):
''' get a list of replies ''' ''' get a list of replies '''
parent = models.Status.objects.create(content='hi', user=self.user) parent = models.Status.objects.create(content='hi', user=self.user)
child = models.Status.objects.create( child = models.Status.objects.create(
@ -54,7 +57,7 @@ class Status(TestCase):
# should select subclasses # should select subclasses
self.assertIsInstance(replies.last(), models.Review) self.assertIsInstance(replies.last(), models.Review)
def test_status_type(self): def test_status_type(self, _):
''' class name ''' ''' class name '''
self.assertEqual(models.Status().status_type, 'Note') self.assertEqual(models.Status().status_type, 'Note')
self.assertEqual(models.Review().status_type, 'Review') self.assertEqual(models.Review().status_type, 'Review')
@ -62,14 +65,14 @@ class Status(TestCase):
self.assertEqual(models.Comment().status_type, 'Comment') self.assertEqual(models.Comment().status_type, 'Comment')
self.assertEqual(models.Boost().status_type, 'Boost') self.assertEqual(models.Boost().status_type, 'Boost')
def test_boostable(self): def test_boostable(self, _):
''' can a status be boosted, based on privacy ''' ''' can a status be boosted, based on privacy '''
self.assertTrue(models.Status(privacy='public').boostable) self.assertTrue(models.Status(privacy='public').boostable)
self.assertTrue(models.Status(privacy='unlisted').boostable) self.assertTrue(models.Status(privacy='unlisted').boostable)
self.assertFalse(models.Status(privacy='followers').boostable) self.assertFalse(models.Status(privacy='followers').boostable)
self.assertFalse(models.Status(privacy='direct').boostable) self.assertFalse(models.Status(privacy='direct').boostable)
def test_to_replies(self): def test_to_replies(self, _):
''' activitypub replies collection ''' ''' activitypub replies collection '''
parent = models.Status.objects.create(content='hi', user=self.user) parent = models.Status.objects.create(content='hi', user=self.user)
child = models.Status.objects.create( child = models.Status.objects.create(
@ -83,7 +86,7 @@ class Status(TestCase):
self.assertEqual(replies['id'], '%s/replies' % parent.remote_id) self.assertEqual(replies['id'], '%s/replies' % parent.remote_id)
self.assertEqual(replies['totalItems'], 2) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create( status = models.Status.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
@ -93,7 +96,7 @@ class Status(TestCase):
self.assertEqual(activity['content'], 'test content') self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['sensitive'], False) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create( status = models.Status.objects.create(
content='test content', user=self.user, content='test content', user=self.user,
@ -103,7 +106,7 @@ class Status(TestCase):
self.assertEqual(activity['type'], 'Tombstone') self.assertEqual(activity['type'], 'Tombstone')
self.assertFalse(hasattr(activity, 'content')) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create( status = models.Status.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
@ -114,7 +117,7 @@ class Status(TestCase):
self.assertEqual(activity['sensitive'], False) self.assertEqual(activity['sensitive'], False)
self.assertEqual(activity['attachment'], []) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.GeneratedNote.objects.create( status = models.GeneratedNote.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
@ -127,7 +130,7 @@ class Status(TestCase):
self.assertEqual(activity['sensitive'], False) self.assertEqual(activity['sensitive'], False)
self.assertEqual(len(activity['tag']), 2) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.GeneratedNote.objects.create( status = models.GeneratedNote.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
@ -149,7 +152,7 @@ class Status(TestCase):
self.assertEqual( self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover') 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Comment.objects.create( status = models.Comment.objects.create(
content='test content', user=self.user, book=self.book) 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['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Comment.objects.create( status = models.Comment.objects.create(
content='test content', user=self.user, book=self.book) content='test content', user=self.user, book=self.book)
@ -176,7 +179,7 @@ class Status(TestCase):
self.assertEqual( self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover') 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Quotation.objects.create( status = models.Quotation.objects.create(
quote='a sickening sense', content='test content', quote='a sickening sense', content='test content',
@ -188,7 +191,7 @@ class Status(TestCase):
self.assertEqual(activity['content'], 'test content') self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Quotation.objects.create( status = models.Quotation.objects.create(
quote='a sickening sense', content='test content', quote='a sickening sense', content='test content',
@ -206,7 +209,7 @@ class Status(TestCase):
self.assertEqual( self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover') 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Review.objects.create( status = models.Review.objects.create(
name='Review name', content='test content', rating=3, name='Review name', content='test content', rating=3,
@ -219,7 +222,7 @@ class Status(TestCase):
self.assertEqual(activity['content'], 'test content') self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id) 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 ''' ''' subclass of the base model version with a "pure" serializer '''
status = models.Review.objects.create( status = models.Review.objects.create(
name='Review name', content='test content', rating=3, name='Review name', content='test content', rating=3,
@ -237,8 +240,15 @@ class Status(TestCase):
self.assertEqual( self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover') activity['attachment'][0].name, 'Test Edition cover')
def test_favorite(self): def test_favorite(self, _):
''' fav a status ''' ''' 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( status = models.Status.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
fav = models.Favorite.objects.create(status=status, 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['type'], 'Like')
self.assertEqual(activity['actor'], self.user.remote_id) self.assertEqual(activity['actor'], self.user.remote_id)
self.assertEqual(activity['object'], status.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 ''' ''' boosting, this one's a bit fussy '''
status = models.Status.objects.create( status = models.Status.objects.create(
content='test content', user=self.user) content='test content', user=self.user)
@ -264,7 +275,7 @@ class Status(TestCase):
self.assertEqual(activity['type'], 'Announce') self.assertEqual(activity['type'], 'Announce')
self.assertEqual(activity, boost.to_activity(pure=True)) self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self): def test_notification(self, _):
''' a simple model ''' ''' a simple model '''
notification = models.Notification.objects.create( notification = models.Notification.objects.create(
user=self.user, notification_type='FAVORITE') 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) job_id=import_job.id, index=index, data=entry, book=self.book)
break break
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book( goodreads_import.handle_imported_book(
self.user, import_item, False, 'public') self.user, import_item, False, 'public')
@ -144,9 +144,10 @@ class GoodreadsImport(TestCase):
def test_handle_imported_book_already_shelved(self): def test_handle_imported_book_already_shelved(self):
''' goodreads import added a book, this adds related connections ''' ''' goodreads import added a book, this adds related connections '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
shelf = self.user.shelf_set.filter(identifier='to-read').first() shelf = self.user.shelf_set.filter(identifier='to-read').first()
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
shelf=shelf, added_by=self.user, book=self.book) shelf=shelf, user=self.user, book=self.book)
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv') 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) job_id=import_job.id, index=index, data=entry, book=self.book)
break break
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book( goodreads_import.handle_imported_book(
self.user, import_item, False, 'public') 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) job_id=import_job.id, index=index, data=entry, book=self.book)
break break
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book( goodreads_import.handle_imported_book(
self.user, import_item, False, 'public') self.user, import_item, False, 'public')
goodreads_import.handle_imported_book( goodreads_import.handle_imported_book(
@ -214,7 +215,7 @@ class GoodreadsImport(TestCase):
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book) 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( goodreads_import.handle_imported_book(
self.user, import_item, True, 'unlisted') self.user, import_item, True, 'unlisted')
review = models.Review.objects.get(book=self.book, user=self.user) review = models.Review.objects.get(book=self.book, user=self.user)
@ -235,7 +236,7 @@ class GoodreadsImport(TestCase):
import_item = models.ImportItem.objects.create( import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book) 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( goodreads_import.handle_imported_book(
self.user, import_item, False, 'unlisted') self.user, import_item, False, 'unlisted')
self.assertFalse(models.Review.objects.filter( self.assertFalse(models.Review.objects.filter(

View file

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

View file

@ -35,6 +35,7 @@ class TemplateTags(TestCase):
def test_get_user_rating(self): def test_get_user_rating(self):
''' get a user's most recent rating of a book ''' ''' get a user's most recent rating of a book '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Review.objects.create( models.Review.objects.create(
user=self.user, book=self.book, rating=3) user=self.user, book=self.book, rating=3)
self.assertEqual( self.assertEqual(
@ -76,14 +77,16 @@ class TemplateTags(TestCase):
def test_get_replies(self): def test_get_replies(self):
''' direct replies to a status ''' ''' direct replies to a status '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
parent = models.Review.objects.create( parent = models.Review.objects.create(
user=self.user, book=self.book) user=self.user, book=self.book, content='hi')
first_child = models.Status.objects.create( first_child = models.Status.objects.create(
reply_parent=parent, user=self.user) reply_parent=parent, user=self.user, content='hi')
second_child = models.Status.objects.create( second_child = models.Status.objects.create(
reply_parent=parent, user=self.user) reply_parent=parent, user=self.user, content='hi')
third_child = models.Status.objects.create( third_child = models.Status.objects.create(
reply_parent=parent, user=self.user, deleted=True) reply_parent=parent, user=self.user,
deleted=True, deleted_date=timezone.now())
replies = bookwyrm_tags.get_replies(parent) replies = bookwyrm_tags.get_replies(parent)
self.assertEqual(len(replies), 2) self.assertEqual(len(replies), 2)
@ -94,10 +97,11 @@ class TemplateTags(TestCase):
def test_get_parent(self): def test_get_parent(self):
''' get the reply parent of a status ''' ''' get the reply parent of a status '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
parent = models.Review.objects.create( parent = models.Review.objects.create(
user=self.user, book=self.book) user=self.user, book=self.book, content='hi')
child = models.Status.objects.create( child = models.Status.objects.create(
reply_parent=parent, user=self.user) reply_parent=parent, user=self.user, content='hi')
result = bookwyrm_tags.get_parent(child) result = bookwyrm_tags.get_parent(child)
self.assertEqual(result, parent) self.assertEqual(result, parent)
@ -110,6 +114,7 @@ class TemplateTags(TestCase):
user=self.remote_user, book=self.book) user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status)) self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Favorite.objects.create( models.Favorite.objects.create(
user=self.user, user=self.user,
status=status status=status
@ -123,6 +128,7 @@ class TemplateTags(TestCase):
user=self.remote_user, book=self.book) user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status)) self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Boost.objects.create( models.Boost.objects.create(
user=self.user, user=self.user,
boosted_status=status boosted_status=status
@ -135,6 +141,7 @@ class TemplateTags(TestCase):
self.assertFalse( self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)) bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.UserFollowRequest.objects.create( models.UserFollowRequest.objects.create(
user_subject=self.user, user_subject=self.user,
user_object=self.remote_user) user_object=self.remote_user)
@ -147,6 +154,7 @@ class TemplateTags(TestCase):
def test_get_boosted(self): def test_get_boosted(self):
''' load a boosted status ''' ''' load a boosted status '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Review.objects.create( status = models.Review.objects.create(
user=self.remote_user, book=self.book) user=self.remote_user, book=self.book)
boost = models.Boost.objects.create( boost = models.Boost.objects.create(
@ -233,6 +241,7 @@ class TemplateTags(TestCase):
def test_get_status_preview_name(self): def test_get_status_preview_name(self):
''' status context string ''' ''' status context string '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(content='hi', user=self.user) status = models.Status.objects.create(content='hi', user=self.user)
result = bookwyrm_tags.get_status_preview_name(status) result = bookwyrm_tags.get_status_preview_name(status)
self.assertEqual(result, 'status') self.assertEqual(result, 'status')
@ -255,6 +264,7 @@ class TemplateTags(TestCase):
def test_related_status(self): def test_related_status(self):
''' gets the subclass model for a notification status ''' ''' gets the subclass model for a notification status '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create(content='hi', user=self.user) status = models.Status.objects.create(content='hi', user=self.user)
notification = models.Notification.objects.create( notification = models.Notification.objects.create(
user=self.user, notification_type='MENTION', user=self.user, notification_type='MENTION',

View file

@ -84,7 +84,7 @@ class AuthorViews(TestCase):
request = self.factory.post('', form.data) request = self.factory.post('', form.data)
request.user = self.local_user 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) view(request, author.id)
author.refresh_from_db() author.refresh_from_db()
self.assertEqual(author.name, 'New Name') self.assertEqual(author.name, 'New Name')

View file

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

View file

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

View file

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

View file

@ -46,7 +46,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.assertEqual(models.UserFollowRequest.objects.count(), 0) 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) views.follow(request)
rel = models.UserFollowRequest.objects.get() rel = models.UserFollowRequest.objects.get()
@ -62,7 +62,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.remote_user.followers.add(self.local_user) self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1) 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) views.unfollow(request)
self.assertEqual(self.remote_user.followers.count(), 0) self.assertEqual(self.remote_user.followers.count(), 0)
@ -77,7 +77,7 @@ class BookViews(TestCase):
user_object=self.local_user 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) views.accept_follow_request(request)
# request should be deleted # request should be deleted
self.assertEqual( self.assertEqual(
@ -96,7 +96,7 @@ class BookViews(TestCase):
user_object=self.local_user 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) views.delete_follow_request(request)
# request should be deleted # request should be deleted
self.assertEqual( self.assertEqual(

View file

@ -100,7 +100,7 @@ class GoalViews(TestCase):
'post-status': True 'post-status': True
}) })
request.user = self.local_user 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) view(request, self.local_user.localname, 2020)
goal = models.AnnualGoal.objects.get() goal = models.AnnualGoal.objects.get()

View file

@ -38,6 +38,7 @@ class ViewsHelpers(TestCase):
) )
self.userdata = json.loads(datafile.read_bytes()) self.userdata = json.loads(datafile.read_bytes())
del self.userdata['icon'] del self.userdata['icon']
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.shelf = models.Shelf.objects.create( self.shelf = models.Shelf.objects.create(
name='Test Shelf', name='Test Shelf',
identifier='test-shelf', identifier='test-shelf',
@ -83,6 +84,7 @@ class ViewsHelpers(TestCase):
rat = models.User.objects.create_user( rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True) 'rat', 'rat@rat.rat', 'password', local=True)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
public_status = models.Comment.objects.create( public_status = models.Comment.objects.create(
content='public status', book=self.book, user=self.local_user) content='public status', book=self.book, user=self.local_user)
direct_status = models.Status.objects.create( direct_status = models.Status.objects.create(
@ -159,6 +161,7 @@ class ViewsHelpers(TestCase):
rat = models.User.objects.create_user( rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True) 'rat', 'rat@rat.rat', 'password', local=True)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
public_status = models.Comment.objects.create( public_status = models.Comment.objects.create(
content='public status', book=self.book, user=self.local_user) content='public status', book=self.book, user=self.local_user)
rat_public = models.Status.objects.create( rat_public = models.Status.objects.create(
@ -240,7 +243,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_to_read(self): def test_handle_reading_status_to_read(self):
''' posts shelve activities ''' ''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='to-read') 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( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public') self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get() status = models.GeneratedNote.objects.get()
@ -251,7 +254,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_reading(self): def test_handle_reading_status_reading(self):
''' posts shelve activities ''' ''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='reading') 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( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public') self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get() status = models.GeneratedNote.objects.get()
@ -262,7 +265,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_read(self): def test_handle_reading_status_read(self):
''' posts shelve activities ''' ''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='read') 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( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, 'public') self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get() status = models.GeneratedNote.objects.get()
@ -272,7 +275,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_other(self): def test_handle_reading_status_other(self):
''' posts shelve activities ''' ''' posts shelve activities '''
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
views.helpers.handle_reading_status( views.helpers.handle_reading_status(
self.local_user, self.shelf, self.book, 'public') self.local_user, self.shelf, self.book, 'public')
self.assertFalse(models.GeneratedNote.objects.exists()) self.assertFalse(models.GeneratedNote.objects.exists())

View file

@ -38,10 +38,10 @@ class InteractionViews(TestCase):
view = views.Favorite.as_view() view = views.Favorite.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.remote_user request.user = self.remote_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi') user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id) view(request, status.id)
fav = models.Favorite.objects.get() fav = models.Favorite.objects.get()
self.assertEqual(fav.status, status) self.assertEqual(fav.status, status)
@ -58,15 +58,15 @@ class InteractionViews(TestCase):
view = views.Unfavorite.as_view() view = views.Unfavorite.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.remote_user request.user = self.remote_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi') user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.Favorite.as_view()(request, status.id) views.Favorite.as_view()(request, status.id)
self.assertEqual(models.Favorite.objects.count(), 1) self.assertEqual(models.Favorite.objects.count(), 1)
self.assertEqual(models.Notification.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) view(request, status.id)
self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
@ -77,10 +77,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.remote_user request.user = self.remote_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi') user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id) view(request, status.id)
boost = models.Boost.objects.get() boost = models.Boost.objects.get()
@ -99,10 +99,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='unlisted') user=self.local_user, content='hi', privacy='unlisted')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id) view(request, status.id)
boost = models.Boost.objects.get() boost = models.Boost.objects.get()
@ -113,10 +113,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='followers') user=self.local_user, content='hi', privacy='followers')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id) view(request, status.id)
self.assertFalse(models.Boost.objects.exists()) self.assertFalse(models.Boost.objects.exists())
@ -125,10 +125,10 @@ class InteractionViews(TestCase):
view = views.Boost.as_view() view = views.Boost.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi') user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, status.id) view(request, status.id)
view(request, status.id) view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Boost.objects.count(), 1)
@ -139,14 +139,14 @@ class InteractionViews(TestCase):
view = views.Unboost.as_view() view = views.Unboost.as_view()
request = self.factory.post('') request = self.factory.post('')
request.user = self.remote_user request.user = self.remote_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.local_user, content='hi') user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.Boost.as_view()(request, status.id) views.Boost.as_view()(request, status.id)
self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Boost.objects.count(), 1)
self.assertEqual(models.Notification.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) view(request, status.id)
self.assertEqual(models.Boost.objects.count(), 0) self.assertEqual(models.Boost.objects.count(), 0)
self.assertEqual(models.Notification.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 from bookwyrm.activitypub import ActivitypubResponse
@patch('bookwyrm.broadcast.broadcast_task.delay')
class ListViews(TestCase): class ListViews(TestCase):
''' tag views''' ''' tag views'''
def setUp(self): def setUp(self):
@ -32,6 +31,7 @@ class ListViews(TestCase):
remote_id='https://example.com/book/1', remote_id='https://example.com/book/1',
parent_work=work, parent_work=work,
) )
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.list = models.List.objects.create( self.list = models.List.objects.create(
name='Test List', user=self.local_user) name='Test List', user=self.local_user)
self.anonymous_user = AnonymousUser self.anonymous_user = AnonymousUser
@ -39,9 +39,10 @@ class ListViews(TestCase):
models.SiteSettings.objects.create() 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 ''' ''' there are so many views, this just makes sure it LOADS '''
view = views.Lists.as_view() view = views.Lists.as_view()
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='Public list', user=self.local_user)
models.List.objects.create( models.List.objects.create(
name='Private list', privacy='private', user=self.local_user) name='Private list', privacy='private', user=self.local_user)
@ -61,8 +62,16 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_lists_create(self, _): def test_lists_create(self):
''' create list view ''' ''' 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() view = views.Lists.as_view()
request = self.factory.post('', { request = self.factory.post('', {
'name': 'A list', 'name': 'A list',
@ -78,9 +87,10 @@ class ListViews(TestCase):
self.assertEqual(new_list.description, 'wow') self.assertEqual(new_list.description, 'wow')
self.assertEqual(new_list.privacy, 'unlisted') self.assertEqual(new_list.privacy, 'unlisted')
self.assertEqual(new_list.curation, 'open') 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 ''' ''' there are so many views, this just makes sure it LOADS '''
view = views.List.as_view() view = views.List.as_view()
request = self.factory.get('') request = self.factory.get('')
@ -116,8 +126,17 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_list_edit(self, _): def test_list_edit(self):
''' edit a list ''' ''' 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() view = views.List.as_view()
request = self.factory.post('', { request = self.factory.post('', {
'name': 'New Name', 'name': 'New Name',
@ -136,11 +155,13 @@ class ListViews(TestCase):
self.assertEqual(self.list.description, 'wow') self.assertEqual(self.list.description, 'wow')
self.assertEqual(self.list.privacy, 'direct') self.assertEqual(self.list.privacy, 'direct')
self.assertEqual(self.list.curation, 'curated') 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 ''' ''' there are so many views, this just makes sure it LOADS '''
view = views.Curate.as_view() view = views.Curate.as_view()
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='Public list', user=self.local_user)
models.List.objects.create( models.List.objects.create(
name='Private list', privacy='private', user=self.local_user) name='Private list', privacy='private', user=self.local_user)
@ -157,12 +178,22 @@ class ListViews(TestCase):
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
def test_curate_approve(self, _): def test_curate_approve(self):
''' approve a pending item ''' ''' 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() view = views.Curate.as_view()
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
pending = models.ListItem.objects.create( pending = models.ListItem.objects.create(
book_list=self.list, book_list=self.list,
added_by=self.local_user, user=self.local_user,
book=self.book, book=self.book,
approved=False approved=False
) )
@ -178,14 +209,16 @@ class ListViews(TestCase):
self.assertEqual(self.list.books.count(), 1) self.assertEqual(self.list.books.count(), 1)
self.assertEqual(self.list.listitem_set.first(), pending) self.assertEqual(self.list.listitem_set.first(), pending)
self.assertTrue(pending.approved) self.assertTrue(pending.approved)
models.ListItem.broadcast = real_broadcast
def test_curate_reject(self, _): def test_curate_reject(self):
''' approve a pending item ''' ''' approve a pending item '''
view = views.Curate.as_view() view = views.Curate.as_view()
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
pending = models.ListItem.objects.create( pending = models.ListItem.objects.create(
book_list=self.list, book_list=self.list,
added_by=self.local_user, user=self.local_user,
book=self.book, book=self.book,
approved=False approved=False
) )
@ -196,13 +229,22 @@ class ListViews(TestCase):
}) })
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request, self.list.id) view(request, self.list.id)
self.assertFalse(self.list.books.exists()) self.assertFalse(self.list.books.exists())
self.assertFalse(models.ListItem.objects.exists()) self.assertFalse(models.ListItem.objects.exists())
def test_add_book(self, _): def test_add_book(self):
''' put a book on a list ''' ''' 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('', { request = self.factory.post('', {
'book': self.book.id, 'book': self.book.id,
}) })
@ -211,14 +253,23 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id) views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) 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) 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 ''' ''' 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.curation = 'open'
self.list.save() self.list.save(broadcast=False)
request = self.factory.post('', { request = self.factory.post('', {
'book': self.book.id, 'book': self.book.id,
}) })
@ -227,14 +278,24 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id) views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.rat) self.assertEqual(item.user, self.rat)
self.assertTrue(item.approved) self.assertTrue(item.approved)
models.ListItem.broadcast = real_broadcast
def test_add_book_pending(self, _): def test_add_book_pending(self):
''' put a book on a list ''' ''' 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.curation = 'curated'
self.list.save() self.list.save(broadcast=False)
request = self.factory.post('', { request = self.factory.post('', {
'book': self.book.id, 'book': self.book.id,
}) })
@ -243,14 +304,24 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id) views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.added_by, self.rat) self.assertEqual(item.user, self.rat)
self.assertFalse(item.approved) self.assertFalse(item.approved)
models.ListItem.broadcast = real_broadcast
def test_add_book_self_curated(self, _): def test_add_book_self_curated(self):
''' put a book on a list ''' ''' 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.curation = 'curated'
self.list.save() self.list.save(broadcast=False)
request = self.factory.post('', { request = self.factory.post('', {
'book': self.book.id, 'book': self.book.id,
}) })
@ -259,18 +330,30 @@ class ListViews(TestCase):
views.list.add_book(request, self.list.id) views.list.add_book(request, self.list.id)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) 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) 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 ''' ''' take an item off a list '''
real_broadcast = models.ListItem.broadcast
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
item = models.ListItem.objects.create( item = models.ListItem.objects.create(
book_list=self.list, book_list=self.list,
added_by=self.local_user, user=self.local_user,
book=self.book, book=self.book,
) )
self.assertTrue(self.list.listitem_set.exists()) 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('', { request = self.factory.post('', {
'item': item.id, 'item': item.id,
}) })
@ -279,13 +362,15 @@ class ListViews(TestCase):
views.list.remove_book(request, self.list.id) views.list.remove_book(request, self.list.id)
self.assertFalse(self.list.listitem_set.exists()) 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 ''' ''' take an item off a list '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
item = models.ListItem.objects.create( item = models.ListItem.objects.create(
book_list=self.list, book_list=self.list,
added_by=self.local_user, user=self.local_user,
book=self.book, book=self.book,
) )
self.assertTrue(self.list.listitem_set.exists()) self.assertTrue(self.list.listitem_set.exists())

View file

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

View file

@ -45,7 +45,7 @@ class ReadingViews(TestCase):
'start_date': '2020-01-05', 'start_date': '2020-01-05',
}) })
request.user = self.local_user 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) views.start_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
@ -65,8 +65,9 @@ class ReadingViews(TestCase):
def test_start_reading_reshelf(self): def test_start_reading_reshelf(self):
''' begin a book ''' ''' begin a book '''
to_read_shelf = self.local_user.shelf_set.get(identifier='to-read') to_read_shelf = self.local_user.shelf_set.get(identifier='to-read')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
shelf=to_read_shelf, book=self.book, added_by=self.local_user) shelf=to_read_shelf, book=self.book, user=self.local_user)
shelf = self.local_user.shelf_set.get(identifier='reading') shelf = self.local_user.shelf_set.get(identifier='reading')
self.assertEqual(to_read_shelf.books.get(), self.book) self.assertEqual(to_read_shelf.books.get(), self.book)
self.assertFalse(shelf.books.exists()) self.assertFalse(shelf.books.exists())
@ -74,7 +75,7 @@ class ReadingViews(TestCase):
request = self.factory.post('') request = self.factory.post('')
request.user = self.local_user 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) views.start_reading(request, self.book.id)
self.assertFalse(to_read_shelf.books.exists()) self.assertFalse(to_read_shelf.books.exists())
@ -98,7 +99,7 @@ class ReadingViews(TestCase):
}) })
request.user = self.local_user 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) views.finish_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book) 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 unittest.mock import patch
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils import timezone from django.utils import timezone
from datetime import datetime
from bookwyrm import models from bookwyrm import models
@patch('bookwyrm.broadcast.broadcast_task.delay') @patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
class ReadThrough(TestCase): class ReadThrough(TestCase):
''' readthrough tests '''
def setUp(self): def setUp(self):
''' basic user and book data '''
self.client = Client() self.client = Client()
self.work = models.Work.objects.create( self.work = models.Work.objects.create(
@ -25,6 +28,7 @@ class ReadThrough(TestCase):
'cinco', 'cinco@example.com', 'seissiete', 'cinco', 'cinco@example.com', 'seissiete',
local=True, localname='cinco') local=True, localname='cinco')
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.client.force_login(self.user) self.client.force_login(self.user)
def test_create_basic_readthrough(self, delay_mock): def test_create_basic_readthrough(self, delay_mock):
@ -38,13 +42,15 @@ class ReadThrough(TestCase):
readthroughs = self.edition.readthrough_set.all() readthroughs = self.edition.readthrough_set.all()
self.assertEqual(len(readthroughs), 1) self.assertEqual(len(readthroughs), 1)
self.assertEqual(readthroughs[0].progressupdate_set.count(), 0) 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)) datetime(2020, 11, 27, tzinfo=timezone.utc))
self.assertEqual(readthroughs[0].progress, None) self.assertEqual(readthroughs[0].progress, None)
self.assertEqual(readthroughs[0].finish_date, None) self.assertEqual(readthroughs[0].finish_date, None)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
def test_create_progress_readthrough(self, delay_mock): def test_create_progress_readthrough(self, delay_mock):
''' a readthrough with progress '''
self.assertEqual(self.edition.readthrough_set.count(), 0) self.assertEqual(self.edition.readthrough_set.count(), 0)
self.client.post('/start-reading/{}'.format(self.edition.id), { self.client.post('/start-reading/{}'.format(self.edition.id), {
@ -54,7 +60,8 @@ class ReadThrough(TestCase):
readthroughs = self.edition.readthrough_set.all() readthroughs = self.edition.readthrough_set.all()
self.assertEqual(len(readthroughs), 1) self.assertEqual(len(readthroughs), 1)
self.assertEqual(readthroughs[0].start_date, self.assertEqual(
readthroughs[0].start_date,
datetime(2020, 11, 27, tzinfo=timezone.utc)) datetime(2020, 11, 27, tzinfo=timezone.utc))
self.assertEqual(readthroughs[0].progress, 50) self.assertEqual(readthroughs[0].progress, 50)
self.assertEqual(readthroughs[0].finish_date, None) self.assertEqual(readthroughs[0].finish_date, None)
@ -76,7 +83,9 @@ class ReadThrough(TestCase):
self.assertEqual(len(progress_updates), 2) self.assertEqual(len(progress_updates), 2)
self.assertEqual(progress_updates[1].mode, models.ProgressMode.PAGE) self.assertEqual(progress_updates[1].mode, models.ProgressMode.PAGE)
self.assertEqual(progress_updates[1].progress, 100) 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', { self.client.post('/delete-readthrough', {
'id': readthroughs[0].id, 'id': readthroughs[0].id,

View file

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

View file

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

View file

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

View file

@ -39,6 +39,7 @@ class TagViews(TestCase):
def test_tag_page(self): def test_tag_page(self):
''' there are so many views, this just makes sure it LOADS ''' ''' there are so many views, this just makes sure it LOADS '''
view = views.Tag.as_view() view = views.Tag.as_view()
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
tag = models.Tag.objects.create(name='hi there') tag = models.Tag.objects.create(name='hi there')
models.UserTag.objects.create( models.UserTag.objects.create(
tag=tag, user=self.local_user, book=self.book) tag=tag, user=self.local_user, book=self.book)
@ -68,7 +69,7 @@ class TagViews(TestCase):
}) })
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request) view(request)
tag = models.Tag.objects.get() tag = models.Tag.objects.get()
@ -82,6 +83,7 @@ class TagViews(TestCase):
def test_untag(self): def test_untag(self):
''' remove a tag from a book ''' ''' remove a tag from a book '''
view = views.RemoveTag.as_view() view = views.RemoveTag.as_view()
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
tag = models.Tag.objects.create(name='A Tag!?') tag = models.Tag.objects.create(name='A Tag!?')
models.UserTag.objects.create( models.UserTag.objects.create(
user=self.local_user, book=self.book, tag=tag) user=self.local_user, book=self.book, tag=tag)
@ -93,7 +95,7 @@ class TagViews(TestCase):
}) })
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request) view(request)
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists()) 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 = self.factory.post('', form.data)
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
view(request) view(request)
self.assertEqual(self.local_user.name, 'New Name') 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 .federation import Federation
from .feed import DirectMessage, Feed, Replies, Status from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow 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 .goal import Goal
from .import_data import Import, ImportStatus from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost from .interaction import Favorite, Unfavorite, Boost, Unboost

View file

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

View file

@ -8,7 +8,6 @@ from django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from .helpers import is_api_request from .helpers import is_api_request
@ -62,5 +61,4 @@ class EditAuthor(View):
return TemplateResponse(request, 'edit_author.html', data) return TemplateResponse(request, 'edit_author.html', data)
author = form.save() author = form.save()
broadcast(request.user, author.to_update_activity(request.user))
return redirect('/author/%s' % author.id) 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 django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast
# pylint: disable= no-self-use # pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
@ -22,15 +21,8 @@ class Block(View):
def post(self, request, user_id): def post(self, request, user_id):
''' block a user ''' ''' block a user '''
to_block = get_object_or_404(models.User, id=user_id) 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) 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') return redirect('/preferences/block')
@ -46,13 +38,5 @@ def unblock(request, user_id):
) )
except models.UserBlocks.DoesNotExist: except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()
if not to_unblock.local:
broadcast(
request.user,
block.to_undo_activity(request.user),
privacy='direct',
direct_recipients=[to_unblock]
)
block.delete() block.delete()
return redirect('/preferences/block') 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 import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_activity_feed, get_edition from .helpers import is_api_request, get_activity_feed, get_edition
@ -78,12 +77,12 @@ class Book(View):
.order_by('-updated_date') .order_by('-updated_date')
user_shelves = models.ShelfBook.objects.filter( user_shelves = models.ShelfBook.objects.filter(
added_by=request.user, book=book user=request.user, book=book
) )
other_edition_shelves = models.ShelfBook.objects.filter( other_edition_shelves = models.ShelfBook.objects.filter(
~Q(book=book), ~Q(book=book),
added_by=request.user, user=request.user,
book__parent_work=book.parent_work, book__parent_work=book.parent_work,
) )
@ -136,7 +135,6 @@ class EditBook(View):
return TemplateResponse(request, 'edit_book.html', data) return TemplateResponse(request, 'edit_book.html', data)
book = form.save() book = form.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@ -170,7 +168,6 @@ def upload_cover(request, book_id):
book.cover = form.files['cover'] book.cover = form.files['cover']
book.save() book.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@ -189,7 +186,6 @@ def add_description(request, book_id):
book.description = description book.description = description
book.save() book.save()
broadcast(request.user, book.to_update_activity(request.user))
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@ -215,12 +211,14 @@ def switch_edition(request):
shelf__user=request.user shelf__user=request.user
) )
for shelfbook in shelfbooks.all(): for shelfbook in shelfbooks.all():
broadcast(request.user, shelfbook.to_remove_activity(request.user)) with transaction.atomic():
models.ShelfBook.objects.create(
shelfbook.book = new_edition created_date=shelfbook.created_date,
shelfbook.save() user=shelfbook.user,
shelf=shelfbook.shelf,
broadcast(request.user, shelfbook.to_add_activity(request.user)) book=new_edition
)
shelfbook.delete()
readthroughs = models.ReadThrough.objects.filter( readthroughs = models.ReadThrough.objects.filter(
book__parent_work=new_edition.parent_work, book__parent_work=new_edition.parent_work,

View file

@ -1,12 +1,10 @@
''' views for actions you can take in the application ''' ''' views for actions you can take in the application '''
from django.db import transaction
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast
from .helpers import get_user_from_username from .helpers import get_user_from_username
@login_required @login_required
@ -19,13 +17,10 @@ def follow(request):
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
relationship, _ = models.UserFollowRequest.objects.get_or_create( models.UserFollowRequest.objects.get_or_create(
user_subject=request.user, user_subject=request.user,
user_object=to_follow, user_object=to_follow,
) )
activity = relationship.to_activity()
broadcast(
request.user, activity, privacy='direct', direct_recipients=[to_follow])
return redirect(to_follow.local_path) return redirect(to_follow.local_path)
@ -39,14 +34,10 @@ def unfollow(request):
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
relationship = models.UserFollows.objects.get( models.UserFollows.objects.get(
user_subject=request.user, user_subject=request.user,
user_object=to_unfollow 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) to_unfollow.followers.remove(request.user)
return redirect(to_unfollow.local_path) return redirect(to_unfollow.local_path)
@ -70,24 +61,11 @@ def accept_follow_request(request):
except models.UserFollowRequest.DoesNotExist: except models.UserFollowRequest.DoesNotExist:
# Request already dealt with. # Request already dealt with.
return redirect(request.user.local_path) return redirect(request.user.local_path)
handle_accept(follow_request) follow_request.accept()
return redirect(request.user.local_path) 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 @login_required
@require_POST @require_POST
def delete_follow_request(request): def delete_follow_request(request):
@ -106,8 +84,5 @@ def delete_follow_request(request):
except models.UserFollowRequest.DoesNotExist: except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
activity = follow_request.to_reject_activity()
follow_request.delete() follow_request.delete()
broadcast(
request.user, activity, privacy='direct', direct_recipients=[requester])
return redirect('/user/%s' % request.user.localname) 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 django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_generated_note from bookwyrm.status import create_generated_note
from .helpers import get_user_from_username, object_visible_to_user from .helpers import get_user_from_username, object_visible_to_user
@ -63,23 +62,10 @@ class Goal(View):
if request.POST.get('post-status'): if request.POST.get('post-status'):
# create status, if appropraite # create status, if appropraite
status = create_generated_note( create_generated_note(
request.user, request.user,
'set a goal to read %d books in %d' % (goal.goal, goal.year), 'set a goal to read %d books in %d' % (goal.goal, goal.year),
privacy=goal.privacy 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', '/')) return redirect(request.headers.get('Referer', '/'))

View file

@ -4,7 +4,6 @@ from requests import HTTPError
from django.db.models import Q from django.db.models import Q
from bookwyrm import activitypub, models from bookwyrm import activitypub, models
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import ConnectorException, get_data from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.status import create_generated_note from bookwyrm.status import create_generated_note
from bookwyrm.utils import regex from bookwyrm.utils import regex
@ -199,7 +198,6 @@ def handle_reading_status(user, shelf, book, privacy):
) )
status.save() status.save()
broadcast(user, status.to_create_activity(user))
def is_blocked(viewer, user): def is_blocked(viewer, user):
''' is this viewer blocked by the 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 django.views import View
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_notification from bookwyrm.status import create_notification
@ -19,7 +18,7 @@ class Favorite(View):
''' create a like ''' ''' create a like '''
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
try: try:
favorite = models.Favorite.objects.create( models.Favorite.objects.create(
status=status, status=status,
user=request.user user=request.user
) )
@ -27,10 +26,6 @@ class Favorite(View):
# you already fav'ed that # you already fav'ed that
return HttpResponseBadRequest() return HttpResponseBadRequest()
fav_activity = favorite.to_activity()
broadcast(
request.user, fav_activity, privacy='direct',
direct_recipients=[status.user])
if status.user.local: if status.user.local:
create_notification( create_notification(
status.user, status.user,
@ -56,9 +51,7 @@ class Unfavorite(View):
# can't find that status, idk # can't find that status, idk
return HttpResponseNotFound() return HttpResponseNotFound()
fav_activity = favorite.to_undo_activity(request.user)
favorite.delete() favorite.delete()
broadcast(request.user, fav_activity, direct_recipients=[status.user])
# check for notification # check for notification
if status.user.local: if status.user.local:
@ -86,15 +79,12 @@ class Boost(View):
# you already boosted that. # you already boosted that.
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
boost = models.Boost.objects.create( models.Boost.objects.create(
boosted_status=status, boosted_status=status,
privacy=status.privacy, privacy=status.privacy,
user=request.user, user=request.user,
) )
boost_activity = boost.to_activity()
broadcast(request.user, boost_activity)
if status.user.local: if status.user.local:
create_notification( create_notification(
status.user, status.user,
@ -114,10 +104,8 @@ class Unboost(View):
boost = models.Boost.objects.filter( boost = models.Boost.objects.filter(
boosted_status=status, user=request.user boosted_status=status, user=request.user
).first() ).first()
activity = boost.to_undo_activity(request.user)
boost.delete() boost.delete()
broadcast(request.user, activity)
# delete related notification # delete related notification
if status.user.local: 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 import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from .helpers import is_api_request, object_visible_to_user, privacy_filter from .helpers import is_api_request, object_visible_to_user, privacy_filter
from .helpers import get_user_from_username from .helpers import get_user_from_username
@ -55,13 +54,6 @@ class Lists(View):
return redirect('lists') return redirect('lists')
book_list = form.save() 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) return redirect(book_list.local_path)
class UserLists(View): class UserLists(View):
@ -136,19 +128,12 @@ class List(View):
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post(self, request, list_id): def post(self, request, list_id):
''' edit a book_list ''' ''' edit a list '''
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
form = forms.ListForm(request.POST, instance=book_list) form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid(): if not form.is_valid():
return redirect('list', book_list.id) return redirect('list', book_list.id)
book_list = form.save() 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) return redirect(book_list.local_path)
@ -182,13 +167,6 @@ class Curate(View):
if approved: if approved:
suggestion.approved = True suggestion.approved = True
suggestion.save() suggestion.save()
# let the world know
broadcast(
request.user,
suggestion.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
else: else:
suggestion.delete() suggestion.delete()
return redirect('list-curate', book_list.id) 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? # do you have permission to add to the list?
if request.user == book_list.user or book_list.curation == 'open': if request.user == book_list.user or book_list.curation == 'open':
# go ahead and add it # go ahead and add it
item = models.ListItem.objects.create( models.ListItem.objects.create(
book=book, book=book,
book_list=book_list, book_list=book_list,
added_by=request.user, user=request.user,
)
# let the world know
broadcast(
request.user,
item.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
) )
elif book_list.curation == 'curated': elif book_list.curation == 'curated':
# make a pending entry # make a pending entry
@ -223,7 +194,7 @@ def add_book(request, list_id):
approved=False, approved=False,
book=book, book=book,
book_list=book_list, book_list=book_list,
added_by=request.user, user=request.user,
) )
else: else:
# you can't add to this list, what were you THINKING # 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) book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get('item')) 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() return HttpResponseNotFound()
activity = item.to_remove_activity(request.user)
item.delete() item.delete()
# let the world know
broadcast(
request.user,
activity,
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect('list', list_id) return redirect('list', list_id)

View file

@ -79,7 +79,7 @@ class PasswordReset(View):
return TemplateResponse(request, 'password_reset.html', data) return TemplateResponse(request, 'password_reset.html', data)
user.set_password(new_password) user.set_password(new_password)
user.save() user.save(broadcast=False)
login(request, user) login(request, user)
reset_code.delete() reset_code.delete()
return redirect('/') return redirect('/')
@ -106,6 +106,6 @@ class ChangePassword(View):
return redirect('preferences/password') return redirect('preferences/password')
request.user.set_password(new_password) request.user.set_password(new_password)
request.user.save() request.user.save(broadcast=False)
login(request, request.user) login(request, request.user)
return redirect(request.user.local_path) 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 django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast
from .helpers import get_edition, handle_reading_status from .helpers import get_edition, handle_reading_status
from .shelf import handle_unshelve from .shelf import handle_unshelve
@ -44,9 +43,8 @@ def start_reading(request, book_id):
except models.Shelf.DoesNotExist: except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves # this just means it isn't currently on the user's shelves
pass pass
shelfbook = models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=shelf, added_by=request.user) book=book, shelf=shelf, user=request.user)
broadcast(request.user, shelfbook.to_add_activity(request.user))
# post about it (if you want) # post about it (if you want)
if request.POST.get('post-status'): if request.POST.get('post-status'):
@ -82,9 +80,8 @@ def finish_reading(request, book_id):
except models.Shelf.DoesNotExist: except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves # this just means it isn't currently on the user's shelves
pass pass
shelfbook = models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=shelf, added_by=request.user) book=book, shelf=shelf, user=request.user)
broadcast(request.user, shelfbook.to_add_activity(request.user))
# post about it (if you want) # post about it (if you want)
if request.POST.get('post-status'): 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 import forms, models
from bookwyrm.activitypub import ActivitypubResponse 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 is_api_request, get_edition, get_user_from_username
from .helpers import handle_reading_status from .helpers import handle_reading_status
@ -50,7 +49,7 @@ class Shelf(View):
return ActivitypubResponse(shelf.to_activity(**request.GET)) return ActivitypubResponse(shelf.to_activity(**request.GET))
books = models.ShelfBook.objects.filter( books = models.ShelfBook.objects.filter(
added_by=user, shelf=shelf user=user, shelf=shelf
).order_by('-updated_date').all() ).order_by('-updated_date').all()
data = { data = {
@ -136,14 +135,8 @@ def shelve(request):
except models.Shelf.DoesNotExist: except models.Shelf.DoesNotExist:
# this just means it isn't currently on the user's shelves # this just means it isn't currently on the user's shelves
pass pass
shelfbook = models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, added_by=request.user) book=book, shelf=desired_shelf, user=request.user)
broadcast(
request.user,
shelfbook.to_add_activity(request.user),
privacy=shelfbook.shelf.privacy,
software='bookwyrm'
)
# post about "want to read" shelves # post about "want to read" shelves
if desired_shelf.identifier == 'to-read': if desired_shelf.identifier == 'to-read':
@ -168,10 +161,8 @@ def unshelve(request):
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
#pylint: disable=unused-argument
def handle_unshelve(user, book, shelf): def handle_unshelve(user, book, shelf):
''' unshelve a book ''' ''' unshelve a book '''
row = models.ShelfBook.objects.get(book=book, shelf=shelf) row = models.ShelfBook.objects.get(book=book, shelf=shelf)
activity = row.to_remove_activity(user)
row.delete() 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 markdown import markdown
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.broadcast import broadcast
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.status import create_notification, delete_status from bookwyrm.status import create_notification, delete_status
@ -35,7 +34,7 @@ class CreateStatus(View):
if not status.sensitive and status.content_warning: if not status.sensitive and status.content_warning:
# the cw text field remains populated when you click "remove" # the cw text field remains populated when you click "remove"
status.content_warning = None status.content_warning = None
status.save() status.save(broadcast=False)
# inspect the text for user tags # inspect the text for user tags
content = status.content content = status.content
@ -83,16 +82,7 @@ class CreateStatus(View):
if hasattr(status, 'quote'): if hasattr(status, 'quote'):
status.quote = to_markdown(status.quote) status.quote = to_markdown(status.quote)
status.save() status.save(created=True)
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')
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
@ -108,7 +98,6 @@ class DeleteStatus(View):
# perform deletion # perform deletion
delete_status(status) delete_status(status)
broadcast(request.user, status.to_delete_activity(request.user))
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
def find_mentions(content): def find_mentions(content):

View file

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

View file

@ -15,7 +15,6 @@ from django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username, is_api_request from .helpers import get_activity_feed, get_user_from_username, is_api_request
from .helpers import is_blocked, object_visible_to_user from .helpers import is_blocked, object_visible_to_user
@ -176,7 +175,6 @@ class EditUser(View):
user.avatar.save(filename, image) user.avatar.save(filename, image)
user.save() user.save()
broadcast(user, user.to_update_activity(user))
return redirect(user.local_path) return redirect(user.local_path)