mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 16:35:18 -05:00
lol
This commit is contained in:
254
.agent/rules/django-standards.md
Normal file
254
.agent/rules/django-standards.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user