I am struggling to figure out the queryset for SlugRelatedField. My data is such that I have a bunch of Object
instances that belong to a Project
. A project has a unique 'top' Object
. Object
s can have the same name only if they below to different Project
s.
class Object(models.Model): project = models.ForeignKey('Project', null=False, related_name='objs') name = models.TextField(null=False, db_index=True) .... class Meta: index_together = unique_together = ('project', 'name') class Project(models.Model): user = models.ForeignKey(get_user_model(), null=False, related_name='+') name = models.TextField(null=False) top = models.OneToOneField(Object, null=True, related_name='+') .... class ObjectSerializer(NonNullSerializer): class Meta: model = Object fields = ('name',) class ProjectSerializer(NonNullSerializer): objs = ObjectSerializer(many=True, required=False) top = serializers.SlugRelatedField(slug_field='name', queryset=Object.objects.filter(????)) class Meta: model = Project fields = ('id', 'name', 'objs', 'top')
What is my queryset going to look like for top
if I want to find only only the one Object
that belongs to the correct Project
? In other words, how to deserialize this:
[{ 'name' : 'Project1', 'objs' : [{ 'name': 'One' }], 'top': 'One' }, { 'name' : 'Project2', 'objs' : [{ 'name': 'One' }], 'top': 'One' <-- This should point to One under Project2, not One under Project1 }]
I have a solution that solves this problem in my case, which I will try to explain here.
The problem, abstracted:
Suppose I have a hierarchy with Foo
as the top-level objects, each associated with several Bar
s:
class Foo(Model): pass class Bar(Model): bar_text = CharField() foo = ForeignKey(Foo, related_name='bars')
Then I can use SlugRelatedField
trivially for read only serializations of Foo
, by which I mean the serializer:
class FooSerializer(ModelSerializer): bars = serializers.SlugRelatedField(slug_field='bar_text', many=True, read_only=True) class Meta: model = Foo fields = ('bars',)
will produce serializations like:
{ 'bars' : [<bar_text>, <bar_text>, ...] }
However, this is read only. To allow writing, I have to provide a queryset class attribute outside of any methods. The problem is, because we have a Foo->Bar
hierarchy, we don't know what the queryset is outside of any request. We would like to be able to override a get_queryset()
method, but none seems to exist. So we can't use SlugRelatedField
. What horribly hacky way can we fix it?
My Solution:
First, add an @property to the Foo model and put this property in the serializer:
In models.py
:
class Foo(Model): @property def bar_texts(self): return [bar.bar_text for bar in self.bars.all()]
In serializers.py
:
class FooSerializer(ModelSerializer): class Meta: model = Foo fields = ('bar_texts',)
This allows for the bar texts to be serialized as before, but we still can't write (we can try - the framework won't reject it but it will hit an exception when trying to save the bar_texts attribute of a Foo
)
So, the hacky part - fix perform_create()
in the Foo
list view.
class FooList: def perform_create(self, serializer): # The serializer contains the bar_text field, which we want, but doesn't correspond # to a writeable attribute of Foo. Extract the strings and save the Foo. Use pop with a default arg in case bar_texts isn't in the serialized data bar_texts = serializer.validated_data.pop('bar_texts', []) # Save the Foo object; it currently has no Bars associated with it foo = serializer.save() # Now add the Bars to the database for bar_text in bar_texts: foo.bars.create(bar_text=bar_text)
I hope that makes sense. It certainly works for me, but I have get to find any glaring bugs with it
I was just revisiting my own question on this topic when I was lead back to here, so here's a way of achieving this (I think).
class ObjectSerializer(NonNullSerializer): class Meta: model = Object fields = ('name',) class TopSerializerField(SlugRelatedField): def get_queryset(self): queryset = self.queryset if hasattr(self.root, 'project_id'): queryset = queryset.filter(project_id=project_id) return queryset class ProjectSerializer(NonNullSerializer): def __init__(self, *args, **kwargs): self.project_id = kwargs.pop('project_id') super().__init__(*args, **kwargs) # I've needed this workaround for some cases... # def __new__(cls, *args, **kwargs): # """When `many=True` is provided then we need to attach the project_id attribute to the ListSerializer instance""" # project_id = kwargs.get('project_id') # serializer = super(ProjectSerializer, cls).__new__(cls, *args, **kwargs) # setattr(serializer, 'project_id', project_id) # return serializer objs = ObjectSerializer(many=True, required=False) top = TopSerializerField(slug_field='name', queryset=Object.objects.all()) class Meta: model = Project fields = ('id', 'name', 'objs', 'top')
When you go to deserialize the data, it would search for objects that belong to the correct project defined on the serializer.