web-dev-qa-db-fra.com

Comment accéder à serializer.data sur la classe parent ListSerializer dans DRF?

Je reçois une erreur lorsque j'essaie d'accéder à serializer.data Avant de le renvoyer dans la Response(serializer.data, status=something):

Obtention de KeyError lors de la tentative d'obtention d'une valeur pour le champ <field> Sur le sérialiseur <serializer>.

Cela se produit sur tous les champs (car il s'avère que j'essaie d'accéder à .data Sur le parent et non sur l'enfant, voir ci-dessous)

La définition de classe ressemble à ceci:

class BulkProductSerializer(serializers.ModelSerializer):

    list_serializer_class = CustomProductListSerializer

    user = serializers.CharField(source='fk_user.username', read_only=False)

    class Meta:
        model = Product
        fields = (
            'user',
            'uuid',
            'product_code',
            ...,
        )

CustomProductListSerializer est un serializers.ListSerializer et possède une méthode save() remplacée qui lui permet de gérer correctement la création et la mise à jour en bloc.

Voici un exemple de vue du produit en vrac ViewSet:

def partial_update(self, request):

    serializer = self.get_serializer(data=request.data,
                        many=isinstance(request.data, list),
                        partial=True)
    if not serializer.is_valid():
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    serializer.save()
    pdb.set_trace()
    return Response(serializer.data, status=status.HTTP_200_OK)

Essayer d'accéder à serializer.data À la trace (ou à la ligne après, évidemment) provoque l'erreur. Voici la trace complète (tl; dr sauter ci-dessous où je diagnostique avec le débogueur):

 Traceback (most recent call last):
  File "/lib/python3.5/site-packages/Django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/lib/python3.5/site-packages/Django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/lib/python3.5/site-packages/Django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/lib/python3.5/site-packages/Django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/lib/python3.5/site-packages/Django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/application/siop/views/API/product.py", line 184, in partial_update
    return Response(serializer.data, status=status.HTTP_200_OK)
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 739, in data
    ret = super(ListSerializer, self).data
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 265, in data
    self._data = self.to_representation(self.validated_data)
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in to_representation
    self.child.to_representation(item) for item in iterable
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in <listcomp>
    self.child.to_representation(item) for item in iterable
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 488, in to_representation
    attribute = field.get_attribute(instance)
  File "/lib/python3.5/site-packages/rest_framework/fields.py", line 464, in get_attribute
    raise type(exc)(msg)
KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'."

Au L657 du traceback ( source ici) j'ai:

iterable = data.all() if isinstance(data, models.Manager) else data
return [
    self.child.to_representation(item) for item in iterable
]

Cela m'a fait me demander (creuser plus loin dans la trace) pourquoi les serializer.fields n'étaient pas disponibles. Je soupçonnais que c'était parce que le sérialiseur était un parent CustomProductListSerializer et non un enfant BulkProductSerializer, et j'avais raison. Dans la trace pdb juste avant de retourner la Response(serializer.data):

(Pdb) serializer.fields
*** AttributeError: 'CustomProductListSerializer' object has no attribute 'fields'
(Pdb) serializer.child.fields
{'uuid': UUIDField(read_only=False, required=False, validators=[]) ...(etc)}
(Pdb) 'user' in serializer.child.fields
True
(Pdb) serializer.data
*** KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'."
(Pdb) serializer.child.data
{'uuid': '08ec13c0-ab6c-45d4-89ab-400019874c63', ...(etc)}

OK, alors quelle est la bonne façon d'obtenir le serializer.data Complet et de le renvoyer dans la resopnse pour la classe sérialiseur parent dans la situation décrite par partial_update Dans mon ViewSet?

Éditer:

class CustomProductListSerializer(serializers.ListSerializer):

    def save(self):
        instances = []
        result = []
        pdb.set_trace()
        for obj in self.validated_data:
            uuid = obj.get('uuid', None)
            if uuid:
                instance = get_object_or_404(Product, uuid=uuid)
                # Specify which fields to update, otherwise save() tries to SQL SET all fields.
                # Gotcha: remove the primary key, because update_fields will throw exception.
                # see https://stackoverflow.com/a/45494046
                update_fields = [k for k,v in obj.items() if k != 'uuid']
                for k, v in obj.items():
                    if k != 'uuid':
                        setattr(instance, k, v)
                instance.save(update_fields=update_fields)
                result.append(instance)
            else:
                instances.append(Product(**obj))

        if len(instances) > 0:
            Product.objects.bulk_create(instances)
            result += instances

        return result
11
Escher

