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:
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)
formatted_value = field.field_from_activity(value)
if isinstance(model_field, ForwardManyToOneDescriptor):
if not formatted_value:
continue
# 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):
mapped_fields[field.name] = value
if isinstance(model_field, ManyToManyDescriptor):
# status mentions book/users
many_to_many_fields[field.name] = formatted_value
many_to_many_fields[field.name] = value
elif isinstance(model_field, ReverseManyToOneDescriptor):
# 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):
# image fields need custom handling
image_fields[field.name] = formatted_value
print(model_field, field.name, value)
image_fields[field.name] = value
else:
if formatted_value == MISSING:
formatted_value = None
mapped_fields[field.name] = formatted_value
if value == MISSING:
value = None
mapped_fields[field.name] = value
if instance:
@ -146,33 +126,14 @@ class ActivityObject:
# add images
for (model_key, value) in image_fields.items():
if not formatted_value:
if not value:
continue
getattr(instance, model_key).save(*formatted_value, save=True)
getattr(instance, model_key).save(*value, save=True)
# add many to many fields
for (model_key, values) in many_to_many_fields.items():
# mention books, mention users, followers
if values == MISSING or not isinstance(values, list):
# 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)
getattr(instance, model_key).set(values)
# add one to many fields
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:])
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):
''' a url that serves as a unique identifier '''
def __init__(self, *args, max_length=255, validators=None, **kwargs):
@ -95,7 +116,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
return value.split('@')[0]
class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
''' activitypub-aware foreign key field '''
def field_to_activity(self, value):
if not value:
@ -103,7 +124,7 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
return value.remote_id
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
''' activitypub-aware foreign key field '''
def field_to_activity(self, value):
if not value:
@ -122,6 +143,15 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
return '%s/%s' % (value.instance.remote_id, self.name)
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):
''' special case of many to many that uses Tags '''
@ -142,6 +172,15 @@ class TagField(ManyToManyField):
))
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):
''' helper for serializing images '''

View file

@ -14,7 +14,7 @@ from django.test import TestCase
from django.utils import timezone
from bookwyrm.models import fields, User
from bookwyrm.settings import DOMAIN
from bookwyrm import activitypub
class ActivitypubFields(TestCase):
def test_validate_remote_id(self):
@ -86,8 +86,23 @@ class ActivitypubFields(TestCase):
instance = fields.ForeignKey('User', on_delete=models.CASCADE)
Serializable = namedtuple('Serializable', ('to_activity', 'remote_id'))
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')
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):
instance = fields.OneToOneField('User', on_delete=models.CASCADE)
Serializable = namedtuple('Serializable', ('to_activity', 'remote_id'))