Handling File Uploads with Django

Introduction

The World Wide Web facilitated the transfer of huge amounts of data between networked computers, and it's a community that creates and shares data in abundance. This data can take various forms and shapes, and some common human-interpretable format are images, videos, and audio files.

Users are so used to file sharing within a wide variety of software, that its novelty is far-gone, and its functionality is oftentimes considered standard.

In this guide, we'll take a look at how to upload a file with Python, to a Django-based web application.

Files that are uploaded can be additionally processed in various forms, or could be left in their raw state. Uploading files also raises a question of storage (where the files end up) as well as display (how they can be retrieved and displayed). Throughout the guide, we'll be taking these questions into consideration, building a small project that offers a user the ability to upload files to a Django web application.

Project Setup

We will be building a small project where we can implement file upload, storage, and display functionalities of Django, with a database, however, storing the images on a hard drive.

Let's assume we live in an imaginary universe where we live alongside the magical creatures of the Harry Potter books, and the magi-zoologists of our world need an application to keep track of information regarding each magical creature they study. We will create a form through which they can log descriptions and images for each beast, then we will render that form, store the information and display it to the user when needed.

If you're unfamiliar with Django and its modules, such as django-admin - you can read out general guide to creating REST APIs that cover the basic elements of Django. Throughout this guide, we'll assume basic knowledge of Django and quickly go through the setup process, though, if you'd like to gain deeper understanding of the project creation process, read our Guide to Creating a REST API in Python with Django!

We start by creating a virtual environment to avoid having our dependencies cause version mismatch issues with other projects. This step is optional, but highly recommended, and considered good practice for keeping Python environments clean. Let's create a directory that will act as a container for the environment.

Open your command prompt/shell and inside the directory we've just created, run:

$ mkdir fileupload
$ cd fileupload
$ python -m venv ./myenv
# OR
$ python3 -m venv ./myenv

Now that our virtual environment has been created, all that's is left to do is to activate it, by running the activate script:

# Windows
$ myenv/Scripts/activate.bat
# Linux
$ source myenv/Scripts/activate
# MacOS
$ source env/bin/activate

Once the environment is activated, if we install dependencies, they'll only be applicable to that environment, and won't collide with other environments, or even the system environment. Here, we can install Django via pip:

$ pip install "Django==3.0.*"

Now, let's create a project, named fantasticbeasts via the startproject command of the django-admin module. Once a project skeleton has been created, we can move into that directory and start the app via startapp:

$ django-admin startproject fantasticbeasts
$ cd fantasticbeasts
$ django-admin startapp beasts

And finally, let's register this app in the fantasticbeasts/settings.py file, by adding it to the list of INSTALLED_APPS:

