How To Upload Multiple Images With Djangorestframework

How To Upload Multiple Images With Djangorestframework

Hey guys, how is it going?

Okay so I have built couple projects with just pure Django and I thought it's time to take a step further by learning how to build APIs with Djangorestframework because I know knowing just Django isn't enough as a Django developer.

So, I decided to learn Djangorestframework by building a microblogging platform. I didn't just want it to be able to perform basic CRUD operations, I wanted it to be a little bit robust so I decided I was going to implement a multiple image upload functionality(where multiple images can be uploaded along with texts-- Think of twitter) along with other functionalities.

I initially thought implementing the thing was going to be a walk in the park until I started it. I checked Stackoverflow, read couple Medium articles but couldn't find any thing really useful so I reached out to a fellow Django developer on twitter, his name is Onasanya Tunde...you can follow him on twitter here and not only did help me implement the functionality, he also took time to explain to me. I then thought of writing an article on how to implement the functionality so it can serve as a reference for me and also help other Django developers out there. So let's get down to it.

I am assuming if you are reading this article, you already have basic Django knowledge, if you don't, I would advise you take some tutorials on django first.

So, to implement multiple image upload functionality, we need two models in our models.py file, the models will look like this:

class TweetFile(models.Model):
    tweep =  models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    media = models.FileField(upload_to='images')

    def __str__(self):
        return f"{self.tweep.username}'s media images"


class Tweets(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    texts = models.TextField()
    file_content = models.ManyToManyField(TweetFile, related_name='file_content', blank=True, null=True)
    date_posted = models.DateTimeField(auto_now_add=True)
    tweep = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    class Meta:

        verbose_name_plural = _('Tweets')

    def __str__(self):
        return f"{self.texts}"

Okay, let me explain the code snippets above.

I created the TweetFile class to hold all our images(or their urls) in our database and inside the class I created the 'tweep' and 'media' attributes to hold the user that uploads the image and image uploaded respectively. N.B: You can name your model and attributes anything you want.

I also created the Tweet model which will hold all the tweets a user uploads I then created an attribute named "file_content" which has a ManyToManyField and linked it to the TweetFile model I created above. I am using a ManyToManyField because we are going to be uploading multiple images.

Next we create a serializers.py in the same directory holding our models.py file, our serializers.py looks something like this:

from rest_framework import serializers
from tweets.models import Tweets, TweetFile, Comments


class TweetSerializer(serializers.ModelSerializer):
    tweep = serializers.SerializerMethodField('get_tweep_username')


    class Meta:
        model = Tweets
        fields = ['id', 'texts', 'file_content', 'date_posted', 'tweep']
        extra_kwargs = {
            "file_content": {
                "required": False,
            }
        }
    # function that returns the owner of a tweet
    def get_tweep_username(self, tweets):
        tweep = tweets.tweep.username
        return tweep

In the code snippets above, I included the "file_content" in the list so the images can be shown when we get our tweets or when we get a particular tweet.

'get_tweep_username' is just a function that returns the username of the tweet author along with the details of the tweet when we receive a response. the 'extra_kwargs' is to ensure that uploading images is optional.

The next thing is to create the logic for our request/response in the views.py file:

from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.decorators import api_view, permission_classes, parser_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from tweets.models import Tweets, TweetFile
from tweets.serializers import TweetSerializer

@api_view(['POST'])
@permission_classes([IsAuthenticated])
@parser_classes([MultiPartParser, FormParser])
def create_tweet(request):
    user = request.user
    if request.method == 'POST':
        files = request.FILES.getlist('file_content')
        if files:
            request.data.pop('file_content')

            serializer = TweetSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(tweep=user)
                tweet_qs = Tweets.objects.get(id=serializer.data['id'])
                uploaded_files = []
                for file in files:
                    content = TweetFile.objects.create(tweep=user, media=file)
                    uploaded_files.append(content)

                tweet_qs.file_content.add(*uploaded_files)
                context = serializer.data
                context["file_content"] = [file.id for file in uploaded_files]
                return Response(context, status=status.HTTP_201_CREATED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        else:
            serializer = TweetSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(tweep=user)         
                context = serializer.data            
                return Response(context, status=status.HTTP_201_CREATED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    else:
        return Response(serializer.errors, status=status.HTTP_405_METHOD_NOT_ALLOWED)

Explanation:

I used the first three decorators(the @ thingy above the create_tweet function) to let djangorestframework know that I am sending post request, users must be logged in before they can create a tweet, and that I may be sending image files along with our post request respectively.

The 'request.FILES.getlist('file_content')' is to get the images from the request data, I then put a check with the if statement to check if the user uploaded an image...if the user did I popped i.e remove those images out of the request data because we want to create the tweet first without the images. After that I then pass the data from the request into the TweetSerializer we created in the serializers.py file by instantiating it, this is so the response can be returned in a JSON format to us in POSTMAN. We check if the serializer is valid and then save it with the logged in user as the tweet author.

After creating and saving the tweet, I did the following:

  1. I got the tweet by using the id from our serializer i.e the id field that was included in the list in the serializers.py file and assigned it to a variable called tweet_qs.

  2. I then created an empty list called uploaded_files.

  3. The next thing I did was loop through 'request.FILES.getlist('file_content')' which has a variable called files assigned to it to access each of the image/ images uploaded by the user so I can use it/them to create TweetFile objects and store in a variable called content.

  4. I then appended content to the uploaded_files I created above.

5.After that, I added uploaded files to tweet_qs by using tweet_qs.file_content.add(*uploaded_files).

context["file_content"] = [file.id for file in uploaded_files] simply means we are creating a new key-value pair where the key will be 'file_content' and the value(s) will be the id(s) of the images uploaded which will then be displayed as response.

If the user did not upload any image then we can just save the serializer and return a response.

That is it, we are all done, we can now test this using postman but don't forget to set up the urls.py files in your project root directory and your app directory.

After testing with postman, you should get a response like this if you uploaded image(s):

Screenshot (59).png

And a response like this if you didn't upload any image: Screenshot (60).png

That's all you need to do to implement multiple image upload functionality with djangorestframework.

I hope this article was helpful. ๐Ÿ™‚๐Ÿ™‚