How to Filter in DRF Serializer using a Self Referencing Recursive Field

Question:

Using Python 3.x and the Django Rest Framework. I have a serializer with a Recursive Field (on self) which works as expected. However, I need a way to initially filter the nested children it returns by active = True.

I’ve tried different ways to filter children by active=True, but I’m unable to get this working on the nested children that are returned in the serializer.

Here is what I have.

class MenuListSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='menu_detail')
    children = RecursiveField(many=True, required=False)


class RecursiveField(serializers.Serializer):
    """
    Self-referential field for MPTT.
    """
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

This is what I have tried but get the error ListSerializer’ object has no attribute ‘queryset’ However, I’m not even sure this would work.

class MenuListSerializer(serializers.ModelSerializer):

    def __init__(self, *args, request_user=None, **kwargs):
        # try and filter active in chrildrend before query set is passed
        super(MenuListSerializer, self).__init__(*args, **kwargs)
        # print(self.fields['children'].parent)
        self.fields['children'].queryset =     self.fields['children'].queryset.filter(active=True)

    url = serializers.HyperlinkedIdentityField(view_name='menu_detail')
    children = RecursiveField(many=True, required=False)
Asked By: Prometheus

||

Answers:

If I understand well, your are trying to serialize Menu objects in a hierarchical way.
To do that, I guess you serialize recursively your top level Menu objects, don’t you? (or else you will get all Menu objects at the top level).

To be able to filter active children only, I would suggest to create a active_children property on your model:

class Menu(MPTTModel, TimeStampedModel):
    name = models.CharField(max_length=100)
    active = models.BooleanField(default=1)
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children')

    @property
    def active_children(self):
        return self.children.filter(active=True)

Then you can use that as a source for your children field in you serializer:

class MenuListSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='menu_detail')
    children = RecursiveField(many=True, required=False, source='active_children')

Now you should only have active children when serializing.

Note that you should also filter top level objects in your queryset as the filtering above only works for the children in Menu objects.

Answered By: Michael Rigoni

There is a more dynamic solution for this issue. When you send the many=True kwarg to the RecursiveField (Serializer), it uses the ListSerializer with the same args and kwargs (with some limitations) for getting the objects and then uses the RecursiveField for all children. In this flow the ListSerializer is the default listing serializer, you can change it by the Meta of the RecursiveField. (you can see in the code)

The ordering will be applied in the to_representation method of the ListSerializer, we have to provide the ordering value, and the only way to send the ordering value is by sending it to RecursiveField. Kwargs are sent to ListSerializer in the __new__ method so, we can pop the ordering value in __init__, thus RecursiveField will not raise an unexpected kwarg error.

The ListSerializer kwargs are limited with constants in the rest_framework.serializers.LIST_SERIALIZER_KWARGS, we have to add ordering kwarg there too, in order to work with ordering value in the ListSerializer. Then we can order the data without any scenario specific code.

import rest_framework

rest_framework.serializers.LIST_SERIALIZER_KWARGS += ('ordering',)


class RecursiveListField(serializers.ListSerializer):

    def __init__(self, *args, **kwargs):
        self.ordering = kwargs.pop('ordering', None)
        super(RecursiveListField, self).__init__(*args, **kwargs)

    def to_representation(self, data):
        data = self.ordering and data.order_by(*self.ordering) or data
        return super(RecursiveListField, self).to_representation(data)


class RecursiveField(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        kwargs.pop('ordering', None)
        super(RecursiveField, self).__init__(*args, **kwargs)

    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

    class Meta:
        list_serializer_class = RecursiveListField

You can use the recursive field like this, anywhere you want:

class SomeSerializer(serializers.ModelSerializer):
    children = RecursiveField(many=True, ordering=('id'))
Answered By: aysum