Загрузка нескольких изображений и вложенных json с использованием multipart / form-data в Django REST Framework - PullRequest
0 голосов
/ 11 апреля 2020

У меня проблема с анализом request.data в наборе. У меня есть модель, которая может добавить несколько изображений в зависимости от продукта.

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

У меня две модели, просто так :

def Product(models.Model):
    name = models.CharField(max_length=20)
    color = models.ForeignKey(Color, on_delete=models.CASCADE)

def Color(models.Model):
    name = models.CharField(max_length=15)

def ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    image = models.ImageField(upload_to='product_pics/')

Запрос, который я хочу отправить на Product (127.0.0.1:8000/products/), просто выглядит так:

{
    "name": "strawberry",
    "color": {
        "name": "red"
    },
    "productimage_set": [
        {"image": "<some_encode_image_data>"}
    ]
}

В сериализаторе нет ничего особенного, он просто извлекает ссылку на теги, поэтому я ее не написал. Как отправить multipart / form-data и как я могу проанализировать его в наборе? или каково решение?

Ответы [ 3 ]

0 голосов
/ 12 апреля 2020

Вы можете разделить изображение в методе обновления / создания сериализатора. Измените ваши данные поста => productimage_set на image_set.

ProductSerializer(serializers.ModelSerializer):
    image_set = ImageSerializer(read_only=True, many=True)

    class Meta:
        model = Product
        fields = ('name', 'color', 'image_set')

    def update(self, instance, validated_data):

        image = validated_data.pop('image_set', None)
        # if you want you can send image another serializer here.

        instance.name = validated_data['name']
        instance.save()

        return instance
0 голосов
/ 13 апреля 2020

Я разработал решение. Используя Почтальон, я отправил данные multipart / form, содержащие несколько изображений, отдельные и вложенные данные.

В своем файле модели я добавил модель Теги как ManyToManyField в качестве примера, а также django -tag git. данные формы будут похожи на изображения.

enter image description here

и models.py

class Product(models.Model):
    name = models.CharField(max_length=20, blank=True)
    tags = models.ManyToManyField(Tags)
    taggit = TaggableManager(blank=True)

class ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    image = models.ImageField(upload_to='image_path/', null=True, blank=True)

class Tags(models.Model):
    name = models.CharField(max_length=15, blank=True)

Перво-наперво; первые данные не были правильно проанализированы. В качестве решения этой проблемы и с помощью этого ответа я создал этот пользовательский парсер :

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}

        for key, value in result.data.items():
            if type(value) != str:
                data[key] = value
                continue
            if '{' in value or "[" in value:
                try:
                    data[key] = json.loads(value)
                except ValueError:
                    data[key] = value
            else:
                data[key] = value
        return parsers.DataAndFiles(data, result.files)

Теперь мы можем проанализировать наши данные с этим парсером и Django REST встроенным JSONParser. Теперь пришло время создать наши наборов .

