Django-rest-framework: Verschachtelter Serialisierer erhält keinen Ansichtskontext

Erstellt am 13. Feb. 2015  ·  26Kommentare  ·  Quelle: encode/django-rest-framework

_Es ist meiner Meinung nach keine Stackoverflow-Frage, also entschuldigen Sie mich, wenn es so ist_

Ich versuche, verschachtelte Serialisierer zu verwenden, ich weiß, dass es schwierig ist, darüber zu diskutieren, da wir in v2-Branch so viele Probleme hatten. Ich möchte, dass das verschachtelte Serializer-Feld den Standard basierend auf dem ausgewählten Projekt verwendet.

Die Projekt-ID wird in QUERY_PARAMS bereitgestellt und gilt in perform_create , wie es jetzt im v3-Zweig sein sollte. Aber ich brauche das Projekt in einem verschachtelten Serializer-Feld VOR dem Speichern - ich möchte Standardwerte für das Objekt in den Serializer-Metadaten bereitstellen, damit der Benutzer einige Dinge ändern kann, die optional aus dem Projektobjekt entnommen werden können.

Also fülle ich context in get_serializer_context mit dem Projektobjekt. Und ich sollte einfach __init__ in den verschachtelten Serializer umschreiben, um die Standardeinstellungen festzulegen, ja?

Das Problem ist, dass der verschachtelte Serializer weder context noch parent in __init__ empfängt. Es scheint, dass es überhaupt keinen Kontext erhält! Es sollte also eine Regression seit Pull-Request 497 sein.

Ich denke, es ist keine Regression, eher eine Designänderung, aber ich brauche wirklich Kontext, um das Serizliser-Verhalten ändern zu können!

Hilfreichster Kommentar

Ich denke, eine Problemumgehung könnte darin bestehen, das verschachtelte ChildSerializer immer in der Methode __init__ von ParentSerializer zu instanziieren, anstatt in der Felddeklaration, dann kann man den Kontext manuell weiterleiten.

So was:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Das scheint in einem ersten Test zu funktionieren. Bin mir nicht sicher, ob es da einen Haken gibt.

Alle 26 Kommentare

Der Kontext wird (kann nicht) an die Felder (oder den verschachtelten Serialisierer) zum Zeitpunkt der Initialisierung übergeben, da sie initialisiert werden, wenn sie auf dem übergeordneten Serialisierer deklariert werden ...

class MySerializer(Serializer):
    child = ChildSerializer()  <--- We've just initialized this.

Am besten ist es, ein solches Verhalten in MySerializer.__init__ auszuführen, z.

def __init__(self, *args, **kwargs):
    super(MySerializer, self).__init__(*args, **kwargs)
    self.fields['child'].default = ...

An diesem Punkt _haben_ Sie Zugriff auf den Kontext.

Wenn Sie wirklich wollen, können Sie einige Aktionen an dem Punkt ausführen, an dem das Feld an sein übergeordnetes Element gebunden wird ...

class ChildSerializer(Serializer):
    def bind(self, *args, **kwargs):
        super(ChildSerializer, self).bind(*args, **kwargs)
        # do stuff with self.context.

Dies sollte als private API betrachtet werden, und der oben aufgeführte übergeordnete __init__ -Stil sollte bevorzugt werden.

Ja, ich verstehe, was du erklärst. Deswegen finde ich es etwas kniffelig. Aus diesem Grund bestand meine Problemumgehung darin, einen verschachtelten Serializer im übergeordneten __ini__ zu erstellen. Aber der verschachtelte Serializer __init__ wird aufgerufen, sobald jede neue Serializer-Instanz erstellt wird, da er Deepcopy verwendet, aber wir können die übergeordnete Instanz nicht hineinschleichen ...
Wie ich also in v2 sehen kann, hatte das Feld die Methode initialize , die perfekt zum Initialisieren von Standardwerten war, basierend auf dem übergeordneten Serializer.

Es scheint also, dass bind gut sein sollten.

Dies sollte als private API betrachtet werden, und der oben aufgeführte übergeordnete __init__ -Stil sollte bevorzugt werden.

Aber da wir __init__ einfach nicht verwenden können, weil wir nicht auf alle verfügbaren Umgebungen zugreifen können, die im Moment von view erstellt wurden, sollten wir eine öffentliche API-Methode haben, die auf field(serializer) aufgerufen wird, wenn wir alle Dinge haben erhältlich. Wie initialize war.

