Configuring URLs for class-based views requires understanding the as_view() method, parameter passing, and URL pattern organization. This chapter covers comprehensive URL configuration strategies for CBVs.
# urls.py
from django.urls import path, include
from . import views
app_name = 'blog'
urlpatterns = [
# Basic patterns
path('', views.PostListView.as_view(), name='post_list'),
path('create/', views.PostCreateView.as_view(), name='post_create'),
# Patterns with parameters
path('<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_edit'),
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
# Slug-based patterns
path('<slug:slug>/', views.PostBySlugView.as_view(), name='post_by_slug'),
path('category/<slug:category_slug>/', views.CategoryPostsView.as_view(), name='category_posts'),
# Date-based patterns
path('<int:year>/', views.PostYearArchiveView.as_view(), name='posts_by_year'),
path('<int:year>/<int:month>/', views.PostMonthArchiveView.as_view(), name='posts_by_month'),
path('<int:year>/<int:month>/<int:day>/', views.PostDayArchiveView.as_view(), name='posts_by_day'),
]
# Main project urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
path('blog/', include('blog.urls')),
path('accounts/', include('accounts.urls')),
path('api/', include('api.urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# blog/urls.py - Organized by functionality
from django.urls import path, include
from . import views
app_name = 'blog'
# Post URLs
post_patterns = [
path('', views.PostListView.as_view(), name='list'),
path('create/', views.PostCreateView.as_view(), name='create'),
path('<int:pk>/', views.PostDetailView.as_view(), name='detail'),
path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='edit'),
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='delete'),
path('<slug:slug>/', views.PostBySlugView.as_view(), name='by_slug'),
]
# Category URLs
category_patterns = [
path('', views.CategoryListView.as_view(), name='list'),
path('create/', views.CategoryCreateView.as_view(), name='create'),
path('<slug:slug>/', views.CategoryDetailView.as_view(), name='detail'),
path('<slug:slug>/posts/', views.CategoryPostsView.as_view(), name='posts'),
]
# Archive URLs
archive_patterns = [
path('', views.ArchiveIndexView.as_view(), name='index'),
path('<int:year>/', views.YearArchiveView.as_view(), name='year'),
path('<int:year>/<int:month>/', views.MonthArchiveView.as_view(), name='month'),
path('<int:year>/<int:month>/<int:day>/', views.DayArchiveView.as_view(), name='day'),
]
# Main URL patterns
urlpatterns = [
path('', views.BlogHomeView.as_view(), name='home'),
path('posts/', include((post_patterns, 'posts'))),
path('categories/', include((category_patterns, 'categories'))),
path('archive/', include((archive_patterns, 'archive'))),
path('search/', views.SearchView.as_view(), name='search'),
path('feed/', views.RSSFeedView.as_view(), name='rss_feed'),
]
# views.py
class ConfigurableListView(ListView):
"""Configurable list view"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
def get_queryset(self):
# Use view configuration
status = getattr(self, 'status_filter', 'published')
return Post.objects.filter(status=status)
# urls.py - Configure views through as_view()
urlpatterns = [
# Published posts (default)
path('', views.ConfigurableListView.as_view(), name='published_posts'),
# Draft posts
path('drafts/', views.ConfigurableListView.as_view(
status_filter='draft',
template_name='blog/draft_list.html'
), name='draft_posts'),
# All posts for staff
path('all/', views.ConfigurableListView.as_view(
status_filter=None,
template_name='blog/all_posts.html',
paginate_by=50
), name='all_posts'),
# Featured posts
path('featured/', views.ConfigurableListView.as_view(
template_name='blog/featured_posts.html',
context_object_name='featured_posts'
), name='featured_posts'),
]
# Advanced configuration
class FlexibleDetailView(DetailView):
"""Flexible detail view with multiple configurations"""
model = Post
def get_template_names(self):
"""Dynamic template selection"""
if hasattr(self, 'template_prefix'):
return [f'{self.template_prefix}/post_detail.html']
return super().get_template_names()
def get_context_object_name(self, obj):
"""Dynamic context object name"""
return getattr(self, 'context_name', 'post')
# Multiple configurations of the same view
urlpatterns = [
# Standard post detail
path('posts/<int:pk>/', views.FlexibleDetailView.as_view(), name='post_detail'),
# Admin post detail with different template
path('admin/posts/<int:pk>/', views.FlexibleDetailView.as_view(
template_prefix='admin',
context_name='admin_post'
), name='admin_post_detail'),
# Mobile post detail
path('mobile/posts/<int:pk>/', views.FlexibleDetailView.as_view(
template_prefix='mobile',
context_name='mobile_post'
), name='mobile_post_detail'),
]
# Dynamic URL generation based on settings
def get_blog_urls():
"""Generate blog URLs based on configuration"""
patterns = [
path('', views.PostListView.as_view(), name='post_list'),
]
# Add category URLs if categories are enabled
if getattr(settings, 'BLOG_ENABLE_CATEGORIES', True):
patterns.extend([
path('category/<slug:slug>/', views.CategoryPostsView.as_view(), name='category_posts'),
path('categories/', views.CategoryListView.as_view(), name='category_list'),
])
# Add tag URLs if tags are enabled
if getattr(settings, 'BLOG_ENABLE_TAGS', True):
patterns.extend([
path('tag/<slug:slug>/', views.TagPostsView.as_view(), name='tag_posts'),
path('tags/', views.TagListView.as_view(), name='tag_list'),
])
# Add archive URLs if archives are enabled
if getattr(settings, 'BLOG_ENABLE_ARCHIVES', True):
patterns.extend([
path('archive/', views.ArchiveIndexView.as_view(), name='archive_index'),
path('archive/<int:year>/', views.YearArchiveView.as_view(), name='year_archive'),
])
return patterns
# Use dynamic URLs
urlpatterns = get_blog_urls()
# Conditional URL inclusion
from django.conf import settings
urlpatterns = [
path('', views.HomeView.as_view(), name='home'),
]
# Add debug URLs in development
if settings.DEBUG:
urlpatterns += [
path('debug/', views.DebugView.as_view(), name='debug'),
path('test/', views.TestView.as_view(), name='test'),
]
# Add API URLs if API is enabled
if getattr(settings, 'ENABLE_API', False):
urlpatterns += [
path('api/', include('api.urls')),
]
# RESTful URL configuration for CBVs
from django.urls import path
from . import views
app_name = 'api'
urlpatterns = [
# Collection endpoints
path('posts/', views.PostListCreateView.as_view(), name='post_list_create'),
path('categories/', views.CategoryListCreateView.as_view(), name='category_list_create'),
# Resource endpoints
path('posts/<int:pk>/', views.PostRetrieveUpdateDestroyView.as_view(), name='post_detail'),
path('categories/<int:pk>/', views.CategoryRetrieveUpdateDestroyView.as_view(), name='category_detail'),
# Nested resource endpoints
path('posts/<int:post_pk>/comments/', views.CommentListCreateView.as_view(), name='post_comments'),
path('posts/<int:post_pk>/comments/<int:pk>/', views.CommentDetailView.as_view(), name='comment_detail'),
# Action endpoints
path('posts/<int:pk>/publish/', views.PostPublishView.as_view(), name='post_publish'),
path('posts/<int:pk>/unpublish/', views.PostUnpublishView.as_view(), name='post_unpublish'),
path('posts/<int:pk>/like/', views.PostLikeView.as_view(), name='post_like'),
]
# Corresponding views
class PostListCreateView(ListCreateAPIView):
"""List posts or create new post"""
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostRetrieveUpdateDestroyView(RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete a post"""
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostPublishView(UpdateView):
"""Publish a post"""
model = Post
fields = []
def form_valid(self, form):
self.object.status = 'published'
self.object.published_at = timezone.now()
self.object.save()
return JsonResponse({'status': 'published'})
# API versioning with CBVs
from django.urls import path, include
# Version 1 URLs
v1_patterns = [
path('posts/', views.v1.PostListView.as_view(), name='post_list'),
path('posts/<int:pk>/', views.v1.PostDetailView.as_view(), name='post_detail'),
]
# Version 2 URLs
v2_patterns = [
path('posts/', views.v2.PostListView.as_view(), name='post_list'),
path('posts/<int:pk>/', views.v2.PostDetailView.as_view(), name='post_detail'),
path('posts/<int:pk>/analytics/', views.v2.PostAnalyticsView.as_view(), name='post_analytics'),
]
# Main API URLs
urlpatterns = [
path('v1/', include((v1_patterns, 'v1'))),
path('v2/', include((v2_patterns, 'v2'))),
# Default to latest version
path('', include((v2_patterns, 'latest'))),
]
# Version-specific views
class BasePostView:
"""Base post view with common functionality"""
model = Post
def get_queryset(self):
return Post.objects.filter(status='published')
# Version 1 views
class v1:
class PostListView(BasePostView, ListView):
template_name = 'api/v1/post_list.html'
class PostDetailView(BasePostView, DetailView):
template_name = 'api/v1/post_detail.html'
# Version 2 views
class v2:
class PostListView(BasePostView, ListView):
template_name = 'api/v2/post_list.html'
paginate_by = 20 # Different pagination
class PostDetailView(BasePostView, DetailView):
template_name = 'api/v2/post_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['analytics'] = self.get_analytics_data()
return context
class PostAnalyticsView(BasePostView, DetailView):
template_name = 'api/v2/post_analytics.html'
# Complex URL patterns with multiple parameters
from django.urls import path, re_path
from . import views
urlpatterns = [
# Multi-parameter patterns
path('posts/<int:year>/<int:month>/<slug:slug>/',
views.PostByDateAndSlugView.as_view(),
name='post_by_date_slug'),
# Optional parameters using re_path
re_path(r'^search/(?P<query>[\w\s]+)(?:/page/(?P<page>\d+))?/$',
views.SearchView.as_view(),
name='search'),
# User-specific patterns
path('users/<str:username>/posts/',
views.UserPostsView.as_view(),
name='user_posts'),
path('users/<str:username>/posts/<slug:slug>/',
views.UserPostDetailView.as_view(),
name='user_post_detail'),
# Hierarchical patterns
path('categories/<slug:category>/subcategories/<slug:subcategory>/posts/',
views.SubcategoryPostsView.as_view(),
name='subcategory_posts'),
# Format-specific patterns
re_path(r'^posts/(?P<pk>\d+)\.(?P<format>json|xml|html)$',
views.PostDetailFormatView.as_view(),
name='post_detail_format'),
]
# Corresponding views
class PostByDateAndSlugView(DetailView):
"""Get post by date and slug"""
model = Post
def get_object(self):
return get_object_or_404(
Post,
created_at__year=self.kwargs['year'],
created_at__month=self.kwargs['month'],
slug=self.kwargs['slug'],
status='published'
)
class SearchView(ListView):
"""Search with optional pagination"""
model = Post
template_name = 'blog/search_results.html'
paginate_by = 10
def get_queryset(self):
query = self.kwargs.get('query', '')
return Post.objects.filter(
title__icontains=query,
status='published'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query'] = self.kwargs.get('query', '')
return context
class UserPostsView(ListView):
"""Posts by specific user"""
model = Post
template_name = 'blog/user_posts.html'
def get_queryset(self):
username = self.kwargs['username']
return Post.objects.filter(
author__username=username,
status='published'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['author'] = get_object_or_404(User, username=self.kwargs['username'])
return context
class PostDetailFormatView(DetailView):
"""Post detail with format support"""
model = Post
def render_to_response(self, context, **response_kwargs):
format_type = self.kwargs.get('format', 'html')
if format_type == 'json':
data = {
'id': self.object.id,
'title': self.object.title,
'content': self.object.content,
'author': self.object.author.username,
}
return JsonResponse(data)
elif format_type == 'xml':
# Return XML response
xml_content = f'''<?xml version="1.0"?>
<post>
<id>{self.object.id}</id>
<title>{self.object.title}</title>
<content>{self.object.content}</content>
</post>'''
return HttpResponse(xml_content, content_type='application/xml')
# Default HTML response
return super().render_to_response(context, **response_kwargs)
# Complex namespace organization
# main urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
path('blog/', include('blog.urls', namespace='blog')),
path('api/', include('api.urls', namespace='api')),
]
# blog/urls.py
app_name = 'blog'
# Admin blog URLs
admin_patterns = [
path('', views.AdminDashboardView.as_view(), name='dashboard'),
path('posts/', views.AdminPostListView.as_view(), name='post_list'),
path('posts/<int:pk>/', views.AdminPostDetailView.as_view(), name='post_detail'),
path('users/', views.AdminUserListView.as_view(), name='user_list'),
]
# Public blog URLs
public_patterns = [
path('', views.PostListView.as_view(), name='home'),
path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
path('categories/', views.CategoryListView.as_view(), name='category_list'),
]
# Author blog URLs
author_patterns = [
path('', views.AuthorDashboardView.as_view(), name='dashboard'),
path('posts/', views.AuthorPostListView.as_view(), name='post_list'),
path('posts/create/', views.AuthorPostCreateView.as_view(), name='post_create'),
path('posts/<int:pk>/edit/', views.AuthorPostEditView.as_view(), name='post_edit'),
]
urlpatterns = [
path('', include((public_patterns, 'public'))),
path('admin/', include((admin_patterns, 'admin'))),
path('author/', include((author_patterns, 'author'))),
]
# Usage in templates and views:
# {% url 'blog:public:home' %}
# {% url 'blog:admin:dashboard' %}
# {% url 'blog:author:post_create' %}
# Dynamic namespace based on user role
class RoleBasedURLMixin:
"""Mixin to handle role-based URL generation"""
def get_success_url(self):
"""Generate success URL based on user role"""
if self.request.user.is_staff:
return reverse('blog:admin:post_list')
elif hasattr(self.request.user, 'profile') and self.request.user.profile.is_author:
return reverse('blog:author:post_list')
else:
return reverse('blog:public:home')
class SmartRedirectMixin:
"""Mixin for intelligent redirects"""
def get_redirect_namespace(self):
"""Determine appropriate namespace for redirect"""
user = self.request.user
if user.is_staff:
return 'blog:admin'
elif hasattr(user, 'profile') and user.profile.is_author:
return 'blog:author'
else:
return 'blog:public'
def get_success_url(self):
namespace = self.get_redirect_namespace()
return reverse(f'{namespace}:dashboard')
# URL pattern generation based on permissions
def generate_permission_urls():
"""Generate URLs based on available permissions"""
patterns = []
# Always include public URLs
patterns.extend([
path('', views.PostListView.as_view(), name='post_list'),
path('<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
])
# Add creation URLs if user can add posts
if Permission.objects.filter(codename='add_post').exists():
patterns.append(
path('create/', views.PostCreateView.as_view(), name='post_create')
)
# Add edit URLs if user can change posts
if Permission.objects.filter(codename='change_post').exists():
patterns.append(
path('<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_edit')
)
# Add delete URLs if user can delete posts
if Permission.objects.filter(codename='delete_post').exists():
patterns.append(
path('<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete')
)
return patterns
# tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from django.contrib.auth.models import User
from blog.models import Post, Category
class URLTests(TestCase):
"""Test URL patterns and resolution"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass'
)
self.category = Category.objects.create(
name='Test Category',
slug='test-category'
)
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user,
category=self.category,
status='published'
)
def test_post_list_url(self):
"""Test post list URL resolution"""
url = reverse('blog:post_list')
self.assertEqual(url, '/blog/')
resolver = resolve('/blog/')
self.assertEqual(resolver.view_name, 'blog:post_list')
def test_post_detail_url(self):
"""Test post detail URL with parameters"""
url = reverse('blog:post_detail', kwargs={'pk': self.post.pk})
self.assertEqual(url, f'/blog/{self.post.pk}/')
resolver = resolve(f'/blog/{self.post.pk}/')
self.assertEqual(resolver.view_name, 'blog:post_detail')
self.assertEqual(int(resolver.kwargs['pk']), self.post.pk)
def test_category_posts_url(self):
"""Test category posts URL with slug"""
url = reverse('blog:category_posts', kwargs={'slug': self.category.slug})
self.assertEqual(url, f'/blog/category/{self.category.slug}/')
resolver = resolve(f'/blog/category/{self.category.slug}/')
self.assertEqual(resolver.view_name, 'blog:category_posts')
self.assertEqual(resolver.kwargs['slug'], self.category.slug)
def test_date_archive_url(self):
"""Test date-based archive URLs"""
year_url = reverse('blog:posts_by_year', kwargs={'year': 2024})
self.assertEqual(year_url, '/blog/2024/')
month_url = reverse('blog:posts_by_month', kwargs={'year': 2024, 'month': 3})
self.assertEqual(month_url, '/blog/2024/3/')
def test_nested_namespace_urls(self):
"""Test nested namespace URL resolution"""
admin_url = reverse('blog:admin:dashboard')
self.assertEqual(admin_url, '/blog/admin/')
author_url = reverse('blog:author:post_create')
self.assertEqual(author_url, '/blog/author/posts/create/')
def test_url_parameters_validation(self):
"""Test URL parameter validation"""
# Valid parameters
valid_resolver = resolve('/blog/123/')
self.assertEqual(valid_resolver.view_name, 'blog:post_detail')
self.assertEqual(valid_resolver.kwargs['pk'], '123')
# Invalid parameters should not match
from django.urls.exceptions import Resolver404
with self.assertRaises(Resolver404):
resolve('/blog/invalid/')
def test_format_specific_urls(self):
"""Test format-specific URL patterns"""
json_url = reverse('blog:post_detail_format',
kwargs={'pk': self.post.pk, 'format': 'json'})
self.assertEqual(json_url, f'/blog/posts/{self.post.pk}.json')
xml_url = reverse('blog:post_detail_format',
kwargs={'pk': self.post.pk, 'format': 'xml'})
self.assertEqual(xml_url, f'/blog/posts/{self.post.pk}.xml')
class URLAccessTests(TestCase):
"""Test URL access and permissions"""
def setUp(self):
self.user = User.objects.create_user('user', 'user@example.com', 'pass')
self.staff = User.objects.create_user('staff', 'staff@example.com', 'pass', is_staff=True)
def test_public_url_access(self):
"""Test public URL access"""
response = self.client.get(reverse('blog:post_list'))
self.assertEqual(response.status_code, 200)
def test_login_required_url_access(self):
"""Test login-required URL access"""
# Anonymous user should be redirected
response = self.client.get(reverse('blog:post_create'))
self.assertEqual(response.status_code, 302)
# Authenticated user should have access
self.client.login(username='user', password='pass')
response = self.client.get(reverse('blog:post_create'))
self.assertEqual(response.status_code, 200)
def test_staff_only_url_access(self):
"""Test staff-only URL access"""
# Regular user should be denied
self.client.login(username='user', password='pass')
response = self.client.get(reverse('blog:admin:dashboard'))
self.assertEqual(response.status_code, 403)
# Staff user should have access
self.client.login(username='staff', password='pass')
response = self.client.get(reverse('blog:admin:dashboard'))
self.assertEqual(response.status_code, 200)
URL configuration for class-based views provides powerful routing capabilities while maintaining clean, organized patterns. Understanding parameter passing, namespacing, and advanced patterns enables you to build scalable, maintainable URL structures that support complex application requirements.
Using Mixins
Mixins are a powerful feature of Django's class-based views that enable code reuse through multiple inheritance. They provide a way to compose functionality from different sources, creating flexible and maintainable view hierarchies.
Subclassing Generic Views
Subclassing Django's generic views allows you to customize behavior while leveraging built-in functionality. This chapter covers advanced customization techniques, method overrides, and creating reusable view hierarchies.