_Ce n'est pas une question de stackoverflow IMO, alors excusez-moi si c'est le cas_
J'essaie d'utiliser des sérialiseurs imbriqués, je sais que c'est difficile à discuter car nous avons eu tellement de problèmes dans la branche v2. Je souhaite que le champ de sérialiseur imbriqué utilise la valeur par défaut en fonction du projet sélectionné.
ID de projet fourni dans QUERY_PARAMS
et s'applique dans perform_create
comme il se doit maintenant, dans la branche v3. Mais j'ai besoin d'un projet dans un champ de sérialiseur imbriqué AVANT l'enregistrement - je souhaite fournir des valeurs par défaut pour l'objet dans les métadonnées du sérialiseur, afin que l'utilisateur puisse modifier certaines choses, qui peuvent éventuellement être extraites de l'objet du projet.
Donc, je remplis context
dans get_serializer_context
avec l'objet projet. Et je devrais simplement réécrire __init__
dans le sérialiseur imbriqué pour définir les valeurs par défaut, vous ?
Le problème est que le sérialiseur imbriqué ne reçoit pas context
ni parent
dans __init__
. Il semble qu'il ne reçoive aucun contexte ! Il devrait donc s'agir d'une régression depuis la pull-request 497 .
Je pense que ce n'est pas de la régression, plutôt du design change, mais j'ai vraiment besoin de contexte pour pouvoir changer le comportement de serizliser !
Le contexte n'est pas (ne peut pas être) transmis aux champs (ou au sérialiseur imbriqué) au point d'initialisation, car ils sont initialisés lorsqu'ils sont déclarés sur le sérialiseur parent...
class MySerializer(Serializer):
child = ChildSerializer() <--- We've just initialized this.
Le mieux est d'effectuer n'importe quel comportement comme celui-ci dans MySerializer.__init__
, par exemple...
def __init__(self, *args, **kwargs):
super(MySerializer, self).__init__(*args, **kwargs)
self.fields['child'].default = ...
À ce stade, vous avez _do_ accès au contexte.
Si vous le souhaitez vraiment, vous pouvez effectuer certaines actions au moment où le champ devient lié à son parent...
class ChildSerializer(Serializer):
def bind(self, *args, **kwargs):
super(ChildSerializer, self).bind(*args, **kwargs)
# do stuff with self.context.
Cela doit être considéré comme une API privée, et le style parent __init__
répertorié ci-dessus doit être préféré.
Oui, je comprends ce que tu expliques. C'est pourquoi je pense que c'est un peu délicat. C'est pourquoi ma solution de contournement consistait à créer un sérialiseur imbriqué dans le __ini__
parent. Mais le sérialiseur imbriqué __init__
est appelé une fois que chaque nouvelle instance de sérialiseur est créée, car il utilise une copie en profondeur, mais nous ne pouvons pas y glisser une instance parente...
Donc, comme je peux le voir dans la v2, le champ avait la méthode initialize
, ce qui était parfait pour initialiser les valeurs par défaut, en fonction du sérialiseur parent.
Donc, il semble bind
devrait être bon.
Cela doit être considéré comme une API privée, et le style parent
__init__
répertorié ci-dessus doit être préféré.
Mais comme nous ne pouvons tout simplement pas utiliser __init__
, car nous ne pouvons pas accéder à tous les environnements disponibles, créés par view pour le moment, nous devrions avoir une méthode API publique, qui est appelée sur field(serializer) lorsque nous avons tout disponible. Comme initialize
était.
Est-ce que ça marche pour DRF 2 ?
c'est dommage que le contexte ne soit plus propagé aux sérialiseurs imbriqués.
c'est dommage que le contexte ne soit plus propagé aux sérialiseurs imbriqués.
Où as-tu eu ça ? Autant que je sache, le contexte _est_ propagé aux sérialiseurs imbriqués.
@xordoquy c'est étrange - j'ai fait un cas de test contre le dernier drf et je ne peux pas reproduire le problème :) de plus je ne peux pas le reproduire contre mon propre code. Problème résolu.
@xiaohanyu en fait, j'ai de nouveau trouvé ce problème. Et c'est un peu étrange :
>>> 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 désolé de déranger, mais peut-être aurez-vous des idées sur la façon dont cela est possible?
En attendant je suis obligé d'utiliser ce hack pour résoudre un problème :
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, nous ne pouvons pas beaucoup aider sans un simple cas de test. Vous utilisez plusieurs sérialiseurs différents qui peuvent avoir du code défini par l'utilisateur qui interrompt la propagation du contexte.
Il s'avère que cela se produit en raison du complexe mro
, où plusieurs classes sont des enfants de serializers.Serializer. Quelque chose comme ca
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
Quoi qu'il en soit, je pense que cela pourrait être considéré comme un problème local ..
Merci pour le retour 👍
@xordoquy @tomchristie en fait, le problème est devenu vraiment étrange :) chaque fois que vous accédez à self.context dans un sérialiseur remplacé, un problème avec un contexte manquant apparaît. J'ai essayé de le faire dans la propriété fields
sans succès. Voici un cas test :
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}
Cependant celui-ci passerait. à mon humble avis, il convient de noter dans les sources que l'accès au contexte dans les méthodes remplacées peut être problématique ...
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}
Pour être clair, ce que je fais ici, c'est changer le comportement des champs en fonction du contexte.
UPD : même le dernier code n'est pas fiable dans certains cas. s'avère que toucher au contexte est dangereux.
Il est dommage que __init__
n'ait pas accès au contexte. Si l'on veut créer dynamiquement des champs dans le sérialiseur en fonction de quelque chose dans le contexte, où feriez-vous cela ?
Cela ne semble pas fonctionner dans bind
comme cela a été suggéré ci-dessus. Par exemple self.fields['asdf'] = serializers.CharField()
. Je suppose que les champs ont déjà été évalués lorsque bind
est appelé.
Le faire dans la propriété fields
ne semble pas génial non plus, puisque cette chose est appelée beaucoup, et en raison de l'évaluation paresseuse, elle a un mécanisme de mise en cache dans self._fields
qui devrait être dupliqué dans le sérialiseur concret. Il serait préférable de pouvoir modifier les champs une seule fois, avant qu'ils ne soient mis en cache en interne.
Je pense que ce ne serait pas un problème si l'imbrication était faite comme un champ, plutôt que d'appeler directement le sérialiseur.
Comme ça:
class ChildSerializer(serializers.Serializer):
...
class ParentSerializer(serializers.Serializer):
child = serializers.NestedField(serializer=ChildSerializer)
Ensuite, le ChildSerializer
pourrait avoir accès au contexte dans __init__
, puisqu'il n'est pas instancié directement dans la déclaration de champ dans ParentSerializer
.
Si l'on veut créer dynamiquement des champs dans le sérialiseur en fonction de quelque chose dans le contexte, où feriez-vous cela ?
Parlons de cela dans le cadre d'un exemple précis. Trouvez un cas d'utilisation simple et agréable et nous pourrons trouver la meilleure façon de l'aborder.
Je pense que ce ne serait pas un problème si l'imbrication était faite comme un champ, plutôt que d'appeler directement le sérialiseur.
Essayez, même si je ne pense pas que nous emprunterons cette voie. C'est une grande rupture dans ce que nous faisons actuellement, et cela ne vous permet pas de définir des arguments sur le sérialiseur enfant. Je pense qu'il y a un cas valable pour ce style, mais je ne nous vois pas particulièrement changer à ce stade.
@tomchristie merci pour la réponse.
Un cas d'utilisation que j'ai actuellement consiste à basculer entre deux ensembles de champs différents dans un sérialiseur enfant imbriqué en fonction d'une propriété de l'utilisateur actuellement connecté.
Les utilisateurs de type A ne doivent jamais voir les champs pertinents pour les utilisateurs de type B, et vice versa.
Il ressemble à ceci :
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()
Mais je modifie régulièrement les champs de différentes manières, à la fois dans les formulaires API et HTML, en utilisant des sérialiseurs drf et/ou des formulaires django. Ajouter dynamiquement des champs, supprimer des champs, modifier des attributs, l'exemple ci-dessus est loin d'être le seul moyen. Je n'avais jamais eu à le faire chez un enfant imbriqué auparavant, et j'ai été complètement perplexe.
@tomchristie mon cas d'utilisation typique est de contrôler si le champ est requis ou non en fonction du contexte (imaginez un sérialiseur pour l'utilisateur et l'administrateur, où l'un d'entre eux n'a pas besoin de fournir l'ID utilisateur puisqu'il est passé via le contexte.) maintenant je suis obligé de utiliser une solution de mes messages précédents dans ce fil.
Je suppose qu'une solution de contournement pourrait être de toujours instancier le ChildSerializer
imbriqué dans la méthode __init__
du ParentSerializer
au lieu de dans la déclaration de champ, puis on peut transférer le contexte manuellement.
Comme ça:
class ChildSerializer(serializers.Serializer):
...
class ParentSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['child'] = ChildSerializer(context=self.context)
Cela semble fonctionner dans un premier test. Je ne sais pas s'il y a un hic avec cela.
J'allais suggérer la même chose, yup.
Eh bien, cela n'a pas fonctionné pour moi dans certains cas. Probablement à cause d'un héritage multiple.
Le mercredi 12 octobre 2016 à 14h56 +0200, "Tom Christie" [email protected] a écrit :
J'allais suggérer la même chose, yup.
—
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub ou désactivez le fil de discussion.
J'ai le même problème, lorsque je définis l'attribut default
(avec la CurrentUserDefault()
dans mon cas) sur le champ de sérialiseur (imbriqué) qui a sous-classé la méthode __init__
qui traite self.context
, le KeyError
apparaît indiquant que request
n'est pas trouvé dans le contexte (qui ne contient rien, c'est-à-dire qu'il n'est pas transmis par le parent).
Exemple de code :
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())
# ...
Journal des erreurs :
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'
Le champ ajouté manque de certaines choses que le sérialiseur fait via les métaclasses. C'est à vous de les définir.
Comme je l'ai déjà dit ci-dessus - une fois que j'ai supprimé la récupération du contexte dans la méthode __init__
, tout fonctionne bien ... Donc, le réglage du champ n'est certainement pas le problème, car le contexte sera passé du parent dans ce Cas.
C'était sur mon téléphone, je n'ai pas remarqué cette partie.
Ensuite, c'est normal puisque les sérialiseurs imbriqués sont instanciés bien avant le sérialiseur supérieur.
Le but du contexte est en effet de le faire passer bien après l'instanciation. Il n'est donc pas étonnant que cela ne fonctionne pas dans init.
D'accord. Pour la dernière note, je veux juste souligner que, comme le montre le journal des erreurs, il n'apparaît pas dans la méthode __init__
, mais dans l'objet CurrentUserDefault()
(dans son set_context
méthode). Mais bien sûr qu'il apparaîtrait aussi dans __init__
s'il y avait un appel direct au paramètre de contexte, c'est-à-dire sans vérifier son existence (car tout le contexte est vide, puisqu'il n'est pas passé).
De plus, je voudrais répéter ce qui a été conclu ci-dessus par quelqu'un d'autre - dans le cas où il n'y a pas d'imbrication (et lorsque le contexte est utilisé dans la méthode d'instanciation), tout fonctionne comme prévu.
Il y avait un problème dans la propagation du contexte aux enfants, il devrait être résolu dans https://github.com/encode/django-rest-framework/pull/5304 Je suppose qu'il aurait dû y avoir de nombreux problèmes liés à la mise en cache None
en tant que racine des sérialiseurs enfants qui doivent être résolus avec ce PR.
J'ai rencontré ce problème récemment. C'est la solution qui a fonctionné pour moi:
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
Dans ce qui précède, je crée une classe de base de sérialiseur qui remplace get_fields()
et passe self._context
à tout sérialiseur enfant ayant la même classe de base. Pour ListSerializers, j'attache le contexte à l'enfant de celui-ci.
Ensuite, je vérifie un paramètre de requête "omit_data" et supprime le champ "data" s'il est demandé.
J'espère que cela sera utile à tous ceux qui cherchent encore des réponses à ce sujet.
Je sais que c'est un vieux fil, mais je voulais juste partager que j'utilise une combinaison de __init__ et de liaison sur mon mixin personnalisé pour filtrer le jeu de requêtes en fonction de la demande (dans le contexte). Ce mixin suppose qu'une méthode get_objects_for_user existe sur le jeu de requêtes, ou utilise guardian.shortcuts.get_objects_for_user sinon. Cela fonctionne très bien pour les sérialiseurs imbriqués, également lors du POST ou du PATCH.
Commentaire le plus utile
Je suppose qu'une solution de contournement pourrait être de toujours instancier le
ChildSerializer
imbriqué dans la méthode__init__
duParentSerializer
au lieu de dans la déclaration de champ, puis on peut transférer le contexte manuellement.Comme ça:
Cela semble fonctionner dans un premier test. Je ne sais pas s'il y a un hic avec cela.