Фронтенд Vue3, бэкенд Django. Ошибка ключа для проверенных данных в сериализаторе
У меня есть Vue front end, который собирает данные (и файлы) от пользователя и отправляет их в конечную точку Django Rest Framework, используя Axios.
Вот код для этой функции:
import { ref } from "vue";
import axios from "axios";
const fields = ref({
audience: "",
cancomment: "",
category: "",
body: "",
errors: [],
previews: [],
images: [],
video: [],
user: user,
});
function submitPost() {
const formData = {
'category': fields.value.category.index,
'body': fields.value.body,
'can_view': fields.value.audience,
'can_comment': fields.value.cancomment,
'video': fields.value.video,
'uploaded_images': fields.value.images,
'user': store.userId
};
console.log(formData['uploaded_images'])
axios
.post('api/v1/posts/create/', formData, {
headers: {
"Content-Type": "multipart/form-data",
"X-CSRFToken": "{{csrf-token}}"
}
})
.then((response) => {
if(response.status == 201){
store.messages.push("Post created successfully")
}
})
.catch((error) => {
messages.value.items.push(error.message)
})
}
Когда я размещаю данные, на стороне сервера я вижу следующий ответ:
uploaded_data = validated_data.pop('uploaded_images')
KeyError: 'uploaded_images'
который приходит от этого сериализатора:
class PostImageSerializer(serializers.ModelSerializer):
class Meta:
model = PostImage
fields = ['image', 'post']
class PostSerializer(serializers.ModelSerializer):
images = PostImageSerializer(many=True, read_only=True, required=False)
uploaded_images = serializers.ListField(required=False, child=serializers.FileField(max_length=1000000, allow_empty_file=False, use_url=False),write_only=True)
class Meta:
model = Post
fields = [
"category",
"body",
"images",
"uploaded_images",
"video",
"can_view",
"can_comment",
"user",
"published",
"pinned",
"created_at",
"updated_at",
]
def create(self, validated_data):
uploaded_data = validated_data.pop('uploaded_images')
new_post = Post.objects.create(**validated_data)
try:
for uploaded_item in uploaded_data:
PostImage.objects.create(post = new_post, images = uploaded_item)
except:
PostImage.objects.create(post=new_post)
return new_post
Пытаюсь разобраться в этом, правильно ли я понимаю, что DRF сохраняет сериализатор, когда данные отправляются на конечную точку? Переменная validated_data, я полагаю, является объектом request.data? Тогда почему я получаю KeyError и как я могу увидеть, что это за данные, которые проверяются или отправляются в post-запросе на стороне сервера. Данные, отправленные в пост-запросе в браузере, выглядят следующим образом:
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="body"
Post
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="can_view"
Everybody
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="can_comment"
Everybody
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="uploaded_images.0"; filename="tumblr_42e2ad7e187aaa1b4c6f4f7e698d03f2_c9a2b230_640.jpg"
Content-Type: image/jpeg
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="body"
Post
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="can_view"
Everybody
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="can_comment"
Everybody
-----------------------------2091287168172869498837072731
Content-Disposition: form-data; name="uploaded_images.0"; filename="tumblr_42e2ad7e187aaa1b4c6f4f7e698d03f2_c9a2b230_640.jpg"
Content-Type: image/jpeg
(¼T¼Þ7ó[®«ý;>7гô
eIqegy[XbkéÉc¤ÎSFÌÔÂåÄAR§*P!I<R,4AP9ÖgÅÖYÔ×éu«ÅÉ<IJª+`,.uòÜtK7xéu.Ô¬]{ù£æÍ÷·n²±×:îã¡`UÐKxªyjxñDUAP¢+ÄÅB1yõçùuS5å
D÷ zö4®n¦Öod&<z¼P
W9©xeúD5ÈMpÖö¬ðÓKÊľO«oµÊMçÇy|z=^<AKêôz¼x##:ù;«OdÞ¢¶WRùººRêÜêú8ø¡ãÄ"¼AãÅj¿3ÆõÙRÆ]_MTÆ^;;
`ttR}mì¤*bêwy¾=d<xòøòxÄ(
Вот набор ViewSet, который находится в конечной точке:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter, django_filters.rest_framework.OrderingFilter]
# filterset_class = PostFilter
ordering_fields = ['created_at',]
search_fields = ['category', 'body']
permission_classes = [permissions.IsAuthenticated]
def get_serializer_context(self):
return {'request': self.request}
parser_classes = [MultiPartParser, FormParser]
lookup_field = 'slug'
Итак, после нескольких часов исследований я смог найти собственное решение. Метод, используемый для чтения нескольких файлов, был взят из этого ответа. Путем разбиения [объекта FileList] на отдельные файлы и добавления их к FormData. Модели основаны на этом answer
На бэкенде переопределите метод create сериализатора и пройдитесь циклом по resquest.POST.data, исключая ненужные ключи, чтобы получить доступ только к файлам. И сохранить их в модели Images (должна быть названа PostImage).
Обратите внимание, что я не обращаюсь к validated_data для файлов, вместо этого они извлекаются непосредственно из запроса.
Я использовал bootstrap5 во фронтенде.
EDIT: Протестировано только два типа запроса GET(list) и POST(create) (как вы видите в компоненте vue)
models.py:
class Post(models.Model):
title = models.CharField(max_length=128)
body = models.CharField(max_length=400)
def get_image_filename(instance, filename):
title = instance.post.title
slug = slugify(title)
return "post_images/%s-%s" % (slug, filename)
class Images(models.Model):
post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE)
image = models.ImageField(upload_to=get_image_filename,
verbose_name='Image')
serializers.py:
from core.models import Images, Post
from rest_framework import serializers
class PostSerializer(serializers.ModelSerializer):
images = serializers.SerializerMethodField()
class Meta:
model = Post
fields = '__all__'
def create(self, validated_data):
new_post = Post.objects.create(**validated_data)
data = self.context['request'].data
for key, image in data.items():
if key != 'title' and key != 'body':
image = Images.objects.create(post=new_post, image=image)
return new_post
def get_images(self, obj):
images = []
qs = Images.objects.filter(post=obj)
for item in qs:
images.append(item.image.name)
return images
views.py:
from rest_framework import viewsets
from core.models import Post
from core.serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
TestComponent.vue:
<template>
<div class="container" style="display: flex; justify-content: center; align-items: center;">
<form @submit.prevent="submit" >
<div class="mb-3">
<label for="exampleInputTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="exampleInputTitle" v-model="title">
</div>
<div class="mb-3">
<label for="exampleInputBody" class="form-label">Body</label>
<input type="text" class="form-control" id="exampleInputBody" v-model="body">
</div>
<div class="mb-3">
<label for="formFileMultiple" class="form-label">Multiple files input example</label>
<input class="form-control" type="file" id="formFileMultiple" ref="file" multiple>
</div>
<div>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
this.title = '',
this.body = ''
},
methods: {
submit() {
const formData = new FormData();
for( var i = 0; i < this.$refs.file.files.length; i++ ){
let file = this.$refs.file.files[i];
formData.append('files[' + i + ']', file);
}
formData.append("title", this.title);
formData.append("body", this.body);
axios.post('http://localhost:8000/posts/', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error.response);
});
}
},
mounted() {
axios.get('http://localhost:8000/posts/')
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error.response);
});
}
}
</script>
Надеюсь, это кому-нибудь поможет.
Наконец, после трех дней борьбы с этим я нашел решение моей проблемы. В моделях у меня есть эта функция, которая генерирует строку, которую я могу использовать в качестве строки upload_to для PostImage:
def post_directory_path(instance, filename):
return 'user_{0}/posts/post_{1}/{2}'.format(instance.user.id, instance.post.id, filename)
На PostImage нет экземпляра пользователя, только на Post, и Django не выбрасывает исключение и не показывает никаких ошибок для этой ошибки, поэтому я не искал проблему там.