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'
8
  • 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
    Commented Nov 26, 2022 at 8: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
    – user9361135
    Commented Nov 26, 2022 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
    Commented Nov 26, 2022 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
    Commented Nov 26, 2022 at 17:37
  • stackoverflow.com/questions/22783108/…
    – pzutils
    Commented Nov 26, 2022 at 17:48

2 Answers 2

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>
1
  • 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
    – user9361135
    Commented Nov 28, 2022 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.