Django Rest Framework: Dynamically return subset of fields

后端 未结 8 674
南笙
南笙 2020-11-28 18:16

Problem

As recommended in the blogpost Best Practices for Designing a Pragmatic RESTful API, I would like to add a fields query parameter to a Django R

相关标签:
8条回答
  • 2020-11-28 18:52

    serializers.py

    class DynamicFieldsSerializerMixin(object):
    
        def __init__(self, *args, **kwargs):
            # Don't pass the 'fields' arg up to the superclass
            fields = kwargs.pop('fields', None)
    
            # Instantiate the superclass normally
            super(DynamicFieldsSerializerMixin, self).__init__(*args, **kwargs)
    
            if fields is not None:
                # Drop any fields that are not specified in the `fields` argument.
                allowed = set(fields)
                existing = set(self.fields.keys())
                for field_name in existing - allowed:
                    self.fields.pop(field_name)
    
    
    class UserSerializer(DynamicFieldsSerializerMixin, serializers.HyperlinkedModelSerializer):
    
        password = serializers.CharField(
            style={'input_type': 'password'}, write_only=True
        )
    
        class Meta:
            model = User
            fields = ('id', 'username', 'password', 'email', 'first_name', 'last_name')
    
    
        def create(self, validated_data):
            user = User.objects.create(
                username=validated_data['username'],
                email=validated_data['email'],
                first_name=validated_data['first_name'],
                last_name=validated_data['last_name']
            )
    
            user.set_password(validated_data['password'])
            user.save()
    
            return user
    

    views.py

    class DynamicFieldsViewMixin(object):
    
     def get_serializer(self, *args, **kwargs):
    
        serializer_class = self.get_serializer_class()
    
        fields = None
        if self.request.method == 'GET':
            query_fields = self.request.QUERY_PARAMS.get("fields", None)
    
            if query_fields:
                fields = tuple(query_fields.split(','))
    
    
        kwargs['context'] = self.get_serializer_context()
        kwargs['fields'] = fields
    
        return serializer_class(*args, **kwargs)
    
    
    
    class UserList(DynamicFieldsViewMixin, ListCreateAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer
    
    0 讨论(0)
  • 2020-11-28 18:56

    Configure a new pagination serializer class

    from rest_framework import pagination, serializers
    
    class DynamicFieldsPaginationSerializer(pagination.BasePaginationSerializer):
        """
        A dynamic fields implementation of a pagination serializer.
        """
        count = serializers.Field(source='paginator.count')
        next = pagination.NextPageField(source='*')
        previous = pagination.PreviousPageField(source='*')
    
        def __init__(self, *args, **kwargs):
            """
            Override init to add in the object serializer field on-the-fly.
            """
            fields = kwargs.pop('fields', None)
            super(pagination.BasePaginationSerializer, self).__init__(*args, **kwargs)
            results_field = self.results_field
            object_serializer = self.opts.object_serializer_class
    
            if 'context' in kwargs:
                context_kwarg = {'context': kwargs['context']}
            else:
                context_kwarg = {}
    
            if fields:
                context_kwarg.update({'fields': fields})
    
            self.fields[results_field] = object_serializer(source='object_list',
                                                           many=True,
                                                           **context_kwarg)
    
    
    # Set the pagination serializer setting
    REST_FRAMEWORK = {
        # [...]
        'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'DynamicFieldsPaginationSerializer',
    }
    

    Make dynamic serializer

    from rest_framework import serializers
    
    class DynamicFieldsModelSerializer(serializers.ModelSerializer):
        """
        A ModelSerializer that takes an additional `fields` argument that
        controls which fields should be displayed.
    
        See:
            http://tomchristie.github.io/rest-framework-2-docs/api-guide/serializers
        """
    
        def __init__(self, *args, **kwargs):
            # Don't pass the 'fields' arg up to the superclass
            fields = kwargs.pop('fields', None)
    
            # Instantiate the superclass normally
            super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
    
            if fields:
                # Drop any fields that are not specified in the `fields` argument.
                allowed = set(fields)
                existing = set(self.fields.keys())
                for field_name in existing - allowed:
                    self.fields.pop(field_name)
    # Use it
    class MyPonySerializer(DynamicFieldsModelSerializer):
        # [...]
    

    Last, use a homemage mixin for your APIViews

    class DynamicFields(object):
        """A mixins that allows the query builder to display certain fields"""
    
        def get_fields_to_display(self):
            fields = self.request.GET.get('fields', None)
            return fields.split(',') if fields else None
    
        def get_serializer(self, instance=None, data=None, files=None, many=False,
                           partial=False, allow_add_remove=False):
            """
            Return the serializer instance that should be used for validating and
            deserializing input, and for serializing output.
            """
            serializer_class = self.get_serializer_class()
            context = self.get_serializer_context()
            fields = self.get_fields_to_display()
            return serializer_class(instance, data=data, files=files,
                                    many=many, partial=partial,
                                    allow_add_remove=allow_add_remove,
                                    context=context, fields=fields)
    
        def get_pagination_serializer(self, page):
            """
            Return a serializer instance to use with paginated data.
            """
            class SerializerClass(self.pagination_serializer_class):
                class Meta:
                    object_serializer_class = self.get_serializer_class()
    
            pagination_serializer_class = SerializerClass
            context = self.get_serializer_context()
            fields = self.get_fields_to_display()
            return pagination_serializer_class(instance=page, context=context, fields=fields)
    
    class MyPonyList(DynamicFields, generics.ListAPIView):
        # [...]
    

    Request

    Now, when you request a resource, you can add a parameter fields to show only specified fields in url. /?fields=field1,field2

    You can find a reminder here : https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

    0 讨论(0)
  • 2020-11-28 18:56

    For nested data, I am using Django Rest Framework with the package recommended in the docs, drf-flexfields

    This allows you to restrict the fields returned on both the parent and child objects. The instructions in the readme are good, just a few things to watch out for:

    The URL seems to need the / like this '/person/?expand=country&fields=id,name,country' instead of as written in the readme '/person?expand=country&fields=id,name,country'

    The naming of the nested object and its related name need to be completely consistent, which isn't required otherwise.

    If you have 'many' e.g. a country can have many states, you'll need to set 'many': True in the Serializer as described in the docs.

    0 讨论(0)
  • 2020-11-28 18:58

    Such functionality we've provided in drf_tweaks / control-over-serialized-fields.

    If you use our serializers, all you need is to pass ?fields=x,y,z parameter in the query.

    0 讨论(0)
  • 2020-11-28 18:59

    If you want something flexible like GraphQL, you can use django-restql. It supports nested data (both flat and iterable).

    Example

    from rest_framework import serializers
    from django.contrib.auth.models import User
    from django_restql.mixins import DynamicFieldsMixin
    
    class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ('id', 'username', 'email', 'groups')
    

    A regular request returns all fields.

    GET /users

        [
          {
            "id": 1,
            "username": "yezyilomo",
            "email": "yezileliilomo@hotmail.com",
            "groups": [1,2]
          },
          ...
        ]
    

    A request with the query parameter on the other hand returns only a subset of the fields:

    GET /users/?query={id, username}

        [
          {
            "id": 1,
            "username": "yezyilomo"
          },
          ...
        ]
    

    With django-restql you can access nested fields of any level. E.g

    GET /users/?query={id, username, date_joined{year}}

        [
          {
            "id": 1,
            "username": "yezyilomo",
            "date_joined": {
                "year": 2018
            }
          },
          ...
        ]
    

    For iterable nested fields, E.g groups on users.

    GET /users/?query={id, username, groups{id, name}}

        [
          {
            "id": 1,
            "username": "yezyilomo",
            "groups": [
                {
                    "id": 2,
                    "name": "Auth_User"
                }
            ]
          },
          ...
        ]
    
    0 讨论(0)
  • 2020-11-28 19:02

    You can override the serializer __init__ method and set the fields attribute dynamically, based on the query params. You can access the request object throughout the context, passed to the serializer.

    Here is a copy&paste from Django Rest Framework documentation example on the matter:

    from rest_framework import serializers
    
    class DynamicFieldsModelSerializer(serializers.ModelSerializer):
        """
        A ModelSerializer that takes an additional `fields` argument that
        controls which fields should be displayed.
        """
    
        def __init__(self, *args, **kwargs):
            # Instantiate the superclass normally
            super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
    
            fields = self.context['request'].query_params.get('fields')
            if fields:
                fields = fields.split(',')
                # Drop any fields that are not specified in the `fields` argument.
                allowed = set(fields)
                existing = set(self.fields.keys())
                for field_name in existing - allowed:
                    self.fields.pop(field_name)
    
    
    class UserSerializer(DynamicFieldsModelSerializer, serializers.HyperlinkedModelSerializer):
    
        class Meta:
            model = User
            fields = ('url', 'username', 'email')
    
    0 讨论(0)
提交回复
热议问题