简体   繁体   English

多文件上传DRF

[英]Multiple file upload DRF

I have a requirement which I would like to allow multiple files to be uploaded within the same post request to create an object.我有一个要求,我希望允许在同一个发布请求中上传多个文件以创建一个对象。 I currently have a method of doing this, but after looking at some other examples it doesn't appear to be intended way to do it.我目前有一种方法可以做到这一点,但是在查看了其他一些示例之后,它似乎不是有意的方法。

models.py模型.py

class Analyzer(models.Model):
    name = models.CharField(max_length=100, editable=False, unique=True)

class Atomic(models.Model):
    name = models.CharField(max_length=20, unique=True)

class Submission(models.Model):
    class Meta:
        ordering = ['-updated_at']

    issued_at = models.DateTimeField(auto_now_add=True, editable=False)
    completed = models.BooleanField(default=False)
    analyzers = models.ManyToManyField(Analyzer, related_name='submissions')
    atomic = models.ForeignKey(Atomic, verbose_name='Atomic datatype', related_name='submission', on_delete=models.CASCADE)

class BinaryFile(models.Model):
    class Meta:
        verbose_name = 'Binary file'
        verbose_name_plural = 'Binary files'
    def __str__(self):
        return self.file.name

    submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name='binary_files')
    file = models.FileField(upload_to='uploads/binary/')

serializers.py序列化程序.py

class BinaryFileSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.BinaryFile
        fields = '__all__'

class SubmissionCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Submission
        fields = ['id', 'completed', 'atomic', 'analyzers', 'binary_files']

    id = serializers.ReadOnlyField()
    completed = serializers.ReadOnlyField()

    atomic = serializers.PrimaryKeyRelatedField(many=False, queryset=models.Atomic.objects.all()
    analyzers = serializers.PrimaryKeyRelatedField(many=True, queryset=models.Analyzer.objects.all()

    binary_files = BinaryFileSerializer(required=True, many=True)

    def validate(self, data):
        # # I dont really like manually taking invalidated input!!
        data['binary_files'] = self.initial_data.getlist('binary_files')
        return data

    def create(self, validated_data):

        submission = models.Submission.objects.create(
            atomic=validated_data['atomic']
        )
        submission.analyzers.set(validated_data['analyzers'])

        # # Serialize the files - this seems too late to be doing this!
        for file in validated_data['binary_files']:
            binary_file = BinaryFileSerializer(
                data={'file': file, 'submission': submission.id}
            )

            if binary_file.is_valid():
                binary_file.save()

        return submission

Main question: While the above works, the child serializer (BinaryFileSerializer) doesn't get called until I explicitly call it in create(), which is after the validation should have occurred.主要问题:虽然上述方法有效,但直到我在 create() 中显式调用它之后才调用子序列化程序 (BinaryFileSerializer),这应该发生在验证之后。 Why does this never get called?为什么这永远不会被调用?

I also don't like the fact I have to manually do a self.initial_data.getlist('binary_files') and manually add it to data - this should have already been added and validated, no?我也不喜欢我必须手动执行self.initial_data.getlist('binary_files')并将其手动添加到data事实 - 这应该已经添加并验证了,不是吗?

My thought is that as I defined binary_files = BinaryFileSerializer , this serializer should be called to validate that particular fields input?我的想法是,当我定义binary_files = BinaryFileSerializer ,应该调用这个序列化器来验证特定的字段输入?

FYI, I'm using the following to test POST uploads:仅供参考,我正在使用以下内容来测试 POST 上传:

curl -F "binary_files=@file2.txt" -F "binary_files=@file2.txt" -F "atomic=7" -F "analyzers=12" -H "Accept: application/json; indent=4"  http://127.0.0.1:8000/api/submit/

TIA!蒂亚!

Update: The question is now, if a validate() funciton is added to the BinaryFileSerializer, why does it not get called?更新:现在的问题是,如果将 validate() 函数添加到 BinaryFileSerializer,为什么它不会被调用?

Possible duplicate --- Django REST: Uploading and serializing multiple images .可能重复--- Django REST:上传和序列化多个图像



From the DRF Writable Nested Serializer doc ,DRF Writable Nested Serializer doc

By default nested serializers are read-only.默认情况下,嵌套的序列化程序是只读的。 If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved.如果要支持对嵌套序列化程序字段的写入操作,则需要创建create()和/或update()方法,以便明确指定应如何保存子关系。

From this, it's clear that the child serializer ( BinaryFileSerializer ) won't call its own create() method unless explicitly called.由此可知,除非明确调用,否则子序列化程序 ( BinaryFileSerializer ) 不会调用自己的create()方法。

The aim of your HTTP POST request is to create new Submission instance (and BinaryFile instance).您的HTTP POST请求的目的是创建新的Submission实例(和BinaryFile实例)。 The creation process undergoes in the create() method of the SubmissionCreateSerializer serializer, which is you'd overridden.创建过程在SubmissionCreateSerializer序列化程序的create()方法中进行,您将重写该方法。 So, it will act/execute as per your code.因此,它将按照您的代码行动/执行。


UPDATE-1更新-1

Things to remember要记住的事情
1. AFAIK, we can't send nested multipart/form-data 1. AFAIK,我们不能发送嵌套的multipart/form-data
2. Here I'm only trying to implementing the least case scenario 2.这里我只是尝试实现最小的情况
3. I'm tested this solution with POSTMAN rest api test tool. 3. 我用POSTMAN rest api 测试工具测试了这个解决方案。
4. This method may be complex (until we found a better one). 4. 这种方法可能很复杂(直到我们找到更好的方法)。
5. Assuming your view class is subclass of ModelViewSet class 5. 假设你的视图类是ModelViewSet类的子类


What I'm going to do?我要做什么?
1. Since we can't send the files/data in a nested fashion, we have to send it flat mode. 1. 由于我们不能以嵌套方式发送文件/数据,我们必须以平面模式发送。

image-1图像 1
img-1

2. Override the __init__() method of the SubmissionSerializer serializer and dynamically add as much FileField() attribute according to the request.FILES data. 2.覆盖SubmissionSerializer序列化器的__init__()方法,根据request.FILES数据动态添加尽可能多的FileField()属性。
We could somehow use ListSerializer or ListField here.我们可以在这里以某种方式使用ListSerializerListField Unfortunately I couldn't find out a way :(不幸的是我找不到办法:(

# init method of "SubmissionSerializer"
def __init__(self, *args, **kwargs):
    file_fields = kwargs.pop('file_fields', None)
    super().__init__(*args, **kwargs)
    if file_fields:
        field_update_dict = {field: serializers.FileField(required=False, write_only=True) for field in file_fields}
        self.fields.update(**field_update_dict)

So, what id file_fields here?那么,这里的 id file_fields是什么?
Since the form-data is a key-value pair, every file data must be associated with a key.由于表单数据是一个键值对,每个文件数据都必须与一个键相关联。 Here in image-1 , you could see file_1 and file_2 .image-1 中,您可以看到file_1file_2

3. Now we need to pass the file_fields values from the view . 3. 现在我们需要从view传递file_fields值。 Since this operation is creating new instance, we need to override the create() method of the API class .由于此操作是创建新实例,因此我们需要覆盖API 类create()方法。

# complete view code
from rest_framework import status
from rest_framework import viewsets


class SubmissionAPI(viewsets.ModelViewSet):
    queryset = Submission.objects.all()
    serializer_class = SubmissionSerializer

    def create(self, request, *args, **kwargs):
        # main thing starts file_fields = list(request.FILES.keys()) # list to be passed to the serializer serializer = self.get_serializer(data=request.data, file_fields=file_fields) # main thing ends

        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)


4. Now, all values will be serialized properly. 4. 现在,所有值都将正确序列化。 It's time to override the create() method of the SubmissionSerializer() to map the relations是时候覆盖SubmissionSerializer()create()方法来映射关系了

def create(self, validated_data):
    from django.core.files.uploadedfile import InMemoryUploadedFile
    validated_data_copy = validated_data.copy()
    validated_files = []
    for key, value in validated_data_copy.items():
        if isinstance(value, InMemoryUploadedFile):
            validated_files.append(value)
            validated_data.pop(key)
    submission_instance = super().create(validated_data)
    for file in validated_files:
        BinaryFile.objects.create(submission=submission_instance, file=file)
    return submission_instance



5. That's it!!! 5. 就这样!!!


Complete Code Snippet完整的代码片段

# serializers.py
from rest_framework import serializers
from django.core.files.uploadedfile import InMemoryUploadedFile


class SubmissionSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs): file_fields = kwargs.pop('file_fields', None) super().__init__(*args, **kwargs) if file_fields: field_update_dict = {field: serializers.FileField(required=False, write_only=True) for field in file_fields} self.fields.update(**field_update_dict) def create(self, validated_data): validated_data_copy = validated_data.copy() validated_files = [] for key, value in validated_data_copy.items(): if isinstance(value, InMemoryUploadedFile): validated_files.append(value) validated_data.pop(key) submission_instance = super().create(validated_data) for file in validated_files: BinaryFile.objects.create(submission=submission_instance, file=file) return submission_instance

    class Meta:
        model = Submission
        fields = '__all__'