Funktioniert es für DRF 2?

Schade, dass der Kontext nicht mehr an verschachtelte Serialisierer weitergegeben wird.

Schade, dass der Kontext nicht mehr an verschachtelte Serialisierer weitergegeben wird.

Woher hast du das ? Afaik, der Kontext _wird_ an die verschachtelten Serialisierer weitergegeben.

@xordoquy das ist seltsam - ich habe einen Testfall gegen den neuesten drf erstellt und kann das Problem nicht reproduzieren :) Außerdem kann ich es nicht mit meinem eigenen Code reproduzieren. Problem gelöst.

@xiaohanyu tatsächlich habe ich dieses Problem wieder einmal gefunden. Und es ist irgendwie seltsam:

>>> serializer
Out[12]: 
# Note there is context passed in constructor
CouponSerializer(<Coupon: Coupon Coupon #0 for offer Offer #0000>, context={'publisher': <User: John ZusPJRryIzYZ>}):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(max_length=100, required=True)
    short_title = CharField(max_length=30)
    offer_details = OfferLimitedSerializer(read_only=True, source='offer'):
        id = IntegerField(label='ID', read_only=True)
        title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)       
        status_name = CharField(read_only=True, source='get_status_display')
        is_access_limited = SerializerMethodField()    
    exclusive = BooleanField(required=False)    
    categories_details = OfferCategorySerializer(many=True, read_only=True, source='categories'):
        id = IntegerField(read_only=True)
        category = CharField(read_only=True, source='code')
        category_name = CharField(read_only=True, source='get_code_display')    

# all fields except one got context propagated
>>> [f.field_name for f in serializer.fields.values() if not f.context]
Out[20]: 
['offer_details']

# offer_details is no different than categories_details, both are nested, read only, model serializers..
>>> serializer.fields['categories_details'].context
Out[21]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# lets look inside problematic serializer
>>> offer_details = serializer.fields['offer_details']
>>> offer_details
Out[25]: 

OfferLimitedSerializer(read_only=True, source='offer'):
    id = IntegerField(label='ID', read_only=True)
    title = CharField(help_text='The Offer Title will be used for the Network, Advertisers and Publishers to identify the specific offer within the application.', read_only=True)    
    status_name = CharField(read_only=True, source='get_status_display')
    url = SerializerMethodField()
    is_access_limited = SerializerMethodField()
>>> offer_details.context
Out[23]: 
{}
>>> offer_details._context
Out[24]: 
{}
# ! surprisingly context is available inside fields... but not in the parent
>>> offer_details.fields['is_access_limited'].context
Out[26]: 
{'publisher': <User: John ZusPJRryIzYZ>}

# context is available in all of them!
>>> [x.field_name for x in offer_details.fields.values() if not x.context]
Out[27]: 
[]

@tomchristie Entschuldigung für die Störung, aber vielleicht hast du eine Idee, wie das möglich ist?

In der Zwischenzeit bin ich gezwungen, diesen Hack zu verwenden, um ein Problem zu lösen:

class Serializer(serializers.Serializer):

    def __init__(self, *args, **kwargs):
        super(Serializer, self).__init__(*args, **kwargs)
        # propagate context to nested complex serializers
        if self.context:
            for field in six.itervalues(self.fields):
                if not field.context:
                    delattr(field, 'context')
                    setattr(field, '_context', self.context)

@pySilver ohne einen einfachen Testfall können wir nicht viel helfen. Sie verwenden mehrere verschiedene Serialisierer, die möglicherweise benutzerdefinierten Code enthalten, der die Kontextweitergabe unterbricht.

Es stellt sich heraus, dass dies auf den Komplex mro zurückzuführen ist, bei dem mehrere Klassen untergeordnete Klassen von serializers.Serializer sind. Sowas in der Art

class MyMixin(serializers.Serializer):
   # some code within __init__, calling parent too

class MyModelSerializer(serializers.ModelSerializer):
   # some code within __init__, calling parent too

class MyProblematicSerializer(MyModelSerializer, MyMixin)
    # this one will not receive context somehow.
     pass

Jedenfalls denke ich, dass dies als lokales Problem angesehen werden könnte.

