Django Rest Framework - Обновление связанной модели с использованием ModelSerializer и ModelViewSet - PullRequest
0 голосов
/ 24 ноября 2018

ФОН

У меня есть два сериализатора: PostSerializer и PostImageSerializer , которые оба наследуют DRF ModelSerializer.Модель PostImage связана с Post с помощью related_name = 'photos'.

Поскольку я хочу, чтобы сериализатор выполнил обновление , PostSerializer переопределяет метод update () из ModelSerializer, как указано в официальном документе DRF.

class PostSerializer(serializers.ModelSerializer):
    photos = PostImageSerializer(many=True)

    class Meta:
        model = Post
        fields = ('title', 'content')

    def update(self, instance, validated_data):
        photos_data = validated_data.pop('photos')
        for photo in photos_data:
            PostImage.objects.create(post=instance, image=photo)
        return super(PostSerializer, self).update(instance, validated_data)

class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = ('image', 'post')

Я также определил ViewSet , который наследует ModelViewSet.

 class PostViewSet(viewsets.ModelViewSet):
        queryset = Post.objects.all()
        serializer_class = PostSerializer

Наконец, PostViewSet зарегистрирован в DefaultRouter.(Опущенный код)

Цель

Цели просты.

  • Отправить запрос PUT через PostMan с URL-адресом типа «PUT http://localhost:8000/api/posts/1/'
  • »Поскольку файлы изображений должны быть включены, запрос будет выполняться с помощью данных формы, таких как ниже.

Проблема

Я получаю 400 Ответ с сообщением об ошибке какследующие.

{
"photos": ["Это поле обязательно для заполнения."],
"title": ["Это поле обязательно для заполнения."],
"content": ["Это поле обязательно для заполнения."]
}

(Если вы, пожалуйста, обратите внимание, что сообщения об ошибках могут не совсем соответствовать сообщениям об ошибках DRF, поскольку они переведены.)

Очевидно, что ни один из моихPUT поля применяются.Поэтому я копался в самом исходном коде остальных компонентов Django и обнаружил, что валидация сериализатора в методе ViewSet update () продолжает давать сбой .

Я сомневаюсь, что это потому, что я помещаю запрос не JSON, а по форме-данным, используя пару ключ-значение, поэтому request.data не проверен должным образом.

Тем не менее, я должен содержать несколько изображений в запросе, что означает, что простой JSON не будет работать.

Какие бы решения были наиболее понятными для этого случая?

Спасибо.

Обновление

Как указал Нил, я добавил print (self) в первую строку метода update () PostSerializer.Однако на моей консоли ничего не распечатано.

Я думаю, что это из-за моего doupt выше, потому что метод execute_update (), который вызывает метод serializer update (), называется AFTER , сериализатор проверен .

Таким образом, основная концепция моего вопроса может быть сужена до следующих:

  1. Как мне исправить запрошенные поля данных, чтобы валидация внутри метода update () ModelViewSet могла пройти?
  2. Нужно ли переопределять метод update () для ModelViewSet (а не для ModelSerializer)?

Еще раз спасибо.

Ответы [ 2 ]

0 голосов
/ 26 ноября 2018

Решение представляет собой смесь или ответы @Neil и @ mon.Однако я исправлюсь немного подробнее.

Анализ

Прямо сейчас Почтальон отправляет данные формы, которые содержат 2 пары ключ-значение (пожалуйста, обратитесь к фотографии, которую я загрузил в моем исходном вопросе),Одно - это ключевое поле «photos», связанное с несколькими файлами фотографий, а другое - ключевое поле «data», связанное с одним большим фрагментом 'JSON-подобной строки' .Хотя это справедливый метод POSTing или PUTting данных вместе с файлами, DRF MultiPartParser или JSONParser не будут анализировать их должным образом.

