4
votes

How do I send a POST request with nested data including files (images) to django REST nested serializers?

Given this JS object:

bookData: {
    title: 'Anne of Green Gables',
    coverImage: File(123456),
    pages: 123,
    author: {
        name: 'Lucy Maud Montgomery',
        born: 1874,
        profilepicture_set: [
            {file: File(234567), description: 'Young L. M. Montgomery'},
            {file: File(234568), description: 'Old L. M. Montgomery}
        ],
        quote_set: [
            {text: "I'm so glad I live in a world where there are Octobers."},
            {text: "True friends are always together in spirit."},
        ]
    },

}

I want to send a POST request to my django REST API (I use VueJS on frontend, but this does not really matter).

# views.py
class CreateBookView(generics.CreateAPIView):
    serializer_class = CreateBookSerializer
    queryset = Book.objects.all()

# serializers.py
class CreateBookSerializer(serializers.ModelSerializer):
    author = CreateAuthorSerializer()

    class Meta:
        model = Book
        fields = ('title', 'pages', 'author')

    @transaction.atomic
    def create(self, validated_data):
        author_data = validated_data.pop('author')
        uploader = self.context['request'].user
        book = Book.objects.create(uploader=uploader, **validated_data)

        Author.objects.create(book=book, **author_data)

        return book


# models.py
class AuthorManager(models.Manager):

    def create(self, **author_data):
        quotes_data = author_data.pop('quote_set')
        photos_data = author_data.pop('profilepicture_set')

        author = Author(**author_data)
        author.save()

        for quote_data in quotes_data:
            Quote.objects.create(author=author, **quote_data)

        for photo_data in photos_data :
            ProfilePicture.objects.create(author=author, **photo_data)

        return author

## Skipping the CreateAuthorSerializer, Quote and ProfilePicture Managers as they are analogous.
## Assume one Author can only write one Book (OneToOneField). No need to look the name up for existing Authors.

EDIT

I wanted to add some info on how I send the data in the frontend in VueJS.

sendData(){
    var fd = new FormData()
    var bookData = {
        title: this.$store.getters.title, # 'Anne of Green Gables"
        coverImage: this.$store.getters.coverImage, # File(somesize)
        pages: this.$store.getters.pages, # 123
        author: this.$store.getters.author, # nested object 
        ...
    }
    fd = objectToFormData(bookData, fd) # external function, see below
    this.$http.post('api/endpoint/create/', fd, headers: {
        'Content-Type': 'multipart/form-data',
        'X-CSRFToken': Cookies.get('csrftoken'),
        }
    }).then(...)
}

The sendData() is invoked on button click. The objectToFormData is an external lib from npm (click) that transforms the nested object into flat form.

The problem with this solution (as well as @Saji Xavier's solution) is that the request.data contains an ugly QueryDict which is far from the original bookData object structure and is not accepted by the REST serializer (namely, the author field is missing - which is true, the object has been flattened).

<QueryDict: {
'title': ['Anne of Green Gables'], 
'coverImage':[InMemoryUploadedFile: cover.jpg (image/jpeg)],
'pages': ['123'], 
'name': ['Lucy Maud Montgomery'], 
'born': ['1874'],
'profilepicture_set[][description]': ['Young L. M. Montgomery', 'Old L. M. Montgomery'], 
'profilepicture_set[][file]': [
    InMemoryUploadedFile: young.jpg (image/jpeg), 
    InMemoryUploadedFile: old.jpg (image/jpeg)
    ],
'quote_set[][text]': [
    "I'm so glad I live in a world where there are Octobers.",
    "True friends are always together in spirit."
    ]
}>

How do I deal with it now? It seems sooooo ugly. What I can think of is to overwrite the create() function.

class CreateBookView(generics.CreateAPIView):
    (...)
    def create(self, request, *args, **kwargs):
        book_data = {}
        book_data['title'] = request.data['title']
        book_data['pages'] = request.data['pages']
        book_data['cover_image'] = request.data['coverImage']
        book_data['author'] = {}
        book_data['author']['name'] = request.data['name']
        book_data['author']['born'] = request.data['born']
        (...)
        serializer = self.get_serializer(data=book_data)
        (...)

This is extremely ugly solution, ineffective for models with multitude of fields and several levels of nestedness (this book/author is just a toy example).

How should I handle it? Do i change the way of POSTing (to get a nice request.data format) or do I handle this ugly one?

1

1 Answers

0
votes

According DRF documentation,

Most parsers, such as e.g. JSON don't support file uploads. Django's regular FILE_UPLOAD_HANDLERS are used for handling uploaded files.

One option is to use Javascript FormData API to send multipart data (works with VueJS/AngularJS)

var fd = new FormData();
fd.append('title', 'Anne of Green Gables')
fd.append('author_name', 'Lucy Maud Montgomery')
fd.append('author_profilepicture_set', files[])
$http.post('url', fd, {
   headers: {
       'Content-Type': 'multipart/form-data'
   }
});