Danke für das Feedback 👍

@xordoquy @tomchristie Tatsächlich wurde das Problem wirklich seltsam :) Wenn Sie auf self.context in überschriebenem Serializer zugreifen, wird ein Problem mit fehlendem Kontext angezeigt. Ich habe versucht, das in fields Eigentum ohne Glück zu tun. Hier ist ein Testfall:

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            def __init__(self, *args, **kwargs):
                super(MyBaseSerializer, self).__init__(*args, **kwargs)
                x = 0
                if self.context.get('x'):
                    x = self.context['x']

        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

Dieser würde jedoch passieren. imho sollte in Quellen angemerkt werden, dass der Zugriff auf den Kontext in überschriebenen Methoden problematisch sein könnte ...

def test_serializer_context(self):
        class MyBaseSerializer(serializers.Serializer):
            <strong i="12">@property</strong>
            def fields(self):
                fields = super(MyBaseSerializer, self).fields
                x = 0
                if self.root.context.get('x'):
                    x = 1
                return fields


        class SubSerializer(MyBaseSerializer):
            char = serializers.CharField()
            integer = serializers.IntegerField()

        class ParentSerializer(MyBaseSerializer):
            with_context = serializers.CharField()
            without_context = SubSerializer()

        serializer = ParentSerializer(data={}, context={'what': 42})
        assert serializer.context == {'what': 42}
        assert serializer.fields['with_context'].context == {'what': 42}
        assert serializer.fields['without_context'].context == {'what': 42}

Um es klar zu sagen – ich ändere hier das Verhalten von Feldern basierend auf dem Kontext.

UPD: Auch der letzte Code ist in einigen Fällen nicht zuverlässig. stellt sich heraus, dass es gefährlich ist, den Kontext zu berühren.

Leider hat __init__ keinen Zugriff auf den Kontext. Wenn man dynamisch Felder im Serializer basierend auf etwas im Kontext erstellen möchte, wo würde man das tun?

Es scheint nicht in bind zu funktionieren, wie oben vorgeschlagen wurde. Zum Beispiel self.fields['asdf'] = serializers.CharField() . Ich gehe davon aus, dass die Felder bereits ausgewertet wurden, wenn bind aufgerufen wird.

Es in der Eigenschaft fields zu tun, scheint auch nicht großartig zu sein, da das Ding viel genannt wird und aufgrund der faulen Auswertung einen Caching-Mechanismus in self._fields hat, der dupliziert werden müsste im konkreten Serializer. Es wäre besser, die Felder einmal ändern zu können, bevor sie intern zwischengespeichert werden.

Ich denke, das wäre kein Problem, wenn die Verschachtelung als Feld erfolgen würde, anstatt den Serializer direkt aufzurufen.

So was:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    child = serializers.NestedField(serializer=ChildSerializer)

Dann könnte ChildSerializer Zugriff auf den Kontext in __init__ haben, da es nicht direkt in der Felddeklaration in ParentSerializer instanziiert wird.

Wenn man dynamisch Felder im Serializer basierend auf etwas im Kontext erstellen möchte, wo würde man das tun?

Lassen Sie uns im Kontext eines konkreten Beispiels darüber sprechen. Überlegen Sie sich einen netten, einfachen Anwendungsfall und wir finden heraus, wie wir ihn am besten angehen.

Ich denke, das wäre kein Problem, wenn die Verschachtelung als Feld erfolgen würde, anstatt den Serializer direkt aufzurufen.

Versuchen Sie es, obwohl ich nicht glaube, dass wir diesen Weg gehen werden. Es ist ein großer Unterschied zu dem, was wir derzeit tun, und es erlaubt Ihnen nicht, Argumente für den untergeordneten Serializer festzulegen. Ich denke, es gibt gute Argumente für diesen Stil, aber ich sehe nicht, dass wir uns zu diesem Zeitpunkt besonders verändern werden.

@tomchristie Danke für die Antwort.

Ein Anwendungsfall, den ich gerade habe, ist das Umschalten zwischen zwei verschiedenen Sätzen von Feldern in einem verschachtelten untergeordneten Serializer, abhängig von einer Eigenschaft des aktuell angemeldeten Benutzers.

