Подтвердить что ты не робот

Как получить доступ к serializer.data в родительском классе ListSerializer в DRF?

Я получаю сообщение об ошибке при попытке доступа к serializer.data, прежде чем возвращать его в Response(serializer.data, status=something):

Получение KeyError при попытке получить значение для поля <field> в сериализаторе <serializer>.

Это происходит во всех полях (потому что получается, что я пытаюсь получить доступ к .data для родителя, а не для ребенка, см. ниже)

Определение класса выглядит следующим образом:

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 является serializers.ListSerializer и имеет переопределенный метод save(), который позволяет ему корректно обрабатывать массовое создание и обновление.

Вот пример из основного продукта 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)

Попытка доступа к serializer.data на трассе (или строка после, очевидно) вызывает ошибку. Здесь полный след (tl; dr пропустить ниже, где я диагностирую с помощью отладчика):

 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'."

На L657 трассировки (источник здесь) У меня есть:

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

Это заставило меня задуматься (копаться дальше в следе), почему поля serializer не были доступны. Я подозревал, что это связано с тем, что сериализатор был родителем CustomProductListSerializer, а не дочерним элементом BulkProductSerializer, и я был прав. На трассе pdb перед возвратом 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)}

ОК, так что верный способ получить полный serializer.data и вернуть его в resopnse для родительского класса сериализатора в ситуации, описанной partial_update в моей ViewSet?

Edit:

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
4b9b3361

Ответ 1

В точке, где я пытаюсь получить доступ к serializer.data и получить KeyError, я отмечаю, что serializer.data содержит только пары key/vaule из initial_data, а не данные экземпляра (следовательно, я полагаю, KeyError, некоторые ключи полей модели отсутствуют, так как это запрос partial_update). Однако serializer.child.data содержит все данные экземпляра для последнего дочернего элемента в списке.

Итак, я перехожу в rest_framework/serializers.py source, где data определен:

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

Строка 265 проблематична. Я могу реплицировать ошибку, вызывая serializer.child.to_representation({'uuid': '87956604-fbcb-4244-bda3-9e39075d510a', 'product_code': 'foobar'}) в точке останова.

Вызов partial_update() отлично работает в одном экземпляре (поскольку self.instance установлен, self.to_representation(self.instance) работает). Однако для реализации partial partial_update() для частичной_папки (self.validated_data) отсутствуют поля модели, а to_representation() не будет работать, поэтому я не смогу получить доступ к свойству .data.

Один из вариантов заключается в том, чтобы поддерживать некоторый тип self.instances списка экземпляров Product и переопределять определение data в строке 265:

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

Я бы предпочел ответ от кого-то более опытного в этой проблеме, хотя, поскольку я не уверен, что это разумное решение, поэтому я оставляю щедрость открытой в надежде, что кто-то может предложить что-то умнее сделать.

Ответ 2

Как упоминалось в комментарии, я все еще считаю, что исключение может быть связано с полем пользователя в классе BulkProductSerializer, не имеющим ничего общего с ListSerializer

В сериализаторе DRF может быть другая незначительная ошибка (но важна), как указано в документации здесь. Вот как указать list_serializer_class:

class CustomListSerializer(serializers.ListSerializer):
    ...

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

Обратите внимание, что он указан внутри класса Meta, а не снаружи. Поэтому я думаю, что в вашем коде не будет переключаться на Сериализатор списка с помощью many=True. Это должно вызвать проблему не обновления.

Обновление - Добавить пример обновления сериализатора вложенных списков

Похоже, что вопрос касался общего способа реализации обновления для вложенного Сериализатора Серий, а не фактической ошибки. Поэтому я попытаюсь предоставить пример кода.

Некоторые примечания:

  • Если мы используем ModelViewSet, маршрут списка не позволит PUT или PATCH, поэтому ни update ни partial_update не будет (ссылка). Поэтому я использую POST напрямую, это намного проще.
  • Если вы хотите использовать PUT/ PATCH, см. этот ответ здесь
  • Мы всегда можем добавить параметр запроса, например allow_update или partial, к запросу Post, чтобы различать POST/PUT/PATCH
  • Вместо использования uuid, как и вопрос, я буду использовать обычный id, он должен быть очень похож на

Это было довольно просто

Для справки, модели выглядят так:

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

    def __unicode__(self):
        return self.name

Шаг 1. Убедитесь, что сериализатор изменился на 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)

Шаг 2. Внедрите ListSerializer, переопределив создать функцию

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

Ответ 3

Ваша ошибка не имеет ничего общего с ListSerializer, но возникает проблема с полем user:

KeyError: "Получил KeyError при попытке получить значение для поля user в serializer BulkProductSerializer.

Поле сериализатора может быть названо неправильно и не соответствует никакому атрибуту или ключу в экземпляре OrderedDict.

Исходный текст исключения был:" fk_user".

Убедитесь, что ваша модель Product имеет поле fk_user.

Вы также определили поле user на BulkProductSerializer как записываемое, но не сказали сериализатору, как его обрабатывать...

Самый простой способ исправить это - использовать 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',
            ...,
        )

Это должно обрабатывать приятные ошибки, например, когда username не существует...

Ответ 4

Удалите источник, если вы используете модель Django auth и установите read_only = True.

user = serializers.CharField(read_only = True)

Надеюсь, это сработает для вас.