【问题标题】:Uploading multiple images and nested json using multipart/form-data in Django REST Framework在 Django REST Framework 中使用 multipart/form-data 上传多个图像和嵌套 json
【发布时间】:2020-07-24 10:31:06
【问题描述】:

我在解析视图集中的 request.data 时遇到问题。我有一个模型可以根据产品添加多个图像。

我想将图像从传入的数据中分离出来,将产品数据发送到ProductSerializer,然后将图像产品数据一起发送到其序列化程序并保存。

我有两个模型,就像这样:

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 以及如何在视图集中解析它?或者有什么解决办法?

【问题讨论】:

    标签: python django django-rest-framework serialization django-rest-viewsets


    【解决方案1】:

    我开发了一个解决方案。使用 Postman,我发送了包含多个图像、单个和嵌套数据的 multipart/form-data。

    在我的模型文件中,我将 Tags 模型添加为 ManyToManyField 作为示例,还添加了 django-taggit。 form-data 会像图片中一样。

    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)
    

    首先要做的事;第一个数据未正确解析。作为解决方案,在answer 的帮助下,我创建了这个自定义解析器

    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
    

    让我们在这里检查一下。正如我在 cmets 中提到的,图像文件将包含在 request.FILES 中。为此,我首先将数据发送到 ProductImageSerializer 并对其进行验证。如果发生验证错误,该过程将停止,API 将发送错误消息作为响应。然后我将数据连同我在 get_serializer_context 方法中附加到上下文的图片信息一起发送到 ProductSerializer。

    我们完成了 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 只对单个文件进行正确的验证。如果你尝试上传两个文件,你甚至可以在图片旁边放一个电影,验证将不起作用。为了防止这种情况,我们使用 django 的内置形式按顺序验证图片;将.mp3格式改成.jpg,尝试上传大文件,都不行。进行验证的东西是纯 django。其他细节在代码中。

    如果你按照我说的做所有事情,响应将是这样的:

    我认为这会让大多数 Postman 用户感到高兴。我希望它有所帮助。如果有什么引起您的注意,让我们在 cmets 见面。

    【讨论】:

    【解决方案2】:

    如果我理解正确,只需创建一个 ImageSerializer 并附加到 ProductSerializer。类似的东西:

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

    【讨论】:

    • 如果你试试这个,图像表会是空的,你可以试试,你会看到的。
    【解决方案3】:

    您可以在序列化程序更新/创建方法中拆分图像。将您的帖子数据 => 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
    

    【讨论】:

    • 我使用了 ForeignKey,正如我在示例 json 数据和模型中提到的那样。当序列化程序相关对象Many = True 时,图像不包含在 validated_data 中;它只保留在 request.FILES 中。所以这个方法是行不通的。
    • 您可以在产品序列化程序中定义您的反向关系。 image_set= ImageSerializer(read_only=True, many=True) 并更改您的帖子数据。 productimage_set 到 image_set
    • hmmm 这个设置也已经在我的序列化器上可用了。不工作。如果您有一种方法可以使用验证添加多对一关系相关图像和数据,您可以编辑您的答案并与我们分享吗?
    • 您的产品和图像模型是一对多的关系,而不是多对一的关系。如果您想将图像用于其他产品,则需要多对多关系。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-23
    • 2011-03-22
    • 1970-01-01
    • 1970-01-01
    • 2015-10-11
    • 2019-03-24
    • 2022-06-14
    相关资源
    最近更新 更多