Django Rest Framework: Teilmenge von Feldern dynamisch zurückgeben

100

Problem

Wie im Blogpost Best Practices für das Entwerfen einer pragmatischen RESTful-API empfohlen , möchte ich fieldseiner auf Django Rest Framework basierenden API einen Abfrageparameter hinzufügen , mit dem der Benutzer nur eine Teilmenge von Feldern pro Ressource auswählen kann.

Beispiel

Serializer:

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

Eine reguläre Abfrage würde alle Felder zurückgeben.

GET /identities/

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

Eine Abfrage mit dem fieldsParameter sollte nur eine Teilmenge der Felder zurückgeben:

GET /identities/?fields=id,data

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

Eine Abfrage mit ungültigen Feldern sollte entweder die ungültigen Felder ignorieren oder einen Clientfehler auslösen.

Tor

Ist das irgendwie sofort möglich? Wenn nicht, wie lässt sich dies am einfachsten umsetzen? Gibt es ein Paket von Drittanbietern, das dies bereits tut?

Danilo Bargen
quelle

Antworten:

121

Sie können die Serializer- __init__Methode überschreiben und das fieldsAttribut basierend auf den Abfrageparametern dynamisch festlegen. Sie können im requestgesamten Kontext auf das Objekt zugreifen , das an den Serializer übergeben wird.

Hier ist ein Beispiel für das Kopieren und Einfügen aus der Dokumentation zu 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')
YAtOff
quelle
4
Ich bin endlich gekommen, um dies umzusetzen, und es funktioniert perfekt! Vielen Dank. Am
Danilo Bargen
8
Sie werden sich ändern müssen , QUERY_PARAMSum query_paramsin den letzten Versionen von Django, aber anders als das dies funktioniert wie ein Charme.
Myk Willis
3
Sie sollten wahrscheinlich überprüfen, ob requestses als Mitglied von existiert context. Während der Produktion werden keine Unit-Tests ausgeführt, bei denen die Objekte manuell erstellt werden.
Smitec
21
Zu Ihrer Information : Dieses Beispiel ist eine wörtliche Kopie der DRF-Dokumentation, die hier zu finden ist: django-rest-framework.org/api-guide/serializers/#example Es ist eine schlechte Form, keinen Link zu Originalautoren bereitzustellen
Alex Bausk
3
Die DRF-Dokumentation , aus der diese Antwort kopiert wurde, wurde verbessert, seit diese Antwort veröffentlicht wurde.
Chris
50

Diese Funktionalität ist in einem Paket eines Drittanbieters verfügbar .

pip install djangorestframework-queryfields

Deklarieren Sie Ihren Serializer wie folgt:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Dann können die Felder jetzt (clientseitig) mithilfe von Abfrageargumenten angegeben werden:

GET /identities/?fields=id,data

Eine Ausschlussfilterung ist ebenfalls möglich, z. B. um jedes Feld außer id zurückzugeben:

GET /identities/?fields!=id

Haftungsausschluss: Ich bin der Autor / Betreuer.

wim
quelle
1
Hallo. Was ist der Unterschied zwischen diesem und github.com/dbrgn/drf-dynamic-fields (wie in den Kommentaren der gewählten Antwort verlinkt)?
Danilo Bargen
5
Vielen Dank, ich habe mir diese Implementierung angesehen und es sieht so aus, als wäre es dieselbe Grundidee. Die dbrgnImplementierung weist jedoch einige Unterschiede auf: 1. unterstützt nicht das Ausschließen mit fields!=key1,key2. 2. Ändert auch Serialisierer außerhalb des GET-Anforderungskontexts, wodurch einige PUT / POST-Anforderungen unterbrochen werden können und werden. 3. sammelt keine Felder mit zB fields=key1&fields=key2, was für Ajax-Apps eine gute Sache ist. Es hat auch keine Testabdeckung, was in OSS etwas ungewöhnlich ist.
wim
1
@wim Welche Versionen von DRF und Django unterstützt Ihre Bibliothek? Ich habe nichts in den Dokumenten gefunden.
Pawelswiecki
1
Django 1.7-1.11 +, im Grunde jede Konfiguration, die DRF unterstützt. Dieser Kommentar ist möglicherweise veraltet. Überprüfen Sie daher hier die Testmatrix für das CI .
wim
1
Funktioniert hervorragend für mich: 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
quelle
3

Konfigurieren Sie eine neue Paginierungs-Serializer-Klasse

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

Dynamischen Serializer erstellen

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

Verwenden Sie zuletzt ein Homemage-Mixin für Ihre 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):
    # [...]

Anfrage

Wenn Sie jetzt eine Ressource anfordern, können Sie einen Parameter hinzufügen fields, um nur bestimmte Felder in der URL anzuzeigen. /?fields=field1,field2

Eine Erinnerung finden Sie hier: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Kmaschta
quelle
2

Sie können Dynamic REST ausprobieren , das dynamische Felder (Einschluss, Ausschluss), eingebettete / seitlich geladene Objekte, Filterung, Reihenfolge, Paginierung und mehr unterstützt.

blueFast
quelle
1

Diese Funktionalität haben wir in den Feldern drf_tweaks / control-over-serialized-fields bereitgestellt .

Wenn Sie unsere Serializer verwenden, müssen Sie lediglich ?fields=x,y,zParameter in der Abfrage übergeben.

Paweł Krzyżaniak
quelle
1

Bei verschachtelten Daten, ich bin mit Django Ruhe Framework mit dem Paket in dem empfohlenen docs , DRF-flexfields

Auf diese Weise können Sie die Felder einschränken, die sowohl für das übergeordnete als auch für das untergeordnete Objekt zurückgegeben werden. Die Anweisungen in der Readme-Datei sind gut, nur ein paar Dinge, auf die Sie achten sollten:

Die URL scheint das / wie folgt zu benötigen: '/ person /? Expand = country & fields = id, name, country' anstatt wie in der Readme geschrieben '/ person? Expand = country & fields = id, name, country'.

Die Benennung des verschachtelten Objekts und des zugehörigen Namens muss vollständig konsistent sein, was sonst nicht erforderlich ist.

Wenn Sie "viele" haben, z. B. kann ein Land viele Bundesstaaten haben, müssen Sie "viele" festlegen: True im Serializer, wie in den Dokumenten beschrieben.

Kleines Gehirn
quelle
1

Wenn Sie etwas Flexibles wie GraphQL wollen, können Sie django-restql verwenden . Es unterstützt verschachtelte Daten (sowohl flach als auch iterierbar).

Beispiel

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

Eine reguläre Anfrage gibt alle Felder zurück.

GET /users

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

Eine Anfrage mit dem queryParameter gibt dagegen nur eine Teilmenge der Felder zurück:

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

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

Mit django-restql können Sie auf verschachtelte Felder jeder Ebene zugreifen. Z.B

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

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

Bei iterierbaren verschachtelten Feldern z. B. Gruppen für Benutzer.

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

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