mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 14:35:17 -05:00
255 lines
8.1 KiB
Markdown
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
|