Part 3, Views, URLs, and templates
Two flavors of view
Django accepts any callable (request, *args, **kwargs) -> HttpResponse.
Function-based views (FBVs), the simpler form:
from django.http import HttpResponseNotFoundfrom django.shortcuts import get_object_or_404, renderfrom .models import Post
def post_list(request): posts = Post.objects.filter(published_at__isnull=False) return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, slug): post = get_object_or_404(Post, slug=slug) return render(request, "blog/post_detail.html", {"post": post})Class-based views (CBVs), composable via mixins, concise for CRUD:
from django.views.generic import DetailView, ListViewfrom .models import Post
class PostListView(ListView): model = Post template_name = "blog/post_list.html" context_object_name = "posts" paginate_by = 10
def get_queryset(self): return Post.objects.filter(published_at__isnull=False)
class PostDetailView(DetailView): model = Post template_name = "blog/post_detail.html" slug_field = "slug" slug_url_kwarg = "slug"When to pick which: FBVs are clearer for one-offs; CBVs shine when you have standard CRUD (ListView, CreateView, UpdateView, DeleteView). Many teams settle on FBVs + helpers because the CBV inheritance tree gets confusing fast.
URL routing
mysite/urls.py (project root):
from django.contrib import adminfrom django.urls import include, path
urlpatterns = [ path("admin/", admin.site.urls), path("blog/", include("blog.urls")),]blog/urls.py (app):
from django.urls import path, re_pathfrom . import views
app_name = "blog"
urlpatterns = [ path("", views.PostListView.as_view(), name="list"), path("<slug:slug>/", views.PostDetailView.as_view(), name="detail"), path("archive/<int:year>/", views.archive_by_year, name="archive"),
# re_path when you need a regex re_path(r"^legacy/(?P<id>\d+)/$", views.legacy_redirect, name="legacy"),]Path converters, built in: str (default), int, slug, uuid, path. You can write custom ones for business types like ISBN.
Reversing URLs
from django.urls import reverse
url = reverse("blog:detail", kwargs={"slug": "hello-world"})In templates:
<a href="{% url 'blog:detail' slug=post.slug %}">{{ post.title }}</a>Why namespaces (app_name) matter, two apps can both name a URL "detail". Without namespacing, reverse("detail") is ambiguous.
The template language
Django’s template language is intentionally weak, no arbitrary Python, to keep logic in views.
{# blog/templates/blog/post_list.html #}{% extends "base.html" %}
{% block title %}Latest posts{% endblock %}
{% block content %} <h1>Latest posts</h1> {% if posts %} <ul> {% for post in posts %} <li> <a href="{% url 'blog:detail' slug=post.slug %}">{{ post.title }}</a> <small>{{ post.published_at|date:"Y-m-d" }}</small> {% if post.tags.all %} ({{ post.tags.all|join:", " }}) {% endif %} </li> {% empty %} <li>No posts yet.</li> {% endfor %} </ul> {% endif %}{% endblock %}The two syntaxes
{{ variable }}, output an expression, with optional filters:{{ value|default:"n/a"|upper }}.{% tag %}, control flow and logic:{% if %},{% for %},{% url %},{% block %},{% extends %},{% include %}.
Template inheritance
Base template (templates/base.html):
<!DOCTYPE html><html><head> <title>{% block title %}Default{% endblock %}</title> {% load static %} <link rel="stylesheet" href="{% static 'css/main.css' %}"></head><body> <header>{% include "partials/header.html" %}</header> <main>{% block content %}{% endblock %}</main></body></html>Any child template can {% extends "base.html" %} and override blocks. {% include %} is reused components.
Static files
STATIC_URL = "static/"STATICFILES_DIRS = [BASE_DIR / "static"] # where you put files in devSTATIC_ROOT = BASE_DIR / "staticfiles" # where collectstatic dumps them for prodIn development, runserver serves static files automatically. In production, you run python manage.py collectstatic and a proper server (or WhiteNoise) serves the STATIC_ROOT directory. Cover in Part 10.
Context processors
A context processor is a function that adds variables to every template’s context.
def site_name(request): return {"SITE_NAME": "My Blog"}Register in settings.TEMPLATES[0]["OPTIONS"]["context_processors"]. Useful for site-wide variables (current user, feature flags, menu items) but don’t overuse, every template renders the same processor, so keep them cheap.
Gotchas
render()vsHttpResponse,render()auto-wires the request context (needed for CSRF, auth). Using bareHttpResponseon HTML breaks forms.- Empty templates,
{% block content %}{% endblock %}in base without default content is fine;{% block content %}{% endblock content %}(named) helps debugging. - Template
DEBUG, set"debug": TrueinTEMPLATES[0]["OPTIONS"]to get usable error pages during development. - Silent failures, by default, if
{{ post.author.name }}fails somewhere in the chain, Django returns empty string. Setstring_if_invalidinTEMPLATESto a sentinel in dev to catch these. - Class-based view learning curve, the CCBV (Classy Class-Based Views) site shows the full inheritance chain for every generic view; bookmark it before writing CBVs.
What’s next
Part 4 adds forms, so users can submit data, not just read it.