# views.py from rest_framework import status
from rest_framework import viewsets


class SubmissionAPI(viewsets.ModelViewSet):
    queryset = Submission.objects.all()
    serializer_class = SubmissionSerializer

    def create(self, request, *args, **kwargs): # main thing starts file_fields = list(request.FILES.keys()) # list to be passed to the serializer serializer = self.get_serializer(data=request.data, file_fields=file_fields) # main thing ends 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)

Screenhots and other stuffs截图和其他东西

1. POSTMAN console 1.邮递员控制台
邮递员控制台
2. Django Shell 2. Django 外壳

In [2]: Submission.objects.all()                                                                                                                                                                                   
Out[2]: <QuerySet [<Submission: Submission object>]>

In [3]: sub_obj = Submission.objects.all()[0]                                                                                                                                                                      

In [4]: sub_obj                                                                                                                                                                                                    
Out[4]: <Submission: Submission object>

In [5]: sub_obj.__dict__                                                                                                                                                                                           
Out[5]: 
{'_state': <django.db.models.base.ModelState at 0x7f529a7ea240>,
 'id': 5,
 'issued_at': datetime.datetime(2019, 3, 27, 8, 45, 42, 193943, tzinfo=<UTC>),
 'completed': False,
 'atomic_id': 1}

In [6]: sub_obj.binary_files.all()                                                                                                                                                                                 
Out[6]: <QuerySet [<BinaryFile: uploads/binary/logo-800.png>, <BinaryFile: uploads/binary/Doc.pdf>, <BinaryFile: uploads/binary/invoice_2018_11_29_04_57_53.pdf>, <BinaryFile: uploads/binary/Screenshot_from_2019-02-13_16-22-53.png>]>

In [7]: for _ in sub_obj.binary_files.all(): 
   ...:     print(_) 
   ...:                                                                                                                                                                                                            
uploads/binary/logo-800.png
uploads/binary/Doc.pdf
uploads/binary/invoice_2018_11_29_04_57_53.pdf
uploads/binary/Screenshot_from_2019-02-13_16-22-53.png


3. Django Admin Screenhot 3. Django 管理屏幕截图在此处输入图片说明

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM