A Django content management system with a user-friendly interface and powerful features for building websites and applications.
REST API endpoints for headless CMS functionality with comprehensive serializers for pages, images, documents, and custom content. Wagtail's API framework enables decoupled front-end applications and third-party integrations.
Base classes and viewsets for creating REST API endpoints.
class BaseAPIViewSet(GenericViewSet):
"""
Base viewset for Wagtail API endpoints with pagination, filtering, and authentication.
Provides common functionality for all API endpoints including:
- Pagination support
- Field filtering and expansion
- Search capabilities
- Content negotiation
"""
base_serializer_class: BaseSerializer
filter_backends: list
known_query_parameters: set
def get_queryset(self):
"""Get the base queryset for this endpoint."""
def get_serializer_class(self):
"""Get the serializer class for the current request."""
def filter_queryset(self, queryset):
"""Apply filters to the queryset."""
def paginate_queryset(self, queryset):
"""Apply pagination to the queryset."""
class PagesAPIViewSet(BaseAPIViewSet):
"""
REST API endpoint for pages with hierarchical navigation and content access.
Supports:
- Page listing with filtering
- Individual page retrieval
- Hierarchical navigation (children, ancestors, descendants)
- Content serialization with StreamField support
"""
base_serializer_class: PageSerializer
model: Page
def get_queryset(self):
"""Get live pages accessible to the current user."""
def find_view(self, request):
"""Find pages by slug or path."""
def listing_view(self, request):
"""List pages with filtering and pagination."""
def detail_view(self, request, pk):
"""Get individual page details."""
class ImagesAPIViewSet(BaseAPIViewSet):
"""
REST API endpoint for images with rendition generation and metadata access.
Supports:
- Image listing and search
- Image metadata access
- Rendition URLs for different sizes
- Collection-based filtering
"""
base_serializer_class: ImageSerializer
model: Image
def get_queryset(self):
"""Get images accessible to the current user."""
class DocumentsAPIViewSet(BaseAPIViewSet):
"""
REST API endpoint for documents with download URLs and metadata.
Supports:
- Document listing and search
- Document metadata access
- Download URLs
- Collection-based filtering
"""
base_serializer_class: DocumentSerializer
model: Document
def get_queryset(self):
"""Get documents accessible to the current user."""Serializer classes for converting models to JSON representations.
class BaseSerializer(serializers.ModelSerializer):
"""
Base serializer for API responses with field selection and expansion.
Features:
- Dynamic field selection via 'fields' parameter
- Field expansion for related objects
- Consistent error handling
"""
def __init__(self, *args, **kwargs):
"""Initialize serializer with dynamic field configuration."""
def to_representation(self, instance):
"""Convert model instance to dictionary representation."""
class PageSerializer(BaseSerializer):
"""
Serializer for Page models with content and metadata serialization.
Handles:
- Page hierarchy information
- StreamField content serialization
- Rich text field processing
- Image and document references
"""
id: int
meta: dict
title: str
html_url: str
slug: str
url_path: str
seo_title: str
search_description: str
show_in_menus: bool
first_published_at: datetime
last_published_at: datetime
def get_meta(self, instance):
"""Get metadata about the page type and properties."""
def get_html_url(self, instance):
"""Get the full HTML URL for this page."""
class ImageSerializer(BaseSerializer):
"""
Serializer for Image models with rendition support.
Provides:
- Image metadata (dimensions, file size, etc.)
- Rendition generation for different sizes
- Collection and tag information
"""
id: int
meta: dict
title: str
original: dict
thumbnail: dict
width: int
height: int
created_at: datetime
def get_original(self, instance):
"""Get original image URL and metadata."""
def get_thumbnail(self, instance):
"""Get thumbnail rendition URL."""
class DocumentSerializer(BaseSerializer):
"""
Serializer for Document models with download information.
Provides:
- Document metadata (file size, type, etc.)
- Download URLs
- Collection information
"""
id: int
meta: dict
title: str
download_url: str
created_at: datetime
def get_download_url(self, instance):
"""Get secure download URL for this document."""Classes for configuring API field exposure and customization.
class APIField:
"""
Configuration for exposing model fields through the API.
Controls how model fields are serialized and what data is included.
"""
def __init__(self, name, serializer=None):
"""
Initialize API field configuration.
Parameters:
name (str): Name of the field to expose
serializer (Serializer): Custom serializer for this field
"""
class WagtailAPIRouter:
"""
URL router for configuring API endpoints and routing.
Manages URL patterns and endpoint registration for the API.
"""
def __init__(self, name='wagtailapi'):
"""
Initialize API router.
Parameters:
name (str): Name for the API URL namespace
"""
def register_endpoint(self, prefix, viewset):
"""
Register an API endpoint.
Parameters:
prefix (str): URL prefix for the endpoint
viewset (ViewSet): ViewSet class handling the endpoint
"""
@property
def urls(self):
"""Get URL patterns for all registered endpoints."""Filter classes for querying and searching API endpoints.
class FieldsFilter:
"""
Filter for selecting specific fields in API responses.
Allows clients to request only the fields they need.
"""
def filter_queryset(self, request, queryset, view):
"""Apply field selection to the response."""
class ChildOfFilter:
"""
Filter for finding pages that are children of a specific page.
"""
def filter_queryset(self, request, queryset, view):
"""Filter to pages that are children of specified parent."""
class DescendantOfFilter:
"""
Filter for finding pages that are descendants of a specific page.
"""
def filter_queryset(self, request, queryset, view):
"""Filter to pages that are descendants of specified ancestor."""
class OrderingFilter:
"""
Filter for ordering API results by specified fields.
"""
def filter_queryset(self, request, queryset, view):
"""Apply ordering to the queryset."""
class SearchFilter:
"""
Filter for full-text search across API endpoints.
"""
def filter_queryset(self, request, queryset, view):
"""Apply search query to the queryset."""Utility functions and classes for API functionality.
def get_base_url(request=None):
"""
Get the base URL for API endpoints.
Parameters:
request (HttpRequest): Current request object
Returns:
str: Base URL for the API
"""
def get_object_detail_url(context, model, pk, url_name=None):
"""
Get detail URL for an API object.
Parameters:
context (dict): Serializer context
model (Model): Model class
pk (int): Primary key of the object
url_name (str): URL name for the endpoint
Returns:
str: Detail URL for the object
"""
class BadRequestError(Exception):
"""Exception for API bad request errors."""
class NotFoundError(Exception):
"""Exception for API not found errors."""# urls.py
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.images.api.v2.views import ImagesAPIViewSet
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
# Create API router
api_router = WagtailAPIRouter('wagtailapi')
# Register default endpoints
api_router.register_endpoint('pages', PagesAPIViewSet)
api_router.register_endpoint('images', ImagesAPIViewSet)
api_router.register_endpoint('documents', DocumentsAPIViewSet)
# Add to URL patterns
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v2/', api_router.urls),
path('', include(wagtail_urls)),
]from wagtail.api.v2.views import BaseAPIViewSet
from wagtail.api.v2.serializers import BaseSerializer
from rest_framework.fields import CharField, DateTimeField
from myapp.models import BlogPage, Author
class AuthorSerializer(BaseSerializer):
"""Custom serializer for Author model."""
name = CharField(read_only=True)
bio = CharField(read_only=True)
posts_count = serializers.SerializerMethodField()
def get_posts_count(self, instance):
"""Get number of blog posts by this author."""
return BlogPage.objects.filter(author=instance).count()
class AuthorAPIViewSet(BaseAPIViewSet):
"""Custom API endpoint for authors."""
base_serializer_class = AuthorSerializer
filter_backends = [SearchFilter, OrderingFilter]
model = Author
known_query_parameters = BaseAPIViewSet.known_query_parameters.union([
'name', 'bio'
])
def get_queryset(self):
return Author.objects.all()
# Register custom endpoint
api_router.register_endpoint('authors', AuthorAPIViewSet)from wagtail.api.v2.serializers import BaseSerializer
from wagtail.api import APIField
from wagtail.models import Page
from wagtail.fields import StreamField
from rest_framework.fields import CharField
class BlogPage(Page):
"""Blog page with API field configuration."""
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = StreamField([
('heading', CharBlock()),
('paragraph', RichTextBlock()),
('image', ImageChooserBlock()),
])
author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
# Configure API field exposure
api_fields = [
APIField('date'),
APIField('intro'),
APIField('body'), # StreamField automatically serialized
APIField('author', serializer=AuthorSerializer()),
]
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('intro'),
FieldPanel('body'),
FieldPanel('author'),
]
# Custom serializer for complex fields
class BlogPageSerializer(BaseSerializer):
"""Custom serializer for blog pages."""
reading_time = serializers.SerializerMethodField()
related_posts = serializers.SerializerMethodField()
def get_reading_time(self, instance):
"""Calculate estimated reading time."""
word_count = len(str(instance.body).split())
return max(1, word_count // 200) # Assume 200 words per minute
def get_related_posts(self, instance):
"""Get related blog posts."""
related = BlogPage.objects.live().exclude(pk=instance.pk)[:3]
return [{'title': post.title, 'url': post.url} for post in related]import requests
# Base API URL
base_url = 'https://example.com/api/v2/'
# Get all pages
response = requests.get(f'{base_url}pages/')
pages = response.json()
# Get specific page
page_id = 123
response = requests.get(f'{base_url}pages/{page_id}/')
page = response.json()
# Search pages
response = requests.get(f'{base_url}pages/', params={
'search': 'django tutorial',
'type': 'blog.BlogPage',
'fields': 'title,slug,date,author'
})
search_results = response.json()
# Get page children
parent_id = 10
response = requests.get(f'{base_url}pages/', params={
'child_of': parent_id,
'limit': 20
})
children = response.json()
# Get images with renditions
response = requests.get(f'{base_url}images/')
images = response.json()
# Get specific image
image_id = 456
response = requests.get(f'{base_url}images/{image_id}/')
image = response.json()
print(f"Original: {image['meta']['download_url']}")
print(f"Thumbnail: {image['meta']['thumbnail']['url']}")
# Filter by collection
collection_id = 5
response = requests.get(f'{base_url}images/', params={
'collection_id': collection_id
})
collection_images = response.json()// API client class
class WagtailAPI {
constructor(baseURL) {
this.baseURL = baseURL;
}
async getPages(params = {}) {
const url = new URL(`${this.baseURL}pages/`);
Object.keys(params).forEach(key => {
url.searchParams.append(key, params[key]);
});
const response = await fetch(url);
return response.json();
}
async getPage(id, fields = []) {
const url = new URL(`${this.baseURL}pages/${id}/`);
if (fields.length > 0) {
url.searchParams.append('fields', fields.join(','));
}
const response = await fetch(url);
return response.json();
}
async searchPages(query, type = null) {
return this.getPages({
search: query,
...(type && { type: type })
});
}
async getImages(params = {}) {
const url = new URL(`${this.baseURL}images/`);
Object.keys(params).forEach(key => {
url.searchParams.append(key, params[key]);
});
const response = await fetch(url);
return response.json();
}
}
// Usage example
const api = new WagtailAPI('https://example.com/api/v2/');
// Load blog posts
async function loadBlogPosts() {
try {
const data = await api.getPages({
type: 'blog.BlogPage',
fields: 'title,slug,date,intro,author',
limit: 10,
order: '-date'
});
const posts = data.items.map(post => ({
title: post.title,
slug: post.slug,
date: post.date,
intro: post.intro,
author: post.author?.name || 'Unknown',
url: post.meta.html_url
}));
renderBlogPosts(posts);
} catch (error) {
console.error('Failed to load blog posts:', error);
}
}
// Search functionality
async function searchContent(query) {
try {
const [pages, images] = await Promise.all([
api.searchPages(query),
api.getImages({ search: query })
]);
return {
pages: pages.items,
images: images.items
};
} catch (error) {
console.error('Search failed:', error);
}
}from wagtail.api.v2.views import BaseAPIViewSet
from wagtail.api.v2.filters import BaseFilterSet
from django_filters import rest_framework as filters
class BlogPageFilter(BaseFilterSet):
"""Custom filter for blog pages."""
date_from = filters.DateFilter(field_name='date', lookup_expr='gte')
date_to = filters.DateFilter(field_name='date', lookup_expr='lte')
author_name = filters.CharFilter(field_name='author__name', lookup_expr='icontains')
class Meta:
model = BlogPage
fields = ['date_from', 'date_to', 'author_name']
class BlogPageAPIViewSet(PagesAPIViewSet):
"""Enhanced blog page API with custom filtering."""
filter_backends = PagesAPIViewSet.filter_backends + [filters.DjangoFilterBackend]
filterset_class = BlogPageFilter
known_query_parameters = PagesAPIViewSet.known_query_parameters.union([
'date_from', 'date_to', 'author_name'
])
def get_queryset(self):
return BlogPage.objects.live().select_related('author')
# Custom endpoint with authentication
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
class PrivateContentAPIViewSet(BaseAPIViewSet):
"""API endpoint requiring authentication."""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get_queryset(self):
# Return content based on user permissions
user = self.request.user
return Page.objects.live().filter(
owner=user
)from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 15), name='listing_view') # 15 minutes
@method_decorator(cache_page(60 * 60), name='detail_view') # 1 hour
class CachedPagesAPIViewSet(PagesAPIViewSet):
"""API viewset with response caching."""
def get_queryset(self):
# Add cache invalidation logic
return super().get_queryset().select_related('locale')
# Custom cache invalidation
from django.core.cache import cache
from wagtail.signals import page_published, page_unpublished
def invalidate_api_cache(sender, **kwargs):
"""Invalidate API cache when pages are published/unpublished."""
cache.delete_pattern('views.decorators.cache.cache_page.*')
page_published.connect(invalidate_api_cache)
page_unpublished.connect(invalidate_api_cache)Install with Tessl CLI
npx tessl i tessl/pypi-wagtail