Django Rest Framework: Динамічно повертається підмножина полів


100

Проблема

Як рекомендовано в « Кращих практиках для створення проекту прагматичного API RESTful» в блозі , я хотів би додати fieldsпараметр запиту до API на основі Django Rest Framework, який дозволяє користувачеві вибирати лише підмножину полів на ресурс.

Приклад

Серіалізатор:

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"
  },
  ...
]

Запит з недійсними полями повинен або ігнорувати недійсні поля, або викликати помилку клієнта.

Мета

Це можливо якось поза коробкою? Якщо ні, то який найпростіший спосіб здійснити це? Чи існує пакет сторонніх продуктів, який вже робить це?

Відповіді:


121

Ви можете замінити __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')

4
Нарешті я прийшов, щоб це здійснити, і це прекрасно працює! Дякую. Я закінчив писати міксин для цього, композиція трохи більш гнучка, ніж підкласифікація :) gist.github.com/dbrgn/4e6fc1fe5922598592d6
Данило Барген

8
Вам потрібно змінити , QUERY_PARAMSщоб query_paramsв останніх версіях Django, але крім того , що це працює як шарм.
Майк Вілліс

3
Ви, ймовірно, повинні перевірити, що requestsіснує як член context. Хоча це робиться у виробництві, він не виконує тести, що створюють об'єкти вручну.
smitec

21
FYI: Цей приклад - це дослівна копія документації щодо DRF, знайдена тут: django-rest-framework.org/api-guide/serializers/#example Це погана форма не надавати посилання на оригінальних авторів
Alex Bausk

3
Документація ДРФ , з якої була скопійована відповідь, була покращена з моменту опублікування цієї відповіді.
Кріс

51

Ця функціональність доступна у стороннього пакету .

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

відмова від відповідальності: я автор / підтримувач.


1
Привіт. Яка різниця між цим і github.com/dbrgn/drf-dynamic-fields (про що йдеться у коментарях до обраної відповіді)?
Данило Барген

5
Дякую, я ознайомився з цією реалізацією, і, схоже, це та сама основна ідея. Але dbrgnреалізація має деякі відмінності: 1. не підтримує виключення з fields!=key1,key2. 2. також модифікує серіалізатори поза контекстом запиту GET, що може і порушує деякі запити PUT / POST. 3. не накопичує поля, наприклад fields=key1&fields=key2, що є приємним для програм ajax. Він також має нульове покриття тесту, що дещо незвично в OSS.
Вім

1
@wim Які версії DRF та Django підтримує ваша бібліотека? Я нічого не знайшов у документах.
pawelswiecki

1
Django 1.7-1.11 +, в основному будь-яка конфігурація, яку підтримує DRF. Цей коментар може застаріти, тому перевірте тестову матрицю на CI тут .
Вім

1
Для мене чудово працює: Django == 2.2.7, djangorestframework == 3.10.3, djangorestframework-queryfields == 1.0.0
Neeraj Kashyap

7

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

3

Налаштуйте новий клас серіалізатора пагинації

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

Нарешті, використовуйте домашній міксин для своїх APIView

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


2

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



1

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

Це дозволяє обмежити поля, повернуті як на батьківський, так і на дочірній об’єкти. Інструкції в читанні хороші, лише кілька речей, на які слід стежити:

URL-адреса, здається, потребує / як ця '/ person /? Expand = країна & поля = id, ім'я, країна' замість того, як написано в readme '/ person?

Іменування вкладеного об'єкта та пов'язаного з ним імені повинно бути повністю узгодженим, що не потрібно інакше.

Якщо у вас є "багато", наприклад, країна може мати багато штатів, вам потрібно встановити "багато": True у серіалізаторі, як описано в документах.


1

Якщо ви хочете щось гнучке, як 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": "yezileliilomo@hotmail.com",
        "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"
            }
        ]
      },
      ...
    ]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.