Round out a real project — serve CSS and JS, paginate long lists, flash success messages, write custom middleware, and use management commands and the shell.
Why: CSS, JS, and images are "static" — served as-is. In development Django serves them automatically; for production collectstatic gathers them into one folder. When WhiteNoise: lets your app serve them efficiently without a separate web server.
# config/settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"] # project-wide assets
STATIC_ROOT = BASE_DIR / "staticfiles" # collectstatic output# gather every app's static files for production
python manage.py collectstaticWhy: never render thousands of rows at once. When ListView: just set paginate_by and loop page_obj in the template. When a function view: use the Paginator class directly. Note: the page number comes from ?page= in the query string.
from django.core.paginator import Paginator
def post_list(request):
paginator = Paginator(Post.objects.all(), 10) # 10 per page
page_obj = paginator.get_page(request.GET.get("page"))
return render(request, "blog/post_list.html", {"page_obj": page_obj}){% for post in page_obj %}
<h2>{{ post.title }}</h2>
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next →</a>
{% endif %}Why: the messages framework shows one-off notifications after an action ("Post saved") that survive the redirect. Note: add the message in the view, then loop messages once in your base template so every page can display them.
from django.contrib import messages
def post_create(request):
# ... save the post ...
messages.success(request, "Your post was published!")
return redirect("blog:post_list"){% for message in messages %}
<div class="alert {{ message.tags }}">{{ message }}</div>
{% endfor %}Why: middleware wraps every request and response — a perfect place for cross-cutting work like timing, logging, or adding a header. Where: write a small callable, then list it in MIDDLEWARE. Note: code before get_response runs on the way in, after it on the way out.
# blog/middleware.py
import time
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request) # call the next layer
response["X-Response-Time"] = f"{time.monotonic() - start:.3f}s"
return response
# config/settings.py
# MIDDLEWARE += ["blog.middleware.TimingMiddleware"]Why: manage.py is your control panel. When the shell: poke at the ORM live. When a custom command: scriptable jobs you run by hand or on a schedule (cron) — put one in blog/management/commands/.
Common built-in commands:
python manage.py shellpython manage.py dbshellpython manage.py showmigrationspython manage.py changepassword ada