INSTALLED_APPS = [
    'beasts',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Awesome! Now we are all set. We can define a simple model for a Beast, create a form and template to display it to an end user, as well as handle the files they send through with the form.

Uploading Files with Django

Creating the Model

Let's start off by defining a model of a Beast, which directly matches to a database table. A form can then be created to represent a blank slate of this model, allowing the user to fill in the details. In the beasts/models.py file, we can define a model that extends the models.Model class, which then inherits the functionality to be saved in the database:

from django.db import models

class Beast(models.Model):
    MOM_CLASSIFICATIONS = [
    ('XXXXX', 'Known Wizard  Killer'),
    ('XXXX', 'Dangerous'),
    ('XXX', 'Competent wizard should cope'),
    ('XX', 'Harmless'),
    ('X', 'Boring'),
 ]
    name = models.CharField(max_length=60)
    mom_classification = models.CharField(max_length=5, choices=MOM_CLASSIFICATIONS)
    description = models.TextField()
    media = models.FileField(null=True, blank=True)

Each beast has a name, description, accompanying media (sightings of the beast) as well as a mom_classification (M.O.M stands for Ministry of Magic).

media is an instance of a FileField which was initialized with the null argument set to True. This initialization lets the database know that it is okay for the media field to be null if the user entering the data simply doesn't have any media to attach. Since we'll be mapping this model to a form - and Django takes care of the validation for us, we need to let Django know that the form's input for the media can be blank, so it doesn't raise any exceptions during validation. null refers to the database, while blank refers to the user-end validation, and generally, you'll want these two to be set to the same value for consistency.

Note: If you'd like to enforce the addition of media by the user, set these arguments to False.

A FileField by default will only handle one file, and allow the user to upload a single item from their file system. In a later section, we'll take a look at how to upload multiple files as well.

Creating the Model Form

Once our model is defined, we'll bind it to a form. We don't need to do this manually on the front-end, as Django can bootstrap this functionality for us:

from django.forms import ModelForm
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
        fields = '__all__'

We just created a BeastForm and bound the Beast model to it. We also set the fields to __all__ so all of our model's fields would be displayed when we use it on an HTML page. You can individually tweak the fields here if you'd like some to stay hidden, though, for our simple model - we'll want to display them all.

Registering Models with Admin

Django automatically creates an admin-site for developers to use throughout the development process. Here, we can test out our models and fields without having to spin up pages ourselves. For users, though, you'll want to create them, and disable the admin website before going live.

Let's register our model to the admin website by adding it to the beasts/admin.py file:

from django.contrib import admin
from .models import Beast

admin.site.register(Beast)

Registering URL Paths

With the application structure ready, a model defined and registered, as well as bound to a form - let's configure the URL paths that will allow a user to use this application. To do this, let's create a urls.py file inside our app. Then we can go on and "include" its content in the project-level urls.py file.

Our beasts/urls.py will look something like this:

from django.urls import path
from .import views

urlpatterns = [
    path("", views.addbeast,  name='addbeast')
 ]

And the project-level [urls.py] will have this added:

urlpatterns = [
    path("", include("reviews.urls"))
]

We are adding an empty string for our URL simply because this is a pocket-size project, and there is no need to complicate it. We haven't already created a view, but registered its name here before its creation. Let's create the HTML template and the views.addbeast view next.

Creating a Template to Display Our Form

To hold our templates, let's create a templates folder under our beasts directory. The name is non-negotiable because Django will look for HTML templates only under the folders named templates.

Inside our new folder, let's add an entry.html file that has a <form> that accepts the fields pertaining to a Beast:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    <form action="/" method="POST" enctype="multipart/form-data">
        {% csrf_token %}
        {% for entry in form %}
           <div>
                {{ entry.label_tag }}
           </div>
           <div>
               {{entry}}
           </div>
        {% endfor %}
        <button>
            Save!
        </button>
    </form>
</body>
</html>

The action="/" attribute points to the request handler we'll be hitting when the user selects the "Save!" button. The input of the form dictates how the data is encoded, so we've set the enctype to a multipart/form-data type, to allow for file uploads. Whenever you add an input of type "file" to a Django form, you'll have to set the enctype to multipart/form-data.

The {% csrf_token %} is another must-have for any form with action = "POST". It is a unique token Django kindly creates for each client to ensure security when accepting requests. A CSRF token is unique to every POST request from this form, and they make CSRF attacks impossible.

CSRF attacks consist of malicious users forging a request in another user's stead, typically through another domain, and if a valid token is missing, the request to the server is rejected.

The form variable we are iterating in the for each loop ({% for entry in form %}) will be passed to this HTML template by the view. This variable is an instance of our BeastForm, and it comes with some cool tricks. We use entry.label_tag, which returns us the label for that Model Form field (the label will be the field's name unless otherwise specified), and we wrap the form field in a div to make our form look decent.

Creating a View to Render Our Template

Now, let's create a view to render this template and connect it to our back-end. We'll start out by importing the render and HttpResponseRedirect classes - both of which are built-in Django classes, alongside our BeastForm object.

Instead of a class-based view, we can opt to make a function-based view which serves well for simple prototypes and demos like this.

If the incoming request is a POST request, a new BeastForm instance is created with the body of the POST request (the fields) and the files sent through the request. Django automatically deserializes the body data into an object, and injects the request.FILES as our file field:

from django.shortcuts import render
from .forms import BeastForm
from django.http import HttpResponseRedirect

def entry(request):
    if request.method == 'POST':
        form = BeastForm(request.POST, request.FILES)
        
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/") 
    else:
        form = BeastForm()

    return render(request, "entry.html", {
        "form": form
    })

To validate the input, as it may be invalid, we can use the is_valid() method of the BeastForm instance, clearing the form if it's invalid. Otherwise, if the form is valid - we save it to the database via the save() method, and redirect the user to the homepage (which is also our entry.html page), prompting the user to enter another beast's information.

Where are the files?

Note: Through this approach, the files are saved to the database and no file-handling is required. Although it works, this is not an advised strategy, and we'll rectify it with a proper file-handling system in the next section.

For now, let's make migrations and migrate to commit changes to the model schema (since we haven't done that before). Once we run the project, we can see how all this looks on the development server. From the terminal, while the virtual environment is still active, run:

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Now, once we hit http://127.0.0.1:8000/ using a browser, you should see something like this:

You can go ahead and fill in the form with some random input and add on a file; any file type will do since we named the field "media" but assigned it a FileField that is generic.

Note: You can enforce certain file types, such as images through Django, which we'll take a look at once we cover a more valid file-storing system, and handling multiple files instead of just one.

After submitting the form, you can see your data on the database through the admin page!

Storing Files on an HDD Instead of a Database

At the moment, our code is capable of storing the files in the database. However, this is not a desirable practice. With time our database will get "fat" and slow, and we do not want that to happen. Images haven't been stored in databases as blobs for a while now, and you'll typically save images on your own server where the application is hosted on, or on an external server or service such as AWS's S3.

If you'd like to read more about uploading files to services such as AWS S3 - read our Guide to Uploading Files to AWS S3 in Python with Django!

Let's see how we can store the uploaded files on disk, in a nice little folder under our project. To house them, let's add an uploads folder under beasts and modify the BeastForm's media field to aim at a folder instead of a database:

media = models.FileField(upload_to="media", null=True, blank=True)

We've set the FileField's target folder to "media", which doesn't yet exist. Since presumably other files could be uploaded, the uploads folder will have a subdirectory named "media" for users to upload images of beasts to.

To let Django know where this "media" directory is, we add it to the settings.py file:

MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads/')

os.path.join(BASE_DIR, 'uploads/') appends "/uploads" to the BASE_DIR -the built-in variable that holds the absolute path to our project folder. MEDIA_ROOT tells Django where our files will reside.

Let's save all the changes we've made and once we apply our migrations, Django will create a folder named "media",as in [upload_to="media"], under uploads.

All the submitted files will get saved in that folder thereafter. Database bloat is fixed!

Uploading Multiple Files with Django

There is not much additional work required to handle the upload of multiple files. All we need to do is let our model's form know that it is okay for the media field to take in more than one input.

We do this by adding a widgets field in our BeastForm:

from django.forms import ModelForm, ClearableFileInput
from .models import Beast

class BeastForm(ModelForm):
    class Meta: 
        model = Beast
        fields = '__all__'
        widgets = {
            'media': ClearableFileInput(attrs={'multiple': True})
        }

Now, when on the entry.html page, a user is allowed to select multiple files and the request.FILES property will contain more files rather than one.

Enforcing Image Files with Django using ImageField

Django defines an additional field type - an ImageField, that can limit the user input to image files. We have collected different types of files for our beasts' documentation, but more often than not, in our applications, we will ask the user for one specific file input.

Let's swap our FileField with an ImageField:

media = models.ImageField(upload_to="media", null=True, blank=True,)

The ImageField is on Pillow images, which is a widely-used Python library for handling and manipulating images, so if you don't have it already installed, you'll be prompted with an exception:

Cannot use ImageField because Pillow is not installed.
        HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".

Let's go ahead and adhere to the terminal's advice. Quit the server for a moment to run:

$ python -m pip install Pillow

Now, if we go ahead and make and apply our migrations and run our development server, we'll see that when we try to upload a file, our options are limited to images.

Displaying Uploaded Images

We are very close to the finish line. Let's see how we can retrieve and display our stored images and call it a day.

Go ahead and open your beasts/views.py file. We will change our if clause so that when a form is submitted successfully, the view does not reload the page but, instead it redirects us to another one, which will contain a list of all beasts and their information, alongside their associated image:

 if form.is_valid():
      form.save()
      return HttpResponseRedirect("/success") 

Now let's go ahead and create a view to render the success page. Inside our beasts/views.py file, insert:

def success(request):
    beasts = Beast.objects.order_by('name')
    return render(request, "success.html", {
        "beasts": beasts
    })

On our "success" page, we will list the names and images of the beasts in our database. To do that, we simply collect the Beast objects, order them by their name and render them in the success.html template:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fantastic Beasts</title>
</head>
<body>
    {% for beast in beasts %}
       <div>
            {{ beast.name }}
       </div>
       {% if beast.media %}
        <div>
            <img src="{{ beast.media.url }}" width="500" height=auto alt="">
        </div>
       {% endif %}
    {% endfor %}   
</body>
</html>

We have already mentioned that the database's job is not to store files, it's job is storing the paths to those files. Any instance of FileField or ImageField will have a URL attribute pointing to the file's location in the file system. In an <img> tag, we feed this attribute to the src attribute to display the images for our beasts.

By default, Django's security kicks in to stop us from serving any files from the project to the outside, which is a welcome security-check. Though, we want to expose the files in the "media" file, so we'll have to define a media URL and add it to the urls.py file:

In the settings.py file, let's add the MEDIA_URL:

MEDIA_URL = "/beast-media/"

Here, the /name-between/ can be anything you want, though it has to be wrapped around in quotes and slashes. Now, modify the project-level urls.py file to include a static folder that serves static files:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("ency.urls"))
]  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

The static() function maps the MEDIA_URL, to the actual path to where our files reside, MEDIA_ROOT. Requests that try to reach any of our files can get access via this MEDIA_URL, which is automatically prefixed to the [url] attribute of FileField and ImageField instances.

If we save our changes and go to our development server, we will now see everything works smoothly.

Note: This method can only be used in the development and only if the MEDIA_URL is local.

If you're not saving the files to your HDD, but a different service, read our Guide to Serving Static Files in Python With Django, AWS S3 and WhiteNoise!

Conclusion

In this guide, we've covered how to upload files, store files and finally, serve files with Django.

We have created a small application that is, for other than educational purposes, not very useful. However, it should be a sturdy stepping stone for you to start experimenting with file uploads.

Last Updated: October 21st, 2021
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms