From eb6206252d7509b45f79fe06a769adb3f7c56f88 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 30 Nov 2020 19:01:43 -0800 Subject: [PATCH] cleans up ordered collection mixin --- bookwyrm/models/base_model.py | 126 ++++++++++------------------------ bookwyrm/models/fields.py | 17 +++-- bookwyrm/models/tag.py | 26 +++---- bookwyrm/models/user.py | 4 +- 4 files changed, 59 insertions(+), 114 deletions(-) diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index af65a36a..0f317dbc 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,18 +1,16 @@ ''' base model with default fields ''' from base64 import b64encode -from dataclasses import dataclass -from typing import Callable from uuid import uuid4 -from urllib.parse import urlencode 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.dispatch import receiver from bookwyrm import activitypub -from bookwyrm.settings import DOMAIN +from bookwyrm.settings import DOMAIN, PAGE_LENGTH from .fields import RemoteIdField @@ -52,15 +50,6 @@ def execute_after_save(sender, instance, created, *args, **kwargs): 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): ''' load reverse lookups (like public key owner or Status attachment ''' if hasattr(related_field, 'all'): @@ -78,15 +67,16 @@ class ActivitypubMixin: def to_activity(self): ''' convert from a model to an activity ''' activity = {} - for field in self.__class__._meta.get_fields(): + for field in self._meta.get_fields(): if not hasattr(field, 'field_to_activity'): continue - key = get_field_name(field) value = field.field_to_activity(getattr(self, field.name)) if value is None: continue + key = field.get_activitypub_field() if key in activity and isinstance(activity[key], list): + # handles tags on status, which accumulate across fields activity[key] += value else: activity[key] = value @@ -125,15 +115,12 @@ class ActivitypubMixin: def to_delete_activity(self, user): ''' notice of deletion ''' - # this should be a tombstone - activity_object = self.to_activity() - 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=activity_object, + object=self.to_activity(), ).serialize() @@ -165,81 +152,53 @@ class OrderedCollectionPageMixin(ActivitypubMixin): ''' this can be overriden if there's a special remote id, ie outbox ''' 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, \ remote_id=None, page=False, **kwargs): ''' an ordered collection of whatevers ''' remote_id = remote_id or self.remote_id if page: - return self.to_ordered_collection_page( + return to_ordered_collection_page( queryset, remote_id, **kwargs) - name = '' - if hasattr(self, 'name'): - name = self.name - owner = '' - if hasattr(self, 'user'): - owner = self.user.remote_id + name = self.name if hasattr(self, 'name') else None + owner = self.user.remote_id if hasattr(self, 'user') else '' - size = queryset.count() + paginated = Paginator(queryset, PAGE_LENGTH) return activitypub.OrderedCollection( id=remote_id, - totalItems=size, + totalItems=paginated.count, name=name, owner=owner, - first='%s%s' % (remote_id, self.page()), - last='%s%s' % (remote_id, self.page(min_id=0)) + first='%s?page=1' % remote_id, + last='%s?page=%d' % (remote_id, paginated.num_pages) ).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): ''' extends activitypub models to work as ordered collections ''' @property @@ -252,12 +211,3 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin): def to_activity(self, **kwargs): ''' an ordered collection of the specified model queryset ''' 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 diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 197b6e8e..ab0f5892 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -36,7 +36,7 @@ class ActivitypubFieldMixin: def field_to_activity(self, value): ''' formatter to convert a model value into activitypub ''' if hasattr(self, 'activitypub_wrapper'): - value = {self.activitypub_wrapper: value} + return {self.activitypub_wrapper: value} return value def from_activity(self, activity_data): @@ -46,6 +46,14 @@ class ActivitypubFieldMixin: value = value.get(self.activitypub_wrapper) 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): ''' a url that serves as a unique identifier ''' @@ -91,8 +99,6 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey): if not value: return None return value.remote_id - def from_activity(self, activity_data): - pass# TODO class OneToOneField(ActivitypubFieldMixin, models.OneToOneField): @@ -102,9 +108,6 @@ class OneToOneField(ActivitypubFieldMixin, models.OneToOneField): return None return value.to_activity() - def from_activity(self, activity_data): - pass# TODO - class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): ''' activitypub-aware many to many field ''' @@ -123,6 +126,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): values = super().from_activity(activity_data) return values# TODO + class TagField(ManyToManyField): ''' special case of many to many that uses Tags ''' def __init__(self, *args, **kwargs): @@ -145,7 +149,6 @@ class TagField(ManyToManyField): def image_serializer(value): ''' helper for serializing images ''' - print(value) if value and hasattr(value, 'url'): url = value.url else: diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py index 8b7efceb..940b4192 100644 --- a/bookwyrm/models/tag.py +++ b/bookwyrm/models/tag.py @@ -5,19 +5,15 @@ from django.db import models from bookwyrm import activitypub 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): ''' 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) - activity_mappings = [ - ActivityMapping('id', 'remote_id'), - ActivityMapping('name', 'name'), - ] - @classmethod def book_queryset(cls, identifier): ''' county of books associated with this tag ''' @@ -44,16 +40,12 @@ class Tag(OrderedCollectionMixin, BookWyrmModel): class UserTag(BookWyrmModel): ''' an instance of a tag on a book by a user ''' - user = models.ForeignKey('User', on_delete=models.PROTECT) - book = models.ForeignKey('Edition', on_delete=models.PROTECT) - tag = models.ForeignKey('Tag', on_delete=models.PROTECT) - - activity_mappings = [ - ActivityMapping('id', 'remote_id'), - ActivityMapping('actor', 'user'), - ActivityMapping('object', 'book'), - ActivityMapping('target', 'tag'), - ] + user = fields.ForeignKey( + 'User', on_delete=models.PROTECT, activitypub_field='actor') + book = fields.ForeignKey( + 'Edition', on_delete=models.PROTECT, activitypub_field='object') + tag = fields.ForeignKey( + 'Tag', on_delete=models.PROTECT, activitypub_field='target') activity_serializer = activitypub.AddBook diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 95dd1e79..0097fcde 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -109,13 +109,13 @@ class User(OrderedCollectionPageMixin, AbstractUser): def to_following_activity(self, **kwargs): ''' activitypub following list ''' 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) def to_followers_activity(self, **kwargs): ''' activitypub followers list ''' 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) def to_activity(self):