Как использовать 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"