mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 09:45:17 -05:00
10 KiB
10 KiB
description
| description |
|---|
| Create a new Django REST API endpoint following ThrillWiki conventions |
New API Workflow
Create a new Django REST Framework API endpoint following ThrillWiki's patterns.
Information Gathering
- Resource Name: What entity is this API for? (e.g., Park, Ride, Review)
- Operations: Which CRUD operations are needed?
- List (GET /resources/)
- Create (POST /resources/)
- Retrieve (GET /resources/{id}/)
- Update (PUT/PATCH /resources/{id}/)
- Delete (DELETE /resources/{id}/)
- Permissions: Who can access?
- Public read, authenticated write?
- Owner only?
- Moderator/Admin only?
- Filtering: What filter options are needed?
- Nested Resources: Does this belong under another resource?
Implementation Steps
1. Create or Update the Model
File: backend/apps/[app]/models.py
from django.db import models
from apps.core.models import BaseModel
class Resource(BaseModel):
"""A resource description"""
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
ARCHIVED = 'archived', 'Archived'
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT
)
owner = models.ForeignKey(
'users.User',
on_delete=models.CASCADE,
related_name='resources'
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
]
def __str__(self):
return self.name
2. Create the Serializer
File: backend/apps/[app]/serializers.py
from rest_framework import serializers
from .models import Resource
class ResourceSerializer(serializers.ModelSerializer):
"""Serializer for Resource listing"""
owner_username = serializers.CharField(source='owner.username', read_only=True)
class Meta:
model = Resource
fields = [
'id', 'name', 'slug', 'description', 'status',
'owner', 'owner_username',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'slug', 'owner', 'created_at', 'updated_at']
class ResourceDetailSerializer(ResourceSerializer):
"""Extended serializer for single resource view"""
# Add related objects for detail view
related_items = RelatedItemSerializer(many=True, read_only=True)
class Meta(ResourceSerializer.Meta):
fields = ResourceSerializer.Meta.fields + ['related_items']
class ResourceCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating resources"""
class Meta:
model = Resource
fields = ['name', 'description', 'status']
def create(self, validated_data):
# Auto-generate slug
validated_data['slug'] = slugify(validated_data['name'])
# Set owner from request
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)
3. Create the ViewSet
File: backend/apps/[app]/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 IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from .models import Resource
from .serializers import (
ResourceSerializer,
ResourceDetailSerializer,
ResourceCreateSerializer
)
from .filters import ResourceFilter
from .permissions import IsOwnerOrReadOnly
class ResourceViewSet(viewsets.ModelViewSet):
"""
API endpoint for resources.
list: Get all resources (with filtering)
create: Create a new resource (authenticated)
retrieve: Get a single resource
update: Update a resource (owner only)
destroy: Delete a resource (owner only)
"""
queryset = Resource.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
lookup_field = 'slug'
# Filtering
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ResourceFilter
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at', 'updated_at']
ordering = ['-created_at']
def get_queryset(self):
"""Optimize queries"""
return Resource.objects.select_related(
'owner'
).prefetch_related(
'related_items'
)
def get_serializer_class(self):
"""Use different serializers for different actions"""
if self.action == 'create':
return ResourceCreateSerializer
if self.action == 'retrieve':
return ResourceDetailSerializer
return ResourceSerializer
@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
"""Publish a draft resource"""
resource = self.get_object()
if resource.status != Resource.Status.DRAFT:
return Response(
{'error': 'Only draft resources can be published'},
status=status.HTTP_400_BAD_REQUEST
)
resource.status = Resource.Status.PUBLISHED
resource.save()
return Response(ResourceSerializer(resource).data)
4. Create Custom Filter
File: backend/apps/[app]/filters.py
import django_filters
from .models import Resource
class ResourceFilter(django_filters.FilterSet):
"""Filters for Resource API"""
status = django_filters.ChoiceFilter(choices=Resource.Status.choices)
owner = django_filters.CharFilter(field_name='owner__username')
created_after = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='gte'
)
created_before = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='lte'
)
class Meta:
model = Resource
fields = ['status', 'owner']
5. Create Custom Permission
File: backend/apps/[app]/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
"""Allow read to all, write only to owner"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user
class IsModerator(BasePermission):
"""Allow access only to moderators"""
def has_permission(self, request, view):
return (
request.user.is_authenticated and
request.user.is_moderator
)
6. Register URLs
File: backend/apps/[app]/urls.py
from rest_framework.routers import DefaultRouter
from .views import ResourceViewSet
router = DefaultRouter()
router.register('resources', ResourceViewSet, basename='resource')
urlpatterns = router.urls
Add to main urls:
# backend/config/urls.py
urlpatterns = [
...
path('api/v1/', include('apps.app_name.urls')),
]
7. Create Migration
python manage.py makemigrations app_name
python manage.py migrate
8. Add Tests
File: backend/apps/[app]/tests/test_views.py
import pytest
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from apps.users.factories import UserFactory
from .factories import ResourceFactory
class TestResourceAPI(APITestCase):
"""Tests for Resource API endpoints"""
def setUp(self):
self.user = UserFactory()
self.resource = ResourceFactory(owner=self.user)
def test_list_resources_unauthenticated(self):
"""Anonymous users can list resources"""
url = reverse('resource-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_resource_authenticated(self):
"""Authenticated users can create resources"""
self.client.force_authenticate(user=self.user)
url = reverse('resource-list')
data = {'name': 'New Resource', 'description': 'Test'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_create_resource_unauthenticated(self):
"""Anonymous users cannot create resources"""
url = reverse('resource-list')
data = {'name': 'New Resource'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_update_own_resource(self):
"""Users can update their own resources"""
self.client.force_authenticate(user=self.user)
url = reverse('resource-detail', args=[self.resource.slug])
response = self.client.patch(url, {'name': 'Updated'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_update_others_resource(self):
"""Users cannot update others' resources"""
other_user = UserFactory()
self.client.force_authenticate(user=other_user)
url = reverse('resource-detail', args=[self.resource.slug])
response = self.client.patch(url, {'name': 'Hacked'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Checklist
After creating the API:
- Model has proper fields and constraints
- Serializers validate input correctly
- ViewSet has proper permissions
- Queries are optimized (select_related, prefetch_related)
- Filtering, search, and ordering work
- Pagination is enabled
- URLs are registered
- Migrations are created and applied
- Tests pass
Output
Report what was created:
Created API: /api/v1/resources/
Methods: GET (list), POST (create), GET (detail), PATCH (update), DELETE
Permissions: IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly
Filters: status, owner, created_after, created_before
Search: name, description
Files:
- backend/apps/[app]/models.py
- backend/apps/[app]/serializers.py
- backend/apps/[app]/views.py
- backend/apps/[app]/filters.py
- backend/apps/[app]/permissions.py
- backend/apps/[app]/urls.py
- backend/apps/[app]/tests/test_views.py