Au point de la trace où j'essaie d'accéder à serializer.data Et d'obtenir la KeyError, je note que serializer.data Contient uniquement des paires clé/vaule du initial_data, Pas les données d'instance (d'où, je suppose, la KeyError; certaines clés de champs de modèle ne sont pas présentes car il s'agit d'une demande partial_update). Cependant, serializer.child.data Contient toutes les données d'instance pour le dernier enfant de la liste.

Donc, je vais à la source rest_framework/serializers.pydata est défini:

249    @property
250    def data(self):
251        if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
252            msg = (
253                'When a serializer is passed a `data` keyword argument you '
254                'must call `.is_valid()` before attempting to access the '
255                'serialized `.data` representation.\n'
256                'You should either call `.is_valid()` first, '
257                'or access `.initial_data` instead.'
258            )
259            raise AssertionError(msg)
260
261        if not hasattr(self, '_data'):
262            if self.instance is not None and not getattr(self, '_errors', None):
263                self._data = self.to_representation(self.instance)
264            Elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
265                self._data = self.to_representation(self.validated_data)
266            else:
267                self._data = self.get_initial()
268        return self._data

La ligne 265 est problématique. Je peux reproduire l'erreur en appelant serializer.child.to_representation({'uuid': '87956604-fbcb-4244-bda3-9e39075d510a', 'product_code': 'foobar'}) au point d'arrêt.

L'appel de partial_update() fonctionne très bien sur une seule instance (car self.instance Est défini, self.to_representation(self.instance) fonctionne). Cependant, pour une implémentation en bloc partial_update (), self.validated_data Ne contient pas de champs de modèle et to_representation() ne fonctionnera pas, donc je ne pourrai pas accéder à .data propriété.

Une option consisterait à conserver une sorte de liste self.instances D'instances de produit et à remplacer la définition de data à la ligne 265:

self._data = self.to_representation(self.instances)

Je préférerais vraiment une réponse de quelqu'un plus expérimenté dans ce genre de problème, car je ne suis pas sûr que ce soit une solution sensée, donc je laisse la prime ouverte dans l'espoir que quelqu'un puisse suggérer quelque chose de plus intelligent à faire.

0
Escher

Comme mentionné dans le commentaire, je pense toujours que l'exception pourrait être due au champ utilisateur dans la classe BulkProductSerializer , pas vraiment à voir avec ListSerializer

Il peut y avoir une autre erreur mineure (mais importante) dans le sérialiseur DRF comme mentionné dans la documentation ici . Voici comment spécifier un list_serializer_class:

class CustomListSerializer(serializers.ListSerializer):
    ...

class CustomSerializer(serializers.Serializer):
    ...
    class Meta:
        list_serializer_class = CustomListSerializer

Notez qu'il est spécifié à l'intérieur de la classe Meta, pas à l'extérieur. Je pense donc que dans votre code, il ne comprendra pas de basculer vers le sérialiseur de liste avec many=True. Cela devrait provoquer le problème de non-mise à jour.

Mise à jour - Ajouter un exemple de mise à jour du sérialiseur de liste imbriquée

Il semble que la question portait davantage sur un moyen générique d'implémentation de la mise à jour pour le sérialiseur de liste imbriqué que sur l'erreur réelle. Par conséquent, je vais essayer de fournir un exemple de code.

Quelques notes:

  • Si nous utilisons ModelViewSet , l'itinéraire de la liste ne permettra pas [~ # ~] de mettre [~ # ~] ou [~ # ~] patch [~ # ~] , donc ni l'un ni l'autre update ni partial_update seront appelés ( référence ). Par conséquent, j'utilise [~ # ~] post [~ # ~] directement, c'est beaucoup plus simple.
  • Si vous souhaitez utiliser [~ # ~] mettez [~ # ~] / [~ # ~] patch [~ # ~] , puis voyez cette réponse ici
  • Nous pouvons toujours ajouter un paramètre de requête comme allow_update ou partial directement au Publier une demande pour faire la différence entre POST/PUT/PATCH
  • Au lieu d'utiliser uuid comme la question, j'utiliserai l'identifiant normal , ce devrait être à peu près la même chose

C'était plutôt simple

Pour référence, les modèles ressemblent à ceci:

class Product(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User, null=True, blank=True)

    def __unicode__(self):
        return self.name

Étape 1: assurez-vous que le sérialiseur passe à ListSerializer

class ProductViewSet(viewsets.ModelViewSet):
    serializer_class = ProductSerializer
    queryset = Product.objects.all()

    def get_serializer(self, *args, **kwargs):
        # checking for post only so that 'get' won't be affected
        if self.request.method.lower() == 'post':
            data = kwargs.get('data')
            kwargs['many'] = isinstance(data, list)
        return super(ProductViewSet, self).get_serializer(*args, **kwargs)

Étape 2: implémentez le ListSerializer en remplaçant la fonction create

class ProductListSerializer(serializers.ListSerializer):
    def create(self, validated_data):
        new_products = [Product(**p) for p in validated_data if not p.get('id')]
        updating_data = {p.get('id'): p for p in validated_data if p.get('id')}
        # query old products
        old_products = Product.objects.filter(id__in=updating_data.keys())
        with transaction.atomic():
            # create new products
            all_products = Product.objects.bulk_create(new_products)
            # update old products
            for p in old_products:
                data = updating_data.get(p.id, {})
                # pop id to remove
                data.pop('id')
                updated_p = Product(id=p.id, **data)
                updated_p.save()
                all_products.append(updated_p)
        return all_products


class ProductSerializer(serializers.ModelSerializer):
    user = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all())
    id = serializers.IntegerField(required=False)

    class Meta:
        model = Product
        fields = '__all__'
        list_serializer_class = ProductListSerializer
7
Nathan Do

Votre erreur n'a rien à voir avec ListSerializer, mais un problème lors de l'obtention du champ user:

KeyError: "Got KeyError lors de la tentative d'obtention d'une valeur pour le champ user sur le sérialiseur BulkProductSerializer.

Le champ du sérialiseur peut être nommé incorrectement et ne correspondre à aucun attribut ou clé de l'instance OrderedDict.

Le texte d'exception d'origine était: "fk_user". "

Assurez-vous que votre modèle Product possède un fk_user champ.

Vous avez également défini le champ user sur BulkProductSerializer comme accessible en écriture, mais vous n'avez pas indiqué au sérialiseur comment le gérer ...

La façon la plus simple de corriger cela est d'utiliser un SlugRelatedField :

class BulkProductSerializer(serializers.ModelSerializer):

    list_serializer_class = CustomProductListSerializer

    user = serializers.SlugRelatedField(
                            slug_field='username',
                            queryset=UserModel.objects.all(),
                            source='fk_user'
    )

    class Meta:
        model = Product
        fields = (
            'user',
            'uuid',
            'product_code',
            ...,
        )

Cela devrait bien gérer les erreurs, par exemple lorsque username n'existe pas ...

0
Michael Rigoni