Django File/Image Uploads Tutorial
Updated
Table of Contents
This tutorial shows how to implement file and then image uploading with Django. We'll build a basic Instagram clone.
Set Up
Whether you're on a Windows or Mac laptop the Desktop
is a convenient place to put our code. The location doesn't matter; it just needs to be easily available.
Open up the command line and navigate to the Desktop
. Then create a directory, insta
, for our project. We will create a new virtual environment and activate it.
# Windows
$ cd onedrive\desktop\
$ mkdir insta
$ cd insta
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv)
# macOS
$ cd ~/desktop/
$ mkdir insta
$ cd insta
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
Then install both Django and pillow, which is the Python image process library Django relies on for image files. For non-image file uploads, pillow
is not needed.
(.venv) $ python -m pip install django~=5.0.0
(.venv) $ python -m pip install pillow~=10.2.0
Now create our new Django project called django_project
and a new app called posts
.
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py startapp posts
Since we've added a new app we need to tell Django about it at the bottom of the INSTALLED_APPS
configuration in settings.py
.
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"posts", # new
]
Now run python manage.py migrate
to set up our project's new database.
(.venv) $ python manage.py migrate
Models
Starting with the database model is a good choice. Our model Post
will only have two fields: title
and cover
. We'll also include a __str__
method below so that the title
appears in our Django admin later.
# posts/models.py
from django.db import models
class Post(models.Model):
title = models.TextField()
cover = models.ImageField(upload_to='images/')
def __str__(self):
return self.title
The uploaded image
location will be in MEDIA_ROOT/images
. In Django, the MEDIA_ROOT
setting defines the location of all user-uploaded items. We'll set that now.
If we wanted to use a regular file here, the only difference could be to change
ImageField
toFileField.
MEDIA_ROOT
Open up django_project/settings.py
in your text editor. We will add two new configurations. By default, MEDIA_URL
and MEDIA_ROOT
are empty and not displayed, so we need to configure them:
MEDIA_ROOT
is the absolute filesystem path to the directory for user-uploaded filesMEDIA_URL
is the URL we can use in our templates for the files
# django_project/settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
We could pick a name other than media
here, but this is the Django convention. We'll also make an images
folder within it to use shortly.
(.venv) $ mkdir media
(.venv) $ mkdir media/images
Admin
Now update the posts/admin.py
file so we can see our Post
app in the Django admin.
# posts/admin.py
from django.contrib import admin
from .models import Post
admin.site.register(Post)
And we're all set! Generate a new migrations file.
(.venv) $ python manage.py makemigrations
Migrations for 'posts':
posts/migrations/0001_initial.py
- Create model Post
Then, run migrate
to update the database.
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, posts, session
s
Running migrations:
Applying posts.0001_initial... OK
Now, we can create a superuser
account to access the admin and execute runserver
to spin up the local web server for the first time.
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver
If you go to http://127.0.0.1:8000/admin you can log in to the Django admin site. It should redirect you to this page:
Click on the "+ Add" link next to Posts
. You can add whatever you like, but for this tutorial, I'm making a post on the Django Pony mascot. You can download it here yourself if you like.
Upon clicking "Save," you will be redirected to the Posts page, where we can see all our posts.
If you look within the local media
folder in your project, you'll see under images
there is now the djangopony.png
image file. See! I told you that was what MEDIA_URL
would do.
Ok, so at this point, we're done with the basics. But let's take it a step further and display our posts which means urls.py
, views.py
, and template files.
URLs
The confusing thing about Django is that you often need 4 different but interconnected files for one webpage: models.py
, urls.py
, views.py
, and a template html
file. I find it easiest to reason about this by going in order from models -> urls -> views -> template files. Our model is already done so that means diving into URL routes.
We'll need two urls.py
file updates. First at the project-level django_project/urls.py
files we need to add imports for settings
, include
, and static
. Then define a route for the posts
app. Note we also need to add the MEDIA_URL
if settings are in DEBUG
mode, otherwise we won't be able to view uploaded images locally.
# django_project/urls.py
from django.contrib import admin
from django.conf import settings # new
from django.urls import path, include # new
from django.conf.urls.static import static # new
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("posts.urls")), # new
]
if settings.DEBUG: # new
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Next, we'll need to sort out the URL routes within the posts
app. First create that new file in your text editor posts/urls.py
. Then we'll put all posts on the homepage, so again, use the empty string "
as our route path.
# posts/urls.py
from django.urls import path
from .views import HomePageView
urlpatterns = [
path("", HomePageView.as_view(), name="home"),
]
This references a view called HomePageView
which we'll create next.
Views
We can use the generic class-based ListView
here, import our Post
model, and then create a HomePageView
that uses the model and a template called home.html
.
# posts/views.py
from django.views.generic import ListView
from .models import Post
class HomePageView(ListView):
model = Post
template_name = "home.html"
Boom! Moving on, the last step is that template file called home.html.
Templates
We have two choices for our template's location. We could put it within the posts
app at posts/templates/posts/home.html
but I find that structure redundant. Plus it's harder to reason about templates when they are all buried within their respective apps. So typically I will instead create a project-level templates
directory.
(.venv) $ mkdir templates
We tell Django to also look here for any templates by updating the TEMPLATES
configuration within django_project/settings.py
.
# django_project/settings.py
TEMPLATES = [
{
...
"DIRS": [BASE_DIR / "templates"], # new
...
},
]
Then create a template file within this directory, templates/home.html
, that will display the title
and image
for all posts. Just like Instagram would :)
<!-- templates/home.html -->
<h1>Django Image Uploading</h1>
<ul>
{% for post in object_list %}
<h2>{{ post.title }}</h2>
<img src="{{ post.cover.url}}" alt="{{ post.title }}">
{% endfor %}
</ul>
Ok, that's it! Make sure the server is running with the python manage.py runserver
command and navigate to our homepage at http://127.0.0.1:8000. Refresh the page if needed.
And voila! If you add additional posts with a title and image via the admin they will appear on the homepage.
Form
Now we can add a form so regular users, who wouldn't have access to the admin, can also add posts. That means creating a new page with a form.
Let's start with the views.py
file. We'll name our new view CreatePostView
which will extend the built-in Django CreateView. We'll also import reverse_lazy to handle the redirect back to our homepage after the form has been submitted.
Within the view, we specify the model,
a form_class
, which we'll create next, the template_name,
and finally, a success_url
, which is what we want to happen after submission.
# posts/views.py
from django.views.generic import ListView, CreateView # new
from django.urls import reverse_lazy # new
from .forms import PostForm # new
from .models import Post
class HomePageView(ListView):
model = Post
template_name = "home.html"
class CreatePostView(CreateView): # new
model = Post
form_class = PostForm
template_name = "post.html"
success_url = reverse_lazy("home")
Next up that form. First create it in your text editor at posts/forms.py
. Then we can extend Django's built-in ModelForm. All we need for our basic form is to specify the correct model Post
and the fields we want displayed which are title
and cover
.
# posts/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "cover"]
We'll make a dedicated page for this form at the path of post/
.
# posts/urls.py
from django.urls import path
from .views import HomePageView, CreatePostView # new
urlpatterns = [
path("", HomePageView.as_view(), name="home"),
path("post/", CreatePostView.as_view(), name="add_post") # new
]
Then create the new template at templates/post.html
. And fill it with a headline and form. It's important to always add csrf_token
for protection. We're specifying form.as_p
which means Django will output each field as a paragraph tag.
<!-- templates/post.html -->
<h1>Create Post Page</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit New Post</button>
</form>
That's it! Make sure your server is running and go to the page at http://127.0.0.1:8000/post/.
After you submit a new post you'll be redirected back to the homepage and will see all the posts.
Next Steps
Hosting this site in production would require a few additional steps. Notably, it's likely that you would use WhiteNoise on the server for your static files, however WhiteNoise explicitly does not support media files. The common practice is to use django-storages for this purpose and connect to something like S3.
What else? You probably want to put restrictions around the image size which can be done initially in the models.py
file or with CSS. Perhaps you want to add edit and delete options as well for the Post. And you'll likely want a thumbnail version of the images too which can be done with sorl-thumbnail.