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
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.py
où data
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.
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.
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:
C'était plutôt simple
class Product(models.Model):
name = models.CharField(max_length=200)
user = models.ForeignKey(User, null=True, blank=True)
def __unicode__(self):
return self.name
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)
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
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érialiseurBulkProductSerializer
.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 ...