forked from mirrors/bookwyrm
Start moving serializing from to_model to fields
This commit is contained in:
parent
8500a7cfe1
commit
7a90aa8f6c
3 changed files with 69 additions and 54 deletions
|
@ -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():
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
Loading…
Reference in a new issue