--- 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 1. **Resource Name**: What entity is this API for? (e.g., Park, Ride, Review) 2. **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}/) 3. **Permissions**: Who can access? - Public read, authenticated write? - Owner only? - Moderator/Admin only? 4. **Filtering**: What filter options are needed? 5. **Nested Resources**: Does this belong under another resource? ## Implementation Steps ### 1. Create or Update the Model File: `backend/apps/[app]/models.py` ```python 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` ```python 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` ```python 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` ```python 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` ```python 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` ```python 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: ```python # backend/config/urls.py urlpatterns = [ ... path('api/v1/', include('apps.app_name.urls')), ] ``` ### 7. Create Migration ```bash python manage.py makemigrations app_name python manage.py migrate ``` ### 8. Add Tests File: `backend/apps/[app]/tests/test_views.py` ```python 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 ```