Files
thrillwiki_django_no_react/.agent/rules/django-standards.md
pacnpal 1adba1b804 lol
2026-01-02 07:58:58 -05:00

8.1 KiB

ThrillWiki Django Backend Standards

Rules for developing the ThrillWiki backend with Django and Django REST Framework.

Project Structure

backend/
├── config/                  # Project settings
│   ├── settings/
│   │   ├── base.py         # Shared settings
│   │   ├── development.py  # Dev-specific
│   │   └── production.py   # Prod-specific
│   ├── urls.py             # Root URL config
│   └── wsgi.py
├── apps/
│   ├── parks/              # Park-related models and APIs
│   ├── rides/              # Ride-related models and APIs
│   ├── companies/          # Manufacturers, operators, etc.
│   ├── users/              # User profiles, authentication
│   ├── reviews/            # User reviews and ratings
│   ├── credits/            # Ride credits tracking
│   ├── submissions/        # Content submission system
│   ├── moderation/         # Moderation queue
│   └── core/               # Shared utilities
└── tests/                  # Test files

Django App Structure

Each app should follow this structure:

app_name/
├── __init__.py
├── admin.py               # Django admin configuration
├── apps.py                # App configuration
├── models.py              # Database models
├── serializers.py         # DRF serializers
├── views.py               # DRF viewsets/views
├── urls.py                # URL routing
├── permissions.py         # Custom permissions (if needed)
├── filters.py             # DRF filters (if needed)
├── signals.py             # Django signals (if needed)
└── tests/
    ├── __init__.py
    ├── test_models.py
    ├── test_views.py
    └── test_serializers.py

Model Conventions

Base Model

All models should inherit from a base model with common fields:

from django.db import models
import uuid

class BaseModel(models.Model):
    """Base model with common fields"""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True

Model Example

class Park(BaseModel):
    """A theme park or amusement park"""
    
    class Status(models.TextChoices):
        OPERATING = 'operating', 'Operating'
        CLOSED = 'closed', 'Closed'
        CONSTRUCTION = 'construction', 'Under Construction'
    
    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.OPERATING
    )
    location = models.PointField()  # GeoDjango
    city = models.CharField(max_length=100)
    country = models.CharField(max_length=100)
    
    class Meta:
        ordering = ['name']
    
    def __str__(self):
        return self.name

Versioning for Moderated Content

For content that needs version history:

class ParkVersion(BaseModel):
    """Version history for park edits"""
    park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name='versions')
    data = models.JSONField()  # Snapshot of park data
    changed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    change_summary = models.CharField(max_length=255)
    is_current = models.BooleanField(default=False)

API Conventions

URL Structure

# apps/parks/urls.py
from rest_framework.routers import DefaultRouter
from .views import ParkViewSet

router = DefaultRouter()
router.register('parks', ParkViewSet, basename='park')

urlpatterns = router.urls

# Results in:
# GET    /api/parks/          - List parks
# POST   /api/parks/          - Create park (submission)
# GET    /api/parks/{slug}/   - Get park detail
# PUT    /api/parks/{slug}/   - Update park (submission)
# DELETE /api/parks/{slug}/   - Delete park (admin only)

ViewSet Structure

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly

class ParkViewSet(viewsets.ModelViewSet):
    """API endpoint for parks"""
    queryset = Park.objects.all()
    serializer_class = ParkSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    lookup_field = 'slug'
    filterset_class = ParkFilter
    search_fields = ['name', 'city', 'country']
    ordering_fields = ['name', 'created_at']
    
    def get_queryset(self):
        """Optimize queries with select_related and prefetch_related"""
        return Park.objects.select_related(
            'operator', 'owner'
        ).prefetch_related(
            'rides', 'photos'
        )
    
    @action(detail=True, methods=['get'])
    def rides(self, request, slug=None):
        """Get rides for a specific park"""
        park = self.get_object()
        rides = park.rides.all()
        serializer = RideSerializer(rides, many=True)
        return Response(serializer.data)

Serializer Patterns

from rest_framework import serializers

class ParkSerializer(serializers.ModelSerializer):
    """Serializer for Park model"""
    ride_count = serializers.IntegerField(read_only=True)
    average_rating = serializers.FloatField(read_only=True)
    
    class Meta:
        model = Park
        fields = [
            'id', 'name', 'slug', 'description', 'status',
            'city', 'country', 'ride_count', 'average_rating',
            'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'slug', 'created_at', 'updated_at']

class ParkDetailSerializer(ParkSerializer):
    """Extended serializer for park detail view"""
    rides = RideSerializer(many=True, read_only=True)
    photos = PhotoSerializer(many=True, read_only=True)
    
    class Meta(ParkSerializer.Meta):
        fields = ParkSerializer.Meta.fields + ['rides', 'photos']

Query Optimization

  • ALWAYS use select_related() for ForeignKey relationships
  • ALWAYS use prefetch_related() for ManyToMany and reverse FK relationships
  • Annotate computed fields at the database level when possible
  • Use pagination for all list endpoints
# Good
parks = Park.objects.select_related('operator').prefetch_related('rides')

# Bad - causes N+1 queries
for park in parks:
    print(park.operator.name)  # Each iteration hits the database

Permissions

Custom Permission Classes

from rest_framework.permissions import BasePermission

class IsModeratorOrReadOnly(BasePermission):
    """Allow read access to all, write access to moderators"""
    
    def has_permission(self, request, view):
        if request.method in ['GET', 'HEAD', 'OPTIONS']:
            return True
        return request.user.is_authenticated and request.user.is_moderator

Submission & Moderation Flow

All user-submitted content goes through moderation:

  1. User submits content → Creates Submission record with status pending
  2. Moderator reviews → Approves or rejects
  3. On approval → Content is published, version record created
class Submission(BaseModel):
    class Status(models.TextChoices):
        PENDING = 'pending', 'Pending Review'
        APPROVED = 'approved', 'Approved'
        REJECTED = 'rejected', 'Rejected'
        CHANGES_REQUESTED = 'changes_requested', 'Changes Requested'
    
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.UUIDField(null=True, blank=True)
    data = models.JSONField()
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    submitted_by = models.ForeignKey(User, on_delete=models.CASCADE)
    reviewed_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    review_notes = models.TextField(blank=True)

Testing Requirements

  • Write tests for all models, views, and serializers
  • Use pytest and pytest-django
  • Use factories (factory_boy) for test data
  • Test permissions thoroughly
  • Test edge cases and error conditions