Posts tagged with #django

Why Django Uses POST for Logout (And What's a CSRF Token?)

A GET request is triggered simply by visiting a URL. Your browser hits the link, the server responds. Simple.

A POST request, on the other hand, requires a form submission — the user has to actively submit data through a form.

This distinction matters more than it sounds.

The Problem with GET-based Logout

Imagine your logout view is wired to a GET request at /logout. Now imagine a malicious website drops this somewhere in their HTML:

<img src="https://yoursite.com/logout">

When a logged-in user visits that page, their browser dutifully tries to load the "image" — which is actually your logout URL. No click required. No warning. The user is silently logged out without knowing why.

This is called a Cross-Site Request Forgery (CSRF) attack — a malicious site tricks the browser into making a request on the user's behalf.

Why POST Stops This

POST requests can't be triggered by a simple <img> tag or a link. They require a proper form submission. But that alone isn't enough — a malicious site could still craft a hidden form and auto-submit it with JavaScript.

That's where the CSRF token comes in.

What Is a CSRF Token?

Django automatically generates a secret, one-time token and embeds it in every form using the {% csrf_token %} template tag:

<form method="POST">
    {% csrf_token %}  <!-- Django injects a hidden secret key here -->
    ...
</form>

This renders in the browser as something like:

<input type="hidden" name="csrfmiddlewaretoken" value="abc123xyz...">

When the form is submitted, Django checks:

  • Does this token match what I generated for this session?
  • ✅ Yes → legitimate request, process it
  • ❌ No → reject it, possible attack

A malicious site has no way to know your token. It's unique per session and never exposed cross-origin. So even if they craft a fake form pointing at your server, the token check will fail and Django will reject it.

The Rule of Thumb

| Action | Method | Why | |---|---|---| | Reading data | GET | Safe, no side effects | | Changing data (login, logout, submit) | POST + CSRF token | Protected from forgery |

Any view that changes state — logging in, logging out, saving a form — should use POST and include Django's CSRF protection. It's a small habit that closes a surprisingly common attack vector.

How Does `document.id` End Up in Your Django Template?

If you've written a Django URL like this:

path('documents/<int:pk>/', views.document_detail, name='document_detail'),

...and then used {{ document.id }} inside your HTML template, you might have wondered: how does the template even know what document is?

The answer is not magic. It's a clean three-step handoff — URL → View → Template. Let's walk through each step.


Step 1: The URL Captures the ID

When a user visits /documents/5/, Django looks through your urls.py to find a matching pattern.

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('documents/<int:pk>/', views.document_detail, name='document_detail'),
]

The <int:pk> part is a URL parameter. It tells Django:

  • Capture whatever integer is in that position of the URL
  • Give it the name pk
  • Pass it to the view function automatically

So when someone visits /documents/5/, Django extracts pk = 5 and hands it to your view.


Step 2: The View Receives the ID and Fetches the Object

Your view function gets called with the captured value as an argument:

# views.py
from django.shortcuts import render, get_object_or_404
from .models import Document

def document_detail(request, pk):
    document = get_object_or_404(Document, pk=pk)
    return render(request, 'documents/detail.html', {'document': document})

Here's what happens line by line:

  1. def document_detail(request, pk) — Django passes the captured pk=5 directly here as an argument.
  2. get_object_or_404(Document, pk=pk) — This queries the database for the Document with id=5. If it doesn't exist, it returns a 404 error.
  3. render(request, 'documents/detail.html', {'document': document}) — This is the key part. The context dictionary {'document': document} is what makes the object available inside the template. The string 'document' becomes the variable name you use in the template.

Think of the context dictionary as the bridge between Python and HTML. Whatever you put in it, the template can access.


Step 3: The Template Uses the Object

Now the template has access to the full Document object under the name document:

<!-- documents/detail.html -->
<h1>{{ document.title }}</h1>
<p>Document ID: {{ document.id }}</p>
<p>Status: {{ document.status }}</p>
<p>Created by: {{ document.author.username }}</p>

When Django renders this template, {{ document.id }} becomes 5 — because document is the Python object you fetched, and .id is just accessing its id attribute.


The Full Picture

Here's the entire flow in one view:

User visits: /documents/5/
       ↓
urls.py captures: pk = 5
       ↓
view receives: document_detail(request, pk=5)
       ↓
view queries DB: Document.objects.get(pk=5) → returns document object
       ↓
view sends context: {'document': <Document: id=5>}
       ↓
template renders: {{ document.id }} → "5"

A Common Mistake to Avoid

The variable name in your context dictionary must match what you use in the template.

# ✅ Correct — 'document' in context, {{ document.id }} in template
return render(request, 'detail.html', {'document': document})

# ❌ Wrong — 'doc' in context, but template uses {{ document.id }}
return render(request, 'detail.html', {'doc': document})
# This will render as empty string — no error, just blank output

Django won't throw an error if the variable name doesn't match. The template will just silently render nothing. This is a subtle bug that can be confusing at first.


Quick Recap

| Layer | What it does | |---|---| | urls.py | Captures the integer from the URL and names it pk | | views.py | Receives pk, queries the database, builds context dict | | template.html | Accesses the object using the key from the context dict |

The template itself has no idea what the URL looks like. It only knows what the view passed into the context. The view is the translator between the URL world and the template world. {% url 'document_detail' document.id %} Read it like this:

{% url              → "go to urls.py and find the pattern named..."
'document_detail'   → "...document_detail"
document.id %}      → "fill in <int:id> with this id from the database"

So Django does this behind the scenes:

urls.py has → path('documents/<int:id>/', ...)

document.id = 5

result → /documents/5/
  1. Looks up the URL pattern by name
  2. Takes the id from the database object
  3. Builds the final URL string

That's the complete pipeline. Once you see it this way — URL captures → view fetches → context bridges → template renders — it becomes a natural mental model for every Django view you write going forward.