Django patterns for models, views, Django REST Framework, and best practices
Modern Django development patterns including models, class-based views, Django REST Framework, and production best practices.
myproject/
├── manage.py
├── config/ # Project configuration
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # Common settings
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── users/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── serializers.py
│ │ ├── services.py
│ │ ├── selectors.py
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ └── test_services.py
│ └── posts/
│ └── ...
├── core/ # Shared utilities
│ ├── __init__.py
│ ├── models.py # Base models
│ ├── permissions.py
│ └── pagination.py
└── requirements/
├── base.txt
├── development.txt
└── production.txt
# core/models.py
from django.db import models
import uuid
class TimeStampedModel(models.Model):
"""Abstract base model with created/updated timestamps."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDModel(models.Model):
"""Abstract base model with UUID primary key."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class BaseModel(UUIDModel, TimeStampedModel):
"""Combined base model with UUID and timestamps."""
class Meta:
abstract = True
# apps/posts/models.py
from django.db import models
from django.contrib.auth import get_user_model
from core.models import BaseModel
User = get_user_model()
class PostQuerySet(models.QuerySet):
def published(self):
return self.filter(status=Post.Status.PUBLISHED)
def draft(self):
return self.filter(status=Post.Status.DRAFT)
def by_author(self, user):
return self.filter(author=user)
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def published(self):
return self.get_queryset().published()
class Post(BaseModel):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
ARCHIVED = 'archived', 'Archived'
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
content = models.TextField()
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='posts'
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT
)
published_at = models.DateTimeField(null=True, blank=True)
objects = PostManager()
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['status', '-published_at']),
]
def __str__(self):
return self.title
def publish(self):
from django.utils import timezone
self.status = self.Status.PUBLISHED
self.published_at = timezone.now()
self.save(update_fields=['status', 'published_at', 'updated_at'])
from django.core.exceptions import ValidationError
from django.db import models
class Event(BaseModel):
title = models.CharField(max_length=200)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
max_attendees = models.PositiveIntegerField(default=0)
def clean(self):
super().clean()
if self.end_date and self.start_date:
if self.end_date <= self.start_date:
raise ValidationError({
'end_date': 'End date must be after start date.'
})
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
# apps/posts/services.py
from django.db import transaction
from django.utils import timezone
from django.utils.text import slugify
from .models import Post
class PostService:
@staticmethod
@transaction.atomic
def create_post(*, author, title: str, content: str, publish: bool = False) -> Post:
"""Create a new post with optional immediate publishing."""
slug = slugify(title)
# Ensure unique slug
base_slug = slug
counter = 1
while Post.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
counter += 1
post = Post.objects.create(
author=author,
title=title,
slug=slug,
content=content,
status=Post.Status.PUBLISHED if publish else Post.Status.DRAFT,
published_at=timezone.now() if publish else None,
)
return post
@staticmethod
def update_post(*, post: Post, title: str = None, content: str = None) -> Post:
"""Update post fields."""
if title is not None:
post.title = title
if content is not None:
post.content = content
post.full_clean()
post.save()
return post
@staticmethod
def publish_post(*, post: Post) -> Post:
"""Publish a draft post."""
if post.status == Post.Status.PUBLISHED:
raise ValueError("Post is already published")
post.publish()
return post
# apps/posts/selectors.py
from django.db.models import QuerySet, Count, Q
from .models import Post
class PostSelector:
@staticmethod
def get_published_posts() -> QuerySet[Post]:
"""Get all published posts."""
return Post.objects.published().select_related('author')
@staticmethod
def get_post_by_slug(slug: str) -> Post:
"""Get a single post by slug."""
return Post.objects.select_related('author').get(slug=slug)
@staticmethod
def get_posts_by_author(author_id: str) -> QuerySet[Post]:
"""Get all posts by a specific author."""
return Post.objects.filter(author_id=author_id).select_related('author')
@staticmethod
def search_posts(query: str) -> QuerySet[Post]:
"""Search posts by title or content."""
return Post.objects.published().filter(
Q(title__icontains=query) | Q(content__icontains=query)
)
@staticmethod
def get_posts_with_stats() -> QuerySet[Post]:
"""Get posts with comment count annotation."""
return Post.objects.published().annotate(
comment_count=Count('comments')
).select_related('author')
# apps/posts/serializers.py
from rest_framework import serializers
from .models import Post
from apps.users.serializers import UserSerializer
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'content', 'author',
'status', 'published_at', 'created_at', 'updated_at'
]
read_only_fields = ['slug', 'published_at', 'created_at', 'updated_at']
class PostCreateSerializer(serializers.Serializer):
title = serializers.CharField(max_length=200)
content = serializers.CharField()
publish = serializers.BooleanField(default=False)
def create(self, validated_data):
from .services import PostService
return PostService.create_post(
author=self.context['request'].user,
**validated_data
)
class PostUpdateSerializer(serializers.Serializer):
title = serializers.CharField(max_length=200, required=False)
content = serializers.CharField(required=False)
def update(self, instance, validated_data):
from .services import PostService
return PostService.update_post(post=instance, **validated_data)
# apps/posts/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from .models import Post
from .serializers import PostSerializer, PostCreateSerializer, PostUpdateSerializer
from .selectors import PostSelector
from .services import PostService
from core.permissions import IsAuthorOrReadOnly
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
lookup_field = 'slug'
def get_queryset(self):
if self.action == 'list':
return PostSelector.get_published_posts()
return Post.objects.select_related('author')
def get_serializer_class(self):
if self.action == 'create':
return PostCreateSerializer
if self.action in ['update', 'partial_update']:
return PostUpdateSerializer
return PostSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def publish(self, request, slug=None):
"""Publish a draft post."""
post = self.get_object()
if post.author != request.user:
return Response(
{'detail': 'You can only publish your own posts.'},
status=status.HTTP_403_FORBIDDEN
)
try:
post = PostService.publish_post(post=post)
return Response(PostSerializer(post).data)
except ValueError as e:
return Response(
{'detail': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def my_posts(self, request):
"""Get current user's posts."""
posts = PostSelector.get_posts_by_author(request.user.id)
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
# core/permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""Allow authors to edit their own content."""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
class IsAdminOrReadOnly(permissions.BasePermission):
"""Allow admins to modify, others can only read."""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_staff
# core/pagination.py
from rest_framework.pagination import PageNumberPagination, CursorPagination
class StandardPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
class CursorPaginationByCreated(CursorPagination):
page_size = 20
ordering = '-created_at'
cursor_query_param = 'cursor'
# apps/posts/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
urlpatterns = [
path('', include(router.urls)),
]
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from core.models import UUIDModel
class User(UUIDModel, AbstractUser):
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
indexes = [
models.Index(fields=['email']),
]
# config/settings/base.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'core.pagination.StandardPagination',
'EXCEPTION_HANDLER': 'core.exceptions.custom_exception_handler',
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
# apps/posts/tests/test_views.py
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from apps.users.models import User
from apps.posts.models import Post
class PostAPITestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user,
status=Post.Status.PUBLISHED
)
def test_list_posts(self):
url = reverse('post-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post_authenticated(self):
self.client.force_authenticate(user=self.user)
url = reverse('post-list')
data = {
'title': 'New Post',
'content': 'New content',
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)
def test_create_post_unauthenticated(self):
url = reverse('post-list')
data = {'title': 'New Post', 'content': 'Content'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_publish_post(self):
self.client.force_authenticate(user=self.user)
draft_post = Post.objects.create(
title='Draft',
slug='draft',
content='Draft content',
author=self.user,
status=Post.Status.DRAFT
)
url = reverse('post-publish', kwargs={'slug': draft_post.slug})
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
draft_post.refresh_from_db()
self.assertEqual(draft_post.status, Post.Status.PUBLISHED)
clean() methodSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer