RizTech Academy logo
RizTech Academy
Views, Templates and FormsLesson 6 of 625 min

Static files and user uploads

Two kinds of files sit outside your database: static files — the CSS, JavaScript and images that make up your site, shipped with the code — and media files — things users upload, like a patient's scanned report. Django treats them differently on purpose, and mixing them up is a common source of "it works in development but not in production" confusion. This lesson covers both, and the crucial development/production split.

Static versus media: the distinction that matters

  • Static files are part of your application: stylesheets, scripts, logos, icons. They are the same for every user, committed to your repository, and change only when you deploy new code.
  • Media files are user-uploaded content: a report PDF, a patient's photo. They are different per user/record, created at runtime, and must never be committed to your repository.

The rule of thumb: static = you ship it; media = users create it. They are configured separately, served separately, and stored separately, because they have opposite lifecycles.

Static files in development

django.contrib.staticfiles is installed by default, and STATIC_URL is set (verified: it defaults to /static/). You put static files in a static/ directory inside an app (namespaced by app to avoid clashes):

patients/
  static/
    patients/
      style.css

And reference them in templates with the {% static %} tag, never a hard-coded path:

{% load static %}
<link rel="stylesheet" href="{% static 'patients/style.css' %}">

{% load static %} (once, near the top of the template) enables the tag; {% static 'patients/style.css' %} produces the correct URL. In development, with DEBUG=True, Django's runserver finds and serves these files automatically — you do nothing else. Using {% static %} rather than writing /static/patients/style.css by hand matters because the real URL changes in production (below), and the tag produces the right one in both environments.

Static files in production: collectstatic

Here is the split that catches people. runserver serving static files is a development-only convenience. In production (DEBUG=False), Django does not serve static files — a real web server or a tool does, and for good reason (serving static files through Python is slow and wasteful). The production step is collectstatic:

python manage.py collectstatic

This gathers every static file from every app into one directory — STATIC_ROOT — from which a web server (or a package like WhiteNoise) serves them efficiently. So production static files need two settings:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"      # where collectstatic gathers files (production)

The deployment module covers wiring this to a server. The thing to internalise now: static files "just work" in development and require collectstatic plus a server in production — the number one "my CSS disappeared when I deployed" cause is skipping this step or expecting runserver's behaviour in production.

Media files: user uploads

To accept uploads, add a FileField or ImageField to a model and configure where uploads go:

# settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"             # where uploaded files are stored on disk
# a report with an uploaded scan
class TestReport(models.Model):
    patient = models.ForeignKey(Patient, on_delete=models.CASCADE)
    scan = models.FileField(upload_to="reports/%Y/%m/")   # stored under MEDIA_ROOT/reports/YYYY/MM/

upload_to gives a subpath (the %Y/%m/ produces year/month folders, which keeps directories from growing without bound). The form must set enctype="multipart/form-data" and pass request.FILES:

<form method="post" enctype="multipart/form-data">
form = TestReportForm(request.POST, request.FILES)   # note request.FILES for uploads

Forgetting enctype="multipart/form-data" or request.FILES is why an upload silently arrives empty — the file simply is not sent/received without both. ImageField is FileField plus image validation (and needs the Pillow library).

Handling uploads safely — this is untrusted input

Uploaded files are untrusted, and a clinic handling patient documents must treat them as such:

  • Validate type and size. Do not accept arbitrary files — restrict to expected types (PDF, image) and a maximum size, in the form's validation. An unbounded upload is a denial-of-service and a storage problem.
  • Never trust the filename. A user-supplied filename can contain path traversal (../) or be crafted to overwrite something. Django's storage sanitises names, but validate and, ideally, generate your own names.
  • Never serve user uploads as executable, and keep MEDIA_ROOT outside your code. A media directory that can execute uploaded files is a serious hole.
  • For sensitive documents (medical reports), do not put them at a guessable public URL — gate access behind a view that checks the user is allowed to see that file. Public MEDIA_URL serving is fine for a logo, wrong for a patient's report. The auth module's permissions apply here.

Media files are the point where your application accepts data from the outside world into your filesystem — the same "validate untrusted input at the boundary" discipline as forms and APIs applies, with the extra weight that these are files.

Check your work

Static versus media. Static = files you ship (CSS/JS/images), committed, same for everyone; media = files users upload, created at runtime, never committed. "You ship it / users create it."

Static in development. {% load static %} + {% static 'app/file.css' %} (never a hard-coded path); runserver serves them automatically with DEBUG=True. Verified: STATIC_URL defaults to /static/.

Static in production. runserver does not serve static files with DEBUG=False; run collectstatic to gather into STATIC_ROOT, served by a web server/WhiteNoise. The usual "CSS disappeared on deploy" cause.

Media configuration. MEDIA_URL/MEDIA_ROOT; a FileField/ImageField with upload_to; the form needs enctype="multipart/form-data" and the view needs request.FILES.

Uploads are untrusted. Validate type and size, do not trust filenames, keep MEDIA_ROOT non-executable and outside code, and gate sensitive files behind a permission-checking view — not a guessable public URL.

Practice

  1. Add a static/patients/style.css, reference it with {% load static %} and {% static %}, and confirm it loads in development.
  2. Set DEBUG=False and confirm the static file stops being served by runserver; run collectstatic, inspect STATIC_ROOT, and reason about what serves it in production.
  3. Hard-code /static/patients/style.css instead of using {% static %}; reason about why that breaks when the static URL changes.
  4. Add a FileField with upload_to="reports/%Y/%m/" to a model; build a form and view, upload a file, and confirm it lands under MEDIA_ROOT.
  5. Omit enctype="multipart/form-data" (or request.FILES) and watch the upload arrive empty; add it back.
  6. Sketch how you would protect a patient's uploaded report so only permitted users can download it — and why a public MEDIA_URL is wrong for it.

Official documentation

Next: the user model, and customising it early.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship