# 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