Learn advanced Django features including admin interface, forms, authentication, permissions, and REST API development for web development.
Learn advanced Django features including admin interface, forms, authentication, permissions, and REST API development for web development.
Master Django's built-in admin interface for managing your application's data and users.
Content by: Manali Trivedi
Python Django Developer
Django's admin interface is a powerful, automatically generated web interface for managing your application's data. It's highly customizable and provides a user-friendly way to interact with your models.
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Post, Category, Tag, Comment
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'status', 'created_date', 'published_date']
list_filter = ['status', 'created_date', 'category', 'author']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'created_date'
ordering = ['-created_date']
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'content', 'excerpt')
}),
('Metadata', {
'fields': ('author', 'category', 'tags', 'status')
}),
('Publishing', {
'fields': ('published_date',),
'classes': ('collapse',)
}),
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('author', 'category')
def save_model(self, request, obj, form, change):
if not change: # New object
obj.author = request.user
super().save_model(request, obj, form, change)
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count']
prepopulated_fields = {'slug': ('name',)}
def post_count(self, obj):
return obj.post_set.count()
post_count.short_description = 'Number of Posts'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'color_display', 'post_count']
search_fields = ['name']
def color_display(self, obj):
if obj.color:
return format_html(
'<span style="color: {};">{}</span>',
obj.color,
obj.color
)
return '-'
color_display.short_description = 'Color'
def post_count(self, obj):
return obj.post_set.count()
post_count.short_description = 'Number of Posts'
# Custom admin actions
@admin.action(description='Mark selected posts as published')
def make_published(modeladmin, request, queryset):
from django.utils import timezone
updated = queryset.update(status='published', published_date=timezone.now())
modeladmin.message_user(request, f'{updated} posts were successfully marked as published.')
@admin.action(description='Mark selected posts as draft')
def make_draft(modeladmin, request, queryset):
updated = queryset.update(status='draft', published_date=None)
modeladmin.message_user(request, f'{updated} posts were successfully marked as draft.')
# Add actions to PostAdmin
PostAdmin.actions = [make_published, make_draft]
# Custom Admin Dashboard
from django.contrib import admin
from django.contrib.admin import AdminSite
from django.utils.html import format_html
from django.urls import path
from django.shortcuts import render
from django.db.models import Count, Sum
from django.utils import timezone
from datetime import timedelta
class CustomAdminSite(AdminSite):
site_header = "My Django Site Administration"
site_title = "My Django Site Admin Portal"
index_title = "Welcome to My Django Site Administration"
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('dashboard/', self.admin_view(self.dashboard_view), name='dashboard'),
path('analytics/', self.admin_view(self.analytics_view), name='analytics'),
]
return custom_urls + urls
def dashboard_view(self, request):
"""Custom dashboard view"""
from .models import Post, User, Comment
# Get statistics
total_posts = Post.objects.count()
published_posts = Post.objects.filter(status='published').count()
total_users = User.objects.count()
total_comments = Comment.objects.count()
# Recent activity
recent_posts = Post.objects.order_by('-created_date')[:5]
recent_comments = Comment.objects.order_by('-created_date')[:5]
# Chart data
last_30_days = timezone.now() - timedelta(days=30)
daily_posts = Post.objects.filter(
created_date__gte=last_30_days
).extra(
select={'day': 'date(created_date)'}
).values('day').annotate(count=Count('id')).order_by('day')
context = {
'total_posts': total_posts,
'published_posts': published_posts,
'total_users': total_users,
'total_comments': total_comments,
'recent_posts': recent_posts,
'recent_comments': recent_comments,
'daily_posts': list(daily_posts),
}
return render(request, 'admin/dashboard.html', context)
def analytics_view(self, request):
"""Analytics view"""
from .models import Post, User
# User engagement metrics
user_engagement = User.objects.annotate(
post_count=Count('post'),
comment_count=Count('comment')
).order_by('-post_count')[:10]
# Category popularity
category_popularity = Post.objects.values('category__name').annotate(
post_count=Count('id')
).order_by('-post_count')
context = {
'user_engagement': user_engagement,
'category_popularity': category_popularity,
}
return render(request, 'admin/analytics.html', context)
# Register the custom admin site
admin_site = CustomAdminSite(name='custom_admin')
# Register models with custom admin site
admin_site.register(Post, PostAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
# Usage in urls.py:
# from .admin import admin_site
# urlpatterns = [
# path('admin/', admin_site.urls),
# ]
Test your understanding of this topic:
Learn to create and handle forms in Django, including form validation, custom widgets, and form processing.
Content by: Manali Trivedi
Python Django Developer
Django forms provide a way to generate HTML forms, validate user input, and convert the input to Python types. They handle the complexity of form creation, validation, and conversion.
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'tags', 'status']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter post title'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': 'Write your post content here...'
}),
'excerpt': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Brief summary of your post'
}),
'category': forms.Select(attrs={'class': 'form-control'}),
'tags': forms.SelectMultiple(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'})
}
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 10:
raise forms.ValidationError("Title must be at least 10 characters long.")
return title
def clean_content(self):
content = self.cleaned_data.get('content')
if len(content) < 100:
raise forms.ValidationError("Content must be at least 100 characters long.")
return content
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Write your comment...'
})
}
def clean_content(self):
content = self.cleaned_data.get('content')
if len(content.strip()) < 3:
raise forms.ValidationError("Comment must be at least 3 characters long.")
return content
class UserRegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=30, required=True)
last_name = forms.CharField(max_length=30, required=True)
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'password1', 'password2']
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError("A user with this email already exists.")
return email
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user
# Custom form widgets
class RichTextWidget(forms.Textarea):
class Media:
css = {
'all': ('css/rich-text-editor.css',)
}
js = ('js/rich-text-editor.js',)
def __init__(self, attrs=None):
default_attrs = {'class': 'rich-text-editor'}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
# Usage in views
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # Save many-to-many relationships
messages.success(request, 'Post created successfully!')
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
# Advanced Form Builder
from django import forms
from django.forms import BaseFormSet, formset_factory
from django.core.exceptions import ValidationError
class DynamicFormBuilder:
"""Build forms dynamically based on configuration"""
def __init__(self):
self.field_types = {
'text': forms.CharField,
'textarea': forms.CharField,
'email': forms.EmailField,
'number': forms.IntegerField,
'decimal': forms.DecimalField,
'date': forms.DateField,
'datetime': forms.DateTimeField,
'boolean': forms.BooleanField,
'choice': forms.ChoiceField,
'multiple_choice': forms.MultipleChoiceField,
'file': forms.FileField,
'image': forms.ImageField,
}
def build_form(self, field_configs):
"""Build a form class from field configurations"""
form_fields = {}
for field_config in field_configs:
field_name = field_config['name']
field_type = field_config['type']
field_options = field_config.get('options', {})
if field_type in self.field_types:
field_class = self.field_types[field_type]
# Handle special cases
if field_type == 'textarea':
field_options['widget'] = forms.Textarea
elif field_type == 'choice':
field_options['choices'] = field_config.get('choices', [])
elif field_type == 'multiple_choice':
field_options['choices'] = field_config.get('choices', [])
field_options['widget'] = forms.CheckboxSelectMultiple
# Add validation
if field_config.get('required', False):
field_options['required'] = True
if 'min_length' in field_config:
field_options['min_length'] = field_config['min_length']
if 'max_length' in field_config:
field_options['max_length'] = field_config['max_length']
form_fields[field_name] = field_class(**field_options)
return type('DynamicForm', (forms.Form,), form_fields)
# Example usage
field_configs = [
{
'name': 'title',
'type': 'text',
'required': True,
'max_length': 200,
'options': {
'label': 'Title',
'help_text': 'Enter the title of your post'
}
},
{
'name': 'content',
'type': 'textarea',
'required': True,
'min_length': 100,
'options': {
'label': 'Content',
'help_text': 'Write your post content here'
}
},
{
'name': 'category',
'type': 'choice',
'required': True,
'choices': [
('tech', 'Technology'),
('lifestyle', 'Lifestyle'),
('travel', 'Travel')
],
'options': {
'label': 'Category',
'help_text': 'Select a category for your post'
}
},
{
'name': 'tags',
'type': 'multiple_choice',
'choices': [
('python', 'Python'),
('django', 'Django'),
('web', 'Web Development'),
('tutorial', 'Tutorial')
],
'options': {
'label': 'Tags',
'help_text': 'Select relevant tags'
}
}
]
# Build the form
form_builder = DynamicFormBuilder()
DynamicPostForm = form_builder.build_form(field_configs)
# Form validation with custom logic
class AdvancedPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category']
def clean(self):
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
# Custom validation logic
if title and content:
# Check for duplicate content
if Post.objects.filter(title__iexact=title).exists():
raise ValidationError("A post with this title already exists.")
# Check content quality
word_count = len(content.split())
if word_count < 50:
raise ValidationError("Content must be at least 50 words long.")
# Check for spam keywords
spam_keywords = ['buy now', 'click here', 'free money']
content_lower = content.lower()
for keyword in spam_keywords:
if keyword in content_lower:
raise ValidationError("Content contains prohibited keywords.")
return cleaned_data
Test your understanding of this topic:
Implement user authentication, authorization, and permission systems in Django applications.
Content by: Manali Trivedi
Python Django Developer
Django provides a comprehensive authentication system that handles user accounts, groups, permissions, and sessions. It's highly customizable and secure by default.
# views.py
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import UserRegistrationForm, UserLoginForm
def register(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, 'Registration successful! Welcome to our site.')
return redirect('home')
else:
form = UserRegistrationForm()
return render(request, 'registration/register.html', {'form': form})
def user_login(request):
if request.method == 'POST':
form = UserLoginForm(request.POST)
if form.is_valid():
username = form.cleaned_data['username']
password = form.cleaned_data['password']
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'Welcome back, {user.username}!')
return redirect('home')
else:
messages.error(request, 'Invalid username or password.')
else:
form = UserLoginForm()
return render(request, 'registration/login.html', {'form': form})
@login_required
def user_logout(request):
logout(request)
messages.info(request, 'You have been logged out.')
return redirect('home')
@login_required
def profile(request):
return render(request, 'registration/profile.html')
# Permission-based views
@permission_required('blog.add_post')
def create_post(request):
# Only users with 'add_post' permission can access
pass
@permission_required('blog.change_post')
def edit_post(request, pk):
# Only users with 'change_post' permission can access
pass
# Class-based views with mixins
from django.views.generic import CreateView, UpdateView, DeleteView
class PostCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = 'blog.add_post'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = 'blog.change_post'
def get_queryset(self):
# Users can only edit their own posts
return Post.objects.filter(author=self.request.user)
# Custom permissions
class Post(models.Model):
# ... fields ...
class Meta:
permissions = [
("can_publish", "Can publish post"),
("can_feature", "Can feature post"),
("can_moderate", "Can moderate comments"),
]
# Permission checking in templates
"""
{% if perms.blog.add_post %}
<a href="{% url 'post_create' %}" class="btn btn-primary">Create Post</a>
{% endif %}
{% if perms.blog.change_post %}
<a href="{% url 'post_edit' post.pk %}" class="btn btn-secondary">Edit</a>
{% endif %}
{% if perms.blog.delete_post %}
<a href="{% url 'post_delete' post.pk %}" class="btn btn-danger">Delete</a>
{% endif %}
"""
# Role-Based Access Control (RBAC) System
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.db import models
from django.contrib.contenttypes.models import ContentType
class Role(models.Model):
"""Custom role model for RBAC"""
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
permissions = models.ManyToManyField(Permission, blank=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
class UserProfile(models.Model):
"""Extended user profile with roles"""
user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
roles = models.ManyToManyField(Role, blank=True)
department = models.CharField(max_length=100, blank=True)
employee_id = models.CharField(max_length=50, unique=True, blank=True)
def __str__(self):
return f"{self.user.username}'s profile"
def has_role(self, role_name):
"""Check if user has a specific role"""
return self.roles.filter(name=role_name, is_active=True).exists()
def has_permission(self, permission_name):
"""Check if user has a specific permission through roles"""
for role in self.roles.filter(is_active=True):
if role.permissions.filter(codename=permission_name).exists():
return True
return False
def get_all_permissions(self):
"""Get all permissions from user's roles"""
permissions = set()
for role in self.roles.filter(is_active=True):
permissions.update(role.permissions.all())
return permissions
class RBACMiddleware:
"""Middleware to add RBAC functionality to request"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
# Add user profile to request
try:
request.user_profile = request.user.userprofile
except UserProfile.DoesNotExist:
request.user_profile = None
response = self.get_response(request)
return response
# Custom decorators for RBAC
from functools import wraps
from django.core.exceptions import PermissionDenied
def role_required(role_name):
"""Decorator to check if user has a specific role"""
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if hasattr(request, 'user_profile') and request.user_profile:
if request.user_profile.has_role(role_name):
return view_func(request, *args, **kwargs)
raise PermissionDenied("You don't have the required role.")
return _wrapped_view
return decorator
def permission_required_rbac(permission_name):
"""Decorator to check if user has a specific permission through RBAC"""
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if hasattr(request, 'user_profile') and request.user_profile:
if request.user_profile.has_permission(permission_name):
return view_func(request, *args, **kwargs)
raise PermissionDenied("You don't have the required permission.")
return _wrapped_view
return decorator
# Usage examples
@role_required('editor')
def edit_post(request, pk):
# Only users with 'editor' role can access
pass
@permission_required_rbac('can_publish')
def publish_post(request, pk):
# Only users with 'can_publish' permission can access
pass
# Role management views
@login_required
@role_required('admin')
def manage_roles(request):
"""Admin view to manage user roles"""
if request.method == 'POST':
user_id = request.POST.get('user_id')
role_id = request.POST.get('role_id')
action = request.POST.get('action')
try:
user = User.objects.get(id=user_id)
role = Role.objects.get(id=role_id)
if action == 'add':
user.userprofile.roles.add(role)
messages.success(request, f'Role {role.name} added to {user.username}')
elif action == 'remove':
user.userprofile.roles.remove(role)
messages.success(request, f'Role {role.name} removed from {user.username}')
except (User.DoesNotExist, Role.DoesNotExist):
messages.error(request, 'User or role not found.')
users = User.objects.all()
roles = Role.objects.all()
return render(request, 'admin/manage_roles.html', {
'users': users,
'roles': roles
})
Test your understanding of this topic:
Build RESTful APIs using Django REST Framework for creating modern web applications and mobile apps.
Content by: Manali Trivedi
Python Django Developer
Django REST Framework (DRF) is a powerful toolkit for building Web APIs. It provides serializers, viewsets, and authentication mechanisms for building robust APIs.
# serializers.py
from rest_framework import serializers
from .models import Post, Category, Comment, User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined']
read_only_fields = ['id', 'date_joined']
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'post_count']
def get_post_count(self, obj):
return obj.post_set.count()
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'created_date']
read_only_fields = ['author', 'created_date']
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'excerpt', 'author',
'category', 'status', 'created_date', 'published_date',
'comments', 'comment_count']
read_only_fields = ['author', 'created_date', 'published_date']
def get_comment_count(self, obj):
return obj.comments.count()
def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
# views.py
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend
class PostPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
pagination_class = PostPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'category', 'author']
search_fields = ['title', 'content', 'excerpt']
ordering_fields = ['created_date', 'published_date', 'title']
ordering = ['-created_date']
def get_queryset(self):
queryset = Post.objects.select_related('author', 'category').prefetch_related('comments')
# Filter by status for non-admin users
if not self.request.user.is_staff:
queryset = queryset.filter(status='published')
return queryset
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
post = self.get_object()
user = request.user
if user in post.likes.all():
post.likes.remove(user)
message = 'Post unliked'
else:
post.likes.add(user)
message = 'Post liked'
return Response({'message': message, 'likes_count': post.likes.count()})
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
post = self.get_object()
if request.user.has_perm('blog.change_post'):
post.status = 'published'
post.published_date = timezone.now()
post.save()
return Response({'message': 'Post published successfully'})
else:
return Response({'error': 'Permission denied'}, status=403)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [permissions.AllowAny]
@action(detail=True)
def posts(self, request, pk=None):
category = self.get_object()
posts = category.post_set.filter(status='published')
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
# urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CategoryViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet)
router.register(r'categories', CategoryViewSet)
urlpatterns = router.urls
# API authentication and permissions
from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class AdminPostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAdminUser]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# API Rate Limiting and Caching
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
from rest_framework.decorators import throttle_classes
from django.core.cache import cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
import hashlib
class PostRateThrottle(UserRateThrottle):
"""Custom rate limiting for post operations"""
rate = '100/hour' # 100 requests per hour for authenticated users
class AnonPostRateThrottle(AnonRateThrottle):
"""Rate limiting for anonymous users"""
rate = '20/hour' # 20 requests per hour for anonymous users
class CachedPostViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet with caching for better performance"""
queryset = Post.objects.all()
serializer_class = PostSerializer
throttle_classes = [PostRateThrottle, AnonPostRateThrottle]
def get_cache_key(self, request):
"""Generate cache key based on request parameters"""
# Include query parameters in cache key
params = request.query_params.dict()
params_str = str(sorted(params.items()))
# Create hash of parameters
params_hash = hashlib.md5(params_str.encode()).hexdigest()
return f"posts_list_{params_hash}"
def list(self, request, *args, **kwargs):
cache_key = self.get_cache_key(request)
cached_data = cache.get(cache_key)
if cached_data is not None:
return Response(cached_data)
response = super().list(request, *args, **kwargs)
# Cache the response for 5 minutes
cache.set(cache_key, response.data, 300)
return response
@method_decorator(cache_page(60 * 15)) # Cache for 15 minutes
@method_decorator(vary_on_cookie)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
# Custom API endpoints with caching
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['GET'])
@throttle_classes([UserRateThrottle])
def post_statistics(request):
"""Get post statistics with caching"""
cache_key = 'post_statistics'
cached_stats = cache.get(cache_key)
if cached_stats is not None:
return Response(cached_stats)
# Calculate statistics
total_posts = Post.objects.count()
published_posts = Post.objects.filter(status='published').count()
draft_posts = Post.objects.filter(status='draft').count()
# Category statistics
category_stats = Post.objects.values('category__name').annotate(
count=Count('id')
).order_by('-count')
# Author statistics
author_stats = Post.objects.values('author__username').annotate(
count=Count('id')
).order_by('-count')[:10]
stats = {
'total_posts': total_posts,
'published_posts': published_posts,
'draft_posts': draft_posts,
'category_distribution': list(category_stats),
'top_authors': list(author_stats),
'cache_timestamp': timezone.now().isoformat()
}
# Cache for 1 hour
cache.set(cache_key, stats, 3600)
return Response(stats)
# API versioning
from rest_framework.versioning import URLPathVersioning
class VersionedPostViewSet(viewsets.ModelViewSet):
"""ViewSet with API versioning"""
versioning_class = URLPathVersioning
queryset = Post.objects.all()
def get_serializer_class(self):
"""Return different serializers based on API version"""
if self.request.version == 'v2':
return PostSerializerV2
return PostSerializer
def get_queryset(self):
"""Return different querysets based on API version"""
queryset = Post.objects.all()
if self.request.version == 'v2':
# V2 includes more fields and relationships
queryset = queryset.select_related('author', 'category').prefetch_related(
'comments', 'tags', 'likes'
)
else:
# V1 is basic
queryset = queryset.select_related('author')
return queryset
# API documentation with drf-spectacular
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
@extend_schema(
tags=['Posts'],
summary="List all posts",
description="Retrieve a paginated list of all posts with optional filtering",
parameters=[
OpenApiParameter(
name='status',
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description='Filter posts by status (published, draft)'
),
OpenApiParameter(
name='category',
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
description='Filter posts by category ID'
),
],
responses={200: PostSerializer}
)
class DocumentedPostViewSet(viewsets.ModelViewSet):
"""ViewSet with comprehensive API documentation"""
queryset = Post.objects.all()
serializer_class = PostSerializer
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 4