在 Django REST Framework 中使用 multipart/form-data 上传多张图片并嵌套 json
Uploading multiple images and nested json using multipart/form-data in Django REST Framework
我在视图集中解析 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>"}
]
}
serializer没有什么特别的,就是抽取标签link,所以没写。如何发送 multipart/form-data 以及如何在视图集中解析它?或者有什么解决办法?
如果我理解正确,只需创建一个 ImageSerializer 并附加到 ProductSerializer。类似的东西:
ImageSerializer(serializers.ModelSerializer):
#attrs
ProductSerializer(serializers.ModelSerializer):
productimage_set = ImageSerializer(read_only=True, many=True)
您可以在序列化程序 update/create 方法中拆分图像。将您的 post 数据 => 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
我开发了一个解决方案。使用 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,我创建了这个自定义 parser:
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 来解析我们的数据。现在是时候构建我们的 viewsets.
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 只对单个文件做正确的验证。如果您尝试上传两个文件,甚至可以在图片旁边放一部电影,验证将不起作用。为了防止这种情况,我们使用django 的内置表单按顺序验证图片;把.mp3的格式改成.jpg,尽量上传大的文件,none个就可以了。进行验证的东西是纯django。其他细节在代码中。
如果你按照我说的做所有事情,响应将是这样的:
我认为这会让大多数 Postman 用户满意。我希望它有所帮助。如果有什么引起你的注意,让我们在评论中见面。
我在视图集中解析 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>"}
]
}
serializer没有什么特别的,就是抽取标签link,所以没写。如何发送 multipart/form-data 以及如何在视图集中解析它?或者有什么解决办法?
如果我理解正确,只需创建一个 ImageSerializer 并附加到 ProductSerializer。类似的东西:
ImageSerializer(serializers.ModelSerializer):
#attrs
ProductSerializer(serializers.ModelSerializer):
productimage_set = ImageSerializer(read_only=True, many=True)
您可以在序列化程序 update/create 方法中拆分图像。将您的 post 数据 => 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
我开发了一个解决方案。使用 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,我创建了这个自定义 parser:
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 来解析我们的数据。现在是时候构建我们的 viewsets.
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 只对单个文件做正确的验证。如果您尝试上传两个文件,甚至可以在图片旁边放一部电影,验证将不起作用。为了防止这种情况,我们使用django 的内置表单按顺序验证图片;把.mp3的格式改成.jpg,尽量上传大的文件,none个就可以了。进行验证的东西是纯django。其他细节在代码中。
如果你按照我说的做所有事情,响应将是这样的:
我认为这会让大多数 Postman 用户满意。我希望它有所帮助。如果有什么引起你的注意,让我们在评论中见面。