Django-rest-framework: Le sérialiseur imbriqué ne reçoit pas le contexte de vue

Créé le 13 févr. 2015  ·  26Commentaires  ·  Source: encode/django-rest-framework

_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 !

Commentaire le plus utile

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.

Tous les 26 commentaires

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.

Cette page vous a été utile?
0 / 5 - 0 notes