Django 6.0 Tutorial 2025: Building Modern Web Apps with the Latest Features
Master Django 6.0 with this complete tutorial. Learn to build modern web applications using the latest features like template partials, background tasks, and Content Security Policy.
StalkTechie
Author
Django 6.0 Tutorial 2025: Building Modern Web Apps with the Latest Features
Learn to build modern web applications with Django 6.0, the latest version of Python's premier web framework. This comprehensive tutorial covers the newest features including template partials, background tasks, Content Security Policy, and modern development practices for 2025.
Table of Contents
Django in 2025: Latest Trends & Features
Django, celebrating its 20th anniversary, continues to evolve with modern web development trends while maintaining its signature stability and security. The 2025 ecosystem shows exciting shifts, particularly in frontend approaches and developer tools.
Key Trends for 2025
- HTMX & Alpine.js Dominance: Server-rendered templates enhanced with HTMX (24% usage) and Alpine.js (14%) are rapidly growing, offering a compelling alternative to SPAs.
- AI Integration: 38% of developers use AI tools for learning and development, with ChatGPT (69%) and GitHub Copilot (34%) leading the way.
- Type Hints Adoption: 63% of developers already use type hints, with 84% supporting their addition to Django core.
- PostgreSQL Leadership: PostgreSQL remains the dominant database choice at 76%, followed by SQLite (42%) and MySQL (27%).
- Latest Version Usage: 75% of developers run the latest Django version, reflecting confidence in the stable release cadence.
Django 6.0 Major Features
Django 6.0, released in December 2025, introduces several groundbreaking features:
- Template Partials: Built-in support for reusable template components
- Background Tasks: Native framework for asynchronous task processing
- Content Security Policy: Built-in protection against content injection attacks
- Modern Email API: Updated to Python's modern email API
- Enhanced Security: Increased PBKDF2 iterations and new security middleware
Project Setup & Installation
Let's create a modern Django 6.0 project with proper environment setup and configuration for 2025 development.
Python Environment Setup
Django 6.0 requires Python 3.12, 3.13, or 3.14. Python 3.10 and 3.11 are no longer supported.
# Create and activate virtual environment
python -m venv .venv
# On macOS/Linux
source .venv/bin/activate
# On Windows
.venv\Scripts\activate
# Install Django 6.0
pip install django==6.0
# Verify installation
python -m django --version
# Should output: 6.0
Creating Project Structure
# Create project with custom directory
django-admin startproject config .
# Create core app
python manage.py startapp core
# Create additional apps
python manage.py startapp blog
python manage.py startapp api
# Project structure
myproject/
├── config/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── core/
├── blog/
├── api/
├── manage.py
└── .env
Modern Settings Configuration
# config/settings.py
from pathlib import Path
import os
from datetime import timedelta
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key-here')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'corsheaders',
'django_filters',
# Local apps
'core.apps.CoreConfig',
'blog.apps.BlogConfig',
'api.apps.ApiConfig',
]
# Django 6.0 Template Partials
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'libraries': {
'partials': 'django.templatetags.partials',
},
},
},
]
# Database - PostgreSQL recommended
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'django_db'),
'USER': os.environ.get('DB_USER', 'postgres'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Django 6.0 Security - Increased PBKDF2 iterations
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
# Static files
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Media files
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Django REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
Modern Frontend with Template Partials
Django 6.0 introduces native template partials, making server-rendered templates more modular and maintainable. This aligns with the growing trend of HTMX and Alpine.js for modern web applications.
Template Partials Implementation
{# templates/base.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Django App{% endblock %}</title>
{# HTMX for modern interactivity #}
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
{# Alpine.js for reactive components #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{# Tailwind CSS #}
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
{# Navigation partial #}
{% partial "navigation.html" %}
<main class="container mx-auto px-4 py-8">
{% block content %}
{% endblock %}
</main>
{# Footer partial #}
{% partial "footer.html" %}
</body>
</html>
{# Defining a partial #}
{% partialdef "navigation.html" %}
<nav class="bg-white shadow-lg">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center py-4">
<a href="/" class="text-xl font-bold text-blue-600">DjangoApp</a>
<div class="space-x-4">
<a href="/blog/" class="text-gray-700 hover:text-blue-600">Blog</a>
<a href="/about/" class="text-gray-700 hover:text-blue-600">About</a>
{% if user.is_authenticated %}
<a href="/dashboard/" class="text-gray-700 hover:text-blue-600">Dashboard</a>
{% else %}
<a href="/login/" class="text-gray-700 hover:text-blue-600">Login</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% endpartialdef %}
{# Using partials in templates #}
{% extends "base.html" %}
{% block title %}Blog Posts{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold text-gray-800 mb-8">Latest Blog Posts</h1>
{# Blog posts partial with HTMX infinite scroll #}
<div id="posts-container"
hx-get="{% url 'blog:posts_partial' %}"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Posts will load here -->
</div>
{# Load more button #}
<button class="mt-8 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
hx-get="{% url 'blog:load_more' %}"
hx-target="#posts-container"
hx-swap="beforeend">
Load More
</button>
</div>
{% endblock %}
HTMX Integration Example
# blog/views.py
from django.http import HttpResponse
from django.shortcuts import render
from django.core.paginator import Paginator
from .models import Post
def blog_home(request):
"""Main blog page with HTMX integration"""
return render(request, 'blog/home.html')
def posts_partial(request):
"""HTMX endpoint for loading posts"""
page = request.GET.get('page', 1)
posts = Post.objects.filter(published=True).order_by('-created_at')
paginator = Paginator(posts, 5)
page_obj = paginator.get_page(page)
return render(request, 'blog/partials/posts_list.html', {
'posts': page_obj,
'page_obj': page_obj,
})
def load_more_posts(request):
"""HTMX endpoint for infinite scroll"""
page = int(request.GET.get('page', 2))
posts = Post.objects.filter(published=True).order_by('-created_at')
paginator = Paginator(posts, 5)
try:
page_obj = paginator.get_page(page)
except:
return HttpResponse('') # No more posts
return render(request, 'blog/partials/posts_list.html', {
'posts': page_obj,
'page_obj': page_obj,
})
Django 6.0 Background Tasks Framework
The new built-in Tasks framework allows running code outside the HTTP request-response cycle, perfect for email sending, data processing, and other asynchronous operations.
Defining and Using Tasks
# core/tasks.py
from django.core.mail import send_mail
from django.tasks import task
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
@task
def send_welcome_email(user_email, username):
"""Send welcome email to new users"""
subject = "Welcome to Our Platform!"
message = f"""
Hi {username},
Welcome to our platform! We're excited to have you on board.
Best regards,
The Team
"""
try:
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[user_email],
fail_silently=False,
)
logger.info(f"Welcome email sent to {user_email}")
return True
except Exception as e:
logger.error(f"Failed to send welcome email: {e}")
raise
@task
def process_user_data(user_id):
"""Process user data asynchronously"""
from django.contrib.auth import get_user_model
User = get_user_model()
try:
user = User.objects.get(id=user_id)
# Simulate heavy processing
# Update user analytics
# Generate reports
# Clean up old data
logger.info(f"Processed data for user {user.email}")
return {"status": "success", "user_id": user_id}
except User.DoesNotExist:
logger.error(f"User {user_id} not found")
raise
except Exception as e:
logger.error(f"Error processing user data: {e}")
raise
@task(max_retries=3, retry_delay=60)
def send_bulk_emails(emails, subject, template_name):
"""Send bulk emails with retry logic"""
from django.template.loader import render_to_string
for email in emails:
try:
context = {'email': email}
message = render_to_string(f'emails/{template_name}.txt', context)
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[email],
fail_silently=False,
)
logger.info(f"Email sent to {email}")
except Exception as e:
logger.warning(f"Failed to send to {email}: {e}")
# This will retry the entire task if configured
continue
return {"sent": len(emails), "failed": 0}
# Using tasks in views
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django.contrib import messages
from .tasks import send_welcome_email, process_user_data
class UserRegistrationView(CreateView):
"""User registration with background tasks"""
model = get_user_model()
form_class = UserRegistrationForm
template_name = 'registration/register.html'
success_url = reverse_lazy('home')
def form_valid(self, form):
response = super().form_valid(form)
user = form.instance
# Enqueue background tasks
send_welcome_email.enqueue(
user_email=user.email,
username=user.get_full_name() or user.username
)
process_user_data.enqueue(user_id=user.id)
messages.success(
self.request,
"Registration successful! Welcome email is being sent."
)
return response
Task Configuration
# config/settings.py
# Django 6.0 Tasks Configuration
TASKS = {
'default': {
'BACKEND': 'django.tasks.backends.database.DatabaseBackend',
'OPTIONS': {
'max_retries': 3,
'retry_delay': 60, # seconds
},
},
'email': {
'BACKEND': 'django.tasks.backends.redis.RedisBackend',
'OPTIONS': {
'connection': 'redis://localhost:6379/0',
'max_retries': 5,
'retry_delay': 30,
},
},
}
# For production, configure Celery or Redis backend
# TASKS = {
# 'default': {
# 'BACKEND': 'django.tasks.backends.celery.CeleryBackend',
# 'OPTIONS': {
# 'broker_url': 'redis://localhost:6379/0',
# 'result_backend': 'redis://localhost:6379/0',
# },
# },
# }
Security & Content Security Policy
Django 6.0 introduces built-in Content Security Policy (CSP) support, providing enhanced protection against content injection attacks like XSS.
CSP Configuration
# config/settings.py
from django.utils.csp import CSP
# Content Security Policy
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [
CSP.SELF,
CSP.NONCE, # For inline scripts
"https://cdn.jsdelivr.net", # CDN for Alpine.js, HTMX
"https://unpkg.com", # HTMX CDN
],
"style-src": [
CSP.SELF,
CSP.UNSAFE_INLINE, # For inline styles if needed
"https://cdn.jsdelivr.net", # Tailwind CSS
],
"img-src": [
CSP.SELF,
"data:", # For data URLs
"https:", # All HTTPS images
],
"font-src": [
CSP.SELF,
"https://cdn.jsdelivr.net",
],
"connect-src": [
CSP.SELF,
"https://api.example.com", # Your API endpoints
],
"frame-src": [CSP.NONE], # No iframes allowed
"object-src": [CSP.NONE], # No Flash/objects
"base-uri": [CSP.SELF],
"form-action": [CSP.SELF],
}
# Report-only mode for testing
SECURE_CSP_REPORT_ONLY = False
# CSP middleware - must be after SecurityMiddleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.csp.ContentSecurityPolicyMiddleware', # New in Django 6.0
]
# Additional security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# CSRF settings
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_USE_SESSIONS = True
# Session security
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
Using CSP Nonces in Templates
{# templates/base.html #}
{% load csp %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Secure App{% endblock %}</title>
{# External scripts with CSP #}
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
{# Inline script with nonce #}
<script nonce="{% csp_nonce %}">
// This inline script is allowed by CSP
console.log('Secure inline script');
// HTMX configuration
htmx.config.useTemplateFragments = true;
</script>
{# Inline styles if needed #}
<style>
/* Inline styles require 'unsafe-inline' in CSP */
body { margin: 0; padding: 0; }
</style>
</head>
<body>
{% block content %}{% endblock %}
{# More scripts with nonce #}
<script nonce="{% csp_nonce %}">
// Initialize Alpine.js components
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() { this.open = !this.open; }
}));
});
</script>
</body>
</html>
View-specific CSP Overrides
# views.py
from django.views.decorators.csp import csp_exempt, csp_update
from django.http import HttpResponse
from django.shortcuts import render
@csp_update(SCRIPT_SRC=["https://maps.googleapis.com"])
def map_view(request):
"""View that needs Google Maps script"""
return render(request, 'map.html')
@csp_exempt
def legacy_view(request):
"""Legacy view that needs exemption"""
return HttpResponse("Legacy content")
@csp_update(
SCRIPT_SRC=["'self'", "https://analytics.example.com"],
CONNECT_SRC=["'self'", "wss://realtime.example.com"]
)
def analytics_dashboard(request):
"""Dashboard with external analytics"""
return render(request, 'analytics/dashboard.html')
Database Models with Django 6.0 Features
Django's ORM continues to be one of its strongest features. With Django 6.0, we get enhanced model capabilities and better database support.
Modern Model Implementation
# blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
import uuid
User = get_user_model()
class Category(models.Model):
"""Blog post category"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['created_at']),
]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
class Tag(models.Model):
"""Blog post tag"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
def __str__(self):
return self.name
class Post(models.Model):
"""Blog post model with Django 6.0 features"""
class Status(models.TextChoices):
DRAFT = 'DF', 'Draft'
PUBLISHED = 'PB', 'Published'
ARCHIVED = 'AR', 'Archived'
# Primary key - using UUID for distributed systems
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
db_comment="Unique identifier for the post"
)
# Core fields
title = models.CharField(
max_length=200,
db_comment="Post title"
)
slug = models.SlugField(
max_length=200,
unique_for_date='publish_date',
db_comment="URL-friendly version of title"
)
content = models.TextField(
db_comment="Main post content in Markdown"
)
excerpt = models.TextField(
max_length=500,
blank=True,
db_comment="Short post summary"
)
# Relationships
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts',
db_comment="Post author"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='posts',
db_comment="Post category"
)
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='posts',
db_comment="Post tags"
)
# Status and dates
status = models.CharField(
max_length=2,
choices=Status.choices,
default=Status.DRAFT,
db_comment="Post publication status"
)
created_at = models.DateTimeField(
auto_now_add=True,
db_comment="When the post was created"
)
updated_at = models.DateTimeField(
auto_now=True,
db_comment="When the post was last updated"
)
publish_date = models.DateTimeField(
default=timezone.now,
db_comment="Scheduled publication date"
)
# Metadata
featured_image = models.ImageField(
upload_to='blog/featured/%Y/%m/',
blank=True,
null=True,
db_comment="Featured image for the post"
)
meta_description = models.CharField(
max_length=160,
blank=True,
db_comment="SEO meta description"
)
meta_keywords = models.CharField(
max_length=255,
blank=True,
db_comment="SEO meta keywords"
)
# Statistics (Django 6.0 GeneratedField example)
word_count = models.GeneratedField(
expression=models.ExpressionWrapper(
models.Func(
models.F('content'),
function='LENGTH',
) - models.Func(
models.F('content'),
function='LENGTH',
template="%(function)s(replace(%(expressions)s, ' ', ''))",
),
output_field=models.IntegerField(),
),
output_field=models.IntegerField(),
db_persist=True, # Stored in database
db_comment="Calculated word count"
)
read_time_minutes = models.GeneratedField(
expression=models.ExpressionWrapper(
models.F('word_count') / 200, # 200 words per minute
output_field=models.IntegerField(),
),
output_field=models.IntegerField(),
db_persist=True,
db_comment="Estimated read time in minutes"
)
# JSON field for flexible metadata (Django 6.0 enhanced)
metadata = models.JSONField(
default=dict,
blank=True,
db_comment="Flexible metadata storage"
)
class Meta:
ordering = ['-publish_date', '-created_at']
indexes = [
models.Index(fields=['slug', 'publish_date']),
models.Index(fields=['status', 'publish_date']),
models.Index(fields=['author', 'publish_date']),
models.Index(
fields=['category', 'status', 'publish_date'],
name='category_status_publish_idx'
),
models.Index(
models.F('publish_date').desc(),
condition=models.Q(status='PB'),
name='published_posts_idx'
),
]
constraints = [
models.CheckConstraint(
check=models.Q(publish_date__lte=timezone.now())
| models.Q(status='DF'),
name='valid_publish_date'
),
models.UniqueConstraint(
fields=['slug', 'publish_date'],
name='unique_slug_per_date'
),
]
permissions = [
('can_publish', 'Can publish posts'),
('can_archive', 'Can archive posts'),
('can_feature', 'Can feature posts'),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={
'year': self.publish_date.year,
'month': self.publish_date.month,
'day': self.publish_date.day,
'slug': self.slug
})
@property
def is_published(self):
"""Check if post is published and not scheduled for future"""
return (
self.status == self.Status.PUBLISHED
and self.publish_date <= timezone.now()
)
@property
def reading_time(self):
"""Human-readable reading time"""
minutes = self.read_time_minutes or 1
return f"{minutes} min read"
def save(self, *args, **kwargs):
# Auto-generate excerpt if not provided
if not self.excerpt and self.content:
self.excerpt = self.content[:497] + '...'
# Auto-generate slug if not provided
if not self.slug:
from django.utils.text import slugify
self.slug = slugify(self.title)
super().save(*args, **kwargs)
class Comment(models.Model):
"""Blog post comments with Django 6.0 features"""
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='comments'
)
content = models.TextField(max_length=1000)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='replies'
)
is_approved = models.BooleanField(default=False)
# Django 6.0 - ArrayField for tags within comments
mentioned_users = models.ArrayField(
models.CharField(max_length=100),
blank=True,
default=list,
db_comment="Users mentioned in the comment"
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['post', 'created_at']),
models.Index(fields=['author', 'created_at']),
models.Index(fields=['is_approved', 'created_at']),
]
def __str__(self):
return f"Comment by {self.author} on {self.post}"
Views & URL Routing with Modern Patterns
Django 6.0 enhances the view layer with better type hints support and improved URL routing patterns.
Class-Based Views with Type Hints
# blog/views.py
from typing import Optional, Dict, Any
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib import messages
from django.urls import reverse_lazy
from django.utils import timezone
from django.db.models import Q, Count
from .models import Post, Category, Comment
from .forms import PostForm, CommentForm
class PostListView(ListView):
"""List all published posts with filtering"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
queryset = Post.objects.filter(
status=Post.Status.PUBLISHED,
publish_date__lte=timezone.now()
).select_related('author', 'category').prefetch_related('tags')
# Filter by category
category_slug = self.kwargs.get('category_slug')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
# Search functionality
search_query = self.request.GET.get('q')
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query) |
Q(excerpt__icontains=search_query)
)
# Order by publish date
return queryset.order_by('-publish_date')
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status=Post.Status.PUBLISHED))
)
context['search_query'] = self.request.GET.get('q', '')
return context
class PostDetailView(DetailView):
"""Display a single post with comments"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(
status=Post.Status.PUBLISHED,
publish_date__lte=timezone.now()
).select_related('author', 'category').prefetch_related('tags', 'comments')
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context['comment_form'] = CommentForm()
context['comments'] = self.object.comments.filter(
is_approved=True
).select_related('author')
return context
class PostCreateView(LoginRequiredMixin, CreateView):
"""Create a new blog post"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form: PostForm) -> HttpResponse:
form.instance.author = self.request.user
response = super().form_valid(form)
messages.success(self.request, 'Post created successfully!')
return response
def get_success_url(self) -> str:
return reverse_lazy('blog:post_detail', kwargs={
'year': self.object.publish_date.year,
'month': self.object.publish_date.month,
'day': self.object.publish_date.day,
'slug': self.object.slug
})
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
"""Update an existing post"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self) -> Optional[bool]:
post = self.get_object()
return self.request.user == post.author or self.request.user.is_staff
def form_valid(self, form: PostForm) -> HttpResponse:
response = super().form_valid(form)
messages.success(self.request, 'Post updated successfully!')
return response
def get_success_url(self) -> str:
return reverse_lazy('blog:post_detail', kwargs={
'year': self.object.publish_date.year,
'month': self.object.publish_date.month,
'day': self.object.publish_date.day,
'slug': self.object.slug
})
# Function-based views with type hints
def category_detail(
request: HttpRequest,
slug: str
) -> HttpResponse:
"""Display posts in a specific category"""
category = get_object_or_404(Category, slug=slug)
posts = Post.objects.filter(
category=category,
status=Post.Status.PUBLISHED,
publish_date__lte=timezone.now()
).select_related('author').order_by('-publish_date')
return render(request, 'blog/category_detail.html', {
'category': category,
'posts': posts,
})
def add_comment(
request: HttpRequest,
post_id: int
) -> HttpResponseRedirect:
"""Add a comment to a post"""
post = get_object_or_404(Post, id=post_id)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, 'Comment added successfully!')
return redirect('blog:post_detail', slug=post.slug)
# HTMX-specific views
def posts_partial(request: HttpRequest) -> HttpResponse:
"""HTMX endpoint for loading posts partial"""
posts = Post.objects.filter(
status=Post.Status.PUBLISHED,
publish_date__lte=timezone.now()
).select_related('author', 'category')[:5]
return render(request, 'blog/partials/posts_list.html', {
'posts': posts,
})
Modern URL Configuration
# blog/urls.py
from django.urls import path, include
from . import views
from .feeds import LatestPostsFeed
app_name = 'blog'
urlpatterns = [
# Main views
path('', views.PostListView.as_view(), name='post_list'),
path('category/<slug:category_slug>/', views.category_detail, name='category_detail'),
# Post detail with date-based URL
path('<int:year>/<int:month>/<int:day>/<slug:slug>/',
views.PostDetailView.as_view(), name='post_detail'),
# CRUD operations
path('create/', views.PostCreateView.as_view(), name='post_create'),
path('<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_edit'),
# Comments
path('<int:post_id>/comment/', views.add_comment, name='add_comment'),
# HTMX endpoints
path('partials/posts/', views.posts_partial, name='posts_partial'),
# Feeds
path('feed/', LatestPostsFeed(), name='post_feed'),
# API endpoints
path('api/', include('blog.api.urls')),
]
# config/urls.py (project level)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.generic import RedirectView
urlpatterns = [
# Admin
path('admin/', admin.site.urls),
# Apps
path('', include('core.urls')),
path('blog/', include('blog.urls')),
path('api/', include('api.urls')),
# Auth
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('allauth.urls')),
# API Documentation
path('api/docs/', include('docs.urls')),
# Health check
path('health/', include('health_check.urls')),
# Redirect root to blog
path('', RedirectView.as_view(pattern_name='blog:post_list', permanent=False)),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Debug toolbar
import debug_toolbar
urlpatterns += [path('__debug__/', include(debug_toolbar.urls))]
Testing & Deployment Best Practices
Django 6.0 improves testing capabilities and deployment readiness with new features and better tooling integration.
Modern Testing with pytest
# tests/test_models.py
import pytest
from django.utils import timezone
from datetime import timedelta
from blog.models import Post, Category
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.mark.django_db
class TestPostModel:
"""Test Post model functionality"""
@pytest.fixture
def user(self):
return User.objects.create_user(
email='test@example.com',
username='testuser',
password='testpass123'
)
@pytest.fixture
def category(self):
return Category.objects.create(
name='Technology',
slug='technology'
)
@pytest.fixture
def post(self, user, category):
return Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=user,
category=category,
status=Post.Status.PUBLISHED,
publish_date=timezone.now()
)
def test_post_creation(self, post):
"""Test post creation"""
assert post.title == 'Test Post'
assert post.slug == 'test-post'
assert post.status == Post.Status.PUBLISHED
assert post.is_published is True
def test_future_publish_date(self, user, category):
"""Test post scheduled for future"""
future_date = timezone.now() + timedelta(days=1)
post = Post.objects.create(
title='Future Post',
slug='future-post',
content='Future content',
author=user,
category=category,
status=Post.Status.PUBLISHED,
publish_date=future_date
)
assert post.is_published is False
def test_word_count_generation(self, post):
"""Test GeneratedField for word count"""
post.content = 'This is a test content with multiple words.'
post.save()
post.refresh_from_db()
assert post.word_count > 0
assert post.read_time_minutes >= 1
def test_get_absolute_url(self, post):
"""Test URL generation"""
url = post.get_absolute_url()
assert str(post.publish_date.year) in url
assert post.slug in url
# tests/test_views.py
import pytest
from django.urls import reverse
from django.test import Client
from blog.models import Post
@pytest.mark.django_db
class TestBlogViews:
"""Test blog views"""
@pytest.fixture
def client(self):
return Client()
@pytest.fixture
def published_post(self, user, category):
return Post.objects.create(
title='Published Post',
slug='published-post',
content='Published content',
author=user,
category=category,
status=Post.Status.PUBLISHED,
publish_date=timezone.now()
)
def test_post_list_view(self, client, published_post):
"""Test post list view"""
url = reverse('blog:post_list')
response = client.get(url)
assert response.status_code == 200
assert 'posts' in response.context
assert published_post in response.context['posts']
def test_post_detail_view(self, client, published_post):
"""Test post detail view"""
url = reverse('blog:post_detail', kwargs={
'year': published_post.publish_date.year,
'month': published_post.publish_date.month,
'day': published_post.publish_date.day,
'slug': published_post.slug
})
response = client.get(url)
assert response.status_code == 200
assert response.context['post'] == published_post
def test_htmx_posts_partial(self, client, published_post):
"""Test HTMX endpoint"""
url = reverse('blog:posts_partial')
response = client.get(url, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert 'posts' in response.context
# tests/test_tasks.py
import pytest
from unittest.mock import patch, MagicMock
from core.tasks import send_welcome_email, process_user_data
@pytest.mark.django_db
class TestTasks:
"""Test background tasks"""
@patch('core.tasks.send_mail')
def test_send_welcome_email(self, mock_send_mail, user):
"""Test welcome email task"""
mock_send_mail.return_value = 1
result = send_welcome_email(
user_email=user.email,
username=user.username
)
assert result is True
mock_send_mail.assert_called_once()
@patch('core.tasks.logger')
def test_process_user_data(self, mock_logger, user):
"""Test user data processing task"""
result = process_user_data(user_id=user.id)
assert result['status'] == 'success'
assert result['user_id'] == user.id
mock_logger.info.assert_called()
Docker Deployment Configuration
# Dockerfile
FROM python:3.12-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND=noninteractive
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create and set working directory
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
# Copy project files
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
# Run as non-root user
RUN useradd -m django && chown -R django:django /app
USER django
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
# Run application
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "config.wsgi:application"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=False
- DATABASE_URL=postgres://postgres:password@db:5432/django_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/media
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=django_db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
celery:
build: .
command: celery -A config worker --loglevel=info
environment:
- DEBUG=False
- DATABASE_URL=postgres://postgres:password@db:5432/django_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/staticfiles
- media_volume:/app/media
- ./ssl:/etc/nginx/ssl
depends_on:
- web
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream django {
server web:8000;
}
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /app/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /app/media/;
expires 30d;
add_header Cache-Control "public";
}
}
}
FAQs: Django 6.0 Development
What are the major new features in Django 6.0?
Django 6.0 introduces Template Partials for reusable components, a built-in Background Tasks framework, Content Security Policy support, modern Email API integration, and enhanced security features including increased PBKDF2 iterations.
How do template partials differ from template includes?
Template partials allow defining reusable components within the same template file
using {% partialdef %} and {% partial %} tags. They're more lightweight
than separate template files and support the template_name#partial_name syntax for
referencing.
What Python versions are supported in Django 6.0?
Django 6.0 supports Python 3.12, 3.13, and 3.14. Python 3.10 and 3.11 support ended with Django 5.2. Always use the latest patch version of each supported Python series.
How do I implement HTMX with Django 6.0?
Use HTMX attributes in your templates and create dedicated views that return HTML fragments. Combine with Django 6.0's template partials for optimal server-rendered interactivity. Ensure your CSP settings allow HTMX CDN sources.
What's the best database for Django in 2025?
PostgreSQL remains the top choice (76% usage) for production, while SQLite (42%) is great for development and small projects. Consider MongoDB (8%) if you need NoSQL capabilities.
How do I upgrade from Django 5.x to 6.0?
Test with python -Wd to see deprecation warnings, update requirements to
Django 6.0, run tests, check for breaking changes in the release notes, update third-party packages,
and deploy gradually.
Conclusion
Django 6.0 represents a significant step forward for the framework, combining modern web development trends with Django's signature stability and developer experience. The new features template partials, background tasks, and CSP support address real-world development needs while maintaining backward compatibility.
For StalkTechie readers, embracing Django 6.0 means leveraging cutting-edge features while building on a proven foundation. The framework's evolution towards better frontend integration (HTMX, template partials), enhanced security (CSP), and modern async patterns (background tasks) ensures Django remains relevant and powerful for web development in 2025 and beyond.
Remember that Django's strength lies in its ecosystem and community. With 75% of developers already on the latest version, you're joining a forward-thinking community that values stability, security, and continuous improvement. Whether you're building APIs, server-rendered applications, or hybrid systems, Django 6.0 provides the tools and patterns you need for success.