Overview
Django 5.0, released on December 4, 2023, opens a new major cycle with features that address long-standing requests. GeneratedField allows defining database-computed columns directly in models, faceted filters in the admin display object counts per filter value, and field defaults can now be computed by the database.
Form rendering is also simplified thanks to Field.template_name, which allows customizing the template for each field type without overriding the entire widget.
Key Features
GeneratedField
The new GeneratedField allows creating columns whose values are automatically computed by the database from other columns. Two modes are available: STORED (the value is physically stored) and VIRTUAL (computed on every read). This is ideal for denormalizations that must always remain consistent.
from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat, Lower
class Employee(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2)
weekly_hours = models.PositiveIntegerField(default=35)
# Full name computed by the database
full_name = models.GeneratedField(
expression=Concat('first_name', Value(' '), 'last_name'),
output_field=models.CharField(max_length=101),
db_persist=True, # STORED: physically persisted
)
# Annual salary (12 months + 1 month bonus)
annual_salary = models.GeneratedField(
expression=F('monthly_salary') * 13,
output_field=models.DecimalField(max_digits=12, decimal_places=2),
db_persist=True,
)
# Normalized lowercase identifier
identifier = models.GeneratedField(
expression=Concat(Lower('first_name'), Value('.'), Lower('last_name')),
output_field=models.CharField(max_length=101),
db_persist=True,
)
# Usage: generated fields are read-only
emp = Employee.objects.create(
first_name='Marie', last_name='Dupont', monthly_salary=3500
)
emp.refresh_from_db()
print(emp.full_name) # Marie Dupont
print(emp.annual_salary) # 45500.00
print(emp.identifier) # marie.dupont
Faceted filters in the admin
The admin interface now displays the number of objects matching each filter value, directly in the sidebar. This allows seeing the data distribution at a glance without clicking on each filter. This feature is enabled simply by setting show_facets on the ModelAdmin.
from django.contrib import admin
class OrderAdmin(admin.ModelAdmin):
list_display = ['reference', 'customer', 'status', 'amount', 'date']
list_filter = ['status', 'date', 'payment_method']
# Enable counts on filters
show_facets = admin.ShowFacets.ALWAYS
# Three options available:
# ShowFacets.ALWAYS — always show counts
# ShowFacets.ALLOW — show via a URL parameter
# ShowFacets.NEVER — disable (default)
# Result in the sidebar:
# Status
# Pending (23)
# Confirmed (156)
# Shipped (89)
# Delivered (412)
# Cancelled (7)
Database-computed defaults
Model fields now accept a db_default parameter to define a default value computed by the database rather than by Python. This guarantees that the default is applied even during direct SQL inserts, and allows using database functions like Now().
from django.db import models
from django.db.models.functions import Now, Pi
class ActivityLog(models.Model):
action = models.CharField(max_length=200)
user = models.CharField(max_length=100)
# Timestamp managed by the database
created_at = models.DateTimeField(db_default=Now())
# Counter initialized to zero by the database
attempts = models.IntegerField(db_default=0)
# Default priority
priority = models.CharField(
max_length=10,
db_default=Value('normal'),
)
# The object is created without specifying default fields
entry = ActivityLog.objects.create(
action='login', user='marie'
)
entry.refresh_from_db()
print(entry.created_at) # 2023-12-04 10:30:00+00:00
print(entry.attempts) # 0
print(entry.priority) # normal
Simplified form rendering
The Field.template_name attribute allows defining a rendering template for each form field type. This simplifies rendering customization without having to override widgets or write complex templatetags. Group templates (div, table, ul) are also customizable.
from django import forms
class RegistrationForm(forms.Form):
# Use a custom template for this field
email = forms.EmailField(
template_name='forms/fields/email_with_help.html',
)
password = forms.CharField(
widget=forms.PasswordInput,
template_name='forms/fields/password.html',
)
terms = forms.BooleanField(
template_name='forms/fields/checkbox.html',
)
# Global form rendering with a custom template
template_name_div = 'forms/registration_div.html'
# In the main template:
# {{ form }} — uses template_name_div by default
# {{ form.email }} — uses the field's template
