Django Rest Framework: retourne dynamiquement un sous-ensemble de champs

102

Problème

Comme recommandé dans l'article de blog Best Practices for Designing a Pragmatic RESTful API , je voudrais ajouter un fieldsparamètre de requête à une API basée sur Django Rest Framework qui permet à l'utilisateur de sélectionner uniquement un sous-ensemble de champs par ressource.

Exemple

Sérialiseur:

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

Une requête régulière renverrait tous les champs.

GET /identities/

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

Une requête avec le fieldsparamètre ne doit renvoyer qu'un sous-ensemble des champs:

GET /identities/?fields=id,data

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

Une requête avec des champs non valides doit ignorer les champs non valides ou générer une erreur client.

Objectif

Est-ce possible d'une manière ou d'une autre? Sinon, quelle est la manière la plus simple de mettre en œuvre cela? Existe-t-il déjà un package tiers qui le fait déjà?

Danilo Bargen
la source

Réponses:

124

Vous pouvez remplacer la __init__méthode du sérialiseur et définir l' fieldsattribut de manière dynamique, en fonction des paramètres de requête. Vous pouvez accéder à l' requestobjet dans tout le contexte, passé au sérialiseur.

Voici un exemple de copier-coller de la documentation de Django Rest Framework à ce sujet:

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
la source
4
Je suis finalement venu mettre en œuvre cela, et cela fonctionne parfaitement! Merci. J'ai fini par écrire un mixin pour cela, la composition est un peu plus flexible que le sous- classement
Danilo Bargen
8
Vous devrez changer QUERY_PARAMSpour query_paramsles versions récentes de Django, mais à part cela, cela fonctionne comme un charme.
Myk Willis
3
Vous devriez probablement vérifier qu'il requestsexiste en tant que membre de context. Alors que c'est le cas en production, ce n'est pas le cas lors de l'exécution de tests unitaires qui créent les objets manuellement.
smitec
21
FYI: Cet exemple est une copie textuelle de la documentation DRF trouvée ici: django-rest-framework.org/api-guide/serializers/#example C'est une mauvaise forme de ne pas fournir de lien vers les auteurs originaux
Alex Bausk
3
La documentation DRF , à partir de laquelle cette réponse a été copiée, a été améliorée depuis que cette réponse a été publiée.
Chris
51

Cette fonctionnalité est disponible à partir d'un package tiers .

pip install djangorestframework-queryfields

Déclarez votre sérialiseur comme ceci:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Ensuite, les champs peuvent maintenant être spécifiés (côté client) en utilisant des arguments de requête:

GET /identities/?fields=id,data

Le filtrage d'exclusion est également possible, par exemple pour renvoyer tous les champs sauf id:

GET /identities/?fields!=id

avertissement: je suis l'auteur / le mainteneur.

wim
la source
1
Salut. Quelle est la différence entre ceci et github.com/dbrgn/drf-dynamic-fields (comme lié dans les commentaires de la réponse choisie)?
Danilo Bargen
5
Merci, j'ai jeté un coup d'œil à cette implémentation, et il semble que ce soit la même idée de base. Mais l' dbrgnimplémentation a quelques différences: 1. ne prend pas en charge exclure avec fields!=key1,key2. 2. modifie également les sérialiseurs en dehors du contexte de requête GET, ce qui peut interrompre et interrompra certaines requêtes PUT / POST. 3. n'accumule pas les champs avec par exemple fields=key1&fields=key2, ce qui est un bon choix pour les applications ajax. Il a également une couverture de test nulle, ce qui est quelque peu inhabituel dans OSS.
wim
1
@wim Quelles versions de DRF et de Django votre bibliothèque prend-elle en charge? Je n'ai rien trouvé dans la documentation.
pawelswiecki
1
Django 1.7-1.11 +, essentiellement toute configuration prise en charge par DRF. Ce commentaire peut devenir obsolète, alors vérifiez la matrice de test du CI, ici .
wim
1
Fonctionne très bien pour moi: 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
la source
3

Configurer une nouvelle classe de sérialiseur de pagination

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

Créer un sérialiseur dynamique

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

Enfin, utilisez un mixin de homemage pour vos 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):
    # [...]

Demande

Désormais, lorsque vous demandez une ressource, vous pouvez ajouter un paramètre fieldspour afficher uniquement les champs spécifiés dans l'url. /?fields=field1,field2

Vous pouvez trouver un rappel ici: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Kmaschta
la source
2

Vous pouvez essayer Dynamic REST , qui prend en charge les champs dynamiques (inclusion, exclusion), les objets incorporés / chargés latéralement, le filtrage, le classement, la pagination, etc.

blueFast
la source
1

Nous avons fourni une telle fonctionnalité dans drf_tweaks / control-over-serialized-fields .

Si vous utilisez nos sérialiseurs, il vous suffit de passer le ?fields=x,y,zparamètre dans la requête.

Paweł Krzyżaniak
la source
1

Pour les données imbriquées, j'utilise Django Rest Framework avec le package recommandé dans la documentation , drf-flexfields

Cela vous permet de restreindre les champs renvoyés sur les objets parent et enfant. Les instructions dans le readme sont bonnes, juste quelques points à surveiller:

L'URL semble avoir besoin du / comme ceci '/ person /? Expand = country & fields = id, name, country' au lieu de comme écrit dans le readme '/ person? Expand = country & fields = id, name, country'

La dénomination de l'objet imbriqué et son nom associé doivent être complètement cohérents, ce qui n'est pas obligatoire autrement.

Si vous avez «plusieurs», par exemple un pays peut avoir plusieurs états, vous devrez définir «plusieurs»: True dans le sérialiseur comme décrit dans la documentation.

Petit cerveau
la source
1

Si vous voulez quelque chose de flexible comme GraphQL, vous pouvez utiliser django-restql . Il prend en charge les données imbriquées (à la fois plates et itérables).

Exemple

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

Une requête régulière renvoie tous les champs.

GET /users

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

Une requête avec le queryparamètre en revanche ne renvoie qu'un sous-ensemble des champs:

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

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

Avec django-restql, vous pouvez accéder aux champs imbriqués de n'importe quel niveau. Par exemple

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

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

Pour les champs imbriqués itérables, par exemple les groupes sur les utilisateurs.

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

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