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

255 lines
8.1 KiB
Markdown

# 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:
```python
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
```python
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:
```python
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
```python
# 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
```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
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
```python
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
```python
# 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
```python
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
```python
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