web-dev-qa-db-fra.com

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

Problème

Comme recommandé dans l'article de blog Meilleures pratiques pour la conception d'une API RESTful pragmatique , j'aimerais ajouter un paramètre de requête fields à 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 normale 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 paramètre fields 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 de sortir de la boîte? Si non, quel est le moyen le plus simple de mettre en œuvre cela? Existe-t-il un package tiers qui le fasse déjà?

71
Danilo Bargen

Vous pouvez remplacer la méthode serializer __init__ et définir l'attribut fields de manière dynamique, en fonction des paramètres de la requête. Vous pouvez accéder à l'objet request via le contexte, transmis au sérialiseur.

Ici, j'ai créé un mixin réutilisable, qui effectue la modification dynamique fields.

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

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 par exclusion est également possible, par exemple pour retourner chaque champ sauf id:

GET /identities/?fields!=id

disclaimer: Je suis l'auteur/le responsable. 

37
wim

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
6
Austin Malerba

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

Faire 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 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

Maintenant, lorsque vous demandez une ressource, vous pouvez ajouter un paramètre fields pour 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

3
Kmaschta

Cette fonctionnalité que nous avons fournie dans drf_tweaks/control-over-serialized-fields .

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

1
Paweł Krzyżaniak

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

Cela vous permet de limiter les champs retournés à la fois sur les objets parent et enfant. Les instructions du fichier Lisez-moi sont bonnes. Quelques points à surveiller:

L'URL semble avoir besoin du/comme ceci '/ person /? Expand = pays & fields = id, nom, pays' au lieu de celui indiqué dans le fichier readme '/ person? Expand = pays & fields = id, nom, pays'

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

Si vous en avez plusieurs, par exemple un pays peut avoir plusieurs états, vous devez définir "plusieurs": Vrai dans le sérialiseur comme décrit dans la documentation.

0
Little Brain

Si vous voulez quelque chose de semblable à GraphQL, essayez Django-restql , il est très flexible et supporte 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 demande régulière renvoie tous les champs.

GET /users

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

Une demande avec le paramètre query, par contre, 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, E.g regroupe des utilisateurs.

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

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

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

0
dangonfast