How to upload multiple files in django rest framework

Question:

In django rest framework, I am able to upload single file using danialfarid/ng-file-upload

views.py:

class PhotoViewSet(viewsets.ModelViewSet):
    serializer_class = PhotoSerializer
    parser_classes = (MultiPartParser, FormParser,)
    queryset=Photo.objects.all()

    def perform_create(self, serializer):
        serializer.save(blogs=Blogs.objects.latest('created_at'),
                   image=self.request.data.get('image'))

serializers.py:

class PhotoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Photo

models.py:

class Photo(models.Model):
    blogs = models.ForeignKey(Blogs, related_name='blogs_img')
    image = models.ImageField(upload_to=content_file_name)

When I try to upload multiple file. I get in

chrome developer tools:
Request Payload

------WebKitFormBoundaryjOsYUxPLKB1N69Zn
Content-Disposition: form-data; name="image[0]"; filename="datacable.jpg"
Content-Type: image/jpeg


------WebKitFormBoundaryjOsYUxPLKB1N69Zn
Content-Disposition: form-data; name="image[1]"; filename="datacable2.jpg"
Content-Type: image/jpeg

Response:

{"image":["No file was submitted."]}

I don’t know how to write serializer for uploading multiple file.

Asked By: Deep 3015

||

Answers:

I manage to solve this issue and I hope it will help community

serializers.py:

class FileListSerializer ( serializers.Serializer ) :
    image = serializers.ListField(
                       child=serializers.FileField( max_length=100000,
                                         allow_empty_file=False,
                                         use_url=False )
                                )
    def create(self, validated_data):
        blogs=Blogs.objects.latest('created_at')
        image=validated_data.pop('image')
        for img in image:
            photo=Photo.objects.create(image=img,blogs=blogs,**validated_data)
        return photo

class PhotoSerializer(serializers.ModelSerializer):

    class Meta:
        model = Photo
        read_only_fields = ("blogs",)

views.py:

class PhotoViewSet(viewsets.ModelViewSet):
    serializer_class = FileListSerializer
    parser_classes = (MultiPartParser, FormParser,)
    queryset=Photo.objects.all()
Answered By: Deep 3015

I dont know it very well, but this is working…
This is for my viewset.

def perform_create(self, serializer):
    obj = serializer.save()
    for f in self.request.data.getlist('files'):
        mf = MyFile.objects.create(file=f)
        obj.files.add(mf)
Answered By: Charles

It took me a while to find out an effective solution to this,
I would like to share with you what finally worked for me.
I am using reactjs and DRF.

Here is my model :

class MediaFile(models.Model):
    article = models.ForeignKey(Articles, on_delete=models.CASCADE, null=True)
    caption = models.CharField(max_length=500, null=True, blank=True)
    file = models.FileField('photo of article', upload_to=set_filename,
                            blank=True, null=True, default='')
    added = models.DateTimeField(auto_now_add=True)

The views are standard viewsets.ModelViewSet

class MediaFilesViewSet(viewsets.ModelViewSet):
    serializer_class = FileListSerializer
    parser_classes = (parsers.MultiPartParser, parsers.FormParser,)
    queryset=MediaFile.objects.all()

In ArticleSerializer I added:

def create(self, validated_data):
    request = self.context.get('request')
    user = request.user
    instance = Articles.objects.create(**validated_data)
    instance.publisher = user
    instance.save()
    images = request.FILES
    if images:
        try:
            for f in images.getlist('mediafiles'):
                instance.mediafile_set.get_or_create(file=f, caption=f.name)
                instance.save()
        except Exception as e:
            print(e)
    return instance

The FRONTEND structure:

postChangeImage = (event) =>{
    this.setState({
        is_images: true
    });
    let files = event.target.files;
    const files_array = Object.values(files);

    this.setState(
        {imagefile: files}
    );

    console.log('IMAGES ARRAY', files_array);
    files_array.map(value => {
        const urls = URL.createObjectURL(value);
        this.setState((prevState)=>(
            {postfolio:[urls, ...prevState.postfolio]}
            ));
    });

};

