Media Files & Uploads
In the last lesson, static files were defined as assets that ship with your codebase — CSS,
JavaScript, the images you designed yourself. Media files are the opposite
category: content that arrives after your app is already running, uploaded by an
actual user through a form. A TaskFlow user attaching a cover image to a project is a media
file. The taskflow/style.css that renders that project's page is a static file.
Same broad idea — "a file the browser can request" — but they're configured differently,
stored differently, and (as you'll see two lessons from now) deployed very differently in
production.
MEDIA_URL and MEDIA_ROOT
Media files are controlled by a pair of settings that mirror the static files settings from
the previous lesson, but they mean something subtly different. MEDIA_URL is the
URL prefix uploaded files are served from. MEDIA_ROOT is the actual folder on disk
where Django saves uploaded files. Unlike STATIC_ROOT, which only gets populated by
running collectstatic, MEDIA_ROOT is written to directly, every time a
user uploads something.
# taskflow_project/settings.py
import os
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
With those two settings defined, Django knows where to physically save an uploaded file
(MEDIA_ROOT) and what URL to hand back so a browser can later request it
(MEDIA_URL).
Adding a File Upload Field to a Model
Let's give TaskFlow's Project model the ability to hold a cover image. Django
provides ImageField for exactly this — a specialized field that validates the
uploaded file is actually a readable image, in addition to storing it.
# taskflow/models.py
from django.db import models
from django.contrib.auth.models import User
class Project(models.Model):
name = models.CharField(max_length=120)
description = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
cover_image = models.ImageField(upload_to='project_covers/', blank=True, null=True)
def __str__(self):
return self.name
The upload_to='project_covers/' argument tells Django where, relative to
MEDIA_ROOT, each uploaded file should be saved. So a cover image uploaded for
a project ends up on disk at media/project_covers/some_image.jpg, and is reachable
in the browser at /media/project_covers/some_image.jpg.
ImageField requires the third-party Pillow package to be installed (pip install Pillow). Django uses it behind the scenes to verify the uploaded file is a genuine, readable image. If you only need to accept arbitrary files without image validation, use the plainer FileField instead.Handling a File Upload in a Form
A regular HTML form does not send file contents by default — it only sends text-encoded field
values. To actually transmit a file's bytes, the form must explicitly opt in with
enctype="multipart/form-data".
{% load static %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save Project</button>
</form>
enctype="multipart/form-data". Without it, the form still submits and looks like it "worked" — no error is thrown — but the uploaded file simply never arrives on the server. request.FILES will be empty every time. If an upload form seems to silently ignore the file field, this attribute is the first thing to check.Accessing the Uploaded File via request.FILES
Once a form is submitted with the correct enctype, Django separates incoming data
into two dictionaries on the request: request.POST for regular text fields, and
request.FILES for anything uploaded. A view handling the project form needs to
read from both.
# taskflow/views.py
from django.shortcuts import render, redirect
from .forms import ProjectForm
def create_project(request):
if request.method == 'POST':
form = ProjectForm(request.POST, request.FILES)
if form.is_valid():
project = form.save(commit=False)
project.owner = request.user
project.save()
return redirect('project-detail', pk=project.pk)
else:
form = ProjectForm()
return render(request, 'taskflow/project_form.html', {'form': form})
Notice the form is constructed with two arguments now —
ProjectForm(request.POST, request.FILES) — instead of just one. Leaving off
request.FILES is the server-side equivalent of the missing enctype
mistake above: the form will report itself invalid, or simply save without the image, because
Django never received the uploaded file's data.
Serving Uploaded Media Files in Development
Static files get served automatically by Django's development server whenever
DEBUG = True, as covered in the previous lesson. Media files do not
get that same automatic treatment — you have to explicitly add a URL pattern for them, even in
development. Django's static() helper makes this a one-line addition to your
project's urls.py.
# taskflow_project/urls.py
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# ... your app's url patterns ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
That if settings.DEBUG: guard is deliberate and important — this snippet is a
development convenience only. It should never end up serving files this way in production,
which is exactly what we'll dig into in the next lesson.
Full Worked Example: Upload, Save, Display
Putting every piece together — the model, the form, the view, and the URL wiring — here's the complete round trip for a TaskFlow project cover image.
# taskflow/forms.py
from django import forms
from .models import Project
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ['name', 'description', 'cover_image']
The template that renders the saved project reads the uploaded file back through the field's
.url attribute — never a hardcoded path, since Django builds that URL from
MEDIA_URL for you.
{# taskflow/templates/taskflow/project_detail.html #}
<h2>{{ project.name }}</h2>
{% if project.cover_image %}
<img src="{{ project.cover_image.url }}" alt="{{ project.name }} cover">
{% else %}
<p>No cover image uploaded yet.</p>
{% endif %}
<p>{{ project.description }}</p>
Checking {% if project.cover_image %} before rendering the <img>
matters because cover_image is optional (blank=True, null=True on the
model) — a project created before this field existed, or one where the user simply skipped the
upload, won't have a file to point at.