Working with Reference-Based Relationships
This how-to guide shows you how to use ReferenceField and RelatedContextField in your Django ActivityPub applications to work with federated data structures without requiring immediate persistence.
Prerequisites
- Complete the Getting Started tutorial
- Understand Django models and relationships
- Familiarity with ActivityPub concepts
Adding ReferenceField to Models
ReferenceField creates many-to-many relationships that work on unsaved model instances:
from activitypub.models import ReferenceField
class ObjectContext(models.Model):
reference = models.OneToOneField(Reference, on_delete=models.CASCADE)
tags = ReferenceField() # Many-to-many with Reference objects
attachments = ReferenceField()
class Meta:
abstract = True
Using RelatedContextField for Navigation
RelatedContextField provides lazy access to ActivityStreams contexts:
from activitypub.models import RelatedContextField
class Site(models.Model):
reference = models.ForeignKey(Reference, on_delete=models.CASCADE)
# Lazy access to contexts
as2 = RelatedContextField(ObjectContext)
actor = RelatedContextField(ActorContext)
Working with Unsaved Instances
Creating Context Relationships Before Persistence
from activitypub.models import Reference
# Create references for federated content
post_ref = Reference.make("https://example.com/posts/123")
tag_ref = Reference.make("https://example.com/tags/python")
# Work with context before saving
post = ObjectContext(reference=post_ref)
post.name = "My Python Post"
post.content = "Learning Python is fun!"
# Add relationships immediately - no database save required
post.tags.add(tag_ref)
# Query relationships on unsaved instances
tag_count = post.tags.count() # Works!
tags = post.tags.all() # Works!
# Persist when ready
if should_save:
post.save()
Navigating Complex Structures
# Get a site with its reference
site = Site.objects.get(pk=1)
# Access ActivityStreams context lazily
site.as2.name = "My Blog" # Creates ObjectContext if needed
# Navigate through relationships
first_tag = site.as2.tags.first()
if first_tag:
tag_context = first_tag.get_by_context(ObjectContext)
print(f"Tag: {tag_context.name}")
# Add more relationships
new_tag_ref = Reference.make("https://example.com/tags/django")
site.as2.tags.add(new_tag_ref)
Managing Relationships
Adding and Removing References
# Add multiple references
tag_refs = [
Reference.make("https://example.com/tags/python"),
Reference.make("https://example.com/tags/django"),
Reference.make("https://example.com/tags/activitypub"),
]
post.tags.add(*tag_refs)
# Remove specific references
post.tags.remove(tag_refs[0])
# Clear all relationships
post.tags.clear()
# Replace all relationships
post.tags.set(tag_refs[1:]) # Keep only django and activitypub
Querying Relationships
# Filter related references
python_tags = post.tags.filter(uri__icontains="python")
# Check existence
has_tags = post.tags.exists()
# Count relationships
tag_count = post.tags.count()
# Get first/last
first_tag = post.tags.first()
last_tag = post.tags.last()
ContextProxy Behavior
Lazy Loading
RelatedContextField returns a ContextProxy that loads contexts on-demand:
# No database queries yet
proxy = site.as2 # Just creates proxy object
# First access loads the context
name = proxy.name # ← Database query happens here
# Subsequent access reuses loaded context
content = proxy.content # ← No additional query
Automatic Context Creation
If a context doesn't exist, ContextProxy creates it automatically:
# Context doesn't exist in database
site.as2 # Proxy created
# Setting attributes creates the context
site.as2.name = "New Site" # ← ObjectContext created and cached
# Context is now available for relationships
site.as2.tags.add(tag_ref) # Works on the created context
Signal Integration
ReferenceField maintains full compatibility with Django's signal system:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
@receiver(m2m_changed, sender=ObjectContext.tags.through)
def handle_tag_changes(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
# Handle new tags added
for tag_id in pk_set:
tag_ref = Reference.objects.get(pk=tag_id)
print(f"Tag {tag_ref.uri} added to {instance}")
elif action == "post_remove":
# Handle tags removed
print(f"Tags removed from {instance}")
Performance Optimization
Prefetching Related References
# Prefetch related references to avoid N+1 queries
sites = Site.objects.prefetch_related(
'reference__objectcontext_tags__target_reference'
).all()
for site in sites:
# This won't trigger additional queries
for tag in site.as2.tags.all():
print(tag.uri)
Selective Persistence
Defer saving contexts until necessary:
def process_federated_content(activity_data):
# Create references and contexts
activity_ref = Reference.make(activity_data['id'])
activity = ActivityContext(reference=activity_ref)
# Process relationships without saving
for tag_uri in activity_data.get('tags', []):
tag_ref = Reference.make(tag_uri)
activity.tags.add(tag_ref)
# Validate and decide whether to persist
if is_valid_activity(activity):
activity.save() # Persist everything at once
return activity
# Don't save invalid content
return None
Common Patterns
Processing Incoming Activities
def handle_create_activity(activity_data):
# Create activity context
activity_ref = Reference.make(activity_data['id'])
activity = ActivityContext(reference=activity_ref)
activity.type = activity_data['type']
# Process object
object_ref = Reference.make(activity_data['object']['id'])
activity.object = object_ref
# Add tags if present
if 'tags' in activity_data['object']:
for tag_data in activity_data['object']['tags']:
tag_ref = Reference.make(tag_data['id'])
activity.object.get_by_context(ObjectContext).tags.add(tag_ref)
# Persist based on business logic
if should_federate(activity):
activity.save()
return activity
return None
Building Response Activities
def create_like_activity(post_ref, actor_ref):
# Create activity reference
activity_ref = Reference.make(f"{actor_ref.uri}/likes/{post_ref.uri.split('/')[-1]}")
activity = ActivityContext(reference=activity_ref)
activity.type = "Like"
activity.actor = actor_ref
activity.object = post_ref
# Add to actor's outbox collection
actor = actor_ref.get_by_context(ActorContext)
if actor.outbox:
outbox = actor.outbox.get_by_context(CollectionContext)
outbox.items.add(activity_ref)
return activity
Troubleshooting
"Cannot resolve keyword 'source_reference' into field"
This error occurs when through tables weren't created properly. Ensure:
- The model containing
ReferenceFieldis not abstract - Migrations have been run after adding the field
- The through model was registered correctly
ContextProxy Not Loading Contexts
If RelatedContextField doesn't load contexts:
- Check that the reference exists
- Verify the context class inherits from
AbstractContextModel - Ensure the context class has the correct
LINKED_DATA_FIELDS
Signal Handlers Not Triggering
Signal handlers use sender=Model.field.through. With ReferenceField:
# Correct - use the through model
@receiver(m2m_changed, sender=MyModel.relationships.through)
# Incorrect - won't work with ReferenceField
@receiver(m2m_changed, sender=MyModel.relationships)
Next Steps
- Read the Reference-Based Relationships topic guide for deeper understanding
- Explore Handling Incoming Activities for processing federated content
- Learn about Sending Activities to publish content to the Fediverse