Typ-A-Benutzer sollten niemals die Felder sehen, die für Typ-B-Benutzer relevant sind, und umgekehrt.

Es sieht aus wie das:

class ChildSerializer(serializers.Serializer):
  # relevant for all users
  name = serializers.CharField()

  # only relevant for users of Type A
  phone = serializers.CharField()

  # only relevant for users of Type B
  ssn = serializers.CharField()

Aber ich ändere regelmäßig Felder auf viele verschiedene Arten, sowohl in APIs als auch in HTML-Formularen, indem ich drf-Serialisierer und/oder Django-Formulare verwende. Dynamisches Hinzufügen von Feldern, Entfernen von Feldern, Ändern von Attributen, das obige Beispiel ist bei weitem nicht der einzige Weg. Ich musste es nur noch nie bei einem verschachtelten Kind tun und war davon völlig ratlos.

@tomchristie Mein typischer Anwendungsfall besteht darin, basierend auf dem Kontext zu steuern, ob ein Feld erforderlich ist oder nicht (stellen Sie sich einen Serialisierer für Benutzer und Administrator vor, bei dem einer von ihnen keine Benutzer-ID angeben muss, da er über den Kontext übergeben wird). Inzwischen bin ich dazu gezwungen Verwenden Sie eine Lösung aus meinen früheren Beiträgen in diesem Thread.

Ich denke, eine Problemumgehung könnte darin bestehen, das verschachtelte ChildSerializer immer in der Methode __init__ von ParentSerializer zu instanziieren, anstatt in der Felddeklaration, dann kann man den Kontext manuell weiterleiten.

So was:

class ChildSerializer(serializers.Serializer):
    ...

class ParentSerializer(serializers.Serializer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['child'] = ChildSerializer(context=self.context)

Das scheint in einem ersten Test zu funktionieren. Bin mir nicht sicher, ob es da einen Haken gibt.

Wollte das gleiche vorschlagen, ja.

Nun, es hat bei mir in einigen Fällen nicht funktioniert. Wahrscheinlich wegen Mehrfachvererbung.

Am Mittwoch, den 12. Oktober 2016 um 14:56 Uhr +0200 schrieb „Tom Christie“ [email protected] :

Wollte das gleiche vorschlagen, ja.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub an oder schalten Sie den Thread stumm.

Ich habe das gleiche Problem, wenn ich das Attribut default (in meinem Fall mit dem Wert CurrentUserDefault() ) auf das (verschachtelte) Serializer-Feld setze, das die Methode __init__ als Unterklasse hat, die verarbeitet self.context -Attribut erscheint das KeyError $-Attribut und zeigt an, dass request nicht im Kontext gefunden wird (der nichts enthält, dh es wird nicht vom übergeordneten Element übergeben).
Beispielcode:

class UserSerializer(serializers.ModelSerializer):
# ...
    def __init__(self, *args, **kwargs):
        kwargs.pop('fields', None)
        super().__init__(*args, **kwargs)
        if 'list' in self.context: # Once I remove this, it will work
            self.fields['friend_count'] = serializers.SerializerMethodField()
# ...

class CommentSerializer(serializers.ModelSerializer):
    person = UserSerializer(read_only=True, default=serializers.CurrentUserDefault())
# ...

Fehlerprotokoll:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/api/comments/98/

Django Version: 1.9.7
Python Version: 3.4.3
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.sites',
 'rest_framework',
 'rest_framework.authtoken',
 'generic_relations',
 'decorator_include',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'allauth.socialaccount.providers.facebook',
 'allauth.socialaccount.providers.google',
 'phonenumber_field',
 'bootstrap3',
 'stronghold',
 'captcha',
 'django_settings_export',
 'main']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'stronghold.middleware.LoginRequiredMiddleware']



