Несколько загрузок файлов DRF - PullRequest
10 голосов
/ 23 марта 2019

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

models.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

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

Основной вопрос: хотя вышеприведенное работает, дочерний сериализатор (BinaryFileSerializer) не вызывается, пока я не вызову его явно в create (), то есть после того, как должна была произойти проверка. Почему это никогда не называют?

Мне также не нравится тот факт, что я должен вручную сделать self.initial_data.getlist('binary_files') и вручную добавить его к data - это уже должно было быть добавлено и проверено, нет?

Я думаю, что, как я определил binary_files = BinaryFileSerializer, этот сериализатор должен вызываться для проверки ввода этих конкретных полей?

К вашему сведению, я использую следующее для проверки 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/

ТИА!

Обновление: Теперь возникает вопрос: если функция validate () добавлена ​​в BinaryFileSerializer, почему он не вызывается?

1 Ответ

8 голосов
/ 26 марта 2019

Возможный дубликат --- Django REST: загрузка и сериализация нескольких изображений .



Из документированного вложенного сериализатора DRF ,

По умолчанию вложенные сериализаторы доступны только для чтения. Если вы хотите поддерживать операции записи во вложенное поле сериализатора, вам нужно создать create() и / или update() методы, чтобы явно указывало, как дочерние отношения должны быть сохранены.

Из этого ясно, что дочерний сериализатор (BinaryFileSerializer) не будет вызывать свой собственный метод create(), если явно не вызывается.

Цель вашего запроса HTTP POST - создать новый экземпляр Submission (и экземпляр BinaryFile) , Процесс создания проходит по методу create() сериализатора SubmissionCreateSerializer, который вы должны переопределить. Таким образом, он будет действовать / выполняться в соответствии с вашим кодом.


UPDATE-1

Что нужно запомнить
1. AFAIK, мы не можем отправить вложенный multipart/form-data
2. Здесь я только пытаюсь реализовать сценарий наименьшего случая
3. Я протестировал это решение с помощью POSTMAN инструмента тестирования остальных API.
4. Этот метод может быть сложным (пока мы не нашли лучший).
5. Предполагая, что ваш класс представления является подклассом ModelViewSet class


Что я собираюсь делать?
1. Поскольку мы не можем отправлять файлы / данные вложенным способом, мы должны отправить их в плоском режиме.

изображение-1
img-1

2. Переопределите метод __init__() сериализатора SubmissionSerializer и динамически добавьте столько же атрибутов FileField() в соответствии с request.FILES data.
Мы могли бы как-то использовать ListSerializer или ListField здесь. К сожалению, я не мог найти способ: (

# 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)

Итак, что ID file_fields здесь?
Так как данные формы является парой значение ключа, каждый файл данных должен быть связан с ключом. Здесь, в image-1 , вы можете увидеть file_1 и file_2.

3. Теперь нам нужно передать значения file_fields из view. Поскольку эта операция создает новый экземпляр, нам необходимо переопределить метод create() класса *1136* API .

<b># complete view code</b>
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):
        <b># 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</b>

        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. Теперь все значения будут правильно сериализованы. Пришло время переопределить create() метод SubmissionSerializer(), чтобы отобразить отношения

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. Вот и все !!!


Полный фрагмент кода

<b># serializers.py</b>
from rest_framework import serializers
<b>from django.core.files.uploadedfile import InMemoryUploadedFile</b>


class SubmissionSerializer(serializers.ModelSerializer):
    <b>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</b>

    class Meta:
        model = Submission
        fields = '__all__'


<b># views.py
from rest_framework import status</b>
from rest_framework import viewsets


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

    <b>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)</b>

Скриншоты и прочее

1. ПОСТМАН консоль
POSTMAN console
2. Джанго Шелл

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 enter image description here

...