How to upload images in Django with Pillow (blog example)
A common feature of Django projects is the ability to upload images. I’m going to show you an example where we add feature images to a blogging application.
For this tutorial, I’m going to assume you already have a project to add images to. If not, you can follow this tutorial by Django Girls to create your own blog, or pull the code from this repository (GitHub).
We are going on work on the Post model and add the ability to upload images from a user form.
This tutorial will cover how to handle uploaded files for development only. When deploying your project, you will need to choose a different strategy to handle files uploaded in your Live environment (Django docs).
These are the steps we need to take:
Provide somewhere to store uploaded files
Update
urls.py
Add an ImageField to the Post model
Update our form
Update the template
Update the view to process uploaded files
The code for this tutorial can be found here (GitHub).
1. Provide somewhere to store uploaded files
Before we can update our model, we need somewhere to put our uploaded files. You may need to add import os
to the top of the file.
# settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
Update .gitignore
I recommend adding your media folder to your .gitignore
. This is because uploaded files won’t be needed on other environments with a separate database.
.vscode
venv
db.sqlite3
__pycache__
media
2. Update the URLs
We need to add a URL pattern to handle the upload of images. We only want to do it this way during development. In production, we won't want to store uploaded images that way. By stating if settings.DEBUG
, we can ensure the URL pattern only gets used when running the application locally.
# projectconfig/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("posts/", include("blog.urls")),
path("admin/", admin.site.urls),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
3. Add an ImageField to the Post model
3a. Install Pillow
Pillow is a Python library (docs) for processing images. Django does not allow you to use image fields in models without installing Pillow first. Trying to use this field without the package will get you this error:
(fields.E210) 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".
To fix this, install Pillow:
$ pip install pillow
3b. Add the ImageField to the Post model
This is the code for my model. The feature_image
field is added on the last line.
I have added blank=True, null=True
, to make the field optional.
# blog/models.py
class Post(models.Model):
STATUS_CHOICES = [
("draft", "Draft"),
("published", "Published"),
]
title = models.CharField(max_length=255)
slug = AutoSlugField(populate_from="title", unique=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
body = models.TextField()
created_date = models.DateTimeField(auto_now_add=True)
published_date = models.DateTimeField(blank=True, null=True)
updated_date = models.DateTimeField(blank=True, null=True)
tags = models.ManyToManyField(to=Tag, related_name="posts", blank=True)
category = models.ForeignKey(
to=Category,
related_name="posts",
default=get_category_id,
on_delete=models.SET_DEFAULT,
blank=False,
null=False,
)
feature_image = models.ImageField(upload_to="uploads/", null=True, blank=True)
4. Update the form
To make the image upload display on the form, add the name of the field to the list of fields
in the Meta
class.
# blog/forms.py
class PostForm(forms.ModelForm):
class Meta:
model = models.Post
fields = ["title", "body", "category", "tags", "feature_image"]
tags = forms.ModelMultipleChoiceField(
queryset=models.Tag.objects.all(), widget=forms.CheckboxSelectMultiple
)
category = forms.ModelChoiceField(
queryset=models.Category.objects.all(), widget=forms.RadioSelect
)
5. Update the template
5a. Update the add post form
We don’t need to make any changes to how the form is displayed but we do need to make changes about how the form is encoded (StackOverflow: What does enctype='multipart/form-data' mean?).
Adding enctype="multipart/form-data"
is necessary to make your form upload files to the server. If you leave it out, images will be ignored when the form is submitted.
# templates/post_form.html
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" class="btn btn-primary" value="Save">
</form>
5b. Update the post detail template to display images
Here, I have updated the post detail template to display the images. The image field provides an attribute called url
which provides the path to the image. This is the path to the file on the server, not a HTTP URL.
I am using Bootstrap, so adding a class of img-fluid
will automatically size the image to fit the parent container.
# templates/post_detail.html
{% if post.feature_image %}
<img class="img-fluid" src="{{post.feature_image.url}}" alt="{{post.title}}">
{% endif %}
5c. Update the post list template to show images
The final step is to update the post list.
# templates/index.html
{% if post.feature_image %}
<img src="{{post.feature_image.url}}" class="card-img-top" alt={{post.title}}>
{% else %}
<img src="https://tinyurl.com/22jvzvfj" class="card-img-top" alt={{post.title}}>
{% endif %}
6. Update the view to process uploaded files
The final step is to update the view.
The uploaded file is stored in request.FILES
. The only change we need to make to the add_post
and edit_post
views is to instantiate the form with request.FILES
as well as request.POST
.
6a. Update the add_post view
# blog/views.py
def add_post(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = PostForm(data=request.POST, files=request.FILES)
if form.is_valid():
# form.save() creates a post from the form
post = form.save()
return redirect("post_detail", slug=post.slug)
else:
form = PostForm()
context = {"form": form, "edit_mode": False}
return render(request, "post_form.html", context)
6b. Update the edit_post view
# blog/views.py
def edit_post(request: HttpRequest, slug: str) -> HttpResponse:
post = get_object_or_404(Post, slug=slug)
form = PostForm(instance=post)
if request.method == "POST":
form = PostForm(data=request.POST, files=request.FILES, instance=post)
if form.is_valid():
post = form.save()
return redirect("post_detail", slug=post.slug)
context = {"form": form, "post": post, "edit_mode": True}
return render(request, "pos