and POSTING:

for (let i=0; i< this.state.imagefile.length; i++) {
                    form_data.append(`mediafiles`, this.state.imagefile[i]);
                }
Answered By: omarb777

Here is how you upload multiple files on blogs api:

models.py

class Blogs(models.Model):
    ...
 

class Photo(models.Model):
    blogs = models.ForeignKey(Blogs, related_name='blogs_img')
    image = models.ImageField(upload_to=content_file_name)

serializers.py

class PhotoSerializer(serializers.ModelSerializer):

    class Meta:
        model = Photo
        fields = ['blogs', 'image',]


class BlogsSerializer(serializers.ModelSerializer):
    photos = serializers.SerializerMethodField()

    def get_photos(self, obj):
        photos = Photo.objects.filter(blogs=obj)
        return PhotoSerializer(photos, many=True, read_only=False).data

    class Meta:
        model = Blogs
        fields = [
            ...
            'photos',
    ]

views.py

class BlogsViewSet(viewsets.ModelViewSet):
    serializer_class = BlogsSerializer
    queryset = Blogs.objects.all()

    def create(self, request, *args, **kwargs):
        instance_data = request.data
        data = {key: value for key, value in instance_data.items()}
        serializer = self.get_serializer(data=data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()

        if request.FILES:
            photos = dict((request.FILES).lists()).get('photos', None)
            if photos:
                for photo in photos:
                    photo_data = {}
                    photo_data["blogs"] = instance.pk
                    photo_data["image"] = photo
                    photo_serializer = PhotoSerializer(data=photo_data)
                    photo_serializer.is_valid(raise_exception=True)
                    photo_serializer.save()

        return Response(serializer.data)

Answered By: Arindam

The best answer to this question did not work for me, but Charles’ suggestion worked very well. In my case, I needed to upload multiple files and assign them to a specific batch. Each batch is assigned to a particular user.

Below is more context using ReactJS to make the POST request, along with the serializers used and Postman window:

api.py

from convert_files.models import File
from rest_framework import viewsets, permissions
from rest_framework.parsers import MultiPartParser, JSONParser
from .serializers import BatchSerializer

class BatchViewSet(viewsets.ModelViewSet):
    permission_classes = [
        permissions.IsAuthenticated
    ]

    def perform_create(self, serializer):
        obj = serializer.save(owner=self.request.user)
        for f in self.request.data.getlist('files'):
            mf = File.objects.create(office_file=f)
            obj.files.add(mf)

    parser_classes = (MultiPartParser, JSONParser, )

    serializer_class = BatchSerializer

    http_method_names = ['get','post','delete','put','patch', 'head']

    def get_queryset(self):
        return self.request.user.batches.all()

serializers.py

from rest_framework import serializers
from convert_files.models import File, Batch

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields = '__all__'


class BatchSerializer(serializers.ModelSerializer):
    files = FileSerializer(many=True, required = False)

    class Meta:
        model = Batch
        fields =  '__all__'

models.py

from django.db import models
from django.conf import settings
from django.contrib.auth.models import User

from .extra import ContentTypeRestrictedFileField

def make_upload_path(instance, filename):
    """Generates upload path for FileField"""
    return settings.OFFICE_OUTPUT_FILES_URL + "/%s" % (filename)

class Batch(models.Model):
    name = models.CharField(max_length=100, blank=True)
    description = models.TextField(blank=True)
    date_posted = models.DateTimeField(default=datetime.datetime.now)
    owner = models.ForeignKey(User, related_name="batches", 
                            on_delete=models.CASCADE, null=True)

class File(models.Model):
    name = models.CharField(max_length=100, blank=True)
    office_file = ContentTypeRestrictedFileField(
        upload_to           = make_upload_path,
        content_types       = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                            'application/vnd.ms-excel','application/msword',
                            'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
        max_upload_size     = 10485760,
    )

    files = models.ForeignKey(Batch, on_delete=models.CASCADE, null=True, 
                                    related_name='files', related_query_name='files')

FileUpload.js

import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { addBatch } from '../actions/batches';

function FileUpload() {

  const dispatch = useDispatch();
  let formData = new FormData()

  const onDrop = useCallback((acceptedFiles) => {
    for (var i = 0; i < acceptedFiles.length; i++) {
      formData.append("files", acceptedFiles[i], acceptedFiles[i].name)
    }
    dispatch(addBatch(formData));
  })

...

Postman

Image of POST request in Postman for Multiple File Upload to DRF

Answered By: Alex Thompson

Working with "dictionary (array) of images"

Ok, so today I tried Arindam’s solution.. it worked perfectly, but after a while, I figgured out that my frontend (port 3000) makes a GET request to itself looking for an image that is not there, and not at the backend(port 8000).. (eg. GET http://localhost:3000/media/images/products/default.png – Returns 404: Not found).. What worked for me was to change the code around a bit and this is my solution..

in models.py

class Product(models.Model):
    title = models.CharField(max_length=255)
    description = models.CharField(max_length=255)
    price = models.FloatField()
    quantity = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=False)
    slug = models.SlugField(max_length=255, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)
...

class ProductImage(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
    image = models.ImageField("Image", upload_to=upload_to, default='products/default.png')

in serializers.py

...
class ProductImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProductImage
        fields = ['id', 'image', 'product']
        extra_kwargs = {
        'product': {'required': False},
        }

class ProductSerializer(serializers.ModelSerializer):
    images = ProductImageSerializer(many=True, required=False)

    class Meta:
        model = Product
        fields = ['id', 'title', 'description', 'images', 'price', 'quantity', 'active', 'slug', 'created_at', 'modified_at']
        read_only_fields = ['slug']
        #lookup_field = 'slug'

    def create(self, validated_data):        
        product = Product.objects.create(**validated_data)
        try:
            # try to get and save images (if any)
            images_data = dict((self.context['request'].FILES).lists()).get('images', None)
            for image in images_data:
                ProductImage.objects.create(product=product, image=image)
        except:
            # if no images are available - create using default image
            ProductImage.objects.create(product=product)
        return product
    

in views.py

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    permission_classes = [permissions.AllowAny]
    serializer_class = ProductSerializer
    parser_classes = [MultiPartParser, FormParser]
    #lookup_field = 'slug'

edit: in settings.py

import os
...
BASE_DIR = Path(__file__).resolve().parent.parent
...
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\', '/')
MEDIA_URL = '/media/'

I am posting this here to help someone (or me again in the future) with multiple files upload or multiple images upload as I spent 2 days looking up tutorials and answeres online to help me solve this issue… I may not be doing everything perfectly as I just recently started exploring Django REST Framework (and Python), but I hope it helps.

Answered By: Petar Ivanov

I have solved the issue with this solution

models.py:

class Product(models.Model):
   title = models.CharField(max_length=255)
   description = models.CharField(max_length=255)

class Images(models.Model):
   product = model.ForeignKey('Product', related_name="images", on_delete=models.CASCADE)
   image = models.ImageField(upload_to=upload_path)

serializers.py

class ImageSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Images
        fields = '__all__'

views.py

class ImagesViewSet(ModelViewSet):
    queryset = models.Images.objects.all()
    serializer_class = serializers.ImageSerializer
    
    # overwrite create method from the CreateModelMixin
    def create(self, request, *args, **kwargs):
        data = request.data
        images = data.getlist('image')
        
        # if no images call parent method it will return error
        if not images:
            return super().create(request, *args, **kwargs)

        # verify only without creating the images
        serializer_lst = []
        for image in images:
            data['image'] = image
            serializer = self.get_serializer(data=data)
            serializer.is_valid(raise_exception=True)
            serializer_lst.append(serializer)
        
        serializers_data = [] # this is to collect data for all created images and return as list in the response
        for serializer in serializer_lst:
            self.perform_create(serializer)
            serializers_data.append(serializer.data)
            headers = self.get_success_headers(serializer.data)
        
        return Response(serializers_data, status=status.HTTP_201_CREATED, headers=headers)
Answered By: Amr