Как использовать Django Rest Framework, как загрузить файл и отправить полезную нагрузку JSON?

Я пытаюсь написать обработчик API Django Rest Framework, который может получать файл, а также полезную нагрузку JSON. Я установил MultiPartParser как обработчик обработчика.

Однако, похоже, я не могу обойти оба. Если я отправлю полезную нагрузку с файлом в виде запроса на несколько частей, полезная нагрузка JSON доступна в искаженном виде в файле request.data(первая текстовая часть до первого двоеточия в качестве ключа, остальное - данные). Я могу отправить параметры в стандартных параметрах формы просто отлично, но остальная часть моего API принимает JSON-полезные нагрузки, и я хотел быть последовательным. Невозможно прочитать запрос .body, поскольку он поднимает *** RawPostDataException: You cannot access body after reading from request data stream

Например, файл и эта полезная нагрузка в теле запроса:
{"title":"Document Title", "description":"Doc Description"}
Становится:
<QueryDict: {u'fileUpload': [<InMemoryUploadedFile: 20150504_115355.jpg (image/jpeg)>, <InMemoryUploadedFile: Front end lead.doc (application/msword)>], u'{%22title%22': [u'"Document Title", "description":"Doc Description"}']}>

Есть ли способ сделать это? Могу ли я есть торт, держать его и не набирать вес?

Изменить: Было высказано предположение, что это может быть копия загружаемого изображения Django REST Framework: "Представленные данные не были файлом" . Это не. Загрузка и запрос выполняются в multipart, и имейте в виду, что файл и загрузка его в порядке. Я могу даже заполнить запрос стандартными переменными формы. Но я хочу посмотреть, могу ли я получить полезную нагрузку JSON там.

Ответ 1

Для кого-то, кому нужно загрузить файл и отправить некоторые данные, нет простого способа заставить его работать. Для этого есть открытый выпуск в спецификациях json api. Я видел одну возможность - использовать multipart/related, как показано здесь, но я думаю, что реализовать его в drf очень сложно.

Наконец, я реализовал отправку запроса в виде formdata. Вы бы отправили каждый файл в виде файла, а все остальные данные в виде текста. Теперь для отправки данных в виде текста вы можете иметь один ключ с именем data и отправлять весь json как строку в значении.

Models.py

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py → никаких особых изменений не требуется, не указав здесь мой сериализатор как слишком длинный из-за возможности записи в поле ManyToMany.

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    parser_classes = (MultipartJsonParser, parsers.JSONParser)
    queryset = Posts.objects.all()
    lookup_field = 'id'

Вам понадобится собственный анализатор, как показано ниже для анализа json.

utils.py

from django.http import QueryDict
import json
from rest_framework import parsers

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 = {}
        # find the data field and parse it
        data = json.loads(result.data["data"])
        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

Пример запроса в почтальоне case2

EDIT:

см. этот расширенный ответ, если вы хотите отправить все данные в виде пары ключ-значение

Ответ 2

Я знаю, что это старый поток, но я просто натолкнулся на это. Мне пришлось использовать MultiPartParser, чтобы получить файл и дополнительные данные. Вот как выглядит мой код:

# views.py
class FileUploadView(views.APIView):
    parser_classes = (MultiPartParser,)

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        ftype    = request.data['ftype']
        caption  = request.data['caption']
        # ...
        # do some stuff with uploaded file
        # ...
        return Response(status=204)

Мой код AngularJS с использованием ng-file-upload:

file.upload = Upload.upload({
  url: "/api/picture/upload/" + file.name,
  data: {
    file: file,
    ftype: 'final',
    caption: 'This is an image caption'
  }
});

Ответ 3

Я отправляю JSON и изображение для создания/обновления объекта продукта. Ниже представлен созданный APIView, который работает для меня.

Serializer

class ProductCreateSerializer(serializers.ModelSerializer):
    class Meta:
         model = Product
        fields = [
            "id",
            "product_name",
            "product_description",
            "product_price",
          ]
    def create(self,validated_data):
         return Product.objects.create(**validated_data)

Вид

from rest_framework  import generics,status
from rest_framework.parsers import FormParser,MultiPartParser

class ProductCreateAPIView(generics.CreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductCreateSerializer
    permission_classes = [IsAdminOrIsSelf,]
    parser_classes = (MultiPartParser,FormParser,)

    def perform_create(self,serializer,format=None):
        owner = self.request.user
        if self.request.data.get('image') is not None:
            product_image = self.request.data.get('image')
            serializer.save(owner=owner,product_image=product_image)
        else:
            serializer.save(owner=owner)

Пример теста:

def test_product_creation_with_image(self):
    url = reverse('products_create_api')
    self.client.login(username='testaccount',password='testaccount')
    data = {
        "product_name" : "Potatoes",
        "product_description" : "Amazing Potatoes",
        "image" : open("local-filename.jpg","rb")
    }
    response = self.client.post(url,data)
    self.assertEqual(response.status_code,status.HTTP_201_CREATED)

Ответ 4

Если это вариант, очень просто использовать составное сообщение и обычный просмотр.

Вы отправляете json как поле, а файлы - как файлы, а затем обрабатываете в одном представлении.

Вот простой клиент Python и сервер Django:

Клиент - отправка нескольких файлов и произвольного json-кодированного объекта:

import json
import requests

payload = {
    "field1": 1,
    "manifest": "special cakes",
    "nested": {"arbitrary":1, "object":[1,2,3]},
    "hello": "word" }

filenames = ["file1","file2"]
request_files = {}
url="example.com/upload"

for filename in filenames:
    request_files[filename] = open(filename, 'rb')

r = requests.post(url, data={'json':json.dumps(payload)}, files=request_files)

Сервер - использование json и сохранение файлов:

@csrf_exempt
def upload(request):
    if request.method == 'POST':
        data = json.loads(request.POST['json']) 
        try:
            manifest = data['manifest']
            #process the json data

        except KeyError:
            HttpResponseServerError("Malformed data!")

        dir = os.path.join(settings.MEDIA_ROOT, "uploads")
        os.makedirs(dir, exist_ok=True)

        for file in request.FILES:
            path = os.path.join(dir,file)
            if not os.path.exists(path):
                save_uploaded_file(path, request.FILES[file])           

    else:
        return HttpResponseNotFound()

    return HttpResponse("Got json data")


def save_uploaded_file(path,f):
    with open(path, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)

Ответ 5

У меня есть аналогичная проблема, вот мое решение:

Сначала добавьте это в свою конфигурацию (settings.py):

'DEFAULT_PARSER_CLASSES': (
    'rest_framework.parsers.JSONParser',
    'rest_framework.parsers.MultiPartParser',
    'rest_framework.parsers.FileUploadParser',
),

Затем в вашем Serializer (ex: 'файл'):

file = serializers.FileField()

И в вашем представлении добавьте:

parser_classes = (FileUploadParser, JSONParser)

С этим я могу опубликовать как файл, так и различные поля, но вам нужно указать:

  • формат сообщения как "multipart"
  • и этот http-заголовок:

HTTP_CONTENT_DISPOSITION = "attachment; filename = your_file_name.jpg"