User authentication: login, logout, and signup
request.user has been referenced since part 13 as if logging in already worked. It hasn't — django.contrib.auth has been in INSTALLED_APPS since part 1 doing the User model and session work, but nothing so far has actually let someone log in through a page.
Login and logout, almost for free
# blogsite/urls.py
from django.contrib.auth import views as auth_views
urlpatterns = [
# ...
path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
]LoginView and LogoutView are built into django.contrib.auth — no view code of your own needed, only a template for the login form:
{# registration/login.html #}
{% extends "blog/base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Log in</button>
</form>
{% endblock %}LoginView specifically looks for registration/login.html — that exact path is a Django convention, not a name you get to pick. Django searches every installed app's templates/ folder for it, so this can live in blog/templates/registration/login.html alongside the blog/ folder used everywhere else.
Signup
# blog/views.py
from django.contrib.auth.forms import UserCreationForm
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
class SignUpView(CreateView):
form_class = UserCreationForm
template_name = "registration/signup.html"
success_url = reverse_lazy("login")# blog/urls.py
path("accounts/signup/", views.SignUpView.as_view(), name="signup"),There's no built-in SignUpView the way there's a LoginView — UserCreationForm, another django.contrib.auth shortcut, handles the username/password/password-confirmation fields and validation (matching passwords, minimum complexity), and CreateView from part 13 does the rest. signup.html is the same {% csrf_token %} {{ form.as_p }} shape as the login template.
What request.user actually is
def some_view(request):
request.user # a User instance, or AnonymousUser if not logged in
request.user.is_authenticated # True or False — always safe to check, on either typerequest.user is never None — it's always a real object, either the logged-in User or Django's AnonymousUser, which exists specifically so request.user.is_authenticated is always safe to call without a null check first. AnonymousUser.is_authenticated is hardcoded False; a real User's is hardcoded True.
Showing login state in the base template
{# blog/templates/blog/base.html — inside <header> #}
{% if user.is_authenticated %}
<span>{{ user.username }}</span>
<form method="post" action="{% url 'logout' %}">{% csrf_token %}<button>Log out</button></form>
{% else %}
<a href="{% url 'login' %}">Log in</a>
<a href="{% url 'signup' %}">Sign up</a>
{% endif %}user (not request.user) is available directly in every template — Django's auth context processor puts it there automatically, on every request, so nothing needs to be passed in manually from every single view.
Logging out with a plain link (<a href="/logout/">) instead of a form. Django's LogoutView only accepts POST by default for the same reason a delete link had to become a form in part 15 — a state-changing action reachable by GET is reachable by anything that follows a link, not just a person choosing to click it.
Next: actually restricting PostCreateView and friends to logged-in users — right now, anyone can reach /post/new/ regardless of request.user.