Как отправить список идентификаторов через multipart/form-data в REST-фреймворке Django

Я работаю над API Django REST Framework, где мне нужно отправить список идентификаторов в запросе multipart/form-data для создания отношения «многие-ко-многим» в базе данных. Если при использовании JSON в качестве формата запроса все работает отлично, то при переходе на multipart/form-data возникают проблемы, поскольку список идентификаторов сериализуется некорректно.

Вот мой сериализатор:

class AddOrUpdateContractSerializer(serializers.ModelSerializer):
    deputy_agent = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    product_type = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    referral_type = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    customer_id = serializers.IntegerField(required=False)

    class Meta:
        model = Contract
        exclude = ["id", "product", "is_deleted", "state", "customer"]

Вот мое мнение

class AddContract(APIView):
    @extend_schema(request=AddOrUpdateContractSerializer, tags=["contract"])
    def post(self, request):
        serialized_data = AddOrUpdateContractSerializer(data=request.data)
        if serialized_data.is_valid(raise_exception=True):
            service = ContractService(
                serialized_data=serialized_data.validated_data, user=request.user
            )
            service.create_contract()
            return Response(
                status=status.HTTP_200_OK,
                data={"detail": "contract created successfully"},
            )

И сервис для создания контракта:

def create_contract(self):
    items = ["customer_id", "deputy_agent", "product_type", "referral_type"]

    customer = CustomerSelector.get_customer_by_id(
        self.serialized_data.get("customer_id", None)
    )
    deputy_agent = self.serialized_data.get("deputy_agent", None)
    product_type = self.serialized_data.get("product_type", None)
    referral_type = self.serialized_data.get("referral_type", None)

    self.remove_unnecessary_items(items)

    contract = Contract(customer=customer, **self.serialized_data)

    contract.full_clean()
    contract.save()

    if deputy_agent:
        deputy_agents = DeputyAgent.objects.filter(id__in=deputy_agent)
        contract.deputy_agent.add(*deputy_agents)

    if product_type:
        product_types = ComboBoxsOptions.objects.filter(id__in=product_type)
        contract.product_type.add(*product_types)

    if referral_type:
        referral_types = ComboBoxsOptions.objects.filter(id__in=referral_type)
        contract.referral_type.add(*referral_types)

ПРОБЛЕМА

При отправке данных в формате multipart/form-data такие списки, как deputy_agent, product_type и referral_type, получаются в виде строк, а не списков

Почему мне нужен multipart/form-data. Я не могу использовать JSON, потому что мне также нужно загрузить файлы изображений вместе с этими идентификаторами в том же запросе.

Пример запроса

Content-Type: multipart/form-data

customer_id: 1  
deputy_agent: [1, 2, 3]  
product_type: [4, 5, 6]  
image: <uploaded_file>

Вопрос. Как правильно обрабатывать списки идентификаторов в multipart/form-data в Django REST Framework? Есть ли лучшая практика или обходной путь для решения этой проблемы?

Проблема, с которой вы столкнулись, связана с тем, как Django обрабатывает multipart/form-data и как он сериализует данные, отправленные в этом формате. Когда вы отправляете списки типа deputy_agent, product_type и referral_type в запросе multipart/form-data, они сериализуются как строки, а не как реальные списки.

Для решения этой проблемы можно использовать пользовательский метод сериализатора, который обрабатывает разбор этих строковых представлений списков в реальные списки Python.

Решение:

  1. Переопределите метод to_internal_value: Это позволит вам вручную разобрать строку на список.

  2. Используйте ListField в сочетании с пользовательским методом: Поскольку в multipart/form-data данные отправляются в виде строки, вам необходимо вручную обработать это преобразование перед проверкой.

Вот как можно модифицировать ваш сериализатор для обработки списков, отправленных в формате multipart/form-data:

Обновленный сериализатор:

from rest_framework import serializers

class AddOrUpdateContractSerializer(serializers.ModelSerializer):
    deputy_agent = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    product_type = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    referral_type = serializers.ListField(
        child=serializers.IntegerField(), required=False, allow_empty=False
    )
    customer_id = serializers.IntegerField(required=False)

    class Meta:
        model = Contract
        exclude = ["id", "product", "is_deleted", "state", "customer"]

    def to_internal_value(self, data):
        # If any of the lists are passed as a string, try to parse them into a list
        for field in ['deputy_agent', 'product_type', 'referral_type']:
            if field in data and isinstance(data[field], str):
                # Try to convert string to a list
                try:
                    data[field] = [int(i) for i in data[field].strip('[]').split(',')]
                except ValueError:
                    raise serializers.ValidationError(f"Invalid format for {field}, should be a list of integers.")
        return super().to_internal_value(data)

Изменения клавиш:

  1. to_internal_value метод: Этот метод проверяет, получено ли поле в виде строки (что может произойти в multipart/form-data), и пытается разобрать его в список целых чисел.

  2. split(','): Предполагает, что список отправляется в формате '1,2,3'. Вы можете настроить логику разбора, если формат отличается.

Пример запроса (данные многокомпонентной формы):

Вот как должен выглядеть запрос, отправленный в виде multipart/form-data:

POST /api/contracts/ HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryE19zNvXG3RzrU5J3

------WebKitFormBoundaryE19zNvXG3RzrU5J3
Content-Disposition: form-data; name="customer_id"

1
------WebKitFormBoundaryE19zNvXG3RzrU5J3
Content-Disposition: form-data; name="deputy_agent"

1,2,3
------WebKitFormBoundaryE19zNvXG3RzrU5J3
Content-Disposition: form-data; name="product_type"

4,5,6
------WebKitFormBoundaryE19zNvXG3RzrU5J3
Content-Disposition: form-data; name="image"; filename="image.jpg"
Content-Type: image/jpeg

<image_binary_data>
------WebKitFormBoundaryE19zNvXG3RzrU5J3--

Финальные заметки:

  • Разбор полей: Метод to_internal_value гарантирует, что поля deputy_agent, product_type и referral_type будут разобраны из строк в правильные списки Python.
  • Загрузка файлов: Поле image по-прежнему будет работать, как и ожидалось в multipart/form-data, так как Django обрабатывает загрузку файлов отдельно.

Это должно исправить проблему, с которой вы столкнулись при работе со списками в multipart/form-data в Django REST Framework.

Дайте мне знать, как это работает!

Вернуться на верх