forked from mirrors/bookwyrm
circular import issues and added_by migration
This commit is contained in:
parent
5a3a6151a6
commit
7381536ad6
19 changed files with 273 additions and 265 deletions
|
@ -81,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,7 +90,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
||||||
user=user
|
user=user
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
|
||||||
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
|
||||||
|
|
23
bookwyrm/migrations/0043_auto_20210204_2223.py
Normal file
23
bookwyrm/migrations/0043_auto_20210204_2223.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,20 +2,25 @@
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
|
from base64 import b64encode
|
||||||
|
from uuid import uuid4
|
||||||
import requests
|
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.apps import apps
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.http import http_date
|
from django.utils.http import http_date
|
||||||
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import USER_AGENT
|
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
|
||||||
from bookwyrm.signatures import make_signature, make_digest
|
from bookwyrm.signatures import make_signature, make_digest
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
from .fields import ImageField, ManyToManyField
|
from bookwyrm.models.fields import ImageField, ManyToManyField
|
||||||
|
|
||||||
|
|
||||||
class ActivitypubMixin:
|
class ActivitypubMixin:
|
||||||
|
@ -247,3 +252,218 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
|
|
||||||
if activity and user and user.local:
|
if activity and user and user.local:
|
||||||
instance.broadcast(activity, user)
|
instance.broadcast(activity, user)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectMixin(ActivitypubMixin):
|
||||||
|
''' add this mixin for object models that are AP serializable '''
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' broadcast updated '''
|
||||||
|
# first off, we want to save normally no matter what
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# we only want to handle updates, not newly created objects
|
||||||
|
if not self.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# this will work for lists, shelves
|
||||||
|
user = self.user if hasattr(self, 'user') else None
|
||||||
|
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 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:
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionItemMixin(ActivitypubMixin):
|
||||||
|
''' for items that are part of an (Ordered)Collection '''
|
||||||
|
activity_serializer = activitypub.Add
|
||||||
|
object_field = collection_field = None
|
||||||
|
|
||||||
|
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 ActivitybMixin(ActivitypubMixin):
|
||||||
|
''' add this mixin for models that are AP serializable '''
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' broadcast activity '''
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
self.broadcast(self.to_activity(), self.user)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
''' nevermind, undo that activity '''
|
||||||
|
self.broadcast(self.to_undo_activity(), self.user)
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def to_undo_activity(self):
|
||||||
|
''' undo an action '''
|
||||||
|
return activitypub.Undo(
|
||||||
|
id='%s#undo' % self.remote_id,
|
||||||
|
actor=self.user.remote_id,
|
||||||
|
object=self.to_activity()
|
||||||
|
).serialize()
|
|
@ -1 +0,0 @@
|
||||||
from . import *
|
|
|
@ -1,25 +0,0 @@
|
||||||
''' activitypub model functionality '''
|
|
||||||
from bookwyrm import activitypub
|
|
||||||
from . import ActivitypubMixin
|
|
||||||
|
|
||||||
class ActivitybMixin(ActivitypubMixin):
|
|
||||||
''' add this mixin for models that are AP serializable '''
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
''' broadcast activity '''
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
self.broadcast(self.to_activity(), self.user)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
''' nevermind, undo that activity '''
|
|
||||||
self.broadcast(self.to_undo_activity(), self.user)
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def to_undo_activity(self):
|
|
||||||
''' undo an action '''
|
|
||||||
return activitypub.Undo(
|
|
||||||
id='%s#undo' % self.remote_id,
|
|
||||||
actor=self.user.remote_id,
|
|
||||||
object=self.to_activity()
|
|
||||||
).serialize()
|
|
|
@ -1,94 +0,0 @@
|
||||||
''' activitypub objects like Person and Book'''
|
|
||||||
from base64 import b64encode
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Signature import pkcs1_15
|
|
||||||
from Crypto.Hash import SHA256
|
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
|
||||||
from . import ActivitypubMixin
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectMixin(ActivitypubMixin):
|
|
||||||
''' add this mixin for object models that are AP serializable '''
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
''' broadcast updated '''
|
|
||||||
# first off, we want to save normally no matter what
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
# we only want to handle updates, not newly created objects
|
|
||||||
if not self.id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# this will work for lists, shelves
|
|
||||||
user = self.user if hasattr(self, 'user') else None
|
|
||||||
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 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:
|
|
||||||
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()
|
|
|
@ -1,115 +0,0 @@
|
||||||
''' lists of objects '''
|
|
||||||
from django.core.paginator import Paginator
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
|
||||||
from . import ActivitypubMixin, ObjectMixin, generate_activity
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionItemMixin(ActivitypubMixin):
|
|
||||||
''' for items that are part of an (Ordered)Collection '''
|
|
||||||
activity_serializer = activitypub.Add
|
|
||||||
object_field = collection_field = None
|
|
||||||
|
|
||||||
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()
|
|
|
@ -7,7 +7,7 @@ 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 ObjectMixin, OrderedCollectionPageMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 }}">
|
||||||
|
|
|
@ -38,7 +38,7 @@ class List(TestCase):
|
||||||
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)
|
||||||
|
|
|
@ -146,7 +146,7 @@ class GoodreadsImport(TestCase):
|
||||||
''' goodreads import added a book, this adds related connections '''
|
''' goodreads import added a book, this adds related connections '''
|
||||||
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')
|
||||||
|
|
|
@ -92,7 +92,7 @@ class FeedMessageViews(TestCase):
|
||||||
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
|
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
|
||||||
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)
|
||||||
|
|
|
@ -162,7 +162,7 @@ class ListViews(TestCase):
|
||||||
view = views.Curate.as_view()
|
view = views.Curate.as_view()
|
||||||
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
|
||||||
)
|
)
|
||||||
|
@ -185,7 +185,7 @@ class ListViews(TestCase):
|
||||||
view = views.Curate.as_view()
|
view = views.Curate.as_view()
|
||||||
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
|
||||||
)
|
)
|
||||||
|
@ -211,7 +211,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -243,7 +243,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ class ListViews(TestCase):
|
||||||
''' take an item off a list '''
|
''' take an item off a list '''
|
||||||
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())
|
||||||
|
@ -285,7 +285,7 @@ class ListViews(TestCase):
|
||||||
''' take an item off a list '''
|
''' take an item off a list '''
|
||||||
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())
|
||||||
|
|
|
@ -66,7 +66,7 @@ class ReadingViews(TestCase):
|
||||||
''' 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')
|
||||||
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())
|
||||||
|
|
|
@ -77,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,7 @@ def add_book(request, list_id):
|
||||||
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,
|
||||||
)
|
)
|
||||||
elif book_list.curation == 'curated':
|
elif book_list.curation == 'curated':
|
||||||
# make a pending entry
|
# make a pending entry
|
||||||
|
@ -190,7 +190,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
|
||||||
|
@ -205,7 +205,7 @@ 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()
|
||||||
|
|
||||||
item.delete()
|
item.delete()
|
||||||
|
|
|
@ -44,7 +44,7 @@ def start_reading(request, book_id):
|
||||||
# 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
|
||||||
models.ShelfBook.objects.create(
|
models.ShelfBook.objects.create(
|
||||||
book=book, shelf=shelf, added_by=request.user)
|
book=book, shelf=shelf, user=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'):
|
||||||
|
@ -81,7 +81,7 @@ def finish_reading(request, book_id):
|
||||||
# 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
|
||||||
models.ShelfBook.objects.create(
|
models.ShelfBook.objects.create(
|
||||||
book=book, shelf=shelf, added_by=request.user)
|
book=book, shelf=shelf, user=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'):
|
||||||
|
|
|
@ -49,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,7 +136,7 @@ def shelve(request):
|
||||||
# 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
|
||||||
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)
|
||||||
|
|
||||||
# post about "want to read" shelves
|
# post about "want to read" shelves
|
||||||
if desired_shelf.identifier == 'to-read':
|
if desired_shelf.identifier == 'to-read':
|
||||||
|
|
Loading…
Reference in a new issue