Start moving serializing from to_model to fields

This commit is contained in:
Mouse Reeve 2020-12-05 21:33:48 -08:00
parent 8500a7cfe1
commit 7a90aa8f6c
3 changed files with 69 additions and 54 deletions

View file

@ -92,44 +92,24 @@ class ActivityObject:
if value is None: if value is None:
continue continue
# value is None if there's a default that isn't supplied
# in the activity but is supplied in the formatter
value = getattr(self, activitypub_field)
model_field = getattr(model, field.name) model_field = getattr(model, field.name)
formatted_value = field.field_from_activity(value)
if isinstance(model_field, ForwardManyToOneDescriptor): if isinstance(model_field, ForwardManyToOneDescriptor):
if not formatted_value: mapped_fields[field.name] = value
continue if isinstance(model_field, ManyToManyDescriptor):
# foreign key remote id reolver (work on Edition, for example)
fk_model = model_field.field.related_model
if isinstance(formatted_value, dict) and \
formatted_value.get('id'):
# if the AP field is a serialized object (as in Add)
# or KeyPair (even though it's OneToOne)
related_model = field.related_model
related_activity = related_model.activity_serializer
mapped_fields[field.name] = related_activity(
**formatted_value
).to_model(related_model)
else:
# if the field is just a remote_id (as in every other case)
remote_id = formatted_value
reference = resolve_remote_id(fk_model, remote_id)
mapped_fields[field.name] = reference
elif isinstance(model_field, ManyToManyDescriptor):
# status mentions book/users # status mentions book/users
many_to_many_fields[field.name] = formatted_value many_to_many_fields[field.name] = value
elif isinstance(model_field, ReverseManyToOneDescriptor): elif isinstance(model_field, ReverseManyToOneDescriptor):
# attachments on Status, for example # attachments on Status, for example
one_to_many_fields[field.name] = formatted_value one_to_many_fields[field.name] = value
elif isinstance(model_field, ImageFileDescriptor): elif isinstance(model_field, ImageFileDescriptor):
# image fields need custom handling # image fields need custom handling
image_fields[field.name] = formatted_value print(model_field, field.name, value)
image_fields[field.name] = value
else: else:
if formatted_value == MISSING: if value == MISSING:
formatted_value = None value = None
mapped_fields[field.name] = formatted_value mapped_fields[field.name] = value
if instance: if instance:
@ -146,33 +126,14 @@ class ActivityObject:
# add images # add images
for (model_key, value) in image_fields.items(): for (model_key, value) in image_fields.items():
if not formatted_value: if not value:
continue continue
getattr(instance, model_key).save(*formatted_value, save=True) getattr(instance, model_key).save(*value, save=True)
# add many to many fields # add many to many fields
for (model_key, values) in many_to_many_fields.items(): for (model_key, values) in many_to_many_fields.items():
# mention books, mention users, followers # mention books, mention users, followers
if values == MISSING or not isinstance(values, list): getattr(instance, model_key).set(values)
# user followers is a link to an orderedcollection, skip it
continue
model_field = getattr(instance, model_key)
model = model_field.model
items = []
for link in values:
if isinstance(link, dict):
# check that the Type matches the model (Status
# tags contain both user mentions and book tags)
if not model.activity_serializer.type == \
link.get('type'):
continue
remote_id = link.get('href')
else:
remote_id = link
items.append(
resolve_remote_id(model, remote_id)
)
getattr(instance, model_key).set(items)
# add one to many fields # add one to many fields
for (model_key, values) in one_to_many_fields.items(): for (model_key, values) in one_to_many_fields.items():

View file

@ -57,6 +57,27 @@ class ActivitypubFieldMixin:
return components[0] + ''.join(x.title() for x in components[1:]) return components[0] + ''.join(x.title() for x in components[1:])
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
''' default (de)serialization for foreign key and one to one '''
def field_from_activity(self, value):
if not value:
return None
related_model = self.related_model
if isinstance(value, dict) and value.get('id'):
# this is an activitypub object, which we can deserialize
activity_serializer = related_model.activity_serializer
return activity_serializer(**value).to_model(related_model)
try:
# make sure the value looks like a remote id
validate_remote_id(value)
except ValidationError:
# we don't know what this is, ignore it
return None
# gets or creates the model field from the remote id
return activitypub.resolve_remote_id(related_model, value)
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 '''
def __init__(self, *args, max_length=255, validators=None, **kwargs): def __init__(self, *args, max_length=255, validators=None, **kwargs):
@ -95,7 +116,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
return value.split('@')[0] return value.split('@')[0]
class ForeignKey(ActivitypubFieldMixin, models.ForeignKey): class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
''' activitypub-aware foreign key field ''' ''' activitypub-aware foreign key field '''
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:
@ -103,7 +124,7 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
return value.remote_id return value.remote_id
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
''' activitypub-aware foreign key field ''' ''' activitypub-aware foreign key field '''
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:
@ -122,6 +143,15 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
return '%s/%s' % (value.instance.remote_id, self.name) return '%s/%s' % (value.instance.remote_id, self.name)
return [i.remote_id for i in value.all()] return [i.remote_id for i in value.all()]
def field_from_activity(self, value):
items = []
for remote_id in value:
validate_remote_id(remote_id)
items.append(
activitypub.resolve_remote_id(self.related_model, remote_id)
)
return items
class TagField(ManyToManyField): class TagField(ManyToManyField):
''' special case of many to many that uses Tags ''' ''' special case of many to many that uses Tags '''
@ -142,6 +172,15 @@ class TagField(ManyToManyField):
)) ))
return tags return tags
def field_from_activity(self, value):
items = []
for link_json in value:
link = activitypub.Link(**link_json)
items.append(
activitypub.resolve_remote_id(self.related_model, link.href)
)
return items
def image_serializer(value): def image_serializer(value):
''' helper for serializing images ''' ''' helper for serializing images '''

View file

@ -14,7 +14,7 @@ from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from bookwyrm.models import fields, User from bookwyrm.models import fields, User
from bookwyrm.settings import DOMAIN from bookwyrm import activitypub
class ActivitypubFields(TestCase): class ActivitypubFields(TestCase):
def test_validate_remote_id(self): def test_validate_remote_id(self):
@ -86,8 +86,23 @@ class ActivitypubFields(TestCase):
instance = fields.ForeignKey('User', on_delete=models.CASCADE) instance = fields.ForeignKey('User', on_delete=models.CASCADE)
Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) Serializable = namedtuple('Serializable', ('to_activity', 'remote_id'))
item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c') item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c')
# returns the remote_id field of the related object
self.assertEqual(instance.field_to_activity(item), 'https://e.b/c') self.assertEqual(instance.field_to_activity(item), 'https://e.b/c')
def test_foreign_key_from_activity(self):
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
# test receiving an unknown remote id and loading data TODO
# test recieving activity json TODO
# test receiving a remote id of an object in the db
user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
value = instance.field_from_activity(user.remote_id)
self.assertEqual(value, user)
def test_one_to_one_field(self): def test_one_to_one_field(self):
instance = fields.OneToOneField('User', on_delete=models.CASCADE) instance = fields.OneToOneField('User', on_delete=models.CASCADE)
Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) Serializable = namedtuple('Serializable', ('to_activity', 'remote_id'))