0

I have a Vue front end that collects data (and files) from a user and POST it to a Django Rest Framework end point using Axios.

Here is the code for that function:

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

When I post data the response I see on the server side is:

uploaded_data = validated_data.pop('uploaded_images')
KeyError: 'uploaded_images'

that comes from this serializer:

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

Trying to make sense of this so am I correct in my thinking that DRF saves the serializer when the data is sent to the endpoint? The variable validated_data I presume is the request.data object? Why am I getting the KeyError then and how can I see what the data is that is being validated, or sent in the post request on the server side. The data sent in the post request in the browser looks like this:

-----------------------------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Ä(

Here is the ViewSet that sits at the endpoint:

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'
  • validated_data has the data which has been validated rather than the raw request data - it's possible the field is failing validation and then wouldn't be in the validated_data – pzutils Nov 26 '22 at 08:28
  • ok thank you how can I check if the data is valid? I think I am sending the files to the back end the wrong way but for 2 days I cannot find the right way to do it –  Nov 26 '22 at 11:21
  • You can test your serializer using a python shell on your project. Import models and serializer, create an object and serialize the object, finally inspect the response and see what you have. Alternatively, you can write a test. – Niko Nov 26 '22 at 13:50
  • I think your formData is not being formed correctly. Try doing formData = new FormData() then formData.append(fieldName, value), so for example formData.append(‘category’, fields.value.category.index) – pzutils Nov 26 '22 at 17:37
  • https://stackoverflow.com/questions/22783108/convert-js-object-to-form-data – pzutils Nov 26 '22 at 17:48
  • @pzutils I followed your instructions and the suggestions in the link you sent me and this formData.append("uploaded_images", fields.value.images) makes the data I send to the backend look like this uploaded_images → "[object File],[object File],[object File],[object File] for the 4 files I m trying to post. If I send a normal object then it shows the files –  Nov 27 '22 at 06:45
  • @Niko I did what you said and I am able to create a post and serialize it and I don't get any errors. Plus if I do not try and create images with the post then there is no problem and I can create a post on Django but somehow the validated data does not contain the uploaded images key. –  Nov 27 '22 at 09:39
  • what does it output when you print(validated_data) just before your uploaded_data = validated_data.pop('uploaded_images') line ? – pzutils Nov 27 '22 at 16:59

2 Answers2

0

So, after a few hours of research I was able to find my own solution. The method used to read multiple files, was taken from this answer. By breaking the [object FileList] into separate files and appending them to the FormData. The models are based on this answer

On the backend, overriding the create method of the serializer and loop through resquest.POST.data excluding unwanted keys to access the just the files. And saving them into the Images model (should be named PostImage).

Note that I do no access the validated_data for the files, instead they are retrieved directly from the request.

I used bootstrap5 in the frontend.

EDIT: Tested only two types of request GET(list) and POST(create) (as you see in vue component)

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>
Niko
  • 3,012
  • 2
  • 8
  • 14
  • Niko thank you for spending the time on this. I am going to try your solution and let you know if it works for me. My Vue is a little different cause I'm using the composition API –  Nov 28 '22 at 15:09
0

Hope this helps someone.

Finally after three days battling with this I found the solution to my issue. In the models I have this function that generates a string I can use as the upload_to string for the PostImage:

def post_directory_path(instance, filename):
    return 'user_{0}/posts/post_{1}/{2}'.format(instance.user.id, instance.post.id, filename)

There is no user instance on the PostImage only on the Post and Django does not not throw an exception or show any errors for this mistake, which is why I did not look for the problem there.