Причина, по которой я получил сообщение об ошибке, была проста.self.get_serializer(instance, data=request.data, partial=partial метод внутри ModelViewSet (особенно UpdateModelMixin) не может понять request.data part.

В настоящее время request.data из отправленных данных формы выглядит следующим образом.

<QueryDict: { "photos": [PhotoObject1, PhotoObject2, ... ],
  "request": ["{'\n 'title': 'title test', \n 'content': 'content test'}",]
}>

Смотреть"запросить" часть тщательно.Значение представляет собой простой string объект.

Однако мой PostSerializer ожидает, что request.data будет выглядеть примерно так, как показано ниже.

{ "photos": [{"image": ImageObject1, "post":1}, {"image": ImageObject2, "post":2}, ... ],
  "title": "test title",
  "content": "test content"
 }

Поэтому давайте проведем некоторый эксперимент и поместим некоторые данные вв соответствии с вышеуказанной формой JSON.то есть

{ "photos": [{"image": "http://tny.im/gMU", "post": 1}],
  "title" : "test title",
  "content": "test content"
}

Вы получите следующее сообщение об ошибке:

"photos": [{"image": ["отправленные данные не являются файлом."]}]

Это означает, что все данные представлены правильно, но url http://tny.im/gMU изображения - это не файл, а строка.

Теперь причина всей этой проблемы стала ясна,Это парсер, который нужно исправить, чтобы сериализатор мог понимать отправленные данные формы.

Решение

1.Создать новый синтаксический анализатор

Новый синтаксический анализатор должен проанализировать 'JSON-подобную' строку для правильных данных JSON.Я заимствовал MultipartJSONParser из здесь.

То, что делает этот анализатор, просто.Если мы передаем 'JSON-подобную' строку с ключом 'data', вызовите json из rest_framework и проанализируйте ее.После этого верните проанализированный JSON с запрошенными файлами.

class MultipartJsonParser(parsers.MultiPartParser):
    # https://stackoverflow.com/a/50514022/8897256
    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}
        data = json.loads(result.data["data"])
        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

2.Редизайн сериализатора

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

  1. Переопределить update метод ModelViewSet
  2. Выскочить пару ключ-значение 'photos' из request.data
  3. Translate popped 'пары 'photos' в список словарей, содержащих ключи 'image' и 'post'.
  4. Добавьте результат к request.data с именем ключа 'photos'.Это потому, что наш PostSerializer ожидает, что имя ключа будет 'photos'.

Однако в основном request.data - это QuerySet, который является неизменяемым по умолчанию.И я весьма скептически отношусь к тому, что нам нужно принудительно изменить QuerySet.Поэтому я скорее назначу процесс создания PostImage для update() метода ModelViewSet.В этом случае нам больше не нужно определять nested serializer.

Просто сделайте это:

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'


class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = '__all__'

3.Переопределить метод update() из ModelViewSet

Чтобы использовать наш класс Parser, нам нужно явно указать его.Мы объединим поведение PATCH и PUT, поэтому установите partial=True.Как мы видели ранее, файлы изображений переносятся с ключом 'photos', поэтому выведите значения и создайте каждый экземпляр Photo.

Наконец, благодаря нашему недавно разработанному Parser, простая 'JSON-like' строка будетпревращается в обычные данные JSON.Так что просто поместите все в serializer_class и perform_update.

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    # New Parser
    parser_classes = (MultipartJsonParser,)

    def update(self, request, *args, **kwargs):
        # Unify PATCH and PUT
        partial = True
        instance = self.get_object()

        # Create each PostImage
        for photo in request.data.pop("photos"):
            PostImage.objects.create(post=instance, image=photo)

        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        # Do ViewSet work.
        self.perform_update(serializer)
        return Response(serializer.data)

Заключение

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

0 голосов
/ 24 ноября 2018

Прежде всего вам нужно установить заголовок:

Content-Type: multipart/form-data;

Но, возможно, если вы задаете данные формы в почтальоне, этот заголовок должен быть по умолчанию.

Вы не можете отправлять изображения в виде данных json (если только вы не кодируете их в строку и не декодируете на стороне сервера, например, base64).

В DRF PUT по умолчанию обязательны для заполнения все поля.Если вы хотите установить только частичные поля, вам нужно использовать PATCH .

Чтобы обойти это и использовать PUT , чтобы обновить частичные поля, у вас есть две опции:

  • редактировать обновить метод в наборе для частичного обновления сериализатора
  • редактировать маршрутизатор, чтобы всегда вызывать метод serial_update в сериализаторах, который является более продвинутым

Вы можете переопределить метод viewset update , чтобы всегда обновлять частичную сериализацию (изменяя только предоставленные поля):

    def update(self, request, *args, **kwargs):
        partial = True # Here I change partial to True
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        return Response(serializer.data)

Add

rest_framework.parsers.MultiPartParser

в основной файл настроек для REST_FRAMEWORK dict:

REST_FRAMEWORK = {
    ...
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.MultiPartParser',
    )
}

Глядя на ваши сериализаторы, странно, что вы не получаете ошибку от PostSerializer , потому что вы нене добавляйте поле "photos" в кортеж Meta.fields.

В этом случае от меня будут советы:

  • добавьте обязательный = False к вашему Фото поле (если вы не хотите, чтобы это было необходимо)
  • как написано выше, добавьте photos field к вам Meta.fields tuple fields = ('title', 'content', 'photos',)
  • добавьте значение по умолчанию Нет значение для вашего validated_data.pop ('photos') , затем проверьте, предоставлены ли данные фотографий перед циклом.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...