Django Rest Framework: retorna dinamicamente o subconjunto de campos

100

Problema

Conforme recomendado na postagem do blog Best Practices para projetar uma API RESTful pragmática , gostaria de adicionar um fieldsparâmetro de consulta a uma API baseada no Django Rest Framework que permite ao usuário selecionar apenas um subconjunto de campos por recurso.

Exemplo

Serializer:

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

Uma consulta regular retornaria todos os campos.

GET /identities/

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

Uma consulta com o fieldsparâmetro deve retornar apenas um subconjunto dos campos:

GET /identities/?fields=id,data

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

Uma consulta com campos inválidos deve ignorar os campos inválidos ou gerar um erro do cliente.

Objetivo

Isso é possível fora da caixa de alguma forma? Se não, qual é a maneira mais simples de implementar isso? Existe um pacote de terceiros que já faz isso?

Danilo Bargen
fonte

Respostas:

121

Você pode substituir o __init__método do serializador e definir o fieldsatributo dinamicamente, com base nos parâmetros de consulta. Você pode acessar o requestobjeto em todo o contexto, passado para o serializador.

Aqui está um exemplo de documentação de copiar e colar do Django Rest Framework sobre o assunto:

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')
YAtOff
fonte
4
Finalmente resolvi implementar isso e funciona perfeitamente! Obrigado. Acabei escrevendo um mixin para isso, a composição é um pouco mais flexível do que a subclasse :) gist.github.com/dbrgn/4e6fc1fe5922598592d6
Danilo Bargen
8
Você precisará mudar QUERY_PARAMSpara query_paramsnas versões recentes do Django, mas fora isso, funciona perfeitamente.
Myk Willis
3
Você provavelmente deve verificar se requestsexiste como membro de context. Enquanto faz na produção, não faz isso ao executar testes de unidade que criam os objetos manualmente.
smitec
21
Para sua informação: Este exemplo é uma cópia literal da documentação DRF encontrada aqui: django-rest-framework.org/api-guide/serializers/#example É uma má forma de não fornecer link para os autores originais
Alex Bausk
3
A documentação DRF , da qual esta resposta foi copiada, foi aprimorada desde que esta resposta foi postada.
Chris
51

Esta funcionalidade está disponível em um pacote de terceiros .

pip install djangorestframework-queryfields

Declare seu serializador assim:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Em seguida, os campos agora podem ser especificados (lado do cliente) usando argumentos de consulta:

GET /identities/?fields=id,data

A filtragem de exclusão também é possível, por exemplo, para retornar todos os campos, exceto id:

GET /identities/?fields!=id

isenção de responsabilidade: eu sou o autor / mantenedor.

wim
fonte
1
Oi. Qual é a diferença entre this e github.com/dbrgn/drf-dynamic-fields (conforme link nos comentários da resposta escolhida)?
Danilo Bargen
5
Obrigado, dei uma olhada nessa implementação e parece que é a mesma ideia básica. Mas a dbrgnimplementação tem algumas diferenças: 1. não suporta excluir com fields!=key1,key2. 2. também modifica serializadores fora do contexto de solicitação GET, o que pode e irá interromper algumas solicitações PUT / POST. 3. não acumula campos com fields=key1&fields=key2, por exemplo , o que é útil para aplicativos ajax. Ele também tem cobertura de teste zero, o que é um tanto incomum no OSS.
wim
1
@wim Quais versões de DRF e Django sua biblioteca suporta? Não encontrei nada nos documentos.
pawelswiecki
1
Django 1.7-1.11 +, basicamente qualquer configuração que o DRF suporta. Este comentário pode estar desatualizado, portanto, verifique a matriz de teste do IC aqui .
wim
1
Funciona muito bem para mim: 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
Austin Malerba
fonte
3

Configure uma nova classe de serializador de paginação

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

Faça serializador dinâmico

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

Por último, use um mixin de imagens caseiras para suas 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):
    # [...]

Solicitação

Agora, ao solicitar um recurso, você pode adicionar um parâmetro fieldspara mostrar apenas os campos especificados no url. /?fields=field1,field2

Você pode encontrar um lembrete aqui: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Kmaschta
fonte
2

Você pode tentar REST dinâmico , que tem suporte para campos dinâmicos (inclusão, exclusão), objetos incorporados / sideload, filtragem, ordenação, paginação e muito mais.

blueFast
fonte
1

Para dados aninhados, estou usando Django Rest Framework com o pacote recomendado na documentação , drf-flexfields

Isso permite que você restrinja os campos retornados nos objetos pai e filho. As instruções no leia-me são boas, apenas algumas coisas a serem observadas:

O URL parece precisar do / assim '/ pessoa /? Expand = país & campos = id, nome, país' em vez de como escrito no readme '/ pessoa? Expand = país & campos = id, nome, país'

A nomenclatura do objeto aninhado e seu nome relacionado precisam ser completamente consistentes, o que não é necessário de outra forma.

Se você tiver 'muitos', por exemplo, um país pode ter muitos estados, você precisará definir 'muitos': True no serializador conforme descrito nos documentos.

Pequeno cérebro
fonte
1

Se você quiser algo flexível como GraphQL, você pode usar django-restql . Ele oferece suporte a dados aninhados (simples e iteráveis).

Exemplo

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

Uma solicitação regular retorna todos os campos.

GET /users

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

Uma solicitação com o queryparâmetro, por outro lado, retorna apenas um subconjunto dos campos:

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

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

Com django-restql você pode acessar campos aninhados de qualquer nível. Por exemplo

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

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

Para campos aninhados iteráveis, por exemplo, grupos de usuários.

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

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