Asyncronously set related fields

This commit is contained in:
Mouse Reeve 2020-12-08 09:43:12 -08:00
parent 4d4ee8b8c3
commit cc42e9d149
4 changed files with 57 additions and 23 deletions

View file

@ -2,12 +2,13 @@
from dataclasses import dataclass, fields, MISSING from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder from json import JSONEncoder
from django.db.models.fields.related_descriptors \ from django.apps import apps
import ForwardManyToOneDescriptor, ManyToManyDescriptor, \ from django.db import transaction
ReverseManyToOneDescriptor
from django.db.models.fields.files import ImageFileDescriptor from django.db.models.fields.files import ImageFileDescriptor
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
from bookwyrm.connectors import ConnectorException, get_data from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.tasks import app
class ActivitySerializerError(ValueError): class ActivitySerializerError(ValueError):
''' routine problems serializing activitypub json ''' ''' routine problems serializing activitypub json '''
@ -64,7 +65,8 @@ class ActivityObject:
setattr(self, field.name, value) setattr(self, field.name, value)
def to_model(self, model, instance=None): @transaction.atomic
def to_model(self, model, instance=None, save=True):
''' convert from an activity to a model instance ''' ''' convert from an activity to a model instance '''
if not isinstance(self, model.activity_serializer): if not isinstance(self, model.activity_serializer):
raise ActivitySerializerError( raise ActivitySerializerError(
@ -97,26 +99,28 @@ class ActivityObject:
many_to_many_fields[field.name] = value many_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
getattr(instance, field.name).save(*value) getattr(instance, field.name).save(*value, save=save)
else: else:
# just a good old fashioned model.field = value # just a good old fashioned model.field = value
setattr(instance, field.name, value) setattr(instance, field.name, value)
if not save:
# we can't set many to many and reverse fields on an unsaved object
return instance
instance.save() instance.save()
# add many to many fields, which have to be set post-save # add many to many fields, which have to be set post-save
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/users, for example
getattr(instance, model_key).set(values) getattr(instance, model_key).set(values)
if not hasattr(model, 'deserialize_reverse_fields'): if not save or not hasattr(model, 'deserialize_reverse_fields'):
return instance return instance
# reversed relationships in the models # reversed relationships in the models
for (model_field_name, activity_field_name) in \ for (model_field_name, activity_field_name) in \
model.deserialize_reverse_fields: model.deserialize_reverse_fields:
if not activity_field_name:
continue
# attachments on Status, for example # attachments on Status, for example
values = getattr(self, activity_field_name) values = getattr(self, activity_field_name)
if values is None or values is MISSING: if values is None or values is MISSING:
@ -131,15 +135,13 @@ class ActivityObject:
values = [values] values = [values]
for item in values: for item in values:
if isinstance(item, str): set_related_field.delay(
item = resolve_remote_id(related_model, item) related_model.__name__,
else: instance.__class__.__name__,
item = related_model.activity_serializer(**item) instance.__class__.__name__.lower(),
item = item.to_model(related_model) instance.remote_id,
related_name = instance.__class__.__name__.lower() item
setattr(item, related_name, instance) )
item.save()
return instance return instance
@ -150,6 +152,28 @@ class ActivityObject:
return data return data
@app.task
@transaction.atomic
def set_related_field(
model_name, origin_model_name,
related_field_name, related_remote_id, data):
''' load reverse related fields (editions, attachments) without blocking '''
model = apps.get_model('bookwyrm.%s' % model_name, require_ready=True)
origin_model = apps.get_model(
'bookwyrm.%s' % origin_model_name,
require_ready=True
)
if isinstance(data, str):
item = resolve_remote_id(model, data, save=False)
else:
item = model.activity_serializer(**data)
item = item.to_model(model, save=False)
instance = find_existing_by_remote_id(origin_model, related_remote_id)
setattr(item, related_field_name, instance)
item.save()
def find_existing_by_remote_id(model, remote_id): def find_existing_by_remote_id(model, remote_id):
''' check for an existing instance of this id in the db ''' ''' check for an existing instance of this id in the db '''
objects = model.objects objects = model.objects
@ -168,7 +192,7 @@ def find_existing_by_remote_id(model, remote_id):
return result return result
def resolve_remote_id(model, remote_id, refresh=False): def resolve_remote_id(model, remote_id, refresh=False, save=True):
''' look up the remote_id in the database or load it remotely ''' ''' look up the remote_id in the database or load it remotely '''
result = find_existing_by_remote_id(model, remote_id) result = find_existing_by_remote_id(model, remote_id)
if result and not refresh: if result and not refresh:
@ -184,4 +208,4 @@ def resolve_remote_id(model, remote_id, refresh=False):
item = model.activity_serializer(**data) item = model.activity_serializer(**data)
# if we're refreshing, "result" will be set and we'll update it # if we're refreshing, "result" will be set and we'll update it
return item.to_model(model, instance=result) return item.to_model(model, instance=result, save=save)

View file

@ -126,6 +126,15 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
return None return None
return value.remote_id return value.remote_id
def field_from_activity(self, value):
print(value)
try:
validate_remote_id(value)
except ValidationError:
return None
return activitypub.resolve_remote_id(self.related_model, value)
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
''' activitypub-aware foreign key field ''' ''' activitypub-aware foreign key field '''

View file

@ -86,8 +86,8 @@
{% endif %} {% endif %}
{% if book.parent_work.edition_set.count > 1 %} {% if book.parent_work.editions.count > 1 %}
<p><a href="/book/{{ book.parent_work.id }}/editions">{{ book.parent_work.edition_set.count }} editions</a></p> <p><a href="/book/{{ book.parent_work.id }}/editions">{{ book.parent_work.editions.count }} editions</a></p>
{% endif %} {% endif %}
</div> </div>

View file

@ -19,8 +19,9 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
app.autodiscover_tasks(['bookwyrm'], related_name='broadcast') app.autodiscover_tasks(['bookwyrm'], related_name='activitypub.base_activity')
app.autodiscover_tasks(['bookwyrm'], related_name='books_manager') app.autodiscover_tasks(['bookwyrm'], related_name='books_manager')
app.autodiscover_tasks(['bookwyrm'], related_name='broadcast')
app.autodiscover_tasks(['bookwyrm'], related_name='emailing') app.autodiscover_tasks(['bookwyrm'], related_name='emailing')
app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import') app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import')
app.autodiscover_tasks(['bookwyrm'], related_name='incoming') app.autodiscover_tasks(['bookwyrm'], related_name='incoming')