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
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.