_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!
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.
Hilfreichster Kommentar
Ich denke, eine Problemumgehung könnte darin bestehen, das verschachtelte
ChildSerializer
immer in der Methode__init__
vonParentSerializer
zu instanziieren, anstatt in der Felddeklaration, dann kann man den Kontext manuell weiterleiten.So was:
Das scheint in einem ersten Test zu funktionieren. Bin mir nicht sicher, ob es da einen Haken gibt.