Uploading images inside Django article

In this part, we'll cover how to implement Django image management so we can upload them from a local computer or clipboard

Welcome everyone to part 17 of the web development in python with Django. At this point, we have already completely implemented email authentication into our Django website. This means that all the users must confirm their email while registering. This enables us to implement password recovery functionality by using user's emails, and we implemented all of this!

The first thing that attracts the users to a website is videos, images, and other attractive stuff, definitely not plain text! But then I thought, what is the website's purpose if we can't control all the media stuff on our articles. So, in this part, we'll cover how to implement the functionality to add images from our computer or copy from somewhere into our articles!

Right now, we have a button "Insert/edit images" in our TinyMCE editor:

But if we try to click on it, we can't upload any images here; this functionality is not enabled.

Let's begin by modifying "TinyMCE" settings in our Django project settings file:

# django_project/django_website/settings.py
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5 MB

TINYMCE_DEFAULT_CONFIG = {
    ...
    "images_upload_url": "upload_image",
}

TinyMCE already has an image uploading functionality, but it doesn't have any function to handle these images, so by feeding the "image_upload_url", we are telling TinyMCE that when we are uploading images, it should target the "upload_image" URL. Although sometimes we want to upload large images to our website, by settings "DATA_UPLOAD_MAX_MEMORY_SIZE", we'll increase the limit to 5 Megabytes.

If we'll go back to our website and we'll try to click on the same "Insert/edit images" button, we'll see a different view now:

Before, we only saw the "General" tab. Now, we can access the "Upload" tab, where we can choose an image from our local computer. But once I choose an image, we see an error:

This means we are not managing what we do with the uploaded file. So to handle these uploaded images inside our Articles, we create the following path line in our main project URLs:

# django_website/main/urls.py
from django.urls import path
from . import views

urlpatterns = [
    ...
    path('<series>/<article>/upload_image', views.upload_image, name="upload_image"),
]

We constructed the path this way because we only upload images to our articles. If you were interested in uploading images all over the place, you would need to check the full URL where you are trying to upload images.

Now, this URL doesn't have the function implemented we are targeting, so let's begin by creating a simple view function:

# django_project/main/views.py
from django.views.decorators.csrf import csrf_exempt
...
@csrf_exempt
@user_is_superuser
def upload_image(request, series: str=None, article: str=None):
    pass

Here it's necessary to disable CSRF protection while uploading images. If we go back to our website and try to upload any image we want, we will see that it doesn't throw any error to us. That's because we are using "pass" in our function and not handling the image. But we are happy it works, and now we can complete the logic to handle uploaded images:

# django_project/main/views.py
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
...
@csrf_exempt
@user_is_superuser
def upload_image(request, series: str=None, article: str=None):
    if request.method != "POST":
        return JsonResponse({'Error Message': "Wrong request"})

    # If it's not series and not article, handle it differently
    matching_article = Article.objects.filter(series__slug=series, article_slug=article).first()
    if not matching_article:
        return JsonResponse({'Error Message': f"Wrong series ({series}) or article ({article})"})

    file_obj = request.FILES['file']
    file_name_suffix = file_obj.name.split(".")[-1]
    if file_name_suffix not in ["jpg", "png", "gif", "jpeg"]:
        return JsonResponse({"Error Message": f"Wrong file suffix ({file_name_suffix}), supported are .jpg, .png, .gif, .jpeg"})

    file_path = os.path.join(settings.MEDIA_ROOT, 'ArticleSeries', matching_article.slug, file_obj.name)

    with open(file_path, 'wb+') as f:
        for chunk in file_obj.chunks():
            f.write(chunk)

        return JsonResponse({
            'message': 'Image uploaded successfully',
            'location': os.path.join(settings.MEDIA_URL, 'ArticleSeries', matching_article.slug, file_obj.name)
        })

First, we check whether series and articles exist in our database. We are returning a JSON response that tells such a thing if it doesn't. Otherwise, we check whether the file suffix is in our supported image file list, and if it is, we create a path where this image should sit. And the last thing we do we save the image on the disc and construct a return message where the image was saved.

Now, let's go to our website and let's try to upload an image:

Everything worked perfectly! It successfully uploaded an image into our Article.

But still, sometimes, we upload images with the same name, and our backend overwrites that image. In this situation, we want to create a unique name for such images. Also, this would solve a problem when copying images from a clipboard. Because TinyMCE copied images from a clipboard saved to a default name, and if we copy a bunch of images, all of them will be overwritten. So let's slightly modify our code to create unique image names when saving them:

# django_project/main/views.py
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

import os
from uuid import uuid4

...

@csrf_exempt
@user_is_superuser
def upload_image(request, series: str=None, article: str=None):
    if request.method != "POST":
        return JsonResponse({'Error Message': "Wrong request"})

    # If it's not series and not article, handle it differently
    matching_article = Article.objects.filter(series__slug=series, article_slug=article).first()
    if not matching_article:
        return JsonResponse({'Error Message': f"Wrong series ({series}) or article ({article})"})

    file_obj = request.FILES['file']
    file_name_suffix = file_obj.name.split(".")[-1]
    if file_name_suffix not in ["jpg", "png", "gif", "jpeg"]:
        return JsonResponse({"Error Message": f"Wrong file suffix ({file_name_suffix}), supported are .jpg, .png, .gif, .jpeg"})

    file_path = os.path.join(settings.MEDIA_ROOT, 'ArticleSeries', matching_article.slug, file_obj.name)

    if os.path.exists(file_path):
        file_obj.name = str(uuid4()) + '.' + file_name_suffix
        file_path = os.path.join(settings.MEDIA_ROOT, 'ArticleSeries', matching_article.slug, file_obj.name)

    with open(file_path, 'wb+') as f:
        for chunk in file_obj.chunks():
            f.write(chunk)

        return JsonResponse({
            'message': 'Image uploaded successfully',
            'location': os.path.join(settings.MEDIA_URL, 'ArticleSeries', matching_article.slug, file_obj.name)
        })

Now, let's try to add the same image we added before:

Here, we can check the source of our second image, and here we can see some strange id, but that's exactly what we wanted, that python generated a unique ID that will never be generated a second time. We click, Save, and everything works fluently!

You can give it a go and try how it handles images copied from the clipboard.

Conclusion:

Now, we can handle images we copy or upload into our Articles, which requires creating only one function, one new URL, and slight modifications to our TinyMCE settings. That's amazing how easy it is to handle images in Django; that's spectacular!

Now, we are in a great place where we can write a fully functional blog with users, etc. But most websites let users subscribe to the newsletter of the page without asking them to register. So, I decided in the next tutorial to cover a topic, how to collect subscribers in our database!

See you in my next tutorial, final tutorial files you can download from my GitHub page.