Traceback:

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  466.             response = self.handle_exception(exc)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/views.py" in dispatch
  463.             response = handler(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/generics.py" in post
  246.         return self.create(request, *args, **kwargs)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/mixins.py" in create
  20.         serializer.is_valid(raise_exception=True)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in is_valid
  213.                 self._validated_data = self.run_validation(self.initial_data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  407.         value = self.to_internal_value(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in to_internal_value
  437.                 validated_value = field.run_validation(primitive_value)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/serializers.py" in run_validation
  403.         (is_empty_value, data) = self.validate_empty_values(data)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in validate_empty_values
  453.             return (True, self.get_default())

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in get_default
  437.                 self.default.set_context(self)

File "/home/mikisoft/django-dev/lib/python3.4/site-packages/rest_framework/fields.py" in set_context
  239.         self.user = serializer_field.context['request'].user

Exception Type: KeyError at /api/comments/98/
Exception Value: 'request'

Dem hinzugefügten Feld fehlen einige Dinge, die der Serializer durch Metaklassen macht. Es liegt an Ihnen, sie einzustellen.

Wie ich bereits oben gesagt habe - sobald ich das Abrufen des Kontexts in der Methode __init__ entferne, funktioniert alles einwandfrei ... Die Feldeinstellung ist also sicherlich nicht das Problem, da der Kontext darin vom übergeordneten Element übergeben wird Fall.

Richtig, war auf meinem Handy, habe diesen Teil nicht bemerkt.
Dann ist es normal, da verschachtelte Serialisierer weit vor dem obersten Serialisierer instanziiert werden.
Der Zweck des Kontexts besteht in der Tat darin, ihn weit nach der Instanziierung weiterzugeben. Kein Wunder also, dass es in init nicht funktioniert.

In Ordnung. Für den letzten Hinweis möchte ich nur darauf hinweisen, dass es, wie das Fehlerprotokoll zeigt, nicht in der Methode __init__ erscheint, sondern im Objekt CurrentUserDefault() (in seiner set_context Methode). Aber sicher, dass es auch in __init__ auftauchen würde, wenn der Kontextparameter direkt aufgerufen würde, dh ohne dessen Existenz zu prüfen (weil der gesamte Kontext leer ist, da er nicht übergeben wird).
Außerdem möchte ich wiederholen, was oben von jemand anderem festgestellt wurde - in dem Fall, in dem es keine Verschachtelung gibt (und wenn der Kontext in der Instanziierungsmethode verwendet wird), funktioniert alles wie erwartet.

Es gab ein Problem bei der Weitergabe von Kontext an Kinder, es sollte in https://github.com/encode/django-rest-framework/pull/5304 gelöst werden. Ich denke, es hätte viele Probleme im Zusammenhang mit dem Caching None geben müssen

Ich bin kürzlich auf dieses Problem gestoßen. Dies ist die Lösung, die für mich funktioniert hat:

class MyBaseSerializer(serializers.HyperlinkedModelSerializer):

    def get_fields(self):
        '''
        Override get_fields() method to pass context to other serializers of this base class.

        If the context contains query param "omit_data" as set to true, omit the "data" field
        '''
        fields = super().get_fields()

        # Cause fields with this same base class to inherit self._context
        for field_name in fields:
            if isinstance(fields[field_name], serializers.ListSerializer):
                if isinstance(fields[field_name].child, MyBaseSerializer):
                    fields[field_name].child._context = self._context

            elif isinstance(fields[field_name], MyBaseSerializer):
                fields[field_name]._context = self._context

        # Check for "omit_data" in the query params and remove data field if true
        if 'request' in self._context:
            omit_data = self._context['request'].query_params.get('omit_data', False)

            if omit_data and omit_data.lower() in ['true', '1']:
                fields.pop('data')

        return fields

Oben erstelle ich eine Serializer-Basisklasse, die get_fields() überschreibt und self._context an alle untergeordneten Serializer übergibt, die dieselbe Basisklasse haben. Für ListSerializers hänge ich den Kontext an das untergeordnete Element an.

Dann suche ich nach einem Abfrageparameter „omit_data“ und entferne das Feld „data“, wenn es angefordert wird.

Ich hoffe, dies ist hilfreich für alle, die noch nach Antworten darauf suchen.

Ich weiß, dass dies ein alter Thread ist, aber ich wollte nur mitteilen, dass ich eine Kombination aus __init__ und bind für mein benutzerdefiniertes Mixin verwende, um den Abfragesatz basierend auf der Anfrage (im Kontext) zu filtern. Dieses Mixin geht davon aus, dass eine get_objects_for_user-Methode im Abfragesatz vorhanden ist, oder verwendet andernfalls guardian.shortcuts.get_objects_for_user. Es funktioniert sehr gut für verschachtelte Serialisierer, auch beim POSTing oder PATCHing.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen