mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 13:35:19 -05:00
8.1 KiB
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:
- User submits content → Creates
Submissionrecord with statuspending - Moderator reviews → Approves or rejects
- 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