cleans up ordered collection mixin
This commit is contained in:
parent
1ec2f20486
commit
eb6206252d
@ -1,18 +1,16 @@
|
|||||||
''' base model with default fields '''
|
''' base model with default fields '''
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Signature import pkcs1_15
|
from Crypto.Signature import pkcs1_15
|
||||||
from Crypto.Hash import SHA256
|
from Crypto.Hash import SHA256
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
|
||||||
from .fields import RemoteIdField
|
from .fields import RemoteIdField
|
||||||
|
|
||||||
|
|
||||||
@ -52,15 +50,6 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
def get_field_name(field):
|
|
||||||
''' model_field_name to activitypubFieldName '''
|
|
||||||
if field.activitypub_field:
|
|
||||||
return field.activitypub_field
|
|
||||||
name = field.name.split('.')[-1]
|
|
||||||
components = name.split('_')
|
|
||||||
return components[0] + ''.join(x.title() for x in components[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def unfurl_related_field(related_field):
|
def unfurl_related_field(related_field):
|
||||||
''' load reverse lookups (like public key owner or Status attachment '''
|
''' load reverse lookups (like public key owner or Status attachment '''
|
||||||
if hasattr(related_field, 'all'):
|
if hasattr(related_field, 'all'):
|
||||||
@ -78,15 +67,16 @@ class ActivitypubMixin:
|
|||||||
def to_activity(self):
|
def to_activity(self):
|
||||||
''' convert from a model to an activity '''
|
''' convert from a model to an activity '''
|
||||||
activity = {}
|
activity = {}
|
||||||
for field in self.__class__._meta.get_fields():
|
for field in self._meta.get_fields():
|
||||||
if not hasattr(field, 'field_to_activity'):
|
if not hasattr(field, 'field_to_activity'):
|
||||||
continue
|
continue
|
||||||
key = get_field_name(field)
|
|
||||||
value = field.field_to_activity(getattr(self, field.name))
|
value = field.field_to_activity(getattr(self, field.name))
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
key = field.get_activitypub_field()
|
||||||
if key in activity and isinstance(activity[key], list):
|
if key in activity and isinstance(activity[key], list):
|
||||||
|
# handles tags on status, which accumulate across fields
|
||||||
activity[key] += value
|
activity[key] += value
|
||||||
else:
|
else:
|
||||||
activity[key] = value
|
activity[key] = value
|
||||||
@ -125,15 +115,12 @@ class ActivitypubMixin:
|
|||||||
|
|
||||||
def to_delete_activity(self, user):
|
def to_delete_activity(self, user):
|
||||||
''' notice of deletion '''
|
''' notice of deletion '''
|
||||||
# this should be a tombstone
|
|
||||||
activity_object = self.to_activity()
|
|
||||||
|
|
||||||
return activitypub.Delete(
|
return activitypub.Delete(
|
||||||
id=self.remote_id + '/activity',
|
id=self.remote_id + '/activity',
|
||||||
actor=user.remote_id,
|
actor=user.remote_id,
|
||||||
to=['%s/followers' % user.remote_id],
|
to=['%s/followers' % user.remote_id],
|
||||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
object=activity_object,
|
object=self.to_activity(),
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
@ -165,81 +152,53 @@ class OrderedCollectionPageMixin(ActivitypubMixin):
|
|||||||
''' this can be overriden if there's a special remote id, ie outbox '''
|
''' this can be overriden if there's a special remote id, ie outbox '''
|
||||||
return self.remote_id
|
return self.remote_id
|
||||||
|
|
||||||
def page(self, min_id=None, max_id=None):
|
|
||||||
''' helper function to create the pagination url '''
|
|
||||||
params = {'page': 'true'}
|
|
||||||
if min_id:
|
|
||||||
params['min_id'] = min_id
|
|
||||||
if max_id:
|
|
||||||
params['max_id'] = max_id
|
|
||||||
return '?%s' % urlencode(params)
|
|
||||||
|
|
||||||
def next_page(self, items):
|
|
||||||
''' use the max id of the last item '''
|
|
||||||
if not items.count():
|
|
||||||
return ''
|
|
||||||
return self.page(max_id=items[items.count() - 1].id)
|
|
||||||
|
|
||||||
def prev_page(self, items):
|
|
||||||
''' use the min id of the first item '''
|
|
||||||
if not items.count():
|
|
||||||
return ''
|
|
||||||
return self.page(min_id=items[0].id)
|
|
||||||
|
|
||||||
def to_ordered_collection_page(self, queryset, remote_id, \
|
|
||||||
id_only=False, min_id=None, max_id=None):
|
|
||||||
''' serialize and pagiante a queryset '''
|
|
||||||
# TODO: weird place to define this
|
|
||||||
limit = 20
|
|
||||||
# filters for use in the django queryset min/max
|
|
||||||
filters = {}
|
|
||||||
if min_id is not None:
|
|
||||||
filters['id__gt'] = min_id
|
|
||||||
if max_id is not None:
|
|
||||||
filters['id__lte'] = max_id
|
|
||||||
page_id = self.page(min_id=min_id, max_id=max_id)
|
|
||||||
|
|
||||||
items = queryset.filter(
|
|
||||||
**filters
|
|
||||||
).all()[:limit]
|
|
||||||
|
|
||||||
if id_only:
|
|
||||||
page = [s.remote_id for s in items]
|
|
||||||
else:
|
|
||||||
page = [s.to_activity() for s in items]
|
|
||||||
return activitypub.OrderedCollectionPage(
|
|
||||||
id='%s%s' % (remote_id, page_id),
|
|
||||||
partOf=remote_id,
|
|
||||||
orderedItems=page,
|
|
||||||
next='%s%s' % (remote_id, self.next_page(items)),
|
|
||||||
prev='%s%s' % (remote_id, self.prev_page(items))
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
def to_ordered_collection(self, queryset, \
|
def to_ordered_collection(self, queryset, \
|
||||||
remote_id=None, page=False, **kwargs):
|
remote_id=None, page=False, **kwargs):
|
||||||
''' an ordered collection of whatevers '''
|
''' an ordered collection of whatevers '''
|
||||||
remote_id = remote_id or self.remote_id
|
remote_id = remote_id or self.remote_id
|
||||||
if page:
|
if page:
|
||||||
return self.to_ordered_collection_page(
|
return to_ordered_collection_page(
|
||||||
queryset, remote_id, **kwargs)
|
queryset, remote_id, **kwargs)
|
||||||
name = ''
|
name = self.name if hasattr(self, 'name') else None
|
||||||
if hasattr(self, 'name'):
|
owner = self.user.remote_id if hasattr(self, 'user') else ''
|
||||||
name = self.name
|
|
||||||
owner = ''
|
|
||||||
if hasattr(self, 'user'):
|
|
||||||
owner = self.user.remote_id
|
|
||||||
|
|
||||||
size = queryset.count()
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
return activitypub.OrderedCollection(
|
return activitypub.OrderedCollection(
|
||||||
id=remote_id,
|
id=remote_id,
|
||||||
totalItems=size,
|
totalItems=paginated.count,
|
||||||
name=name,
|
name=name,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
first='%s%s' % (remote_id, self.page()),
|
first='%s?page=1' % remote_id,
|
||||||
last='%s%s' % (remote_id, self.page(min_id=0))
|
last='%s?page=%d' % (remote_id, paginated.num_pages)
|
||||||
).serialize()
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def to_ordered_collection_page(queryset, remote_id, id_only=False, page=1):
|
||||||
|
''' 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):
|
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||||
''' extends activitypub models to work as ordered collections '''
|
''' extends activitypub models to work as ordered collections '''
|
||||||
@property
|
@property
|
||||||
@ -252,12 +211,3 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
|||||||
def to_activity(self, **kwargs):
|
def to_activity(self, **kwargs):
|
||||||
''' an ordered collection of the specified model queryset '''
|
''' an ordered collection of the specified model queryset '''
|
||||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ActivityMapping:
|
|
||||||
''' translate between an activitypub json field and a model field '''
|
|
||||||
activity_key: str
|
|
||||||
model_key: str
|
|
||||||
activity_formatter: Callable = lambda x: x
|
|
||||||
model_formatter: Callable = lambda x: x
|
|
||||||
|
@ -36,7 +36,7 @@ class ActivitypubFieldMixin:
|
|||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
''' formatter to convert a model value into activitypub '''
|
''' formatter to convert a model value into activitypub '''
|
||||||
if hasattr(self, 'activitypub_wrapper'):
|
if hasattr(self, 'activitypub_wrapper'):
|
||||||
value = {self.activitypub_wrapper: value}
|
return {self.activitypub_wrapper: value}
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def from_activity(self, activity_data):
|
def from_activity(self, activity_data):
|
||||||
@ -46,6 +46,14 @@ class ActivitypubFieldMixin:
|
|||||||
value = value.get(self.activitypub_wrapper)
|
value = value.get(self.activitypub_wrapper)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_activitypub_field(self):
|
||||||
|
''' model_field_name to activitypubFieldName '''
|
||||||
|
if self.activitypub_field:
|
||||||
|
return self.activitypub_field
|
||||||
|
name = self.name.split('.')[-1]
|
||||||
|
components = name.split('_')
|
||||||
|
return components[0] + ''.join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
|
||||||
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||||
''' a url that serves as a unique identifier '''
|
''' a url that serves as a unique identifier '''
|
||||||
@ -91,8 +99,6 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
|
|||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
return value.remote_id
|
return value.remote_id
|
||||||
def from_activity(self, activity_data):
|
|
||||||
pass# TODO
|
|
||||||
|
|
||||||
|
|
||||||
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
|
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
|
||||||
@ -102,9 +108,6 @@ class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
|
|||||||
return None
|
return None
|
||||||
return value.to_activity()
|
return value.to_activity()
|
||||||
|
|
||||||
def from_activity(self, activity_data):
|
|
||||||
pass# TODO
|
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
''' activitypub-aware many to many field '''
|
''' activitypub-aware many to many field '''
|
||||||
@ -123,6 +126,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
|||||||
values = super().from_activity(activity_data)
|
values = super().from_activity(activity_data)
|
||||||
return values# TODO
|
return values# TODO
|
||||||
|
|
||||||
|
|
||||||
class TagField(ManyToManyField):
|
class TagField(ManyToManyField):
|
||||||
''' special case of many to many that uses Tags '''
|
''' special case of many to many that uses Tags '''
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -145,7 +149,6 @@ class TagField(ManyToManyField):
|
|||||||
|
|
||||||
def image_serializer(value):
|
def image_serializer(value):
|
||||||
''' helper for serializing images '''
|
''' helper for serializing images '''
|
||||||
print(value)
|
|
||||||
if value and hasattr(value, 'url'):
|
if value and hasattr(value, 'url'):
|
||||||
url = value.url
|
url = value.url
|
||||||
else:
|
else:
|
||||||
|
@ -5,19 +5,15 @@ 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, ActivityMapping
|
from .base_model import OrderedCollectionMixin, BookWyrmModel
|
||||||
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
class Tag(OrderedCollectionMixin, BookWyrmModel):
|
class Tag(OrderedCollectionMixin, BookWyrmModel):
|
||||||
''' freeform tags for books '''
|
''' freeform tags for books '''
|
||||||
name = models.CharField(max_length=100, unique=True)
|
name = fields.CharField(max_length=100, unique=True)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
|
|
||||||
activity_mappings = [
|
|
||||||
ActivityMapping('id', 'remote_id'),
|
|
||||||
ActivityMapping('name', 'name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def book_queryset(cls, identifier):
|
def book_queryset(cls, identifier):
|
||||||
''' county of books associated with this tag '''
|
''' county of books associated with this tag '''
|
||||||
@ -44,16 +40,12 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
|
|||||||
|
|
||||||
class UserTag(BookWyrmModel):
|
class UserTag(BookWyrmModel):
|
||||||
''' an instance of a tag on a book by a user '''
|
''' an instance of a tag on a book by a user '''
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = fields.ForeignKey(
|
||||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||||
tag = models.ForeignKey('Tag', on_delete=models.PROTECT)
|
book = fields.ForeignKey(
|
||||||
|
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
||||||
activity_mappings = [
|
tag = fields.ForeignKey(
|
||||||
ActivityMapping('id', 'remote_id'),
|
'Tag', on_delete=models.PROTECT, activitypub_field='target')
|
||||||
ActivityMapping('actor', 'user'),
|
|
||||||
ActivityMapping('object', 'book'),
|
|
||||||
ActivityMapping('target', 'tag'),
|
|
||||||
]
|
|
||||||
|
|
||||||
activity_serializer = activitypub.AddBook
|
activity_serializer = activitypub.AddBook
|
||||||
|
|
||||||
|
@ -109,13 +109,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||||||
def to_following_activity(self, **kwargs):
|
def to_following_activity(self, **kwargs):
|
||||||
''' activitypub following list '''
|
''' activitypub following list '''
|
||||||
remote_id = '%s/following' % self.remote_id
|
remote_id = '%s/following' % self.remote_id
|
||||||
return self.to_ordered_collection(self.following, \
|
return self.to_ordered_collection(self.following.all(), \
|
||||||
remote_id=remote_id, id_only=True, **kwargs)
|
remote_id=remote_id, id_only=True, **kwargs)
|
||||||
|
|
||||||
def to_followers_activity(self, **kwargs):
|
def to_followers_activity(self, **kwargs):
|
||||||
''' activitypub followers list '''
|
''' activitypub followers list '''
|
||||||
remote_id = '%s/followers' % self.remote_id
|
remote_id = '%s/followers' % self.remote_id
|
||||||
return self.to_ordered_collection(self.followers, \
|
return self.to_ordered_collection(self.followers.all(), \
|
||||||
remote_id=remote_id, id_only=True, **kwargs)
|
remote_id=remote_id, id_only=True, **kwargs)
|
||||||
|
|
||||||
def to_activity(self):
|
def to_activity(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user