forked from mirrors/bookwyrm
Asyncronously set related fields
This commit is contained in:
parent
4d4ee8b8c3
commit
cc42e9d149
4 changed files with 57 additions and 23 deletions
|
@ -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)
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue