Подтвердить что ты не робот

Django Rest Framework: динамическое возвращение подмножества полей

Проблема

Как рекомендовано в blogpost Рекомендации по разработке API-интерфейса Pragmatic RESTful, я хотел бы добавить параметр запроса fields к API-интерфейсу Django Rest Framework, который позволяет пользователю выбирать только подмножество полей для каждого ресурса.

Пример

Serializer:

class IdentitySerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Identity
        fields = ('id', 'url', 'type', 'data')

Обычный запрос будет возвращать все поля.

GET /identities/

[
  {
    "id": 1,
    "url": "http://localhost:8000/api/identities/1/",
    "type": 5,
    "data": "John Doe"
  },
  ...
]

Запрос с параметром fields должен возвращать только подмножество полей:

GET /identities/?fields=id,data

[
  {
    "id": 1,
    "data": "John Doe"
  },
  ...
]

Запрос с недопустимыми полями должен либо игнорировать недопустимые поля, либо вызывать ошибку клиента.

Цель

Возможно ли это из коробки? Если нет, то какой самый простой способ реализовать это? Есть ли сторонний пакет, который делает это уже?

4b9b3361

Ответ 1

Вы можете переопределить метод __init__ сериализатора и динамически установить атрибут fields, основываясь на параметрах запроса. Вы можете получить доступ к объекту request по всему контексту, переданному в сериализатор.

Вот копия и вставка из примера документации Django Rest Framework по этому вопросу:

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')

Ответ 2

Эта функциональность доступна из стороннего пакета.

pip install djangorestframework-queryfields

Объявите ваш сериализатор следующим образом:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Тогда поля теперь можно указывать (на стороне клиента), используя аргументы запроса:

GET /identities/?fields=id,data

Также возможна фильтрация исключений, например, для возврата каждого поля, кроме id:

GET /identities/?fields!=id

Отказ от ответственности: я автор/сопровождающий.

Ответ 3

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

Ответ 4

Настроить новый класс сериализатора страниц

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',
}

Сделать динамический сериализатор

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):
    # [...]

Наконец, используйте домашнюю смесь для ваших 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):
    # [...]

Запрос

Теперь, когда вы запрашиваете ресурс, вы можете добавить параметр fields, чтобы показывать только указанные поля в URL-адресе. /?fields=field1,field2

Здесь вы можете найти напоминание: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Ответ 5

Такая функциональность, которую мы предоставили в drf_tweaks/control-over-serialized-fields.

Если вы используете наши сериализаторы, вам нужно передать параметр ?fields=x,y,z в запрос.

Ответ 6

Вы можете попробовать Dynamic REST, который поддерживает динамические поля (включение, исключение), внедренные/загруженные объекты, фильтрацию, упорядочивание, разбиение на страницы и многое другое.

Ответ 7

Для вложенных данных я использую Django Rest Framework с пакетом, рекомендованным в документации, drf-flexfields

Это позволяет ограничить поля, возвращаемые как для родительских, так и для дочерних объектов. Инструкции в readme хороши, только некоторые вещи, на которые следует обратить внимание:

Похоже, что URL нужен/как этот '/person/? Expand = country & fields = id, name, country' вместо того, как написано в файле readme '/person? Expand = country & fields = id, name, country'

Именование вложенного объекта и связанного с ним имени должны быть полностью согласованными, в противном случае это не требуется.

Если у вас "много", например, в стране может быть много штатов, вам нужно установить "много": True в Сериализаторе, как описано в документации.

Ответ 8

Если вы хотите что-то похожее на GraphQL, попробуйте django-restql, оно очень гибкое и поддерживает вложенные данные (как плоские, так и повторяемые).

пример

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')

Обычный запрос возвращает все поля.

GET/users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "[email protected]",
        "groups": [1,2]
      },
      ...
    ]

Запрос с параметром query с другой стороны, возвращает только подмножество полей:

GET/users/?query=["id", "username"]

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

С django-restql вы можете получить доступ к вложенным полям любого уровня. Например

GET/users/?query=["id", "username" {"date_joined": ["year"]}]

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

Например, для повторяющихся вложенных полей, например, группы пользователей.

GET/users/?query=["id", "username" {"groups": [[ "id", "name" ]]}]

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
        ]
      },
      ...
    ]