Django Basics #7: Django Admin and Built-in Authentication
Time to cash in the promise from #1 What is Django — Admin comes for free. This post covers the admin page Django auto-generates for you, plus the built-in authentication that ships alongside it (User, login, permissions), in one breath. The final post of the series.
Admin — register a model in one line #
A single line in blog/admin.py.
from django.contrib import admin
from .models import Post
admin.site.register(Post)That’s it. Run runserver and go to http://127.0.0.1:8000/admin/ — but it’s still the login page. You need a superuser.
Creating a superuser #
uv run python manage.py createsuperuserIt interactively asks for username / email / password — that’s all. Now you can log in to /admin/ with that account.
After login — Auth (User, Group), Blog (Post) appear in the left menu. Click Post and you see an auto-generated CRUD screen. List, create, edit, delete, and search.
You didn’t write a single extra line of code. This is Django’s most powerful promise.
ModelAdmin — customize the Admin UI #
The default screen is sparse (Post object (1), Post object (2), …). Make it richer with ModelAdmin.
from django.contrib import admin
from .models import Post, Tag, Comment
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# List view
list_display = ("title", "author", "is_published", "created_at")
list_filter = ("is_published", "created_at", "author")
search_fields = ("title", "content")
date_hierarchy = "created_at"
ordering = ("-created_at",)
list_editable = ("is_published",)
list_per_page = 25
# Detail view
fields = ("title", "author", "content", "tags", "is_published")
readonly_fields = ("created_at", "updated_at")
filter_horizontal = ("tags",) # nicer ManyToMany widget
autocomplete_fields = ("author",) # autocomplete for FK with many options
prepopulated_fields = {"slug": ("title",)} # auto-fill slug from title
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ("name", "slug")
search_fields = ("name",)
prepopulated_fields = {"slug": ("name",)}
admin.site.register(Comment)The most common options:
| Option | Effect |
|---|---|
list_display | Columns to show in the list |
list_filter | Filters in the right sidebar |
search_fields | Top search box (target fields) |
date_hierarchy | Top nav drilling down by date |
ordering | Default sort |
list_editable | Edit directly in the list |
list_per_page | Items per page |
fields / fieldsets | Field layout in the detail view |
readonly_fields | Read-only fields |
filter_horizontal / filter_vertical | Two-pane widget for M2M |
autocomplete_fields | Autocomplete for FK / M2M (target model needs search_fields) |
prepopulated_fields | Auto-fill from another field (slug is common) |
The @admin.register(Post) decorator is equivalent to admin.site.register(Post, PostAdmin). Cleaner.
Inline — edit children alongside the parent #
If you want to show a Post’s Comments right in the post detail view.
from django.contrib import admin
from .models import Post, Comment
class CommentInline(admin.TabularInline): # or StackedInline
model = Comment
extra = 1 # one empty row added
fields = ("author", "body", "created_at")
readonly_fields = ("created_at",)
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "author")
inlines = [CommentInline]TabularInline— table style (one row per comment)StackedInline— card style (fields stacked vertically)
Fits places where the parent ↔ child relationship is clear, like cart / order items / post attachments.
Admin’s strengths beyond the surface #
- Search + filter + pagination — automatic
- Change history — who changed what, when (Recent Actions)
- Permission system — per-model add/change/delete/view permissions are auto-generated
- Localization — UI follows the user’s LANGUAGE_CODE
- i18n + RTL support
For a small internal tool, Admin alone can serve as your operations UI. Some companies run their entire back-office on it.
Admin security — the production basics #
- Don’t leave
/admin/at the default — change it (path("private/", admin.site.urls)) - IP restriction (Nginx or middleware)
- Two-factor auth (
django-otp,django-allauth-2fa) - superuser only for those who really need it
Detailed production security is in Advanced #7 Deployment security.
Built-in authentication — django.contrib.auth
#
The foundation of Admin is django.contrib.auth. It’s already in INSTALLED_APPS, and migrate already created the User, Group, and Permission tables.
The User model
#
from django.contrib.auth import get_user_model
User = get_user_model() # convention — follows settings.AUTH_USER_MODEL
user = User.objects.create_user(
username="curtis",
email="me@example.com",
password="secret123", # auto-hashed
)
user.set_password("new-password") # change password
user.save()
user.check_password("new-password") # → Truecreate_user and set_password handle password hashing (PBKDF2 by default) automatically. Never store plaintext.
Custom User — recommended from the start #
The default User works, but defining a custom User model from day one is strongly recommended. Switching later is extremely painful.
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)AUTH_USER_MODEL = "accounts.User"Deeper customization (email login, permissions, etc.) is in Intermediate #4 Users/Permissions.
Login / logout — built-in views #
You don’t have to build them. They’re all in django.contrib.auth.urls.
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("blog/", include("blog.urls")),
]URLs that come with include("django.contrib.auth.urls"):
| Name | Path | Role |
|---|---|---|
login | /accounts/login/ | Login |
logout | /accounts/logout/ | Logout |
password_change | /accounts/password_change/ | Change password |
password_change_done | /accounts/password_change/done/ | Change complete |
password_reset | /accounts/password_reset/ | Reset password (via email) |
password_reset_done / confirm / complete | … | Reset flow |
The views are all provided, but you have to build the templates.
{% extends "base.html" %}
{% block content %}
<h1>Login</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
{% if form.errors %}
<p>Username or password is incorrect.</p>
{% endif %}
<p><a href="{% url 'password_reset' %}">Forgot your password?</a></p>
{% endblock %}This single file gets login working. logout / password_* go in the same place (templates/registration/).
Redirect after login #
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/blog/"
LOGOUT_REDIRECT_URL = "/blog/"Sign-up is on you #
The built-ins give you only login / logout / password. Sign-up you build yourself, or use a package like django-allauth.
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import redirect, render
def signup(request):
if request.method == "POST":
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("blog:post_list")
else:
form = UserCreationForm()
return render(request, "registration/signup.html", {"form": form})UserCreationForm is also built-in — a ModelForm with username + password + password confirmation.
@login_required — login protection
#
A one-line decorator on a view function.
from django.contrib.auth.decorators import login_required
@login_required
def post_new(request):
...If an unauthenticated user hits it, they’re auto-redirected to LOGIN_URL (with the original URL passed as ?next=... so they come back after login).
request.user — the current user
#
def my_page(request):
if request.user.is_authenticated:
username = request.user.username
...{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}.</p>
<a href="{% url 'logout' %}">Logout</a>
{% else %}
<a href="{% url 'login' %}">Login</a>
{% endif %}When not logged in, request.user is an AnonymousUser instance (is_authenticated is False). A bare if request.user: is therefore meaningless — always check is_authenticated.
Permissions — @permission_required / user.has_perm
#
Django auto-creates four permissions per model — add_<model>, change_<model>, delete_<model>, view_<model>.
from django.contrib.auth.decorators import permission_required
@permission_required("blog.delete_post", raise_exception=True)
def post_delete(request, post_id):
...if request.user.has_perm("blog.change_post"):
...{% if perms.blog.change_post %}
<a href="...">Edit</a>
{% endif %}The production pattern is to bundle permissions into a Group and assign groups to users. The deeper permission model is in Intermediate #4 Users/Permissions.
Recap #
What this post nailed down:
admin.site.register(Model)— auto CRUD UI in one linecreatesuperuserfor an admin accountModelAdmin—list_display,list_filter,search_fields,date_hierarchy, …- Inline — edit children alongside the parent
- Built-in
User—create_user,set_password,check_password - For new projects, custom User from the start is recommended
include("django.contrib.auth.urls")for login/logout/password flows- Templates go in
templates/registration/— you build them @login_required,request.user.is_authenticated- Permissions — auto-generated per model,
@permission_required,user.has_perm, templateperms.app.codename - Admin is good enough to use as an operations tool — but security (path change, 2FA, IP restriction) is required
Series wrap-up #
The seven posts in one breath:
- #1 What is Django — where the full-stack monolith fits
- #2 Project setup — uv + startproject + startapp
- #3 Models and ORM basics — models, migrations, QuerySet
- #4 URL and Views — URLconf, FBV, render
- #5 Templates and static files — template inheritance, static
- #6 Forms and ModelForm — form validation, ModelForm, file upload
- #7 Admin and authentication — auto CRUD, login, permissions ← here
These seven cover all the parts you need to build a small blog. You’re at the level where you can ship a small side project on your own.
The next series, Django Intermediate #1 CBV in depth, covers class-based views, which compress FBV’s repeating patterns. You’ll see how the same CRUD becomes far shorter with five generics — ListView, DetailView, CreateView, UpdateView, DeleteView. From there, Intermediate #2 ORM advanced tackles select_related / prefetch_related, and Intermediate #7 Testing rounds out the flow of taking a project all the way to production.
Thank you for following along. May building your first full-stack app be just the beginning.