class ProductViewSet(ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    parser_classes = [MultipartJsonParser, JSONParser]

    def get_serializer_context(self):
        context = super(ProductViewSet, self).get_serializer_context()

        # appending extra data to context
        if len(self.request.FILES) > 0:
            context.update({
                'included_images': self.request.FILES
            })

        return context

    def create(self, request, *args, **kwargs):
        # Validating images with its own serializer, but not creating.
        # The adding process must be through Serializer.
        try:
            image_serializer = ProductImageSerializer(data=request.FILES)
            image_serializer.is_valid(raise_exception=True)
        except Exception:
            raise NotAcceptable(
                detail={
                    'message': 'Upload a valid image. The file you uploaded was either not '
                               'an image or a corrupted image.'}, code=406)

        # the rest of method is about the product serialization(with extra context), 
        # validation and creation.
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

class ProductImageViewSet(ModelViewSet):
    queryset = ProductImage.objects.all()
    serializer_class = ProductImageSerializer

class TagsViewSet(ModelViewSet):
    queryset = Tags.objects.all()
    serializer_class = TagsSerializer

Давайте рассмотрим здесь. Как я уже упоминал в комментариях, файлы изображений будут включены в request.FILES. По этой причине я сначала отправил данные в ProductImageSerializer и проверил их. Если произойдет ошибка проверки, процесс остановится, и API отправит сообщение об ошибке в качестве ответа. Затем я отправил данные в ProductSerializer с информацией об изображении, которую я добавил к контексту в методе get_serializer_context .

Мы закончили с create метод, другие детали записаны в коде.

Наконец, serializer.py

from django.forms import ImageField as DjangoImageField

class TagsSerializer(HyperlinkedModelSerializer):
    class Meta:
    model = Tags
    fields = ['url', 'pk', 'name']

class ProductImageSerializer(HyperlinkedModelSerializer):
    class Meta:
        model = ProductImage
        fields = ['url', 'pk', 'product', 'image']
        # attention!!! if you not use this bottom line,
        # it will show error like "product required" and
        # indirectly our validation at ProductViewSet will raise error.
        extra_kwargs = {
            'product': {'required': False}
        }
    # we created Object-level custom validation because validation not working correctly.
    # when ProductImageSerializer get single image, everything just fine but
    # when it get multiple image, serializer is just passing all the files.
    def validate(self, attrs):
        default_error_messages = {
            'invalid_image':
                'Upload a valid image. The file you uploaded was either not an image or a corrupted image.',
        }
        # in here we're verifying image with using django.forms; Pillow not necessary !!
        for i in self.initial_data.getlist('image'):
            django_field = DjangoImageField()
            django_field.error_messages = default_error_messages
            django_field.clean(i)
        return attrs

class ProductSerializer(HyperlinkedModelSerializer, TaggitSerializer):
    tags = TagsSerializer(allow_null=True, many=True, required=False)
    # you can delete this line. If you delete it, it will appear as url in response.
    productimage_set = ProductImageSerializer(allow_null=True, many=True, required=False)
    taggit = TagListSerializerField(allow_null=True, required=False)

    class Meta:
        model = Product
        fields = ['url', 'pk', 'name', 'tags', 'taggit', 'productimage_set']

    def create(self, validated_data):
        # create product
        try:
            product_obj = Product.objects.create(
                name=validated_data['name']
            )
        except Exception:
            raise NotAcceptable(detail={'message': 'The request is not acceptable.'}, code=406)

        if 'included_images' in self.context:  # checking if key is in context
            images_data = self.context['included_images']
            for i in images_data.getlist('image'):
                ProductImage.objects.create(
                    product=product_obj,
                    image=i
                )

        # pop taggit and create
        if 'taggit' in validated_data:
            taggit_data = validated_data.pop('taggit')
            for taggit_data in taggit_data:
                taggit_obj, created = Tag.objects.get_or_create(name=taggit_data)
                product_obj.taggit.add(taggit_obj)

        # pop tags and create
        if 'tags' in validated_data:
            tags_data = validated_data.pop('tags')
            for tags_data in tags_data:
                for i in tags_data.items():
                    tags_obj, created = Tags.objects.get_or_create(name=i[1])
                    product_obj.tags.add(tags_obj)

        return product_obj

Так что здесь произошло? Почему мы создали дополнительную проверку изображения? Хотя я не знаю почему, ImageSerializer делает правильную проверку только для одного файла. Если вы попытаетесь загрузить два файла, вы можете даже поставить mov ie рядом с изображением, проверка не будет работать. Чтобы предотвратить это, мы проверяем изображения по порядку, используя встроенную форму django; Измените формат .mp3 и сделайте его .jpg, попробуйте загрузить файлы большого размера, ни один из них не будет работать. То, что делает проверку, является чистым django. Другие детали в коде.

Если вы сделаете все, как я сказал, ответ будет таким:

enter image description here

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

0 голосов
/ 11 апреля 2020

Если я правильно понял, просто создайте ImageSerializer и присоедините к ProductSerializer. Примерно так:

ImageSerializer(serializers.ModelSerializer):
   #attrs 

ProductSerializer(serializers.ModelSerializer):
    productimage_set = ImageSerializer(read_only=True, many=True)
...