mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 19:31:09 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
576
docs/FUTURE_WORK.md
Normal file
576
docs/FUTURE_WORK.md
Normal file
@@ -0,0 +1,576 @@
|
||||
# Future Work & Deferred Features
|
||||
|
||||
This document tracks features that have been deferred for future implementation. Each item includes context, implementation guidance, and priority.
|
||||
|
||||
## Priority Levels
|
||||
- **P0 (Critical)**: Blocks major functionality or has security implications
|
||||
- **P1 (High)**: Significantly improves user experience or performance
|
||||
- **P2 (Medium)**: Nice-to-have features that add value
|
||||
- **P3 (Low)**: Optional enhancements
|
||||
|
||||
## Feature Tracking
|
||||
|
||||
### Map Service Enhancements
|
||||
|
||||
#### THRILLWIKI-106: Map Clustering Algorithm
|
||||
|
||||
**Priority**: P1 (High)
|
||||
**Estimated Effort**: 3-5 days
|
||||
**Dependencies**: None
|
||||
|
||||
**Context**:
|
||||
Currently, the map API returns all locations within bounds without clustering. At high zoom levels (zoomed out), this can result in hundreds of overlapping markers, degrading performance and UX.
|
||||
|
||||
**Proposed Solution**:
|
||||
Implement a server-side clustering algorithm using one of these approaches:
|
||||
|
||||
1. **Grid-based clustering** (Recommended for simplicity):
|
||||
- Divide the map into a grid based on zoom level
|
||||
- Group locations within each grid cell
|
||||
- Return cluster center and count for cells with multiple locations
|
||||
|
||||
2. **DBSCAN clustering** (Better quality, more complex):
|
||||
- Use scikit-learn's DBSCAN algorithm
|
||||
- Cluster based on geographic distance
|
||||
- Adjust epsilon parameter based on zoom level
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create `backend/apps/core/services/map_clustering.py` with clustering logic
|
||||
2. Add `cluster_locations()` method that accepts:
|
||||
- List of `UnifiedLocation` objects
|
||||
- Zoom level (1-20)
|
||||
- Clustering strategy ('grid' or 'dbscan')
|
||||
3. Update `MapLocationsAPIView._build_response()` to call clustering service when `params["cluster"]` is True
|
||||
4. Update `MapClusterSerializer` to include cluster metadata
|
||||
5. Add tests in `backend/tests/services/test_map_clustering.py`
|
||||
|
||||
**API Changes**:
|
||||
- Response includes `clusters` array with cluster objects
|
||||
- Each cluster has: `id`, `coordinates`, `count`, `bounds`, `representative_location`
|
||||
|
||||
**Performance Considerations**:
|
||||
- Cache clustered results separately from unclustered
|
||||
- Use spatial indexes on location tables
|
||||
- Limit clustering to zoom levels 1-12 (zoomed out views)
|
||||
|
||||
**References**:
|
||||
- [Supercluster.js](https://github.com/mapbox/supercluster) - JavaScript implementation for reference
|
||||
- [PostGIS ST_ClusterKMeans](https://postgis.net/docs/ST_ClusterKMeans.html) - Database-level clustering
|
||||
|
||||
---
|
||||
|
||||
#### THRILLWIKI-107: Nearby Locations
|
||||
|
||||
**Priority**: P2 (Medium)
|
||||
**Estimated Effort**: 2-3 days
|
||||
**Dependencies**: None
|
||||
|
||||
**Context**:
|
||||
Location detail views currently don't show nearby parks or rides. This would help users discover attractions in the same area.
|
||||
|
||||
**Proposed Solution**:
|
||||
Use PostGIS spatial queries to find locations within a radius:
|
||||
|
||||
```python
|
||||
from django.contrib.gis.measure import D # Distance
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
|
||||
def get_nearby_locations(location_obj, radius_miles=25, limit=10):
|
||||
"""Get nearby locations using spatial query."""
|
||||
point = location_obj.point
|
||||
|
||||
# Query parks within radius
|
||||
nearby_parks = Park.objects.filter(
|
||||
location__point__distance_lte=(point, D(mi=radius_miles))
|
||||
).annotate(
|
||||
distance=Distance('location__point', point)
|
||||
).exclude(
|
||||
id=location_obj.park.id # Exclude self
|
||||
).order_by('distance')[:limit]
|
||||
|
||||
return nearby_parks
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Add `get_nearby_locations()` method to `backend/apps/core/services/location_service.py`
|
||||
2. Update `MapLocationDetailAPIView.get()` to call this method
|
||||
3. Update `MapLocationDetailSerializer.get_nearby_locations()` to return actual data
|
||||
4. Add distance field to nearby location objects
|
||||
5. Add tests for spatial queries
|
||||
|
||||
**API Response Example**:
|
||||
```json
|
||||
{
|
||||
"nearby_locations": [
|
||||
{
|
||||
"id": "park_123",
|
||||
"name": "Cedar Point",
|
||||
"type": "park",
|
||||
"distance_miles": 5.2,
|
||||
"coordinates": [41.4793, -82.6833]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Considerations**:
|
||||
- Use spatial indexes (already present on `location__point` fields)
|
||||
- Cache nearby locations for 1 hour
|
||||
- Limit radius to 50 miles maximum
|
||||
|
||||
---
|
||||
|
||||
#### THRILLWIKI-108: Search Relevance Scoring
|
||||
|
||||
**Priority**: P2 (Medium)
|
||||
**Estimated Effort**: 2-3 days
|
||||
**Dependencies**: None
|
||||
|
||||
**Context**:
|
||||
Search results currently return a hardcoded relevance score of 1.0. Implementing proper relevance scoring would improve search result quality.
|
||||
|
||||
**Proposed Solution**:
|
||||
Implement a weighted scoring algorithm based on:
|
||||
|
||||
1. **Text Match Quality** (40% weight):
|
||||
- Exact name match: 1.0
|
||||
- Name starts with query: 0.8
|
||||
- Name contains query: 0.6
|
||||
- City/state match: 0.4
|
||||
|
||||
2. **Popularity** (30% weight):
|
||||
- Based on `average_rating` and `ride_count`/`coaster_count`
|
||||
- Normalize to 0-1 scale
|
||||
|
||||
3. **Recency** (15% weight):
|
||||
- Recently opened attractions score higher
|
||||
- Normalize based on `opening_date`
|
||||
|
||||
4. **Status** (15% weight):
|
||||
- Operating: 1.0
|
||||
- Seasonal: 0.8
|
||||
- Closed temporarily: 0.5
|
||||
- Closed permanently: 0.2
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create `backend/apps/core/services/search_scoring.py` with scoring logic
|
||||
2. Add `calculate_relevance_score()` method
|
||||
3. Update `MapSearchAPIView.get()` to calculate scores
|
||||
4. Sort results by relevance score (descending)
|
||||
5. Add tests for scoring algorithm
|
||||
|
||||
**Example Implementation**:
|
||||
```python
|
||||
def calculate_relevance_score(location, query):
|
||||
score = 0.0
|
||||
|
||||
# Text match (40%)
|
||||
name_lower = location.name.lower()
|
||||
query_lower = query.lower()
|
||||
if name_lower == query_lower:
|
||||
score += 0.40
|
||||
elif name_lower.startswith(query_lower):
|
||||
score += 0.32
|
||||
elif query_lower in name_lower:
|
||||
score += 0.24
|
||||
|
||||
# Popularity (30%)
|
||||
if location.average_rating:
|
||||
score += (location.average_rating / 5.0) * 0.30
|
||||
|
||||
# Status (15%)
|
||||
status_weights = {
|
||||
'OPERATING': 1.0,
|
||||
'SEASONAL': 0.8,
|
||||
'CLOSED_TEMP': 0.5,
|
||||
'CLOSED_PERM': 0.2
|
||||
}
|
||||
score += status_weights.get(location.status, 0.5) * 0.15
|
||||
|
||||
return min(score, 1.0)
|
||||
```
|
||||
|
||||
**Performance Considerations**:
|
||||
- Calculate scores in Python (not database) for flexibility
|
||||
- Cache search results with scores for 5 minutes
|
||||
- Consider using PostgreSQL full-text search for better performance
|
||||
|
||||
---
|
||||
|
||||
#### THRILLWIKI-109: Cache Statistics Tracking
|
||||
|
||||
**Priority**: P2 (Medium)
|
||||
**Estimated Effort**: 1-2 hours
|
||||
**Dependencies**: None
|
||||
**Status**: IMPLEMENTED
|
||||
|
||||
**Context**:
|
||||
The `MapStatsAPIView` returns hardcoded cache statistics (0 hits, 0 misses). Implementing real cache statistics provides visibility into caching effectiveness.
|
||||
|
||||
**Implementation**:
|
||||
Added `get_cache_statistics()` method to `EnhancedCacheService` that retrieves Redis INFO statistics when available. The `MapStatsAPIView` now returns real cache hit/miss data.
|
||||
|
||||
---
|
||||
|
||||
### User Features
|
||||
|
||||
#### THRILLWIKI-104: Full User Statistics Tracking
|
||||
|
||||
**Priority**: P2 (Medium)
|
||||
**Estimated Effort**: 3-4 days
|
||||
**Dependencies**: THRILLWIKI-105 (Photo counting)
|
||||
|
||||
**Context**:
|
||||
Current user statistics are calculated on-demand by querying multiple tables. This is inefficient and doesn't track all desired metrics.
|
||||
|
||||
**Proposed Solution**:
|
||||
Implement a `UserStatistics` model with periodic updates:
|
||||
|
||||
```python
|
||||
class UserStatistics(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
# Content statistics
|
||||
parks_visited = models.IntegerField(default=0)
|
||||
rides_ridden = models.IntegerField(default=0)
|
||||
reviews_written = models.IntegerField(default=0)
|
||||
photos_uploaded = models.IntegerField(default=0)
|
||||
top_lists_created = models.IntegerField(default=0)
|
||||
|
||||
# Engagement statistics
|
||||
helpful_votes_received = models.IntegerField(default=0)
|
||||
comments_made = models.IntegerField(default=0)
|
||||
badges_earned = models.IntegerField(default=0)
|
||||
|
||||
# Activity tracking
|
||||
last_review_date = models.DateTimeField(null=True, blank=True)
|
||||
last_photo_upload_date = models.DateTimeField(null=True, blank=True)
|
||||
streak_days = models.IntegerField(default=0)
|
||||
|
||||
# Timestamps
|
||||
last_calculated = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "User statistics"
|
||||
```
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create migration for `UserStatistics` model in `backend/apps/accounts/models.py`
|
||||
2. Create Celery task `update_user_statistics` in `backend/apps/accounts/tasks.py`
|
||||
3. Update statistics on user actions using Django signals:
|
||||
- `post_save` signal on `ParkReview`, `RideReview` -> increment `reviews_written`
|
||||
- `post_save` signal on `ParkPhoto`, `RidePhoto` -> increment `photos_uploaded`
|
||||
4. Add management command `python manage.py recalculate_user_stats` for bulk updates
|
||||
5. Update `get_user_statistics` view to read from `UserStatistics` model
|
||||
6. Add periodic Celery task to recalculate statistics daily
|
||||
|
||||
**Performance Benefits**:
|
||||
- Reduces database queries from 5+ to 1
|
||||
- Enables leaderboards and ranking features
|
||||
- Supports gamification (badges, achievements)
|
||||
|
||||
**Migration Strategy**:
|
||||
1. Create model and migration
|
||||
2. Run `recalculate_user_stats` for existing users
|
||||
3. Enable signal handlers for new activity
|
||||
4. Monitor for 1 week before removing old calculation logic
|
||||
|
||||
---
|
||||
|
||||
#### THRILLWIKI-105: Photo Upload Counting
|
||||
|
||||
**Priority**: P2 (Medium)
|
||||
**Estimated Effort**: 30 minutes
|
||||
**Dependencies**: None
|
||||
**Status**: IMPLEMENTED
|
||||
|
||||
**Context**:
|
||||
The user statistics endpoint returns `photos_uploaded: 0` for all users. Photo uploads should be counted from `ParkPhoto` and `RidePhoto` models.
|
||||
|
||||
**Implementation**:
|
||||
Updated `get_user_statistics()` in `backend/apps/api/v1/accounts/views.py` to query `ParkPhoto` and `RidePhoto` models where `uploaded_by=user`.
|
||||
|
||||
---
|
||||
|
||||
### Infrastructure
|
||||
|
||||
#### THRILLWIKI-101: Geocoding Service Integration
|
||||
|
||||
**Priority**: P3 (Low)
|
||||
**Estimated Effort**: 2-3 days
|
||||
**Dependencies**: None
|
||||
|
||||
**Context**:
|
||||
`CompanyHeadquarters` model has address fields but no coordinates. This prevents companies from appearing on the map.
|
||||
|
||||
**Proposed Solution**:
|
||||
Integrate a geocoding service to convert addresses to coordinates:
|
||||
|
||||
**Recommended Services**:
|
||||
1. **Google Maps Geocoding API** (Paid, high quality)
|
||||
2. **Nominatim (OpenStreetMap)** (Free, rate-limited)
|
||||
3. **Mapbox Geocoding API** (Paid, good quality)
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create `backend/apps/core/services/geocoding_service.py`:
|
||||
```python
|
||||
class GeocodingService:
|
||||
def geocode_address(self, address: str) -> tuple[float, float] | None:
|
||||
"""Convert address to (latitude, longitude)."""
|
||||
# Implementation using chosen service
|
||||
```
|
||||
|
||||
2. Add geocoding to `CompanyHeadquarters` model:
|
||||
- Add `latitude` and `longitude` fields
|
||||
- Add `geocoded_at` timestamp field
|
||||
- Create migration
|
||||
|
||||
3. Update `CompanyLocationAdapter.to_unified_location()` to use coordinates if available
|
||||
|
||||
4. Add management command `python manage.py geocode_companies` for bulk geocoding
|
||||
|
||||
5. Add Celery task for automatic geocoding on company creation/update
|
||||
|
||||
**Configuration**:
|
||||
Add to `backend/config/settings/base.py`:
|
||||
```python
|
||||
GEOCODING_SERVICE = env('GEOCODING_SERVICE', default='nominatim')
|
||||
GEOCODING_API_KEY = env('GEOCODING_API_KEY', default='')
|
||||
GEOCODING_RATE_LIMIT = env.int('GEOCODING_RATE_LIMIT', default=1) # requests per second
|
||||
```
|
||||
|
||||
**Rate Limiting**:
|
||||
- Implement exponential backoff for API errors
|
||||
- Cache geocoding results to avoid redundant API calls
|
||||
- Use Celery for async geocoding to avoid blocking requests
|
||||
|
||||
**Cost Considerations**:
|
||||
- Nominatim: Free but limited to 1 request/second
|
||||
- Google Maps: $5 per 1000 requests (first $200/month free)
|
||||
- Mapbox: $0.50 per 1000 requests (first 100k free)
|
||||
|
||||
**Alternative Approach**:
|
||||
Store coordinates manually in admin interface for the ~50-100 companies in the database.
|
||||
|
||||
---
|
||||
|
||||
#### THRILLWIKI-110: ClamAV Malware Scanning Integration
|
||||
|
||||
**Priority**: P1 (High) - Security feature
|
||||
**Estimated Effort**: 2-3 days
|
||||
**Dependencies**: ClamAV daemon installation
|
||||
|
||||
**Context**:
|
||||
File uploads currently use magic number validation and PIL integrity checks, but don't scan for malware. This is a security gap for user-generated content.
|
||||
|
||||
**Proposed Solution**:
|
||||
Integrate ClamAV antivirus scanning for all file uploads.
|
||||
|
||||
**Implementation Steps**:
|
||||
|
||||
1. **Install ClamAV**:
|
||||
```bash
|
||||
# Docker
|
||||
docker run -d -p 3310:3310 clamav/clamav:latest
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install clamav clamav-daemon
|
||||
sudo freshclam # Update virus definitions
|
||||
sudo systemctl start clamav-daemon
|
||||
```
|
||||
|
||||
2. **Install Python client**:
|
||||
```bash
|
||||
uv add clamd
|
||||
```
|
||||
|
||||
3. **Update `backend/apps/core/utils/file_scanner.py`**:
|
||||
```python
|
||||
import clamd
|
||||
|
||||
def scan_file_for_malware(file: UploadedFile) -> Tuple[bool, str]:
|
||||
"""Scan file for malware using ClamAV."""
|
||||
try:
|
||||
# Connect to ClamAV daemon
|
||||
cd = clamd.ClamdUnixSocket() # or ClamdNetworkSocket for remote
|
||||
|
||||
# Scan file
|
||||
file.seek(0)
|
||||
scan_result = cd.instream(file)
|
||||
file.seek(0)
|
||||
|
||||
# Check result
|
||||
if scan_result['stream'][0] == 'OK':
|
||||
return True, ""
|
||||
else:
|
||||
virus_name = scan_result['stream'][1]
|
||||
return False, f"Malware detected: {virus_name}"
|
||||
|
||||
except clamd.ConnectionError:
|
||||
# ClamAV not available - log warning and allow upload
|
||||
logger.warning("ClamAV daemon not available, skipping malware scan")
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
logger.error(f"Malware scan error: {e}")
|
||||
return False, "Malware scan failed"
|
||||
```
|
||||
|
||||
4. **Configuration**:
|
||||
Add to `backend/config/settings/base.py`:
|
||||
```python
|
||||
CLAMAV_ENABLED = env.bool('CLAMAV_ENABLED', default=False)
|
||||
CLAMAV_SOCKET = env('CLAMAV_SOCKET', default='/var/run/clamav/clamd.ctl')
|
||||
CLAMAV_HOST = env('CLAMAV_HOST', default='localhost')
|
||||
CLAMAV_PORT = env.int('CLAMAV_PORT', default=3310)
|
||||
```
|
||||
|
||||
5. **Update file upload views**:
|
||||
- Call `scan_file_for_malware()` in avatar upload view
|
||||
- Call in media upload views
|
||||
- Log all malware detections for security monitoring
|
||||
|
||||
6. **Testing**:
|
||||
- Use EICAR test file for testing: `X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`
|
||||
- Add unit tests with mocked ClamAV responses
|
||||
|
||||
**Deployment Considerations**:
|
||||
- ClamAV requires ~1GB RAM for virus definitions
|
||||
- Update virus definitions daily via `freshclam`
|
||||
- Monitor ClamAV daemon health in production
|
||||
- Consider using cloud-based scanning service (AWS GuardDuty, VirusTotal) for serverless deployments
|
||||
|
||||
**Fallback Strategy**:
|
||||
If ClamAV is unavailable, log warning and allow upload (fail open). This prevents blocking legitimate uploads if ClamAV daemon crashes.
|
||||
|
||||
---
|
||||
|
||||
### Management Commands
|
||||
|
||||
#### THRILLWIKI-111: Sample Data Creation Command
|
||||
|
||||
**Priority**: P3 (Low) - Development utility
|
||||
**Estimated Effort**: 1-2 days
|
||||
**Dependencies**: None
|
||||
|
||||
**Context**:
|
||||
The `create_sample_data` management command is incomplete. This command is useful for:
|
||||
- Local development with realistic data
|
||||
- Demo environments
|
||||
- Testing with diverse data sets
|
||||
|
||||
**Proposed Solution**:
|
||||
Complete the implementation with comprehensive sample data:
|
||||
|
||||
**Sample Data to Create**:
|
||||
1. **Parks** (10-15):
|
||||
- Major theme parks (Disney, Universal, Cedar Point)
|
||||
- Regional parks
|
||||
- Water parks
|
||||
- Mix of operating/closed/seasonal statuses
|
||||
|
||||
2. **Rides** (50-100):
|
||||
- Roller coasters (various types)
|
||||
- Flat rides
|
||||
- Water rides
|
||||
- Dark rides
|
||||
- Mix of statuses and manufacturers
|
||||
|
||||
3. **Companies** (20-30):
|
||||
- Operators (Disney, Six Flags, Cedar Fair)
|
||||
- Manufacturers (Intamin, B&M, RMC)
|
||||
- Mix of active/inactive
|
||||
|
||||
4. **Users** (10):
|
||||
- Admin user
|
||||
- Regular users with various activity levels
|
||||
- Test user for authentication testing
|
||||
|
||||
5. **Reviews** (100-200):
|
||||
- Park reviews with ratings
|
||||
- Ride reviews with ratings
|
||||
- Mix of helpful/unhelpful votes
|
||||
|
||||
6. **Media** (50):
|
||||
- Park photos
|
||||
- Ride photos
|
||||
- Mix of approved/pending/rejected
|
||||
|
||||
**Implementation Steps**:
|
||||
1. Create fixtures in `backend/fixtures/sample_data.json`
|
||||
2. Update `create_sample_data.py` to load fixtures
|
||||
3. Add `--clear` flag to delete existing data before creating
|
||||
4. Add `--minimal` flag for quick setup (10 parks, 20 rides)
|
||||
5. Document usage in `backend/README.md`
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
# Full sample data
|
||||
python manage.py create_sample_data
|
||||
|
||||
# Minimal data for quick testing
|
||||
python manage.py create_sample_data --minimal
|
||||
|
||||
# Clear existing data first
|
||||
python manage.py create_sample_data --clear
|
||||
```
|
||||
|
||||
**Alternative Approach**:
|
||||
Use Django fixtures with `loaddata` command:
|
||||
```bash
|
||||
python manage.py loaddata sample_parks sample_rides sample_users
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Completed Items
|
||||
|
||||
### THRILLWIKI-103: Admin Permission Checks
|
||||
|
||||
**Status**: COMPLETED (Already Implemented)
|
||||
|
||||
**Context**:
|
||||
The `MapCacheView` delete and post methods had TODO comments for adding admin permission checks. Upon review, these checks were already implemented using `request.user.is_authenticated and request.user.is_staff`.
|
||||
|
||||
**Resolution**: Removed outdated TODO comments.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Creating GitHub Issues
|
||||
|
||||
Each item in this document can be converted to a GitHub issue using this template:
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
[Copy from Context section]
|
||||
|
||||
## Implementation
|
||||
[Copy from Implementation Steps section]
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Feature implemented as specified
|
||||
- [ ] Unit tests added with >80% coverage
|
||||
- [ ] Integration tests pass
|
||||
- [ ] Documentation updated
|
||||
- [ ] Code reviewed and approved
|
||||
|
||||
## Priority
|
||||
[Copy Priority value]
|
||||
|
||||
## Related
|
||||
- THRILLWIKI issue number
|
||||
- Related features or dependencies
|
||||
```
|
||||
|
||||
### Priority Order for Implementation
|
||||
|
||||
Based on business value and effort, recommended implementation order:
|
||||
|
||||
1. **THRILLWIKI-110**: ClamAV Malware Scanning (P1, security)
|
||||
2. **THRILLWIKI-106**: Map Clustering (P1, performance)
|
||||
3. **THRILLWIKI-107**: Nearby Locations (P2, UX)
|
||||
4. **THRILLWIKI-108**: Search Relevance Scoring (P2, UX)
|
||||
5. **THRILLWIKI-104**: Full User Statistics (P2, engagement)
|
||||
6. **THRILLWIKI-101**: Geocoding Service (P3, completeness)
|
||||
7. **THRILLWIKI-111**: Sample Data Command (P3, development)
|
||||
414
docs/HEALTH_CHECKS.md
Normal file
414
docs/HEALTH_CHECKS.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# ThrillWiki Health Check Documentation
|
||||
|
||||
This document describes the health check endpoints available in ThrillWiki for monitoring and operational purposes.
|
||||
|
||||
## Overview
|
||||
|
||||
ThrillWiki provides three health check endpoints with different levels of detail:
|
||||
|
||||
| Endpoint | Purpose | Authentication |
|
||||
|----------|---------|----------------|
|
||||
| `/api/v1/health/` | Comprehensive health check | Public |
|
||||
| `/api/v1/health/simple/` | Simple OK/ERROR for load balancers | Public |
|
||||
| `/api/v1/health/performance/` | Performance metrics | Debug mode only |
|
||||
|
||||
## Endpoint Details
|
||||
|
||||
### Comprehensive Health Check
|
||||
|
||||
**Endpoint**: `GET /api/v1/health/`
|
||||
|
||||
Returns detailed health information including system metrics, database status, cache status, and individual health checks.
|
||||
|
||||
#### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2025-12-23T10:30:00Z",
|
||||
"version": "1.0.0",
|
||||
"environment": "production",
|
||||
"response_time_ms": 45.23,
|
||||
"checks": {
|
||||
"DatabaseBackend": {
|
||||
"status": "healthy",
|
||||
"critical": true,
|
||||
"errors": [],
|
||||
"response_time_ms": 12.5
|
||||
},
|
||||
"CacheBackend": {
|
||||
"status": "healthy",
|
||||
"critical": false,
|
||||
"errors": [],
|
||||
"response_time_ms": 2.1
|
||||
},
|
||||
"DiskUsage": {
|
||||
"status": "healthy",
|
||||
"critical": false,
|
||||
"errors": []
|
||||
},
|
||||
"MemoryUsage": {
|
||||
"status": "healthy",
|
||||
"critical": false,
|
||||
"errors": []
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"cache": {
|
||||
"redis": {
|
||||
"used_memory": "45.2MB",
|
||||
"connected_clients": 15,
|
||||
"hit_rate": 94.5
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"vendor": "postgresql",
|
||||
"connection_status": "connected",
|
||||
"test_query_time_ms": 1.2,
|
||||
"active_connections": 8,
|
||||
"cache_hit_ratio": 98.3
|
||||
},
|
||||
"system": {
|
||||
"debug_mode": false,
|
||||
"memory": {
|
||||
"total_mb": 8192,
|
||||
"available_mb": 4096,
|
||||
"percent_used": 50
|
||||
},
|
||||
"cpu": {
|
||||
"percent_used": 25.3,
|
||||
"core_count": 4
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": 100,
|
||||
"free_gb": 45,
|
||||
"percent_used": 55
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Status Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 200 | All checks passed or non-critical failures |
|
||||
| 503 | Critical service failure (database, etc.) |
|
||||
|
||||
### Simple Health Check
|
||||
|
||||
**Endpoint**: `GET /api/v1/health/simple/`
|
||||
|
||||
Lightweight health check designed for load balancer health probes.
|
||||
|
||||
#### Response Format (Healthy)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response Format (Unhealthy)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": "Database connection failed",
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Status Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 200 | Service healthy |
|
||||
| 503 | Service unhealthy |
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
**Endpoint**: `GET /api/v1/health/performance/`
|
||||
|
||||
Detailed performance metrics for debugging (only available when `DEBUG=True`).
|
||||
|
||||
#### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-23T10:30:00Z",
|
||||
"database_analysis": {
|
||||
"total_queries": 0,
|
||||
"query_analysis": {}
|
||||
},
|
||||
"cache_performance": {
|
||||
"redis": {
|
||||
"used_memory": "45.2MB",
|
||||
"hit_rate": 94.5
|
||||
}
|
||||
},
|
||||
"recent_slow_queries": []
|
||||
}
|
||||
```
|
||||
|
||||
#### Status Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 200 | Metrics returned |
|
||||
| 403 | Not available (DEBUG=False) |
|
||||
|
||||
## Health Checks Included
|
||||
|
||||
### Database Check
|
||||
|
||||
Verifies PostgreSQL connectivity by executing a simple query.
|
||||
|
||||
```python
|
||||
# Test query executed
|
||||
cursor.execute("SELECT 1")
|
||||
```
|
||||
|
||||
**Critical**: Yes (503 returned if fails)
|
||||
|
||||
### Cache Check
|
||||
|
||||
Verifies Redis connectivity and operation.
|
||||
|
||||
**Critical**: No (200 returned with warning if fails)
|
||||
|
||||
### Disk Usage Check
|
||||
|
||||
Monitors disk space to prevent storage exhaustion.
|
||||
|
||||
**Threshold**: Configurable via `HEALTH_CHECK_DISK_USAGE_MAX` (default: 90%)
|
||||
|
||||
### Memory Usage Check
|
||||
|
||||
Monitors available memory.
|
||||
|
||||
**Threshold**: Configurable via `HEALTH_CHECK_MEMORY_MIN` (default: 100MB)
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Kubernetes Liveness Probe
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health/simple/
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
```
|
||||
|
||||
### Kubernetes Readiness Probe
|
||||
|
||||
```yaml
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health/simple/
|
||||
port: 8000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
```
|
||||
|
||||
### AWS Application Load Balancer
|
||||
|
||||
```json
|
||||
{
|
||||
"HealthCheckPath": "/api/v1/health/simple/",
|
||||
"HealthCheckIntervalSeconds": 30,
|
||||
"HealthyThresholdCount": 2,
|
||||
"UnhealthyThresholdCount": 3,
|
||||
"HealthCheckTimeoutSeconds": 5,
|
||||
"Matcher": {
|
||||
"HttpCode": "200"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Compose Health Check
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health/simple/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
### Nginx Upstream Health Check
|
||||
|
||||
```nginx
|
||||
upstream django {
|
||||
server web:8000;
|
||||
# With nginx-plus
|
||||
health_check uri=/api/v1/health/simple/ interval=10s fails=3 passes=2;
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring Integration
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
Health check data can be exposed as Prometheus metrics using django-prometheus:
|
||||
|
||||
```python
|
||||
# Example custom metrics
|
||||
from prometheus_client import Gauge
|
||||
|
||||
database_response_time = Gauge(
|
||||
'thrillwiki_database_response_time_seconds',
|
||||
'Database query response time'
|
||||
)
|
||||
|
||||
cache_hit_rate = Gauge(
|
||||
'thrillwiki_cache_hit_rate',
|
||||
'Cache hit rate percentage'
|
||||
)
|
||||
```
|
||||
|
||||
### Alerting Thresholds
|
||||
|
||||
Recommended alerting thresholds:
|
||||
|
||||
| Metric | Warning | Critical |
|
||||
|--------|---------|----------|
|
||||
| Response time | > 1s | > 5s |
|
||||
| Database query time | > 100ms | > 500ms |
|
||||
| Cache hit rate | < 80% | < 50% |
|
||||
| Disk usage | > 80% | > 90% |
|
||||
| Memory usage | > 80% | > 90% |
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
Import the health check dashboard:
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "ThrillWiki Health",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Health Status",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "probe_success{job='thrillwiki'}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Response Time",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "thrillwiki_health_response_time_ms"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `HEALTH_CHECK_DISK_USAGE_MAX` | 90 | Max disk usage percentage |
|
||||
| `HEALTH_CHECK_MEMORY_MIN` | 100 | Min available memory (MB) |
|
||||
|
||||
### Custom Health Checks
|
||||
|
||||
Add custom health checks by extending the health check system:
|
||||
|
||||
```python
|
||||
# backend/apps/core/health_checks/custom_checks.py
|
||||
from health_check.backends import BaseHealthCheckBackend
|
||||
|
||||
class CloudflareImagesHealthCheck(BaseHealthCheckBackend):
|
||||
"""Check Cloudflare Images API connectivity."""
|
||||
|
||||
critical_service = False
|
||||
|
||||
def check_status(self):
|
||||
try:
|
||||
# Test Cloudflare Images API
|
||||
response = cloudflare_service.test_connection()
|
||||
if not response.ok:
|
||||
self.add_error("Cloudflare Images API unavailable")
|
||||
except Exception as e:
|
||||
self.add_error(f"Cloudflare Images error: {e}")
|
||||
|
||||
def identifier(self):
|
||||
return "CloudflareImages"
|
||||
```
|
||||
|
||||
Register in `INSTALLED_APPS`:
|
||||
|
||||
```python
|
||||
HEALTH_CHECK = {
|
||||
'DISK_USAGE_MAX': 90,
|
||||
'MEMORY_MIN': 100,
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Health Check Returns 503
|
||||
|
||||
1. Check database connectivity:
|
||||
```bash
|
||||
uv run manage.py dbshell
|
||||
```
|
||||
|
||||
2. Check Redis connectivity:
|
||||
```bash
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
3. Review application logs:
|
||||
```bash
|
||||
tail -f logs/django.log
|
||||
```
|
||||
|
||||
### Slow Health Check Response
|
||||
|
||||
1. Check database query performance:
|
||||
```bash
|
||||
uv run manage.py shell -c "from django.db import connection; print(connection.ensure_connection())"
|
||||
```
|
||||
|
||||
2. Check cache response time:
|
||||
```bash
|
||||
redis-cli --latency
|
||||
```
|
||||
|
||||
### Missing Metrics
|
||||
|
||||
Ensure `psutil` is installed for system metrics:
|
||||
|
||||
```bash
|
||||
uv add psutil
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use simple endpoint for load balancers**: The `/simple/` endpoint is lightweight and fast
|
||||
2. **Monitor comprehensive endpoint**: Use `/health/` for detailed monitoring dashboards
|
||||
3. **Set appropriate timeouts**: Health check timeouts should be shorter than intervals
|
||||
4. **Alert on degraded state**: Don't wait for complete failure
|
||||
5. **Log health check failures**: Include health status in application logs
|
||||
164
docs/MIGRATION_GUIDE.md
Normal file
164
docs/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Dependency Update Migration Guide
|
||||
|
||||
This guide covers the migration to updated dependencies including critical security patches.
|
||||
|
||||
## For Developers
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
1. **Update Python to 3.13+**
|
||||
```bash
|
||||
python --version # Should be 3.13+
|
||||
```
|
||||
|
||||
2. **Install UV** (if not already installed)
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
3. **Update Dependencies**
|
||||
```bash
|
||||
cd backend
|
||||
uv sync --frozen # Use locked versions
|
||||
```
|
||||
|
||||
4. **Run Tests**
|
||||
```bash
|
||||
uv run manage.py test
|
||||
```
|
||||
|
||||
## Security Patches Applied
|
||||
|
||||
### Critical (CVSS 9.0+)
|
||||
- **CVE-2025-64459** (Django) - SQL injection vulnerability
|
||||
- Fixed in: Django 5.2.8+
|
||||
- Previous version: 5.0.x
|
||||
|
||||
### High Severity
|
||||
- **CVE-2024-21520** (DRF) - XSS in break_long_headers filter
|
||||
- Fixed in: djangorestframework 3.15.2+
|
||||
- Previous version: 3.14.x
|
||||
|
||||
### Medium Severity
|
||||
- **CVE-2024-28219** (Pillow) - Buffer overflow
|
||||
- Fixed in: Pillow 10.4.0+
|
||||
- Previous version: 10.2.0
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Python Version
|
||||
- **Minimum**: 3.13+ (previously 3.11+)
|
||||
- Update your local Python installation before proceeding
|
||||
|
||||
### django-allauth (0.60 → 65.3)
|
||||
Major version jump. Review your configuration:
|
||||
|
||||
```python
|
||||
# Check your SOCIALACCOUNT_PROVIDERS settings
|
||||
# Some provider configurations may have changed
|
||||
```
|
||||
|
||||
### sentry-sdk (1.x → 2.x)
|
||||
If using Sentry, review the SDK v2 migration guide:
|
||||
- https://docs.sentry.io/platforms/python/migration/
|
||||
|
||||
### Removed Packages
|
||||
These packages were removed (not used in codebase):
|
||||
- channels, channels-redis, daphne
|
||||
- django-simple-history (using django-pghistory)
|
||||
- django-oauth-toolkit (using dj-rest-auth)
|
||||
- django-webpack-loader
|
||||
- reactivated
|
||||
|
||||
## Dependency Groups
|
||||
|
||||
Dependencies are now organized into groups:
|
||||
|
||||
```bash
|
||||
# Production only
|
||||
uv sync
|
||||
|
||||
# Development (includes dev tools)
|
||||
uv sync --group dev
|
||||
|
||||
# Testing
|
||||
uv sync --group test
|
||||
|
||||
# Profiling (optional)
|
||||
uv sync --group profiling
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: `uv sync` fails with dependency conflicts
|
||||
**Solution:** Delete `uv.lock` and regenerate:
|
||||
```bash
|
||||
rm uv.lock
|
||||
uv lock
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Issue: Tests fail after update
|
||||
**Solution:**
|
||||
1. Check for deprecated API usage in test files
|
||||
2. Review django-allauth changes for auth tests
|
||||
3. Run `uv run manage.py check` for specific warnings
|
||||
|
||||
### Issue: Import errors for removed packages
|
||||
**Solution:** Search for and remove any imports of:
|
||||
- `channels`
|
||||
- `simple_history`
|
||||
- `oauth2_provider`
|
||||
- `webpack_loader`
|
||||
- `reactivated`
|
||||
|
||||
### Issue: `ModuleNotFoundError: cryptography`
|
||||
**Solution:** This is now included. Run:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
## CI/CD Changes
|
||||
|
||||
The CI/CD pipeline now uses UV:
|
||||
|
||||
```yaml
|
||||
- name: Install Dependencies
|
||||
working-directory: backend
|
||||
run: uv sync --frozen
|
||||
```
|
||||
|
||||
## Generating requirements.txt
|
||||
|
||||
For tools that need requirements.txt:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./scripts/generate_requirements.sh
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `requirements.txt` - Production
|
||||
- `requirements-dev.txt` - Development
|
||||
- `requirements-test.txt` - Testing
|
||||
|
||||
## Lock File
|
||||
|
||||
The project now uses `uv.lock` for reproducible builds:
|
||||
|
||||
```bash
|
||||
# Use locked versions (recommended for CI/deployment)
|
||||
uv sync --frozen
|
||||
|
||||
# Update lock file
|
||||
uv lock --upgrade
|
||||
```
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If you need to rollback:
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 backend/pyproject.toml uv.lock
|
||||
uv sync --frozen
|
||||
```
|
||||
262
docs/PRODUCTION_CHECKLIST.md
Normal file
262
docs/PRODUCTION_CHECKLIST.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# ThrillWiki Production Deployment Checklist
|
||||
|
||||
Use this checklist to verify your ThrillWiki deployment is production-ready.
|
||||
|
||||
> **Documentation References:**
|
||||
> - [Environment Variables Reference](./configuration/environment-variables.md) - Complete variable documentation
|
||||
> - [`.env.example`](../.env.example) - Example configuration file with production notes
|
||||
|
||||
## Pre-Deployment
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
> **Important:** These variables are **required** for production. See [Environment Variables Reference](./configuration/environment-variables.md#production-required-variables) for detailed documentation.
|
||||
|
||||
- [ ] `DEBUG=False` is set (**security critical**)
|
||||
- [ ] `SECRET_KEY` is unique, secure, and not committed to version control
|
||||
- [ ] `ALLOWED_HOSTS` contains only production domains (no default in production)
|
||||
- [ ] `CSRF_TRUSTED_ORIGINS` contains all production origins with `https://` prefix
|
||||
- [ ] `DJANGO_SETTINGS_MODULE=config.django.production`
|
||||
- [ ] `REDIS_URL` points to production Redis instance (**required for caching/sessions**)
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] PostgreSQL with PostGIS is configured
|
||||
- [ ] `DATABASE_URL` uses production credentials
|
||||
- [ ] Database user has minimum required privileges
|
||||
- [ ] Connection pooling is configured (`CONN_MAX_AGE=60`)
|
||||
- [ ] Statement timeout is set (30 seconds recommended)
|
||||
- [ ] Database backups are scheduled
|
||||
- [ ] Point-in-time recovery is enabled (if required)
|
||||
|
||||
### Cache (Redis)
|
||||
|
||||
- [ ] `REDIS_URL` points to production Redis
|
||||
- [ ] Redis is password protected (if exposed)
|
||||
- [ ] Redis maxmemory policy is set (allkeys-lru recommended)
|
||||
- [ ] Redis persistence is configured (if required)
|
||||
- [ ] Separate Redis databases for cache, sessions, and API
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] HTTPS is enforced (`SECURE_SSL_REDIRECT=True`)
|
||||
- [ ] HSTS is enabled (`SECURE_HSTS_SECONDS=31536000`)
|
||||
- [ ] Secure cookies enabled (`SESSION_COOKIE_SECURE=True`, `CSRF_COOKIE_SECURE=True`)
|
||||
- [ ] X-Frame-Options set to DENY
|
||||
- [ ] Content-Type-Nosniff enabled
|
||||
- [ ] Referrer-Policy configured
|
||||
- [ ] CORS properly configured (only trusted origins)
|
||||
- [ ] Rate limiting is enabled
|
||||
- [ ] SSL certificate is valid and auto-renewed
|
||||
|
||||
### Static Files
|
||||
|
||||
- [ ] `collectstatic` has been run
|
||||
- [ ] Static files are served via CDN or nginx
|
||||
- [ ] WhiteNoise is configured for compression
|
||||
- [ ] Cache headers are set (long expiry for hashed files)
|
||||
|
||||
### Media Files
|
||||
|
||||
- [ ] Cloudflare Images credentials are configured
|
||||
- [ ] Upload size limits are set
|
||||
- [ ] File type validation is in place
|
||||
- [ ] Cleanup tasks for orphaned images are scheduled
|
||||
|
||||
### Email
|
||||
|
||||
- [ ] Email backend is configured for production
|
||||
- [ ] SPF, DKIM, and DMARC records are set
|
||||
- [ ] Email sending is tested
|
||||
- [ ] Admin email addresses are set
|
||||
|
||||
### Monitoring
|
||||
|
||||
- [ ] Sentry is configured (`SENTRY_DSN` set)
|
||||
- [ ] Health check endpoints are accessible
|
||||
- [ ] Logging is configured (JSON format recommended)
|
||||
- [ ] Log rotation is configured
|
||||
- [ ] Alerting is set up for errors
|
||||
|
||||
## Deployment
|
||||
|
||||
### Application
|
||||
|
||||
- [ ] All migrations are applied (`manage.py migrate`)
|
||||
- [ ] Database schema matches expected state
|
||||
- [ ] Application starts without errors
|
||||
- [ ] Health check returns 200
|
||||
|
||||
### Container/Server
|
||||
|
||||
- [ ] Gunicorn is configured with appropriate workers
|
||||
- [ ] Process manager (systemd/supervisor) is configured
|
||||
- [ ] Container resource limits are set
|
||||
- [ ] Restart policy is configured
|
||||
|
||||
### Reverse Proxy
|
||||
|
||||
- [ ] Nginx is configured correctly
|
||||
- [ ] SSL termination is working
|
||||
- [ ] HTMX headers are forwarded
|
||||
- [ ] Static/media files are served correctly
|
||||
- [ ] Gzip compression is enabled
|
||||
- [ ] Security headers are set at proxy level
|
||||
|
||||
### Celery (if used)
|
||||
|
||||
- [ ] Celery worker is running
|
||||
- [ ] Celery beat is running (for scheduled tasks)
|
||||
- [ ] Task queues are configured
|
||||
- [ ] Task monitoring is set up
|
||||
|
||||
## Post-Deployment Verification
|
||||
|
||||
### Functional Tests
|
||||
|
||||
- [ ] Homepage loads correctly
|
||||
- [ ] Authentication works (login, logout)
|
||||
- [ ] Park/ride pages load correctly
|
||||
- [ ] Search functionality works
|
||||
- [ ] HTMX interactions work
|
||||
- [ ] Admin panel is accessible
|
||||
- [ ] API endpoints respond correctly
|
||||
- [ ] Image uploads work
|
||||
|
||||
### Performance Tests
|
||||
|
||||
- [ ] Page load time < 3 seconds
|
||||
- [ ] API response time < 500ms
|
||||
- [ ] No N+1 query issues (check Django Debug Toolbar/Silk)
|
||||
- [ ] Cache hit rate > 80%
|
||||
- [ ] Database query time reasonable
|
||||
|
||||
### Security Tests
|
||||
|
||||
- [ ] HTTPS redirect works
|
||||
- [ ] HSTS header present
|
||||
- [ ] Security headers present
|
||||
- [ ] CORS blocks unauthorized origins
|
||||
- [ ] Rate limiting triggers on excessive requests
|
||||
- [ ] SQL injection attempts blocked
|
||||
- [ ] XSS attempts blocked
|
||||
|
||||
### Monitoring Verification
|
||||
|
||||
- [ ] Sentry receives test error
|
||||
- [ ] Health check endpoint monitored
|
||||
- [ ] Logs are being collected
|
||||
- [ ] Alerts trigger on test conditions
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Preparation
|
||||
|
||||
- [ ] Previous version container/image available
|
||||
- [ ] Database rollback script ready (if migrations changed schema)
|
||||
- [ ] DNS rollback plan (if CDN change)
|
||||
- [ ] Communication plan for stakeholders
|
||||
|
||||
### Rollback Steps
|
||||
|
||||
1. Stop current deployment
|
||||
2. Restore previous container/code
|
||||
3. Rollback database migrations (if needed)
|
||||
4. Verify rollback with health check
|
||||
5. Communicate status
|
||||
|
||||
## Emergency Contacts
|
||||
|
||||
| Role | Contact |
|
||||
|------|---------|
|
||||
| On-call Engineer | |
|
||||
| Database Admin | |
|
||||
| Infrastructure | |
|
||||
| Product Owner | |
|
||||
|
||||
## Sign-off
|
||||
|
||||
| Role | Name | Date | Signature |
|
||||
|------|------|------|-----------|
|
||||
| Developer | | | |
|
||||
| DevOps | | | |
|
||||
| QA | | | |
|
||||
| Tech Lead | | | |
|
||||
|
||||
---
|
||||
|
||||
## Quick Verification Commands
|
||||
|
||||
```bash
|
||||
# Check Django configuration
|
||||
uv run manage.py check --deploy
|
||||
|
||||
# Verify database connection
|
||||
uv run manage.py dbshell -c "SELECT 1;"
|
||||
|
||||
# Test Redis connection
|
||||
redis-cli -u $REDIS_URL ping
|
||||
|
||||
# Check health endpoint
|
||||
curl -s https://yourdomain.com/api/v1/health/ | jq .status
|
||||
|
||||
# Verify static files
|
||||
curl -I https://yourdomain.com/static/css/design-tokens.css
|
||||
|
||||
# Test HTTPS redirect
|
||||
curl -I http://yourdomain.com
|
||||
|
||||
# Check security headers
|
||||
curl -I https://yourdomain.com | grep -E "(Strict-Transport|X-Frame|X-Content)"
|
||||
```
|
||||
|
||||
## Environment Variables Checklist
|
||||
|
||||
> **Complete Reference:** See [Environment Variables Reference](./configuration/environment-variables.md) for detailed documentation of all variables.
|
||||
|
||||
### Required (Production Will Fail Without These)
|
||||
|
||||
These variables have **no defaults** in production settings and must be explicitly set.
|
||||
See [Production-Required Variables](./configuration/environment-variables.md#production-required-variables) for details.
|
||||
|
||||
```bash
|
||||
# Security & Core (CRITICAL)
|
||||
SECRET_KEY=<secure-random-key> # Generate with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
DEBUG=False # NEVER True in production
|
||||
DJANGO_SETTINGS_MODULE=config.django.production
|
||||
|
||||
# Host Configuration (NO DEFAULTS IN PRODUCTION)
|
||||
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com # MUST include https://
|
||||
|
||||
# Database & Cache (REQUIRED)
|
||||
DATABASE_URL=postgis://user:pass@host:5432/dbname
|
||||
REDIS_URL=redis://host:6379/0 # Required for caching, sessions, and Celery
|
||||
```
|
||||
|
||||
### Recommended
|
||||
|
||||
```bash
|
||||
SENTRY_DSN=https://xxx@sentry.io/xxx
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_ID=xxx
|
||||
CLOUDFLARE_IMAGES_API_TOKEN=xxx
|
||||
EMAIL_HOST=smtp.provider.com
|
||||
EMAIL_HOST_USER=xxx
|
||||
EMAIL_HOST_PASSWORD=xxx
|
||||
```
|
||||
|
||||
### Optional
|
||||
|
||||
```bash
|
||||
REDIS_SESSIONS_URL=redis://host:6379/1
|
||||
REDIS_API_URL=redis://host:6379/2
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
### Validation Command
|
||||
|
||||
Run this to validate your production configuration:
|
||||
|
||||
```bash
|
||||
DJANGO_SETTINGS_MODULE=config.django.production python manage.py check --deploy
|
||||
```
|
||||
458
docs/README.md
458
docs/README.md
@@ -1,344 +1,370 @@
|
||||
# ThrillWiki Django + Vue.js Monorepo
|
||||
# ThrillWiki - Django + HTMX Application
|
||||
|
||||
A comprehensive theme park and roller coaster information system built with a modern monorepo architecture combining Django REST API backend with Vue.js frontend.
|
||||
A comprehensive theme park and roller coaster information system built with Django and HTMX, providing a progressive enhancement approach to modern web development.
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
## Architecture Overview
|
||||
|
||||
This project uses a monorepo structure that cleanly separates backend and frontend concerns while maintaining shared resources and documentation:
|
||||
ThrillWiki is a **Django monolith with HTMX-driven interactivity**, not a traditional SPA. This architecture provides:
|
||||
|
||||
- **Server-side rendering** for SEO and initial page load performance
|
||||
- **Progressive enhancement** with HTMX for dynamic updates without full page reloads
|
||||
- **REST API** for programmatic access and potential mobile app integration
|
||||
- **Alpine.js** for minimal client-side state (limited to form validation and UI toggles)
|
||||
|
||||
```
|
||||
thrillwiki-monorepo/
|
||||
├── backend/ # Django REST API (Port 8000)
|
||||
thrillwiki/
|
||||
├── backend/ # Django application (main codebase)
|
||||
│ ├── apps/ # Modular Django applications
|
||||
│ │ ├── accounts/ # User management and authentication
|
||||
│ │ ├── api/v1/ # REST API endpoints
|
||||
│ │ ├── core/ # Shared utilities and base classes
|
||||
│ │ ├── location/ # Geographic data and services
|
||||
│ │ ├── media/ # File management (Cloudflare Images)
|
||||
│ │ ├── moderation/ # Content moderation workflows
|
||||
│ │ ├── parks/ # Theme park data and operations
|
||||
│ │ └── rides/ # Ride information and relationships
|
||||
│ ├── config/ # Django settings and configuration
|
||||
│ ├── templates/ # Django templates
|
||||
│ └── static/ # Static assets
|
||||
├── frontend/ # Vue.js SPA (Port 5174)
|
||||
│ ├── src/ # Vue.js source code
|
||||
│ ├── public/ # Static assets
|
||||
│ └── dist/ # Build output
|
||||
├── shared/ # Shared resources and documentation
|
||||
│ ├── docs/ # Comprehensive documentation
|
||||
│ │ ├── django/ # Environment-specific settings
|
||||
│ │ └── settings/ # Modular settings modules
|
||||
│ ├── templates/ # Django templates with HTMX
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── htmx/ # HTMX partial templates
|
||||
│ │ └── layouts/ # Base layout templates
|
||||
│ └── static/ # Static assets (CSS, JS, images)
|
||||
├── shared/ # Shared resources
|
||||
│ ├── scripts/ # Development and deployment scripts
|
||||
│ ├── config/ # Shared configuration
|
||||
│ └── media/ # Shared media files
|
||||
├── architecture/ # Architecture documentation
|
||||
└── profiles/ # Development profiles
|
||||
├── docs/ # Project documentation
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Technology Stack
|
||||
|
||||
| Technology | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| **Django** | 5.2.8+ | Web framework |
|
||||
| **Django REST Framework** | 3.15.2+ | API framework |
|
||||
| **HTMX** | 1.20.0+ | Dynamic UI updates |
|
||||
| **Alpine.js** | 3.x | Minimal client-side state |
|
||||
| **Tailwind CSS** | 3.x | Utility-first styling |
|
||||
| **PostgreSQL/PostGIS** | 14+ | Database with geospatial support |
|
||||
| **Redis** | 6+ | Caching and sessions |
|
||||
| **Celery** | 5.5+ | Background task processing |
|
||||
| **UV** | Latest | Python package management |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python 3.11+** with [uv](https://docs.astral.sh/uv/) for backend dependencies
|
||||
- **Node.js 18+** with [pnpm](https://pnpm.io/) for frontend dependencies
|
||||
- **PostgreSQL 14+** (optional, defaults to SQLite for development)
|
||||
- **Redis 6+** (optional, for caching and sessions)
|
||||
- **Python 3.13+** with [uv](https://docs.astral.sh/uv/) for package management
|
||||
- **PostgreSQL 14+** with PostGIS extension
|
||||
- **Redis 6+** for caching and sessions
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd thrillwiki-monorepo
|
||||
cd thrillwiki
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
# Install frontend dependencies
|
||||
pnpm install
|
||||
|
||||
# Install backend dependencies
|
||||
cd backend && uv sync && cd ..
|
||||
cd backend
|
||||
uv sync --frozen # Use locked versions for reproducibility
|
||||
```
|
||||
|
||||
3. **Environment configuration**
|
||||
```bash
|
||||
# Copy environment files
|
||||
cp .env.example .env
|
||||
cp backend/.env.example backend/.env
|
||||
cp frontend/.env.development frontend/.env.local
|
||||
|
||||
# Edit .env files with your settings
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
4. **Database setup**
|
||||
```bash
|
||||
cd backend
|
||||
uv run manage.py migrate
|
||||
uv run manage.py createsuperuser
|
||||
cd ..
|
||||
```
|
||||
|
||||
5. **Start development servers**
|
||||
5. **Start development server**
|
||||
```bash
|
||||
# Start both servers concurrently
|
||||
pnpm run dev
|
||||
|
||||
# Or start individually
|
||||
pnpm run dev:frontend # Vue.js on :5174
|
||||
pnpm run dev:backend # Django on :8000
|
||||
uv run manage.py runserver
|
||||
```
|
||||
|
||||
## 📁 Project Structure Details
|
||||
The application will be available at `http://localhost:8000`.
|
||||
|
||||
### Backend (`/backend`)
|
||||
- **Django 5.0+** with REST Framework for API development
|
||||
- **Modular app architecture** with separate apps for parks, rides, accounts, etc.
|
||||
- **UV package management** for fast, reliable Python dependency management
|
||||
- **PostgreSQL/SQLite** database with comprehensive entity relationships
|
||||
- **Redis** for caching, sessions, and background tasks
|
||||
- **Comprehensive API** with frontend serializers for camelCase conversion
|
||||
## Project Structure Details
|
||||
|
||||
### Frontend (`/frontend`)
|
||||
- **Vue 3** with Composition API and `<script setup>` syntax
|
||||
- **TypeScript** for type safety and better developer experience
|
||||
- **Vite** for lightning-fast development and optimized production builds
|
||||
- **Tailwind CSS** with custom design system and dark mode support
|
||||
- **Pinia** for state management with modular stores
|
||||
- **Vue Router** for client-side routing
|
||||
- **Comprehensive UI component library** with shadcn-vue components
|
||||
### Django Applications
|
||||
|
||||
### Shared Resources (`/shared`)
|
||||
- **Documentation** - Comprehensive guides and API documentation
|
||||
- **Development scripts** - Automated setup, build, and deployment scripts
|
||||
- **Configuration** - Shared Docker, CI/CD, and infrastructure configs
|
||||
- **Media management** - Centralized media file handling and optimization
|
||||
| App | Description |
|
||||
|-----|-------------|
|
||||
| **accounts** | User authentication, profiles, preferences, and social auth |
|
||||
| **api/v1** | RESTful API endpoints with OpenAPI documentation |
|
||||
| **core** | Shared utilities, managers, middleware, and services |
|
||||
| **location** | Geographic models, geocoding, and map services |
|
||||
| **media** | Cloudflare Images integration and file management |
|
||||
| **moderation** | Content review workflows and moderation queue |
|
||||
| **parks** | Theme park models, views, and related operations |
|
||||
| **rides** | Ride models, coaster statistics, and ride history |
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
### HTMX Patterns
|
||||
|
||||
### Available Scripts
|
||||
ThrillWiki uses HTMX for server-driven interactivity:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm run dev # Start both servers concurrently
|
||||
pnpm run dev:frontend # Frontend only (:5174)
|
||||
pnpm run dev:backend # Backend only (:8000)
|
||||
- **Partial templates** (`*_partial.html`) for dynamic content updates
|
||||
- **HX-Trigger** for cross-component communication
|
||||
- **hx-indicator** with skeleton loaders for loading states
|
||||
- **Field-level validation** via HTMX for form feedback
|
||||
|
||||
# Building
|
||||
pnpm run build # Build frontend for production
|
||||
pnpm run build:staging # Build for staging environment
|
||||
pnpm run build:production # Build for production environment
|
||||
See [HTMX Patterns](./htmx-patterns.md) for detailed conventions.
|
||||
|
||||
# Testing
|
||||
pnpm run test # Run all tests
|
||||
pnpm run test:frontend # Frontend unit and E2E tests
|
||||
pnpm run test:backend # Backend unit and integration tests
|
||||
### Hybrid API/HTML Endpoints
|
||||
|
||||
# Code Quality
|
||||
pnpm run lint # Lint all code
|
||||
pnpm run type-check # TypeScript type checking
|
||||
Many views serve dual purposes:
|
||||
|
||||
# Setup and Maintenance
|
||||
pnpm run install:all # Install all dependencies
|
||||
./shared/scripts/dev/setup-dev.sh # Full development setup
|
||||
./shared/scripts/dev/start-all.sh # Start all services
|
||||
```
|
||||
- **HTML response** for browser requests (template rendering)
|
||||
- **JSON response** for API requests (DRF serialization)
|
||||
|
||||
### Backend Development
|
||||
This is achieved through content negotiation and hybrid view mixins.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Development Server
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run manage.py runserver
|
||||
```
|
||||
|
||||
# Django management commands
|
||||
### Django Management Commands
|
||||
|
||||
```bash
|
||||
# Database operations
|
||||
uv run manage.py migrate
|
||||
uv run manage.py makemigrations
|
||||
uv run manage.py createsuperuser
|
||||
|
||||
# Static files
|
||||
uv run manage.py collectstatic
|
||||
|
||||
# Testing and quality
|
||||
# Validate configuration
|
||||
uv run manage.py validate_settings
|
||||
|
||||
# Testing
|
||||
uv run manage.py test
|
||||
uv run black . # Format code
|
||||
uv run flake8 . # Lint code
|
||||
uv run isort . # Sort imports
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
# Format code
|
||||
uv run black .
|
||||
uv run isort .
|
||||
|
||||
# Vue.js development
|
||||
pnpm run dev # Start dev server
|
||||
pnpm run build # Production build
|
||||
pnpm run preview # Preview production build
|
||||
pnpm run test:unit # Vitest unit tests
|
||||
pnpm run test:e2e # Playwright E2E tests
|
||||
pnpm run lint # ESLint
|
||||
pnpm run type-check # TypeScript checking
|
||||
# Lint code
|
||||
uv run ruff check .
|
||||
uv run flake8 .
|
||||
|
||||
# Type checking
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### Root `.env`
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:pass@localhost/thrillwiki
|
||||
REDIS_URL=redis://localhost:6379
|
||||
ThrillWiki uses environment variables for configuration. Key variables:
|
||||
|
||||
# Security
|
||||
SECRET_KEY=your-secret-key
|
||||
DEBUG=True
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `SECRET_KEY` | Django secret key (required) |
|
||||
| `DEBUG` | Debug mode (True/False) |
|
||||
| `DATABASE_URL` | PostgreSQL connection URL |
|
||||
| `REDIS_URL` | Redis connection URL |
|
||||
| `DJANGO_SETTINGS_MODULE` | Settings module to use |
|
||||
|
||||
# API Configuration
|
||||
API_BASE_URL=http://localhost:8000/api
|
||||
See [Environment Variables](./configuration/environment-variables.md) for complete reference.
|
||||
|
||||
### Settings Architecture
|
||||
|
||||
ThrillWiki uses a modular settings architecture:
|
||||
|
||||
```
|
||||
config/
|
||||
├── django/ # Environment-specific settings
|
||||
│ ├── base.py # Core settings
|
||||
│ ├── local.py # Development overrides
|
||||
│ ├── production.py # Production overrides
|
||||
│ └── test.py # Test overrides
|
||||
└── settings/ # Modular settings
|
||||
├── cache.py # Redis caching
|
||||
├── database.py # Database configuration
|
||||
├── email.py # Email settings
|
||||
├── logging.py # Logging configuration
|
||||
├── rest_framework.py # DRF and CORS
|
||||
├── security.py # Security headers
|
||||
└── storage.py # Static/media files
|
||||
```
|
||||
|
||||
#### Backend `.env`
|
||||
```bash
|
||||
# Django Settings
|
||||
DJANGO_SETTINGS_MODULE=config.django.local
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
## API Documentation
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:pass@localhost/thrillwiki
|
||||
### Interactive Documentation
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
- **Swagger UI**: `/api/docs/`
|
||||
- **ReDoc**: `/api/redoc/`
|
||||
- **OpenAPI Schema**: `/api/schema/`
|
||||
|
||||
# Email (optional)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USE_TLS=True
|
||||
```
|
||||
### API Overview
|
||||
|
||||
#### Frontend `.env.local`
|
||||
```bash
|
||||
# API Configuration
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
- **Base URL**: `/api/v1/`
|
||||
- **Authentication**: JWT Bearer tokens or session-based
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
# Development
|
||||
VITE_APP_TITLE=ThrillWiki (Development)
|
||||
See [API Documentation](./THRILLWIKI_API_DOCUMENTATION.md) for complete endpoint reference.
|
||||
|
||||
# Feature Flags
|
||||
VITE_ENABLE_DEBUG=true
|
||||
```
|
||||
## Key Features
|
||||
|
||||
## 📊 Key Features
|
||||
### Park Database
|
||||
- Comprehensive theme park information worldwide
|
||||
- Operator and property owner relationships
|
||||
- Geographic data with PostGIS support
|
||||
- Operating status and seasonal information
|
||||
|
||||
### Backend Features
|
||||
- **Comprehensive Park Database** - Detailed information about theme parks worldwide
|
||||
- **Extensive Ride Database** - Complete roller coaster and ride information
|
||||
- **User Management** - Authentication, profiles, and permissions
|
||||
- **Content Moderation** - Review and approval workflows
|
||||
- **API Documentation** - Auto-generated OpenAPI/Swagger docs
|
||||
- **Background Tasks** - Celery integration for long-running processes
|
||||
- **Caching Strategy** - Redis-based caching for performance
|
||||
- **Search Functionality** - Full-text search across all content
|
||||
### Ride Database
|
||||
- Complete ride specifications and statistics
|
||||
- Roller coaster technical data (height, speed, inversions)
|
||||
- Manufacturer and designer tracking
|
||||
- Historical status and name changes
|
||||
|
||||
### Frontend Features
|
||||
- **Responsive Design** - Mobile-first approach with Tailwind CSS
|
||||
- **Dark Mode Support** - Complete dark/light theme system
|
||||
- **Real-time Search** - Instant search with debouncing and highlighting
|
||||
- **Interactive Maps** - Park and ride location visualization
|
||||
- **Photo Galleries** - High-quality image management
|
||||
- **User Dashboard** - Personalized content and contributions
|
||||
- **Progressive Web App** - PWA capabilities for mobile experience
|
||||
- **Accessibility** - WCAG 2.1 AA compliance
|
||||
### User Features
|
||||
- User authentication with social login (Google, Discord)
|
||||
- Profile management and preferences
|
||||
- Review and rating system
|
||||
- Personalized content and contributions
|
||||
|
||||
## 📖 Documentation
|
||||
### Content Moderation
|
||||
- Review and approval workflows
|
||||
- Moderation queue management
|
||||
- User moderation actions
|
||||
- Bulk operations support
|
||||
|
||||
### API Capabilities
|
||||
- Full CRUD for all entities
|
||||
- Advanced filtering and search
|
||||
- Pagination and sorting
|
||||
- Rate limiting and caching
|
||||
|
||||
## Documentation
|
||||
|
||||
### Core Documentation
|
||||
- **[Backend Documentation](./backend/README.md)** - Django setup and API details
|
||||
- **[Frontend Documentation](./frontend/README.md)** - Vue.js setup and development
|
||||
- **[API Documentation](./shared/docs/api/README.md)** - Complete API reference
|
||||
- **[Development Workflow](./shared/docs/development/workflow.md)** - Daily development processes
|
||||
- [Backend Documentation](../backend/README.md) - Setup and development details
|
||||
- [API Documentation](./THRILLWIKI_API_DOCUMENTATION.md) - Complete API reference
|
||||
- [Setup Guide](./SETUP_GUIDE.md) - Comprehensive setup instructions
|
||||
|
||||
### Architecture & Deployment
|
||||
- **[Architecture Overview](./architecture/)** - System design and decisions
|
||||
- **[Deployment Guide](./shared/docs/deployment/)** - Production deployment instructions
|
||||
- **[Development Scripts](./shared/scripts/)** - Automation and tooling
|
||||
### Configuration
|
||||
- [Environment Variables](./configuration/environment-variables.md) - Complete reference
|
||||
- [Secret Management](./configuration/secret-management.md) - Secret handling
|
||||
|
||||
### Additional Resources
|
||||
- **[Contributing Guide](./CONTRIBUTING.md)** - How to contribute to the project
|
||||
- **[Code of Conduct](./CODE_OF_CONDUCT.md)** - Community guidelines
|
||||
- **[Security Policy](./SECURITY.md)** - Security reporting and policies
|
||||
### Architecture
|
||||
- [Architecture Decisions](./architecture/) - ADRs for key decisions
|
||||
- [Deployment Guide](../architecture/deployment-guide.md) - Production deployment
|
||||
- [Health Checks](./HEALTH_CHECKS.md) - Monitoring endpoints
|
||||
|
||||
## 🚀 Deployment
|
||||
### Accessibility
|
||||
- [Keyboard Navigation](./accessibility/keyboard-navigation.md) - Keyboard shortcuts
|
||||
- [Screen Reader Testing](./accessibility/screen-reader-testing.md) - Testing checklist
|
||||
- [Component Patterns](./accessibility/component-patterns.md) - Accessible patterns
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run manage.py test
|
||||
|
||||
# Run specific app tests
|
||||
uv run manage.py test apps.parks
|
||||
uv run manage.py test apps.rides
|
||||
|
||||
# Run with coverage
|
||||
uv run coverage run manage.py test
|
||||
uv run coverage report
|
||||
```
|
||||
|
||||
### Accessibility Testing
|
||||
|
||||
```bash
|
||||
# Run accessibility tests
|
||||
uv run manage.py test backend.tests.accessibility
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# Quick start with all services
|
||||
./shared/scripts/dev/start-all.sh
|
||||
# Start development server
|
||||
uv run manage.py runserver
|
||||
|
||||
# Full development setup
|
||||
./shared/scripts/dev/setup-dev.sh
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
```bash
|
||||
# Build all components
|
||||
./shared/scripts/build/build-all.sh
|
||||
|
||||
# Deploy to production
|
||||
./shared/scripts/deploy/deploy.sh
|
||||
```
|
||||
See [Deployment Guide](../architecture/deployment-guide.md) for detailed production setup instructions.
|
||||
|
||||
See [Deployment Guide](./shared/docs/deployment/) for detailed production setup instructions.
|
||||
Key production requirements:
|
||||
- `DEBUG=False`
|
||||
- SSL/HTTPS enforcement
|
||||
- Redis for caching and sessions
|
||||
- PostgreSQL with connection pooling
|
||||
- Proper secret management
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
### Production Checklist
|
||||
|
||||
### Backend Testing
|
||||
- **Unit Tests** - Individual function and method testing
|
||||
- **Integration Tests** - API endpoint and database interaction testing
|
||||
- **E2E Tests** - Full user journey testing with Selenium
|
||||
See [Production Checklist](./PRODUCTION_CHECKLIST.md) for deployment verification.
|
||||
|
||||
### Frontend Testing
|
||||
- **Unit Tests** - Component and utility function testing with Vitest
|
||||
- **Integration Tests** - Component interaction testing
|
||||
- **E2E Tests** - User journey testing with Playwright
|
||||
## Contributing
|
||||
|
||||
### Code Quality
|
||||
- **Linting** - ESLint for JavaScript/TypeScript, Flake8 for Python
|
||||
- **Type Checking** - TypeScript for frontend, mypy for Python
|
||||
- **Code Formatting** - Prettier for frontend, Black for Python
|
||||
We welcome contributions! Please follow these guidelines:
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on:
|
||||
|
||||
1. **Development Setup** - Getting your development environment ready
|
||||
2. **Code Standards** - Coding conventions and best practices
|
||||
3. **Pull Request Process** - How to submit your changes
|
||||
4. **Issue Reporting** - How to report bugs and request features
|
||||
1. **Development Setup** - Follow the setup instructions above
|
||||
2. **Code Standards** - Follow Django coding standards and use provided linters
|
||||
3. **Testing** - Write tests for new features
|
||||
4. **Documentation** - Update documentation as needed
|
||||
|
||||
### Quick Contribution Start
|
||||
|
||||
```bash
|
||||
# Fork and clone the repository
|
||||
git clone https://github.com/your-username/thrillwiki-monorepo.git
|
||||
cd thrillwiki-monorepo
|
||||
git clone https://github.com/your-username/thrillwiki.git
|
||||
cd thrillwiki
|
||||
|
||||
# Set up development environment
|
||||
./shared/scripts/dev/setup-dev.sh
|
||||
cd backend && uv sync
|
||||
|
||||
# Create a feature branch
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# Make your changes and test
|
||||
pnpm run test
|
||||
uv run manage.py test
|
||||
|
||||
# Submit a pull request
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
||||
This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
## Support
|
||||
|
||||
- **Theme Park Community** - For providing data and inspiration
|
||||
- **Open Source Contributors** - For the amazing tools and libraries
|
||||
- **Vue.js and Django Communities** - For excellent documentation and support
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Issues** - [GitHub Issues](https://github.com/your-repo/thrillwiki-monorepo/issues)
|
||||
- **Discussions** - [GitHub Discussions](https://github.com/your-repo/thrillwiki-monorepo/discussions)
|
||||
- **Documentation** - [Project Wiki](https://github.com/your-repo/thrillwiki-monorepo/wiki)
|
||||
- **Issues**: [GitHub Issues](https://github.com/your-repo/thrillwiki/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/your-repo/thrillwiki/discussions)
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ for the theme park and roller coaster community**
|
||||
**Built with Django + HTMX for the theme park and roller coaster community**
|
||||
|
||||
368
docs/SETUP_GUIDE.md
Normal file
368
docs/SETUP_GUIDE.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# ThrillWiki Setup Guide
|
||||
|
||||
This guide provides comprehensive instructions for setting up the ThrillWiki development environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Software
|
||||
|
||||
| Software | Minimum Version | Notes |
|
||||
|----------|----------------|-------|
|
||||
| Python | 3.13+ | Required for Django 5.2 |
|
||||
| PostgreSQL | 14+ | With PostGIS extension |
|
||||
| Redis | 6+ | For caching and sessions |
|
||||
| UV | Latest | Python package manager |
|
||||
|
||||
### Platform-Specific Prerequisites
|
||||
|
||||
#### macOS (with Homebrew)
|
||||
|
||||
```bash
|
||||
# Install Homebrew if not present
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# Install dependencies
|
||||
brew install python@3.13 postgresql@15 postgis redis gdal geos proj
|
||||
|
||||
# Start services
|
||||
brew services start postgresql@15
|
||||
brew services start redis
|
||||
|
||||
# Install UV
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
# Update package list
|
||||
sudo apt update
|
||||
|
||||
# Install Python 3.13
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt install python3.13 python3.13-venv python3.13-dev
|
||||
|
||||
# Install PostgreSQL with PostGIS
|
||||
sudo apt install postgresql-15 postgresql-15-postgis-3 postgresql-contrib
|
||||
|
||||
# Install Redis
|
||||
sudo apt install redis-server
|
||||
|
||||
# Install GDAL and GEOS for GeoDjango
|
||||
sudo apt install libgdal-dev libgeos-dev binutils libproj-dev
|
||||
|
||||
# Start services
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl start redis
|
||||
|
||||
# Install UV
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
#### Windows (with WSL2)
|
||||
|
||||
```bash
|
||||
# Use WSL2 with Ubuntu
|
||||
wsl --install -d Ubuntu
|
||||
|
||||
# Inside WSL2, follow Linux instructions above
|
||||
```
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd thrillwiki
|
||||
```
|
||||
|
||||
### 2. Database Setup
|
||||
|
||||
#### Create PostgreSQL Database
|
||||
|
||||
```bash
|
||||
# Access PostgreSQL
|
||||
sudo -u postgres psql
|
||||
|
||||
# Create database and user
|
||||
CREATE USER thrillwiki WITH PASSWORD 'your-password';
|
||||
CREATE DATABASE thrillwiki OWNER thrillwiki;
|
||||
|
||||
# Enable PostGIS extension
|
||||
\c thrillwiki
|
||||
CREATE EXTENSION postgis;
|
||||
CREATE EXTENSION postgis_topology;
|
||||
|
||||
# Grant privileges
|
||||
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki;
|
||||
\q
|
||||
```
|
||||
|
||||
#### Verify PostGIS Installation
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -d thrillwiki -c "SELECT PostGIS_version();"
|
||||
```
|
||||
|
||||
### 3. Install Python Dependencies
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Install dependencies with UV
|
||||
uv sync --frozen
|
||||
|
||||
# Or allow updates within version constraints
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 4. Configure Environment Variables
|
||||
|
||||
```bash
|
||||
# Copy example environment file
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your settings:
|
||||
|
||||
```bash
|
||||
# Required settings
|
||||
SECRET_KEY=generate-a-secure-key-here
|
||||
DEBUG=True
|
||||
DJANGO_SETTINGS_MODULE=config.django.local
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgis://thrillwiki:your-password@localhost:5432/thrillwiki
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/1
|
||||
|
||||
# GeoDjango (macOS with Homebrew)
|
||||
GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
|
||||
GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
|
||||
```
|
||||
|
||||
Generate a secure secret key:
|
||||
|
||||
```bash
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
```
|
||||
|
||||
### 5. Run Database Migrations
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run manage.py migrate
|
||||
```
|
||||
|
||||
### 6. Create Superuser
|
||||
|
||||
```bash
|
||||
uv run manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 7. Collect Static Files (Optional for Development)
|
||||
|
||||
```bash
|
||||
uv run manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### 8. Start Development Server
|
||||
|
||||
```bash
|
||||
uv run manage.py runserver
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:8000`.
|
||||
|
||||
## Verification
|
||||
|
||||
### Check Django Configuration
|
||||
|
||||
```bash
|
||||
uv run manage.py check
|
||||
```
|
||||
|
||||
### Validate Settings
|
||||
|
||||
```bash
|
||||
uv run manage.py validate_settings
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
uv run manage.py test
|
||||
```
|
||||
|
||||
### Access Admin Panel
|
||||
|
||||
1. Navigate to `http://localhost:8000/admin/`
|
||||
2. Login with superuser credentials
|
||||
|
||||
## Common Setup Issues
|
||||
|
||||
### Issue: PostGIS Not Found
|
||||
|
||||
**Error**: `Could not find the GDAL library`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# macOS
|
||||
export GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
|
||||
export GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
|
||||
|
||||
# Add to .env file for persistence
|
||||
```
|
||||
|
||||
### Issue: Database Connection Error
|
||||
|
||||
**Error**: `could not connect to server`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check PostgreSQL is running
|
||||
sudo systemctl status postgresql # Linux
|
||||
brew services list # macOS
|
||||
|
||||
# Check connection string
|
||||
psql $DATABASE_URL
|
||||
```
|
||||
|
||||
### Issue: Redis Connection Error
|
||||
|
||||
**Error**: `Error connecting to Redis`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check Redis is running
|
||||
redis-cli ping
|
||||
# Should return: PONG
|
||||
|
||||
# Start Redis if not running
|
||||
sudo systemctl start redis # Linux
|
||||
brew services start redis # macOS
|
||||
```
|
||||
|
||||
### Issue: Python Version Mismatch
|
||||
|
||||
**Error**: `Python 3.13+ required`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check Python version
|
||||
python --version
|
||||
|
||||
# Use specific Python version with UV
|
||||
UV_PYTHON=python3.13 uv sync
|
||||
```
|
||||
|
||||
### Issue: Migration Errors
|
||||
|
||||
**Error**: `django.db.utils.ProgrammingError`
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Reset migrations (development only!)
|
||||
uv run manage.py migrate --fake-initial
|
||||
|
||||
# Or recreate database
|
||||
sudo -u postgres dropdb thrillwiki
|
||||
sudo -u postgres createdb thrillwiki -O thrillwiki
|
||||
sudo -u postgres psql -d thrillwiki -c "CREATE EXTENSION postgis;"
|
||||
uv run manage.py migrate
|
||||
```
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
### Enable Debug Toolbar
|
||||
|
||||
```bash
|
||||
# Install with profiling group
|
||||
uv sync --group profiling
|
||||
|
||||
# Debug toolbar is enabled by default in local.py
|
||||
```
|
||||
|
||||
### Configure Email for Development
|
||||
|
||||
```python
|
||||
# Uses console backend by default
|
||||
# Emails are printed to console in development
|
||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
```
|
||||
|
||||
### Load Sample Data
|
||||
|
||||
```bash
|
||||
# If sample data fixtures exist
|
||||
uv run manage.py loaddata sample_parks.json
|
||||
uv run manage.py loaddata sample_rides.json
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Server
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run manage.py runserver
|
||||
```
|
||||
|
||||
### Running with Celery (for background tasks)
|
||||
|
||||
```bash
|
||||
# Terminal 1: Django server
|
||||
uv run manage.py runserver
|
||||
|
||||
# Terminal 2: Celery worker
|
||||
uv run celery -A config.celery worker -l info
|
||||
|
||||
# Terminal 3: Celery beat (for scheduled tasks)
|
||||
uv run celery -A config.celery beat -l info
|
||||
```
|
||||
|
||||
### Code Quality Commands
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
uv run black .
|
||||
uv run isort .
|
||||
|
||||
# Lint code
|
||||
uv run ruff check .
|
||||
uv run flake8 .
|
||||
|
||||
# Type checking
|
||||
uv run pyright
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
|
||||
```bash
|
||||
# Create migrations after model changes
|
||||
uv run manage.py makemigrations
|
||||
|
||||
# Apply migrations
|
||||
uv run manage.py migrate
|
||||
|
||||
# Run tests
|
||||
uv run manage.py test
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After completing setup:
|
||||
|
||||
1. **Explore the Admin Panel**: `/admin/`
|
||||
2. **View API Documentation**: `/api/docs/`
|
||||
3. **Read HTMX Patterns**: [htmx-patterns.md](./htmx-patterns.md)
|
||||
4. **Review Architecture**: [Architecture Decisions](./architecture/)
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check [Common Issues](#common-setup-issues) above
|
||||
- Review [Environment Variables](./configuration/environment-variables.md)
|
||||
- See [Backend README](../backend/README.md) for development details
|
||||
File diff suppressed because it is too large
Load Diff
350
docs/accessibility/component-patterns.md
Normal file
350
docs/accessibility/component-patterns.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Accessible Component Patterns
|
||||
|
||||
This document provides code examples for creating accessible components in ThrillWiki. Follow these patterns to ensure WCAG 2.1 AA compliance.
|
||||
|
||||
## Button
|
||||
|
||||
### Standard Button
|
||||
```django
|
||||
{% include 'components/ui/button.html' with
|
||||
text='Save Changes'
|
||||
variant='primary'
|
||||
type='submit'
|
||||
%}
|
||||
```
|
||||
|
||||
### Icon Button (REQUIRED: aria_label)
|
||||
```django
|
||||
{% include 'components/ui/button.html' with
|
||||
icon=close_svg
|
||||
size='icon'
|
||||
aria_label='Close dialog'
|
||||
variant='ghost'
|
||||
%}
|
||||
```
|
||||
|
||||
**Important**: Icon-only buttons MUST include `aria_label` for screen reader accessibility.
|
||||
|
||||
### Disabled Button
|
||||
```django
|
||||
{% include 'components/ui/button.html' with
|
||||
text='Submit'
|
||||
disabled=True
|
||||
%}
|
||||
```
|
||||
|
||||
### Button with HTMX
|
||||
```django
|
||||
{% include 'components/ui/button.html' with
|
||||
text='Load More'
|
||||
hx_get='/api/items?page=2'
|
||||
hx_target='#item-list'
|
||||
hx_swap='beforeend'
|
||||
%}
|
||||
```
|
||||
|
||||
## Form Field
|
||||
|
||||
### Standard Field
|
||||
```django
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=form.email
|
||||
%}
|
||||
```
|
||||
|
||||
The form_field.html component automatically handles:
|
||||
- Label association via `for` attribute
|
||||
- Error message display with proper ARIA
|
||||
- Required field indication
|
||||
- Help text with `aria-describedby`
|
||||
|
||||
### Field with Help Text
|
||||
```django
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=form.password
|
||||
help_text='Must be at least 8 characters'
|
||||
%}
|
||||
```
|
||||
|
||||
### Field with HTMX Validation
|
||||
```django
|
||||
{% include 'forms/partials/form_field.html' with
|
||||
field=form.username
|
||||
hx_validate=True
|
||||
hx_validate_url='/api/validate-username/'
|
||||
%}
|
||||
```
|
||||
|
||||
### Complete Form Example
|
||||
```django
|
||||
<form method="post" role="form" aria-label="User registration form">
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset>
|
||||
<legend class="sr-only">Account Information</legend>
|
||||
{% include 'forms/partials/form_field.html' with field=form.username %}
|
||||
{% include 'forms/partials/form_field.html' with field=form.email %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend class="sr-only">Security</legend>
|
||||
{% include 'forms/partials/form_field.html' with field=form.password1 %}
|
||||
{% include 'forms/partials/form_field.html' with field=form.password2 %}
|
||||
</fieldset>
|
||||
|
||||
{% include 'components/ui/button.html' with
|
||||
text='Create Account'
|
||||
type='submit'
|
||||
variant='primary'
|
||||
%}
|
||||
</form>
|
||||
```
|
||||
|
||||
## Modal
|
||||
|
||||
### Basic Modal
|
||||
```django
|
||||
{% extends 'components/modals/modal_base.html' %}
|
||||
|
||||
{% block modal_body %}
|
||||
<p>Are you sure you want to delete this item?</p>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Modal with Actions
|
||||
```django
|
||||
{% extends 'components/modals/modal_base.html' %}
|
||||
|
||||
{% block modal_body %}
|
||||
<p>This action cannot be undone.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<button @click="{{ show_var }} = false" type="button">
|
||||
Cancel
|
||||
</button>
|
||||
{% include 'components/ui/button.html' with
|
||||
text='Delete'
|
||||
variant='destructive'
|
||||
x_on_click='confirmDelete()'
|
||||
%}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
The modal_inner.html component automatically provides:
|
||||
- `role="dialog"` and `aria-modal="true"`
|
||||
- `aria-labelledby` pointing to title
|
||||
- `aria-describedby` pointing to body (and subtitle if present)
|
||||
- Focus trap with Tab/Shift+Tab cycling
|
||||
- Home/End key support for first/last focusable element
|
||||
- Escape key to close (configurable)
|
||||
- Auto-focus on first focusable element
|
||||
|
||||
## Navigation Menu
|
||||
|
||||
### Dropdown Menu
|
||||
```django
|
||||
<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
||||
<button
|
||||
@click="open = !open"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="open.toString()"
|
||||
aria-label="User menu">
|
||||
<span>Menu</span>
|
||||
<i class="fas fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
role="menu"
|
||||
aria-label="User account options"
|
||||
class="dropdown-menu">
|
||||
|
||||
<a role="menuitem" href="/profile" class="menu-item">Profile</a>
|
||||
<a role="menuitem" href="/settings" class="menu-item">Settings</a>
|
||||
<div role="separator" class="border-t"></div>
|
||||
<button role="menuitem" type="submit" class="menu-item">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
### Accessible Search with Results
|
||||
```django
|
||||
<div role="search">
|
||||
<label for="search-input" class="sr-only">Search parks and rides</label>
|
||||
<input
|
||||
id="search-input"
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
hx-get="/search"
|
||||
hx-target="#search-results"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
autocomplete="off"
|
||||
aria-describedby="search-status"
|
||||
aria-controls="search-results"
|
||||
/>
|
||||
|
||||
<div
|
||||
id="search-results"
|
||||
role="listbox"
|
||||
aria-label="Search results"
|
||||
aria-live="polite">
|
||||
<!-- Results populated by HTMX -->
|
||||
</div>
|
||||
|
||||
<div id="search-status" class="sr-only" aria-live="polite" aria-atomic="true">
|
||||
<!-- Status announcements -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Search Result Item
|
||||
```django
|
||||
<div role="option" aria-selected="false" class="search-result">
|
||||
<a href="{{ result.url }}">
|
||||
<span>{{ result.name }}</span>
|
||||
<span class="text-muted-foreground">{{ result.type }}</span>
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Live Regions
|
||||
|
||||
### Status Announcements
|
||||
```django
|
||||
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
|
||||
{{ status_message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
Use `aria-live="polite"` for non-urgent updates that can wait for user to finish current action.
|
||||
|
||||
### Alert Messages
|
||||
```django
|
||||
<div role="alert" aria-live="assertive">
|
||||
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
|
||||
{{ error_message }}
|
||||
</div>
|
||||
```
|
||||
|
||||
Use `aria-live="assertive"` for critical errors that require immediate attention.
|
||||
|
||||
### Loading State
|
||||
```django
|
||||
<div role="status" aria-live="polite" aria-busy="true">
|
||||
<span class="sr-only">Loading results...</span>
|
||||
<div class="htmx-indicator" aria-hidden="true">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
### Meaningful Image
|
||||
```django
|
||||
<img
|
||||
src="{{ park.image.url }}"
|
||||
alt="{{ park.name }} - {{ park.location }}"
|
||||
/>
|
||||
```
|
||||
|
||||
### Decorative Image
|
||||
```django
|
||||
<img src="/decorative-pattern.svg" alt="" role="presentation" />
|
||||
```
|
||||
|
||||
### Avatar with Name
|
||||
```django
|
||||
{% if user.profile.avatar %}
|
||||
<img
|
||||
src="{{ user.profile.avatar.url }}"
|
||||
alt="{{ user.get_full_name|default:user.username }}'s profile picture"
|
||||
class="avatar"
|
||||
/>
|
||||
{% else %}
|
||||
<div class="avatar-placeholder" aria-hidden="true">
|
||||
{{ user.username.0|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
```django
|
||||
{% include 'components/navigation/breadcrumbs.html' %}
|
||||
```
|
||||
|
||||
The breadcrumbs component automatically provides:
|
||||
- `<nav>` element with `aria-label="Breadcrumb"`
|
||||
- Ordered list for semantic structure
|
||||
- `aria-current="page"` on current page
|
||||
- Hidden separators with `aria-hidden="true"`
|
||||
- Schema.org JSON-LD structured data
|
||||
|
||||
## Skip Link
|
||||
|
||||
Add to the top of your base template:
|
||||
```django
|
||||
<a href="#main-content" class="skip-link sr-only-focusable">
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<!-- ... header and navigation ... -->
|
||||
|
||||
<main id="main-content" tabindex="-1">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
```
|
||||
|
||||
## Theme Toggle
|
||||
|
||||
```django
|
||||
<button
|
||||
@click="toggleTheme()"
|
||||
aria-label="Toggle theme"
|
||||
:aria-pressed="isDarkMode.toString()"
|
||||
class="theme-toggle">
|
||||
<i class="fas fa-sun" aria-hidden="true"></i>
|
||||
<i class="fas fa-moon" aria-hidden="true"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
## Focus Management Utilities
|
||||
|
||||
### Focus Trap (Alpine.js)
|
||||
```javascript
|
||||
// Already implemented in modal_inner.html
|
||||
@keydown.tab.prevent="trapFocus($event)"
|
||||
```
|
||||
|
||||
### Return Focus After Action
|
||||
```javascript
|
||||
// Store the trigger element
|
||||
const trigger = document.activeElement;
|
||||
|
||||
// After modal closes
|
||||
trigger.focus();
|
||||
```
|
||||
|
||||
## Testing Your Components
|
||||
|
||||
### Quick Accessibility Audit
|
||||
1. Can you Tab through all interactive elements?
|
||||
2. Is focus indicator visible?
|
||||
3. Does Enter/Space activate buttons?
|
||||
4. Does Escape close modals/dropdowns?
|
||||
5. Does screen reader announce all content?
|
||||
6. Is color not the only indicator of state?
|
||||
|
||||
### Automated Testing
|
||||
```bash
|
||||
# Run accessibility tests
|
||||
python manage.py test backend.tests.accessibility
|
||||
|
||||
# Use axe browser extension for quick audits
|
||||
# Install from: https://www.deque.com/axe/
|
||||
```
|
||||
138
docs/accessibility/keyboard-navigation.md
Normal file
138
docs/accessibility/keyboard-navigation.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Keyboard Navigation Guide
|
||||
|
||||
ThrillWiki is designed to be fully accessible via keyboard. This guide documents all keyboard shortcuts and navigation patterns.
|
||||
|
||||
## Global Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Move focus forward |
|
||||
| Shift + Tab | Move focus backward |
|
||||
| Enter | Activate focused element |
|
||||
| Space | Activate buttons/checkboxes |
|
||||
| Escape | Close modals/dropdowns |
|
||||
|
||||
## Navigation Menu
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Focus menu button |
|
||||
| Enter/Space | Open menu |
|
||||
| Arrow Down | Next menu item |
|
||||
| Arrow Up | Previous menu item |
|
||||
| Escape | Close menu |
|
||||
| Home | First menu item |
|
||||
| End | Last menu item |
|
||||
|
||||
## Search
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Focus search input |
|
||||
| Type | Start searching (debounced 300ms) |
|
||||
| Arrow Down | Navigate to next result |
|
||||
| Arrow Up | Navigate to previous result (or back to input) |
|
||||
| Enter | Select current result (navigate to link) |
|
||||
| Escape | Close results and blur input |
|
||||
| Home | Jump to first result |
|
||||
| End | Jump to last result |
|
||||
|
||||
## Modals
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Next focusable element (trapped within modal) |
|
||||
| Shift + Tab | Previous focusable element (trapped within modal) |
|
||||
| Escape | Close modal |
|
||||
| Home | Jump to first focusable element |
|
||||
| End | Jump to last focusable element |
|
||||
|
||||
## Forms
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Tab | Move to next field |
|
||||
| Shift + Tab | Move to previous field |
|
||||
| Enter | Submit form (when focused on submit button) |
|
||||
| Space | Toggle checkbox/radio button |
|
||||
| Arrow Down/Up | Navigate select options |
|
||||
| Escape | Close select dropdown |
|
||||
|
||||
## Dropdowns (User Menu, Browse Menu)
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Enter/Space | Toggle dropdown open/closed |
|
||||
| Escape | Close dropdown |
|
||||
| Arrow Down | Navigate to next item |
|
||||
| Arrow Up | Navigate to previous item |
|
||||
| Tab | Move through items (or exit dropdown) |
|
||||
|
||||
## Mobile Menu
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Enter/Space | Open mobile menu |
|
||||
| Escape | Close mobile menu |
|
||||
| Tab | Navigate through menu items |
|
||||
|
||||
## Theme Toggle
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Enter/Space | Toggle between light and dark mode |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Focus Management
|
||||
- All interactive elements must be reachable via keyboard
|
||||
- Focus order follows logical reading order (top to bottom, left to right)
|
||||
- Focus is trapped within modals when open
|
||||
- Focus returns to trigger element when modal/dropdown closes
|
||||
|
||||
### Focus Indicators
|
||||
- All focusable elements have visible focus indicators
|
||||
- Focus ring: 2px solid primary color with 2px offset
|
||||
- Enhanced visibility in high contrast mode
|
||||
- Custom focus styles for complex components
|
||||
|
||||
### ARIA Attributes
|
||||
- `aria-expanded`: Indicates dropdown/menu open state
|
||||
- `aria-haspopup`: Indicates element triggers a popup
|
||||
- `aria-label`: Provides accessible name for icon-only buttons
|
||||
- `aria-modal`: Indicates modal dialog
|
||||
- `aria-controls`: Links trigger to controlled element
|
||||
- `aria-describedby`: Links element to description
|
||||
- `aria-live`: Announces dynamic content changes
|
||||
|
||||
### Skip Links
|
||||
- Skip to main content link available (hidden until focused)
|
||||
- Appears at top of page when Tab is pressed first
|
||||
|
||||
## Testing Keyboard Navigation
|
||||
|
||||
### Manual Testing Checklist
|
||||
1. Navigate entire site using only keyboard
|
||||
2. Verify all interactive elements are reachable with Tab
|
||||
3. Verify Enter/Space activates all buttons and links
|
||||
4. Verify Escape closes all modals and dropdowns
|
||||
5. Verify focus indicators are visible
|
||||
6. Verify focus order is logical
|
||||
7. Verify focus is trapped in modals
|
||||
8. Verify focus returns to trigger after closing modals
|
||||
|
||||
### Automated Testing
|
||||
Run accessibility tests:
|
||||
```bash
|
||||
python manage.py test backend.tests.accessibility
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
Keyboard navigation is tested on:
|
||||
- Chrome (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Edge (latest)
|
||||
|
||||
Note: Some keyboard shortcuts may conflict with browser shortcuts. In those cases, browser shortcuts take precedence.
|
||||
200
docs/accessibility/screen-reader-testing.md
Normal file
200
docs/accessibility/screen-reader-testing.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Screen Reader Testing Checklist
|
||||
|
||||
This document provides a comprehensive checklist for testing ThrillWiki with screen readers to ensure WCAG 2.1 AA compliance.
|
||||
|
||||
## Recommended Screen Readers
|
||||
|
||||
| Screen Reader | Platform | Cost | Notes |
|
||||
|--------------|----------|------|-------|
|
||||
| NVDA | Windows | Free | Most widely used free option |
|
||||
| JAWS | Windows | Commercial | Industry standard |
|
||||
| VoiceOver | macOS/iOS | Built-in | Activate with Cmd+F5 |
|
||||
| TalkBack | Android | Built-in | Enable in Accessibility settings |
|
||||
| Narrator | Windows | Built-in | Basic testing |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Navigation
|
||||
|
||||
- [ ] Skip to main content link works and is announced
|
||||
- [ ] All navigation items are announced with their role
|
||||
- [ ] Current page is indicated (aria-current="page")
|
||||
- [ ] Dropdown menus announce as "button, collapsed" when closed
|
||||
- [ ] Dropdown menus announce as "button, expanded" when open
|
||||
- [ ] Menu items announce with role="menuitem"
|
||||
- [ ] Breadcrumbs are announced as navigation landmark
|
||||
|
||||
### Search
|
||||
|
||||
- [ ] Search input is announced with label "Search parks and rides"
|
||||
- [ ] Search results count is announced when results appear
|
||||
- [ ] Each result is announced with its content
|
||||
- [ ] Arrow key navigation announces current selection
|
||||
- [ ] "No results found" is announced when applicable
|
||||
|
||||
### Forms
|
||||
|
||||
- [ ] All form fields have associated labels announced
|
||||
- [ ] Required fields announce "required"
|
||||
- [ ] Error messages are announced immediately when form validates
|
||||
- [ ] Success messages are announced after form submission
|
||||
- [ ] Help text is associated and announced with fields
|
||||
- [ ] Field types are announced (text input, checkbox, select, etc.)
|
||||
|
||||
### Interactive Components
|
||||
|
||||
- [ ] Buttons announce their purpose/label
|
||||
- [ ] Icon-only buttons announce their aria-label
|
||||
- [ ] Links announce destination or purpose
|
||||
- [ ] Modals announce their title when opened
|
||||
- [ ] Modal content is announced as dialog
|
||||
- [ ] Closing modal announces return to previous context
|
||||
- [ ] Theme toggle announces current state (pressed/not pressed)
|
||||
|
||||
### Dynamic Content
|
||||
|
||||
- [ ] Search results are announced via aria-live region
|
||||
- [ ] Filter changes announce result count
|
||||
- [ ] Status updates (success, error) are announced
|
||||
- [ ] Loading states are announced ("Loading...")
|
||||
- [ ] HTMX content swaps announce via live regions
|
||||
|
||||
### Images
|
||||
|
||||
- [ ] All meaningful images have descriptive alt text
|
||||
- [ ] Decorative images are hidden (alt="" or aria-hidden)
|
||||
- [ ] Complex images have long descriptions available
|
||||
- [ ] Avatar images include user's name in alt text
|
||||
|
||||
### Tables (if applicable)
|
||||
|
||||
- [ ] Tables have proper headers (th elements)
|
||||
- [ ] Table caption or aria-label describes table purpose
|
||||
- [ ] Data cells associate with headers
|
||||
|
||||
### Headings
|
||||
|
||||
- [ ] Page has exactly one h1
|
||||
- [ ] Heading hierarchy is logical (h1 > h2 > h3)
|
||||
- [ ] No skipped heading levels
|
||||
- [ ] Headings describe section content
|
||||
|
||||
## Testing Commands
|
||||
|
||||
### NVDA (Windows)
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| Insert + Down Arrow | Read next item |
|
||||
| Insert + Up Arrow | Read previous item |
|
||||
| Insert + Space | Read current item |
|
||||
| Insert + F7 | Elements list (links, headings, landmarks) |
|
||||
| H | Next heading |
|
||||
| Shift + H | Previous heading |
|
||||
| K | Next link |
|
||||
| F | Next form field |
|
||||
| D | Next landmark |
|
||||
| Insert + Ctrl + N | Read notifications |
|
||||
|
||||
### VoiceOver (macOS)
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| VO + Right Arrow | Next item |
|
||||
| VO + Left Arrow | Previous item |
|
||||
| VO + U | Rotor (elements list) |
|
||||
| VO + A | Read all from current position |
|
||||
| VO + Cmd + H | Next heading |
|
||||
| VO + Cmd + J | Next form control |
|
||||
| VO + Cmd + L | Next link |
|
||||
|
||||
Note: VO = Control + Option
|
||||
|
||||
### VoiceOver (iOS)
|
||||
|
||||
| Gesture | Action |
|
||||
|---------|--------|
|
||||
| Swipe Right | Next item |
|
||||
| Swipe Left | Previous item |
|
||||
| Double Tap | Activate item |
|
||||
| Two-finger Swipe Up | Read all from current position |
|
||||
| Rotor (two-finger twist) | Change navigation mode |
|
||||
|
||||
### JAWS (Windows)
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| Down Arrow | Next item |
|
||||
| Up Arrow | Previous item |
|
||||
| Insert + F5 | Forms list |
|
||||
| Insert + F6 | Headings list |
|
||||
| Insert + F7 | Links list |
|
||||
| H | Next heading |
|
||||
| F | Next form field |
|
||||
| T | Next table |
|
||||
| R | Next region/landmark |
|
||||
|
||||
### TalkBack (Android)
|
||||
|
||||
| Gesture | Action |
|
||||
|---------|--------|
|
||||
| Swipe Right | Next item |
|
||||
| Swipe Left | Previous item |
|
||||
| Double Tap | Activate item |
|
||||
| Swipe Up then Down | Navigation settings |
|
||||
|
||||
## Common Issues to Watch For
|
||||
|
||||
### Problematic Patterns
|
||||
1. **Missing labels**: Form fields without associated labels
|
||||
2. **Duplicate IDs**: Multiple elements with same ID breaks aria-describedby
|
||||
3. **Empty buttons**: Buttons with no text or aria-label
|
||||
4. **Inaccessible modals**: Focus not trapped, no escape to close
|
||||
5. **Auto-playing media**: Audio/video that plays automatically
|
||||
6. **Timeout without warning**: Sessions expiring without notice
|
||||
7. **Moving focus unexpectedly**: Focus jumping after interactions
|
||||
8. **Color-only information**: Status conveyed only by color
|
||||
|
||||
### Good Patterns
|
||||
1. **Clear labels**: Every form field has descriptive label
|
||||
2. **Error prevention**: Clear instructions, validation before submit
|
||||
3. **Focus management**: Logical order, visible indicators, trapped in modals
|
||||
4. **Consistent navigation**: Same navigation pattern on all pages
|
||||
5. **Multiple ways**: Multiple paths to same content
|
||||
6. **Descriptive links**: Link text describes destination (not "click here")
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
When reporting accessibility issues, include:
|
||||
1. Screen reader and version used
|
||||
2. Browser and version
|
||||
3. Page URL
|
||||
4. Steps to reproduce
|
||||
5. Expected behavior
|
||||
6. Actual behavior (what was announced)
|
||||
7. WCAG success criterion violated
|
||||
|
||||
## WCAG 2.1 AA Quick Reference
|
||||
|
||||
### Level A (Must Have)
|
||||
- 1.1.1 Non-text Content (alt text)
|
||||
- 1.3.1 Info and Relationships (semantic HTML)
|
||||
- 2.1.1 Keyboard (all functionality via keyboard)
|
||||
- 2.4.1 Bypass Blocks (skip links)
|
||||
- 4.1.2 Name, Role, Value (ARIA)
|
||||
|
||||
### Level AA (Should Have)
|
||||
- 1.4.3 Contrast (Minimum) (4.5:1 ratio)
|
||||
- 1.4.4 Resize Text (200% zoom)
|
||||
- 2.4.6 Headings and Labels (descriptive)
|
||||
- 2.4.7 Focus Visible (visible focus indicator)
|
||||
- 3.2.3 Consistent Navigation
|
||||
- 3.2.4 Consistent Identification
|
||||
|
||||
## Resources
|
||||
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/TR/WCAG21/)
|
||||
- [WebAIM Screen Reader Survey](https://webaim.org/projects/screenreadersurvey9/)
|
||||
- [NVDA User Guide](https://www.nvaccess.org/files/nvda/documentation/userGuide.html)
|
||||
- [VoiceOver User Guide](https://support.apple.com/guide/voiceover/welcome/mac)
|
||||
- [JAWS Quick Start](https://www.freedomscientific.com/products/software/jaws/)
|
||||
138
docs/admin/CHANGELOG.md
Normal file
138
docs/admin/CHANGELOG.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Admin Interface Changelog
|
||||
|
||||
## Phase 11: Django Admin Standardization
|
||||
|
||||
### Added
|
||||
|
||||
- **Base Admin Classes and Mixins** (`apps/core/admin/`)
|
||||
- `BaseModelAdmin` - Standard base class with consistent pagination and settings
|
||||
- `QueryOptimizationMixin` - Automatic select_related/prefetch_related optimization
|
||||
- `ReadOnlyAdminMixin` - Disable modifications for auto-generated data
|
||||
- `TimestampFieldsMixin` - Standard handling for created_at/updated_at
|
||||
- `SlugFieldMixin` - Automatic slug prepopulation
|
||||
- `ExportActionMixin` - CSV/JSON export functionality
|
||||
- `BulkStatusChangeMixin` - Bulk status change actions
|
||||
- `ModerationMixin` - Standard moderation approve/reject actions
|
||||
|
||||
- **Query Optimization Across All Admin Classes**
|
||||
- `list_select_related` for all ForeignKey fields in list_display
|
||||
- `list_prefetch_related` for reverse relations and M2M fields
|
||||
- Queryset annotations for calculated fields (ride counts, average ratings)
|
||||
|
||||
- **Custom Bulk Actions**
|
||||
- Parks: `bulk_activate`, `bulk_deactivate`, `recalculate_stats`
|
||||
- Rides: `bulk_set_operating`, `bulk_set_closed`, `bulk_set_sbno`, `recalculate_ratings`
|
||||
- Accounts: `activate_users`, `deactivate_users`, `ban_users`, `unban_users`, `send_verification_email`, `recalculate_credits`
|
||||
- Reviews: `bulk_approve`, `bulk_reject`, `flag_for_review`
|
||||
- Moderation: `bulk_approve`, `bulk_reject`, `bulk_escalate`
|
||||
- Photos: `set_primary`, `remove_primary`, `flag_missing_alt`
|
||||
- Tokens: `resend_verification`, `delete_expired`, `cleanup_old_tokens`
|
||||
- Lists: `publish_lists`, `unpublish_lists`, `move_up`, `move_down`
|
||||
|
||||
- **Export Functionality**
|
||||
- CSV export for all major models
|
||||
- JSON export for all major models
|
||||
- Audit trail export for state logs
|
||||
- SEO redirect export for slug history
|
||||
|
||||
- **Enhanced Display Methods**
|
||||
- Color-coded status badges for all status fields
|
||||
- Clickable links to related objects in list views
|
||||
- Rating displays with star formatting
|
||||
- Thumbnail previews for images
|
||||
- Profile completeness indicators
|
||||
- Moderation status indicators
|
||||
|
||||
- **Comprehensive Fieldsets**
|
||||
- Standardized fieldset organization across all admins
|
||||
- Collapsible sections for advanced/metadata fields
|
||||
- Descriptive help text for all fieldsets
|
||||
|
||||
- **Documentation**
|
||||
- `docs/admin/base_classes.md` - Base admin architecture documentation
|
||||
- `docs/admin/CHANGELOG.md` - This changelog
|
||||
|
||||
- **Test Coverage**
|
||||
- `apps/core/tests/test_admin.py` - Tests for base classes and mixins
|
||||
- `apps/parks/tests/test_admin.py` - Tests for parks admin
|
||||
- `apps/rides/tests/test_admin.py` - Tests for rides admin
|
||||
- `apps/accounts/tests/test_admin.py` - Tests for accounts admin
|
||||
- `apps/moderation/tests/test_admin.py` - Tests for moderation admin
|
||||
|
||||
### Changed
|
||||
|
||||
- **Parks Admin** (`apps/parks/admin.py`)
|
||||
- Optimized all querysets with select_related/prefetch_related
|
||||
- Added ride count and average rating annotations
|
||||
- Enhanced list_display with clickable links
|
||||
- Added autocomplete_fields for ForeignKeys
|
||||
- Standardized fieldsets with descriptions
|
||||
|
||||
- **Rides Admin** (`apps/rides/admin.py`)
|
||||
- Optimized all querysets with select_related/prefetch_related
|
||||
- Added review count and average rating annotations
|
||||
- Enhanced category and status badges with color coding
|
||||
- Added FSM-aware status change actions
|
||||
- Maintained read-only status for rankings
|
||||
|
||||
- **Accounts Admin** (`apps/accounts/admin.py`)
|
||||
- Optimized user queryset with profile select_related
|
||||
- Added total credits display with breakdown tooltip
|
||||
- Enhanced user status badge display
|
||||
- Added profile completeness indicator
|
||||
- Added social media presence indicator
|
||||
|
||||
- **Core Admin** (`apps/core/admin.py`)
|
||||
- Enhanced SlugHistory with content type display
|
||||
- Added admin URL linking for related objects
|
||||
- Added SEO export functionality
|
||||
|
||||
- **Media Admin** (`shared/media/admin.py`)
|
||||
- Enhanced thumbnail preview with lazy loading
|
||||
- Added alt text validation warnings
|
||||
- Added primary photo management actions
|
||||
|
||||
- **Moderation Admin** (`apps/moderation/admin.py`)
|
||||
- Enhanced dashboard with pending counts
|
||||
- Added changes preview formatting
|
||||
- Enhanced state log display with badges
|
||||
- Added audit trail export
|
||||
|
||||
### Fixed
|
||||
|
||||
- N+1 query issues in all admin list views
|
||||
- Inconsistent permission handling across admins
|
||||
- Missing help text on admin fields
|
||||
- Content object link errors when objects are deleted
|
||||
|
||||
### Removed
|
||||
|
||||
- Duplicate admin file at `apps/accounts/admin.py` (root level)
|
||||
- Redundant code patterns replaced by mixins
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- **Query Optimization**
|
||||
- Parks list view: Reduced from ~50+ queries to <10 queries
|
||||
- Rides list view: Reduced from ~100+ queries to <15 queries
|
||||
- User list view: Reduced from ~30+ queries to <10 queries
|
||||
- Moderation views: Reduced from ~40+ queries to <12 queries
|
||||
|
||||
- **Page Settings**
|
||||
- Consistent pagination at 50 items per page
|
||||
- Disabled full result count for large datasets
|
||||
- Added date hierarchy for time-based filtering
|
||||
|
||||
### Security Enhancements
|
||||
|
||||
- Standardized read-only permissions for auto-generated data
|
||||
- Superuser-only delete permissions for audit logs
|
||||
- Self-protection in user ban/deactivate actions
|
||||
- FSM validation in status change actions
|
||||
|
||||
### Developer Experience
|
||||
|
||||
- Comprehensive docstrings on all admin classes
|
||||
- Consistent patterns across all apps
|
||||
- Reusable mixins reduce code duplication
|
||||
- Clear fieldset descriptions guide admin usage
|
||||
180
docs/admin/base_classes.md
Normal file
180
docs/admin/base_classes.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Admin Base Classes and Mixins
|
||||
|
||||
This document describes the base admin classes and mixins available for building standardized Django admin interfaces in ThrillWiki.
|
||||
|
||||
## Overview
|
||||
|
||||
The admin infrastructure provides reusable components that ensure consistency, optimize performance, and reduce code duplication across all admin interfaces.
|
||||
|
||||
## Base Classes
|
||||
|
||||
### BaseModelAdmin
|
||||
|
||||
The foundational admin class that all model admins should inherit from.
|
||||
|
||||
```python
|
||||
from apps.core.admin import BaseModelAdmin
|
||||
|
||||
class MyModelAdmin(BaseModelAdmin):
|
||||
list_display = ['name', 'status', 'created_at']
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Consistent pagination (50 items per page)
|
||||
- Optimized result count behavior
|
||||
- Standard empty value display
|
||||
- Save buttons at top of forms
|
||||
- Filter preservation after saves
|
||||
|
||||
## Mixins
|
||||
|
||||
### QueryOptimizationMixin
|
||||
|
||||
Provides automatic query optimization to prevent N+1 queries.
|
||||
|
||||
```python
|
||||
from apps.core.admin import QueryOptimizationMixin, BaseModelAdmin
|
||||
|
||||
class RideAdmin(QueryOptimizationMixin, BaseModelAdmin):
|
||||
list_display = ['name', 'park', 'manufacturer']
|
||||
list_select_related = ['park', 'manufacturer']
|
||||
list_prefetch_related = ['reviews', 'photos']
|
||||
```
|
||||
|
||||
**Attributes:**
|
||||
- `list_select_related`: List of ForeignKey fields to select
|
||||
- `list_prefetch_related`: List of related fields to prefetch
|
||||
|
||||
### ReadOnlyAdminMixin
|
||||
|
||||
Disables add, change, and delete permissions for auto-generated data.
|
||||
|
||||
```python
|
||||
from apps.core.admin import ReadOnlyAdminMixin, BaseModelAdmin
|
||||
|
||||
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
|
||||
list_display = ['ride', 'rank', 'calculated_at']
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Rankings and leaderboards
|
||||
- Audit logs and history
|
||||
- Calculated statistics
|
||||
- State transition logs
|
||||
|
||||
### TimestampFieldsMixin
|
||||
|
||||
Provides standard handling for `created_at` and `updated_at` fields.
|
||||
|
||||
```python
|
||||
from apps.core.admin import TimestampFieldsMixin, BaseModelAdmin
|
||||
|
||||
class MyModelAdmin(TimestampFieldsMixin, BaseModelAdmin):
|
||||
fieldsets = [
|
||||
('Basic Info', {'fields': ['name', 'description']}),
|
||||
] + TimestampFieldsMixin.get_timestamp_fieldset()
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Automatically adds timestamp fields to readonly_fields
|
||||
- Provides a collapsible fieldset for metadata
|
||||
|
||||
### SlugFieldMixin
|
||||
|
||||
Configures automatic slug population from name field.
|
||||
|
||||
```python
|
||||
from apps.core.admin import SlugFieldMixin, BaseModelAdmin
|
||||
|
||||
class ParkAdmin(SlugFieldMixin, BaseModelAdmin):
|
||||
slug_source_field = 'name' # Optional, defaults to 'name'
|
||||
```
|
||||
|
||||
### ExportActionMixin
|
||||
|
||||
Adds CSV and JSON export functionality.
|
||||
|
||||
```python
|
||||
from apps.core.admin import ExportActionMixin, BaseModelAdmin
|
||||
|
||||
class ParkAdmin(ExportActionMixin, BaseModelAdmin):
|
||||
export_fields = ['id', 'name', 'status', 'created_at']
|
||||
export_filename_prefix = 'parks'
|
||||
```
|
||||
|
||||
**Actions added:**
|
||||
- Export selected to CSV
|
||||
- Export selected to JSON
|
||||
|
||||
### BulkStatusChangeMixin
|
||||
|
||||
Provides bulk status change actions.
|
||||
|
||||
```python
|
||||
from apps.core.admin import BulkStatusChangeMixin, BaseModelAdmin
|
||||
|
||||
class RideAdmin(BulkStatusChangeMixin, BaseModelAdmin):
|
||||
status_field = 'status'
|
||||
status_choices = [
|
||||
('active', 'Activate'),
|
||||
('inactive', 'Deactivate'),
|
||||
]
|
||||
```
|
||||
|
||||
### ModerationMixin
|
||||
|
||||
Adds moderation actions for user-generated content.
|
||||
|
||||
```python
|
||||
from apps.core.admin import ModerationMixin, BaseModelAdmin
|
||||
|
||||
class ReviewAdmin(ModerationMixin, BaseModelAdmin):
|
||||
moderation_status_field = 'moderation_status'
|
||||
moderated_by_field = 'moderated_by'
|
||||
moderated_at_field = 'moderated_at'
|
||||
```
|
||||
|
||||
**Actions added:**
|
||||
- Approve selected items
|
||||
- Reject selected items
|
||||
|
||||
## Combining Mixins
|
||||
|
||||
Mixins can be combined to create feature-rich admin classes:
|
||||
|
||||
```python
|
||||
from apps.core.admin import (
|
||||
BaseModelAdmin,
|
||||
QueryOptimizationMixin,
|
||||
ExportActionMixin,
|
||||
TimestampFieldsMixin,
|
||||
SlugFieldMixin,
|
||||
)
|
||||
|
||||
class ParkAdmin(
|
||||
QueryOptimizationMixin,
|
||||
ExportActionMixin,
|
||||
TimestampFieldsMixin,
|
||||
SlugFieldMixin,
|
||||
BaseModelAdmin
|
||||
):
|
||||
list_display = ['name', 'operator', 'status', 'created_at']
|
||||
list_select_related = ['operator', 'location']
|
||||
list_prefetch_related = ['areas', 'rides']
|
||||
export_fields = ['id', 'name', 'slug', 'status']
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use BaseModelAdmin** as the final parent class
|
||||
2. **List mixins before BaseModelAdmin** in inheritance order
|
||||
3. **Define list_select_related** for all ForeignKey fields in list_display
|
||||
4. **Use prefetch_related** for reverse relations and M2M fields
|
||||
5. **Test query counts** using Django Debug Toolbar
|
||||
6. **Add export_fields** explicitly for control over exported data
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- List views: < 10 queries
|
||||
- Change views: < 15 queries
|
||||
- Page load time: < 500ms for 100 records
|
||||
267
docs/admin/best_practices.md
Normal file
267
docs/admin/best_practices.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Admin Best Practices
|
||||
|
||||
This guide outlines best practices for developing and maintaining Django admin interfaces in ThrillWiki.
|
||||
|
||||
## Query Optimization
|
||||
|
||||
### Always Use select_related for ForeignKeys
|
||||
|
||||
```python
|
||||
class RideAdmin(BaseModelAdmin):
|
||||
list_display = ['name', 'park', 'manufacturer']
|
||||
list_select_related = ['park', 'manufacturer'] # Prevents N+1 queries
|
||||
```
|
||||
|
||||
### Use prefetch_related for Reverse Relations
|
||||
|
||||
```python
|
||||
class ParkAdmin(BaseModelAdmin):
|
||||
list_display = ['name', 'ride_count']
|
||||
list_prefetch_related = ['rides'] # For efficient count queries
|
||||
```
|
||||
|
||||
### Use Annotations for Calculated Fields
|
||||
|
||||
```python
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
return qs.annotate(
|
||||
_ride_count=Count('rides', distinct=True),
|
||||
_avg_rating=Avg('reviews__rating'),
|
||||
)
|
||||
```
|
||||
|
||||
## Display Methods
|
||||
|
||||
### Always Use format_html for Safety
|
||||
|
||||
```python
|
||||
@admin.display(description="Status")
|
||||
def status_badge(self, obj):
|
||||
return format_html(
|
||||
'<span style="color: {};">{}</span>',
|
||||
'green' if obj.is_active else 'red',
|
||||
obj.get_status_display(),
|
||||
)
|
||||
```
|
||||
|
||||
### Handle Missing Data Gracefully
|
||||
|
||||
```python
|
||||
@admin.display(description="Park")
|
||||
def park_link(self, obj):
|
||||
if obj.park:
|
||||
url = reverse("admin:parks_park_change", args=[obj.park.pk])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.park.name)
|
||||
return "-" # Or format_html for styled "N/A"
|
||||
```
|
||||
|
||||
## Fieldsets Organization
|
||||
|
||||
### Standard Fieldset Structure
|
||||
|
||||
1. Basic Information - Name, slug, description
|
||||
2. Relationships - ForeignKeys, ManyToMany
|
||||
3. Status & Dates - Status fields, timestamps
|
||||
4. Specifications - Technical details (collapsed)
|
||||
5. Media - Images, photos (collapsed)
|
||||
6. Metadata - created_at, updated_at (collapsed)
|
||||
|
||||
### Include Descriptions
|
||||
|
||||
```python
|
||||
fieldsets = (
|
||||
(
|
||||
"Basic Information",
|
||||
{
|
||||
"fields": ("name", "slug", "description"),
|
||||
"description": "Core identification for this record.",
|
||||
},
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Actions
|
||||
|
||||
### Use Proper Action Decorator
|
||||
|
||||
```python
|
||||
@admin.action(description="Approve selected items")
|
||||
def bulk_approve(self, request, queryset):
|
||||
count = queryset.update(status='approved')
|
||||
self.message_user(request, f"Approved {count} items.")
|
||||
```
|
||||
|
||||
### Handle Errors Gracefully
|
||||
|
||||
```python
|
||||
@admin.action(description="Process selected items")
|
||||
def process_items(self, request, queryset):
|
||||
processed = 0
|
||||
errors = 0
|
||||
for item in queryset:
|
||||
try:
|
||||
item.process()
|
||||
processed += 1
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
self.message_user(
|
||||
request,
|
||||
f"Error processing {item}: {e}",
|
||||
level=messages.ERROR,
|
||||
)
|
||||
if processed:
|
||||
self.message_user(request, f"Processed {processed} items.")
|
||||
```
|
||||
|
||||
### Protect Against Dangerous Operations
|
||||
|
||||
```python
|
||||
@admin.action(description="Ban selected users")
|
||||
def ban_users(self, request, queryset):
|
||||
# Prevent banning self
|
||||
queryset = queryset.exclude(pk=request.user.pk)
|
||||
# Prevent banning superusers
|
||||
queryset = queryset.exclude(is_superuser=True)
|
||||
updated = queryset.update(is_banned=True)
|
||||
self.message_user(request, f"Banned {updated} users.")
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
### Read-Only for Auto-Generated Data
|
||||
|
||||
```python
|
||||
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
|
||||
"""Rankings are calculated automatically - no manual editing."""
|
||||
pass
|
||||
```
|
||||
|
||||
### Field-Level Permissions
|
||||
|
||||
```python
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
readonly = list(super().get_readonly_fields(request, obj))
|
||||
if not request.user.is_superuser:
|
||||
readonly.extend(['sensitive_field', 'admin_notes'])
|
||||
return readonly
|
||||
```
|
||||
|
||||
### Object-Level Permissions
|
||||
|
||||
```python
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if obj is None:
|
||||
return super().has_change_permission(request)
|
||||
# Only allow editing own content or moderator access
|
||||
if obj.user == request.user:
|
||||
return True
|
||||
return request.user.has_perm('app.moderate_content')
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Disable Full Result Count
|
||||
|
||||
```python
|
||||
class MyAdmin(BaseModelAdmin):
|
||||
show_full_result_count = False # Faster for large datasets
|
||||
```
|
||||
|
||||
### Use Pagination
|
||||
|
||||
```python
|
||||
class MyAdmin(BaseModelAdmin):
|
||||
list_per_page = 50 # Reasonable page size
|
||||
```
|
||||
|
||||
### Limit Inline Items
|
||||
|
||||
```python
|
||||
class ItemInline(admin.TabularInline):
|
||||
model = Item
|
||||
extra = 1 # Only show 1 empty form
|
||||
max_num = 20 # Limit total items
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Query Counts
|
||||
|
||||
```python
|
||||
def test_list_view_query_count(self):
|
||||
with self.assertNumQueries(10): # Set target
|
||||
response = self.client.get('/admin/app/model/')
|
||||
```
|
||||
|
||||
### Test Permissions
|
||||
|
||||
```python
|
||||
def test_readonly_permissions(self):
|
||||
request = self.factory.get('/admin/')
|
||||
request.user = User(is_superuser=False)
|
||||
|
||||
assert self.admin.has_add_permission(request) is False
|
||||
assert self.admin.has_change_permission(request) is False
|
||||
```
|
||||
|
||||
### Test Custom Actions
|
||||
|
||||
```python
|
||||
def test_bulk_approve_action(self):
|
||||
items = [create_item(status='pending') for _ in range(5)]
|
||||
queryset = Item.objects.filter(pk__in=[i.pk for i in items])
|
||||
|
||||
self.admin.bulk_approve(self.request, queryset)
|
||||
|
||||
for item in items:
|
||||
item.refresh_from_db()
|
||||
assert item.status == 'approved'
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid N+1 Queries
|
||||
|
||||
```python
|
||||
# BAD: Creates N+1 queries
|
||||
@admin.display()
|
||||
def related_count(self, obj):
|
||||
return obj.related_set.count()
|
||||
|
||||
# GOOD: Use annotation
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).annotate(
|
||||
_related_count=Count('related_set')
|
||||
)
|
||||
|
||||
@admin.display()
|
||||
def related_count(self, obj):
|
||||
return obj._related_count
|
||||
```
|
||||
|
||||
### Don't Forget Error Handling
|
||||
|
||||
```python
|
||||
# BAD: May raise AttributeError
|
||||
@admin.display()
|
||||
def user_name(self, obj):
|
||||
return obj.user.username
|
||||
|
||||
# GOOD: Handle missing relations
|
||||
@admin.display()
|
||||
def user_name(self, obj):
|
||||
return obj.user.username if obj.user else "-"
|
||||
```
|
||||
|
||||
### Use Autocomplete for Large Relations
|
||||
|
||||
```python
|
||||
# BAD: Loads all options
|
||||
class MyAdmin(BaseModelAdmin):
|
||||
raw_id_fields = ['park'] # Works but poor UX
|
||||
|
||||
# GOOD: Search with autocomplete
|
||||
class MyAdmin(BaseModelAdmin):
|
||||
autocomplete_fields = ['park']
|
||||
```
|
||||
128
docs/admin/overview.md
Normal file
128
docs/admin/overview.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Admin Interface Overview
|
||||
|
||||
This document provides an overview of the Django admin interface architecture for ThrillWiki.
|
||||
|
||||
## Architecture
|
||||
|
||||
The admin interface is built on a layered architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ App-Specific Admins │
|
||||
│ (ParkAdmin, RideAdmin, UserAdmin, etc.) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Mixins │
|
||||
│ (QueryOptimizationMixin, ExportActionMixin, etc.) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ BaseModelAdmin │
|
||||
│ (Standard settings, base functionality) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ django.contrib.admin │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### Base Classes (`apps/core/admin/`)
|
||||
|
||||
- **BaseModelAdmin**: Standard settings (pagination, display, ordering)
|
||||
- **Mixins**: Reusable functionality (query optimization, export, permissions)
|
||||
|
||||
### Admin Sites
|
||||
|
||||
- **Default Admin Site**: Full admin access at `/admin/`
|
||||
- **Moderation Admin Site**: Dedicated moderation interface at `/moderation/`
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| View Type | Query Target | Load Time |
|
||||
|-----------|--------------|-----------|
|
||||
| List View | < 15 queries | < 500ms |
|
||||
| Change View | < 20 queries | < 500ms |
|
||||
| Bulk Actions | < 2 seconds | per 100 records |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Adding a New Admin
|
||||
|
||||
```python
|
||||
from apps.core.admin import (
|
||||
BaseModelAdmin,
|
||||
QueryOptimizationMixin,
|
||||
ExportActionMixin,
|
||||
)
|
||||
|
||||
class MyModelAdmin(
|
||||
QueryOptimizationMixin,
|
||||
ExportActionMixin,
|
||||
BaseModelAdmin,
|
||||
):
|
||||
list_display = ['name', 'related_obj', 'status', 'created_at']
|
||||
list_select_related = ['related_obj']
|
||||
list_prefetch_related = ['many_to_many_field']
|
||||
|
||||
export_fields = ['id', 'name', 'status']
|
||||
export_filename_prefix = 'my_model'
|
||||
```
|
||||
|
||||
### Read-Only Admin for Auto-Generated Data
|
||||
|
||||
```python
|
||||
from apps.core.admin import ReadOnlyAdminMixin, BaseModelAdmin
|
||||
|
||||
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
|
||||
list_display = ['ride', 'rank', 'calculated_at']
|
||||
```
|
||||
|
||||
### Admin with Moderation Actions
|
||||
|
||||
```python
|
||||
from apps.core.admin import ModerationMixin, BaseModelAdmin
|
||||
|
||||
class ReviewAdmin(ModerationMixin, BaseModelAdmin):
|
||||
moderation_status_field = 'status'
|
||||
```
|
||||
|
||||
## Files Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── apps/
|
||||
│ ├── core/
|
||||
│ │ ├── admin/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py
|
||||
│ │ │ └── mixins.py
|
||||
│ │ └── admin.py
|
||||
│ ├── parks/
|
||||
│ │ └── admin.py
|
||||
│ ├── rides/
|
||||
│ │ └── admin.py
|
||||
│ ├── accounts/
|
||||
│ │ └── admin.py
|
||||
│ └── moderation/
|
||||
│ └── admin.py
|
||||
└── shared/
|
||||
└── media/
|
||||
└── admin.py
|
||||
|
||||
docs/admin/
|
||||
├── overview.md
|
||||
├── base_classes.md
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use BaseModelAdmin** as the final parent class
|
||||
2. **List mixins before BaseModelAdmin** in inheritance order
|
||||
3. **Define list_select_related** for all ForeignKeys in list_display
|
||||
4. **Use prefetch_related** for reverse relations and M2M fields
|
||||
5. **Add export_fields** explicitly for control over exported data
|
||||
6. **Include descriptive help text** in fieldset descriptions
|
||||
7. **Test query counts** using Django Debug Toolbar
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Base Classes and Mixins](base_classes.md)
|
||||
- [Changelog](CHANGELOG.md)
|
||||
57
docs/architecture/README.md
Normal file
57
docs/architecture/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Architecture Decision Records (ADRs)
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs) documenting the key architectural decisions made in the ThrillWiki project.
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record is a document that captures an important architectural decision made along with its context and consequences.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Future Work](../FUTURE_WORK.md) - Deferred features and implementation plans
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [ADR-001](./adr-001-django-htmx-architecture.md) | Django + HTMX Architecture | Accepted | 2025-01 |
|
||||
| [ADR-002](./adr-002-hybrid-api-design.md) | Hybrid API Design Pattern | Accepted | 2025-01 |
|
||||
| [ADR-003](./adr-003-state-machine-pattern.md) | State Machine Pattern | Accepted | 2025-01 |
|
||||
| [ADR-004](./adr-004-caching-strategy.md) | Caching Strategy | Accepted | 2025-01 |
|
||||
| [ADR-005](./adr-005-authentication-approach.md) | Authentication Approach | Accepted | 2025-01 |
|
||||
| [ADR-006](./adr-006-media-handling-cloudflare.md) | Media Handling with Cloudflare | Accepted | 2025-01 |
|
||||
|
||||
## ADR Template
|
||||
|
||||
New ADRs should follow this template:
|
||||
|
||||
```markdown
|
||||
# ADR-XXX: Title
|
||||
|
||||
## Status
|
||||
|
||||
[Proposed | Accepted | Deprecated | Superseded]
|
||||
|
||||
## Context
|
||||
|
||||
What is the issue that we're seeing that is motivating this decision or change?
|
||||
|
||||
## Decision
|
||||
|
||||
What is the change that we're proposing and/or doing?
|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or more difficult to do because of this change?
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
What other options were considered and why were they rejected?
|
||||
```
|
||||
|
||||
## Decision Status
|
||||
|
||||
- **Proposed**: The decision is under discussion
|
||||
- **Accepted**: The decision has been accepted and implemented
|
||||
- **Deprecated**: The decision is no longer relevant
|
||||
- **Superseded**: The decision has been replaced by a newer ADR
|
||||
108
docs/architecture/adr-001-django-htmx-architecture.md
Normal file
108
docs/architecture/adr-001-django-htmx-architecture.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# ADR-001: Django + HTMX Architecture
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki needed to choose a frontend architecture for building an interactive web application. The options considered were:
|
||||
|
||||
1. **Single Page Application (SPA)** with React/Vue.js and a separate API backend
|
||||
2. **Django monolith with HTMX** for server-driven interactivity
|
||||
3. **Traditional Multi-Page Application (MPA)** with full page reloads
|
||||
|
||||
The team needed an architecture that would:
|
||||
- Minimize development complexity
|
||||
- Provide good SEO out of the box
|
||||
- Enable fast initial page loads
|
||||
- Support dynamic interactions without full page reloads
|
||||
- Be maintainable by a small team
|
||||
|
||||
## Decision
|
||||
|
||||
We chose to build ThrillWiki as a **Django monolith with HTMX** for dynamic interactivity, supplemented by minimal Alpine.js for client-side UI state.
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Django Templates**: Server-side rendering for all pages
|
||||
2. **HTMX**: Dynamic partial updates without full page reloads
|
||||
3. **Alpine.js**: Minimal client-side state (form validation, UI toggles)
|
||||
4. **Tailwind CSS**: Utility-first styling
|
||||
5. **REST API**: Available for programmatic access (mobile apps, integrations)
|
||||
|
||||
### Architecture Pattern
|
||||
|
||||
```
|
||||
Browser Request
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Django │
|
||||
│ Views │
|
||||
└─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ HTMX Request? │
|
||||
│ ├── Yes: Render partial │
|
||||
│ └── No: Render full page │
|
||||
└─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Response │
|
||||
│ (HTML) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Reduced Complexity**: Single codebase, no separate frontend build process
|
||||
2. **SEO-Friendly**: Server-rendered HTML by default
|
||||
3. **Fast Initial Load**: No JavaScript bundle to download before content appears
|
||||
4. **Progressive Enhancement**: Works without JavaScript, enhanced with HTMX
|
||||
5. **Easier Debugging**: Server logs show all application state
|
||||
6. **Simpler Deployment**: Single Django container
|
||||
7. **Django Ecosystem**: Full access to Django's batteries-included features
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Learning Curve**: Developers need to learn HTMX patterns
|
||||
2. **Limited Offline Support**: No client-side data caching
|
||||
3. **Network Dependency**: Every interaction requires a server round-trip
|
||||
4. **Complex Client State**: Harder to manage complex client-side state (mitigated by Alpine.js)
|
||||
|
||||
### HTMX Patterns Adopted
|
||||
|
||||
1. **Partial Templates**: Views return partial HTML for HTMX requests
|
||||
2. **HX-Trigger Events**: Cross-component communication via custom events
|
||||
3. **Loading Indicators**: Skeleton loaders shown during requests
|
||||
4. **Field Validation**: Real-time form validation via HTMX
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### React/Vue.js SPA
|
||||
|
||||
**Rejected because:**
|
||||
- Increased development complexity with separate codebases
|
||||
- SEO requires server-side rendering setup (Next.js, Nuxt.js)
|
||||
- Larger bundle sizes for initial load
|
||||
- More complex deployment with API + frontend containers
|
||||
- Overkill for this application's interactivity needs
|
||||
|
||||
### Traditional MPA
|
||||
|
||||
**Rejected because:**
|
||||
- Poor user experience with full page reloads
|
||||
- Higher server load for every interaction
|
||||
- Slower perceived performance
|
||||
|
||||
## References
|
||||
|
||||
- [HTMX Documentation](https://htmx.org/docs/)
|
||||
- [Django + HTMX Guide](https://htmx.org/essays/hypermedia-applications/)
|
||||
- [Alpine.js Documentation](https://alpinejs.dev/)
|
||||
- [ThrillWiki HTMX Patterns](../htmx-patterns.md)
|
||||
160
docs/architecture/adr-002-hybrid-api-design.md
Normal file
160
docs/architecture/adr-002-hybrid-api-design.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# ADR-002: Hybrid API Design Pattern
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki serves two types of clients:
|
||||
|
||||
1. **Web browsers**: Need HTML responses for rendering pages
|
||||
2. **API clients**: Need JSON responses for mobile apps and integrations
|
||||
|
||||
We needed to decide how to handle these different client types efficiently without duplicating business logic.
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented a **Hybrid API Design Pattern** where views can serve both HTML and JSON responses based on content negotiation.
|
||||
|
||||
### Implementation Strategy
|
||||
|
||||
1. **API-first approach**: All business logic is implemented in serializers and services
|
||||
2. **Content negotiation**: Views check request headers to determine response format
|
||||
3. **Dual endpoints**: Some resources are available at both web and API URLs
|
||||
4. **Shared serializers**: Same serializers used for both API responses and template context
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
# Web URLs (HTML responses)
|
||||
/parks/ # Park list page
|
||||
/parks/cedar-point/ # Park detail page
|
||||
|
||||
# API URLs (JSON responses)
|
||||
/api/v1/parks/ # Park list (JSON)
|
||||
/api/v1/parks/cedar-point/ # Park detail (JSON)
|
||||
```
|
||||
|
||||
### View Implementation
|
||||
|
||||
```python
|
||||
class HybridViewMixin:
|
||||
"""
|
||||
Mixin that enables views to serve both HTML and JSON responses.
|
||||
"""
|
||||
serializer_class = None
|
||||
|
||||
def get_response_format(self, request):
|
||||
"""Determine response format from Accept header or query param."""
|
||||
if request.htmx:
|
||||
return 'html'
|
||||
if 'application/json' in request.headers.get('Accept', ''):
|
||||
return 'json'
|
||||
if request.GET.get('format') == 'json':
|
||||
return 'json'
|
||||
return 'html'
|
||||
|
||||
def render_response(self, request, context, **kwargs):
|
||||
"""Render appropriate response based on format."""
|
||||
format = self.get_response_format(request)
|
||||
if format == 'json':
|
||||
serializer = self.serializer_class(context['object'])
|
||||
return JsonResponse(serializer.data)
|
||||
return super().render_to_response(context, **kwargs)
|
||||
```
|
||||
|
||||
### Serializer Patterns
|
||||
|
||||
```python
|
||||
# API serializers use camelCase for JSON responses
|
||||
class ParkSerializer(serializers.ModelSerializer):
|
||||
operatorName = serializers.CharField(source='operator.name')
|
||||
rideCount = serializers.IntegerField(source='ride_count')
|
||||
|
||||
class Meta:
|
||||
model = Park
|
||||
fields = ['id', 'name', 'slug', 'operatorName', 'rideCount']
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Code Reuse**: Single set of business logic serves both web and API
|
||||
2. **Consistency**: API and web views always return consistent data
|
||||
3. **Flexibility**: Easy to add new response formats
|
||||
4. **Progressive Enhancement**: API available without duplicating work
|
||||
5. **Mobile-Ready**: API ready for mobile app development
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Complexity**: Views need to handle multiple response formats
|
||||
2. **Testing**: Need to test both HTML and JSON responses
|
||||
3. **Documentation**: Must document both web and API interfaces
|
||||
|
||||
### Response Format Decision
|
||||
|
||||
| Request Type | Response Format |
|
||||
|-------------|-----------------|
|
||||
| HTMX request | HTML partial |
|
||||
| Browser (Accept: text/html) | Full HTML page |
|
||||
| API (Accept: application/json) | JSON |
|
||||
| Query param (?format=json) | JSON |
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Separate API and Web Views
|
||||
|
||||
**Rejected because:**
|
||||
- Duplicate business logic
|
||||
- Risk of divergence between API and web
|
||||
- More code to maintain
|
||||
|
||||
### API-Only with JavaScript Frontend
|
||||
|
||||
**Rejected because:**
|
||||
- Conflicts with ADR-001 (Django + HTMX architecture)
|
||||
- Poor SEO without SSR
|
||||
- Increased complexity
|
||||
|
||||
### GraphQL
|
||||
|
||||
**Rejected because:**
|
||||
- Overkill for current requirements
|
||||
- Steeper learning curve
|
||||
- Less mature Django ecosystem support
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Hybrid Loader Services
|
||||
|
||||
For complex data loading scenarios, we use hybrid loader services:
|
||||
|
||||
```python
|
||||
# backend/apps/parks/services/hybrid_loader.py
|
||||
class ParkHybridLoader:
|
||||
"""
|
||||
Loads park data optimized for both API and web contexts.
|
||||
"""
|
||||
def load_park_detail(self, slug, context='web'):
|
||||
park = Park.objects.optimized_for_detail().get(slug=slug)
|
||||
if context == 'api':
|
||||
return ParkSerializer(park).data
|
||||
return {'park': park}
|
||||
```
|
||||
|
||||
### API Versioning
|
||||
|
||||
API endpoints are versioned to allow breaking changes:
|
||||
|
||||
```
|
||||
/api/v1/parks/ # Current version
|
||||
/api/v2/parks/ # Future version (if needed)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Django REST Framework](https://www.django-rest-framework.org/)
|
||||
- [Content Negotiation](https://www.django-rest-framework.org/api-guide/content-negotiation/)
|
||||
- [ThrillWiki API Documentation](../THRILLWIKI_API_DOCUMENTATION.md)
|
||||
186
docs/architecture/adr-003-state-machine-pattern.md
Normal file
186
docs/architecture/adr-003-state-machine-pattern.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# ADR-003: State Machine Pattern
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Parks and rides in ThrillWiki go through various operational states:
|
||||
- Parks: Operating, Closed Temporarily, Closed Permanently, Under Construction
|
||||
- Rides: Operating, Closed, Under Construction, Removed, Relocated
|
||||
|
||||
Managing these state transitions requires:
|
||||
- Valid state transition enforcement
|
||||
- Audit trail of state changes
|
||||
- Business logic tied to state changes (notifications, cache invalidation)
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented a **Finite State Machine (FSM) pattern** for managing entity states, using django-fsm with custom enhancements.
|
||||
|
||||
### State Model
|
||||
|
||||
```python
|
||||
from django_fsm import FSMField, transition
|
||||
|
||||
class Park(models.Model):
|
||||
status = FSMField(default='OPERATING')
|
||||
|
||||
@transition(
|
||||
field=status,
|
||||
source=['OPERATING', 'CLOSED_TEMP'],
|
||||
target='CLOSED_PERM'
|
||||
)
|
||||
def close_permanently(self, reason=None):
|
||||
"""Close the park permanently."""
|
||||
self.closure_reason = reason
|
||||
self.closure_date = timezone.now()
|
||||
|
||||
@transition(
|
||||
field=status,
|
||||
source='CLOSED_TEMP',
|
||||
target='OPERATING'
|
||||
)
|
||||
def reopen(self):
|
||||
"""Reopen a temporarily closed park."""
|
||||
self.closure_reason = None
|
||||
```
|
||||
|
||||
### State Diagram
|
||||
|
||||
```
|
||||
Park States:
|
||||
┌──────────────┐
|
||||
│ PLANNED │
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ UNDER_CONSTRUCTION │
|
||||
└──────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ OPERATING │◄────┐
|
||||
└──────────────────┬───────────────────┘ │
|
||||
│ │
|
||||
┌───────────┼───────────┐ │
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
┌────────────┐ ┌────────┐ ┌────────────┐ │
|
||||
│CLOSED_TEMP │ │SEASONAL│ │CLOSED_PERM │ │
|
||||
└─────┬──────┘ └────────┘ └────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Transition Validation
|
||||
|
||||
```python
|
||||
class ParkStateTransition(models.Model):
|
||||
"""Audit log for park state transitions."""
|
||||
park = models.ForeignKey(Park, on_delete=models.CASCADE)
|
||||
from_state = models.CharField(max_length=20)
|
||||
to_state = models.CharField(max_length=20)
|
||||
transition_date = models.DateTimeField(auto_now_add=True)
|
||||
transitioned_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
reason = models.TextField(blank=True)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Valid Transitions Only**: Invalid state changes are rejected at the model level
|
||||
2. **Audit Trail**: All transitions are logged with timestamps and users
|
||||
3. **Business Logic Encapsulation**: Transition methods contain related logic
|
||||
4. **Testability**: State machines are easy to unit test
|
||||
5. **Documentation**: State diagrams document valid workflows
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Learning Curve**: Developers need to understand FSM concepts
|
||||
2. **Migration Complexity**: Adding new states requires careful migration
|
||||
3. **Flexibility**: Rigid state transitions can be limiting for edge cases
|
||||
|
||||
### State Change Hooks
|
||||
|
||||
```python
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
@receiver(pre_save, sender=Park)
|
||||
def park_state_change(sender, instance, **kwargs):
|
||||
if instance.pk:
|
||||
old_instance = Park.objects.get(pk=instance.pk)
|
||||
if old_instance.status != instance.status:
|
||||
# Log transition
|
||||
ParkStateTransition.objects.create(
|
||||
park=instance,
|
||||
from_state=old_instance.status,
|
||||
to_state=instance.status,
|
||||
)
|
||||
# Invalidate caches
|
||||
invalidate_park_caches(instance)
|
||||
# Send notifications
|
||||
notify_state_change(instance, old_instance.status)
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Simple Status Field
|
||||
|
||||
**Rejected because:**
|
||||
- No validation of state transitions
|
||||
- Business logic scattered across codebase
|
||||
- No built-in audit trail
|
||||
|
||||
### Event Sourcing
|
||||
|
||||
**Rejected because:**
|
||||
- Overkill for current requirements
|
||||
- Significant complexity increase
|
||||
- Steeper learning curve
|
||||
|
||||
### Workflow Engine
|
||||
|
||||
**Rejected because:**
|
||||
- External dependency overhead
|
||||
- More complex than needed
|
||||
- FSM sufficient for current use cases
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Ride Status States
|
||||
|
||||
```python
|
||||
class RideStatus(models.TextChoices):
|
||||
OPERATING = 'OPERATING', 'Operating'
|
||||
CLOSED_TEMP = 'CLOSED_TEMP', 'Temporarily Closed'
|
||||
CLOSED_PERM = 'CLOSED_PERM', 'Permanently Closed'
|
||||
UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION', 'Under Construction'
|
||||
REMOVED = 'REMOVED', 'Removed'
|
||||
RELOCATED = 'RELOCATED', 'Relocated'
|
||||
```
|
||||
|
||||
### Testing State Transitions
|
||||
|
||||
```python
|
||||
class ParkStateTransitionTest(TestCase):
|
||||
def test_cannot_reopen_permanently_closed_park(self):
|
||||
park = ParkFactory(status='CLOSED_PERM')
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
park.reopen()
|
||||
|
||||
def test_can_close_operating_park_temporarily(self):
|
||||
park = ParkFactory(status='OPERATING')
|
||||
park.close_temporarily(reason='Maintenance')
|
||||
self.assertEqual(park.status, 'CLOSED_TEMP')
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [django-fsm Documentation](https://github.com/viewflow/django-fsm)
|
||||
- [State Machine Diagrams](../state_machines/diagrams.md)
|
||||
- [Finite State Machine Wikipedia](https://en.wikipedia.org/wiki/Finite-state_machine)
|
||||
222
docs/architecture/adr-004-caching-strategy.md
Normal file
222
docs/architecture/adr-004-caching-strategy.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# ADR-004: Caching Strategy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki serves data that is:
|
||||
- Read-heavy (browsing parks and rides)
|
||||
- Moderately updated (user contributions, moderation)
|
||||
- Geographically queried (map views, location searches)
|
||||
|
||||
We needed a caching strategy that would:
|
||||
- Reduce database load for common queries
|
||||
- Provide fast response times for users
|
||||
- Handle cache invalidation correctly
|
||||
- Support different caching needs (sessions, API, geographic)
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented a **Multi-Layer Caching Strategy** using Redis with multiple cache backends for different purposes.
|
||||
|
||||
### Cache Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Application │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Default Cache │ │ Session Cache │ │ API Cache │
|
||||
│ (General data) │ │ (User sessions)│ │ (API responses)│
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└────────────────┼────────────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Redis │
|
||||
│ (with pools) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Cache Configuration
|
||||
|
||||
```python
|
||||
# backend/config/django/production.py
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": redis_url,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PARSER_CLASS": "redis.connection.HiredisParser",
|
||||
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
|
||||
"CONNECTION_POOL_CLASS_KWARGS": {
|
||||
"max_connections": 100,
|
||||
"timeout": 20,
|
||||
},
|
||||
},
|
||||
"KEY_PREFIX": "thrillwiki",
|
||||
},
|
||||
"sessions": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": redis_sessions_url,
|
||||
"KEY_PREFIX": "sessions",
|
||||
},
|
||||
"api": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": redis_api_url,
|
||||
"OPTIONS": {
|
||||
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
|
||||
},
|
||||
"KEY_PREFIX": "api",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Caching Layers
|
||||
|
||||
| Layer | Purpose | TTL | Invalidation |
|
||||
|-------|---------|-----|--------------|
|
||||
| QuerySet | Expensive database queries | 1 hour | On model save |
|
||||
| API Response | Serialized API responses | 30 min | On data change |
|
||||
| Geographic | Map data and location queries | 30 min | On location update |
|
||||
| Template Fragment | Rendered template parts | 15 min | On context change |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Reduced Database Load**: Common queries served from cache
|
||||
2. **Fast Response Times**: Sub-millisecond cache hits
|
||||
3. **Scalability**: Cache can be distributed across Redis cluster
|
||||
4. **Flexibility**: Different TTLs for different data types
|
||||
5. **Compression**: Reduced memory usage with zlib compression
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Cache Invalidation**: Must carefully invalidate on data changes
|
||||
2. **Memory Usage**: Redis memory must be monitored
|
||||
3. **Consistency**: Potential for stale data during TTL window
|
||||
4. **Complexity**: Multiple cache backends to manage
|
||||
|
||||
### Cache Key Naming Convention
|
||||
|
||||
```
|
||||
{prefix}:{entity_type}:{identifier}:{context}
|
||||
|
||||
Examples:
|
||||
thrillwiki:park:123:detail
|
||||
thrillwiki:park:123:rides
|
||||
api:parks:list:page1:filter_operating
|
||||
geo:bounds:40.7:-74.0:41.0:-73.5:z10
|
||||
```
|
||||
|
||||
### Cache Invalidation Patterns
|
||||
|
||||
```python
|
||||
# Model signal for cache invalidation
|
||||
@receiver(post_save, sender=Park)
|
||||
def invalidate_park_cache(sender, instance, **kwargs):
|
||||
cache_service = EnhancedCacheService()
|
||||
|
||||
# Invalidate specific park cache
|
||||
cache_service.invalidate_model_cache('park', instance.id)
|
||||
|
||||
# Invalidate list caches
|
||||
cache_service.invalidate_pattern('api:parks:list:*')
|
||||
|
||||
# Invalidate geographic caches if location changed
|
||||
if instance.location_changed:
|
||||
cache_service.invalidate_pattern('geo:*')
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Database-Only (No Caching)
|
||||
|
||||
**Rejected because:**
|
||||
- High database load for read-heavy traffic
|
||||
- Slower response times
|
||||
- Database as bottleneck for scaling
|
||||
|
||||
### Memcached
|
||||
|
||||
**Rejected because:**
|
||||
- Less feature-rich than Redis
|
||||
- No data persistence
|
||||
- No built-in data structures
|
||||
|
||||
### Application-Level Caching Only
|
||||
|
||||
**Rejected because:**
|
||||
- Not shared across application instances
|
||||
- Memory per-instance overhead
|
||||
- Cache cold on restart
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### EnhancedCacheService
|
||||
|
||||
```python
|
||||
# backend/apps/core/services/enhanced_cache_service.py
|
||||
class EnhancedCacheService:
|
||||
"""Comprehensive caching service with multiple cache backends."""
|
||||
|
||||
def cache_queryset(self, cache_key, queryset_func, timeout=3600, **kwargs):
|
||||
"""Cache expensive querysets with logging."""
|
||||
cached = self.default_cache.get(cache_key)
|
||||
if cached is None:
|
||||
result = queryset_func(**kwargs)
|
||||
self.default_cache.set(cache_key, result, timeout)
|
||||
return result
|
||||
return cached
|
||||
|
||||
def invalidate_pattern(self, pattern):
|
||||
"""Invalidate cache keys matching pattern."""
|
||||
if hasattr(self.default_cache, 'delete_pattern'):
|
||||
return self.default_cache.delete_pattern(pattern)
|
||||
```
|
||||
|
||||
### Cache Warming
|
||||
|
||||
```python
|
||||
# Proactive cache warming for common queries
|
||||
class CacheWarmer:
|
||||
"""Context manager for batch cache warming."""
|
||||
|
||||
def warm_popular_parks(self):
|
||||
parks = Park.objects.operating()[:100]
|
||||
for park in parks:
|
||||
self.cache_service.warm_cache(
|
||||
f'park:{park.id}:detail',
|
||||
lambda: ParkSerializer(park).data,
|
||||
timeout=3600
|
||||
)
|
||||
```
|
||||
|
||||
### Cache Monitoring
|
||||
|
||||
```python
|
||||
class CacheMonitor:
|
||||
"""Monitor cache performance and statistics."""
|
||||
|
||||
def get_cache_stats(self):
|
||||
redis_client = self.cache_service.default_cache._cache.get_client()
|
||||
info = redis_client.info()
|
||||
hits = info.get('keyspace_hits', 0)
|
||||
misses = info.get('keyspace_misses', 0)
|
||||
return {
|
||||
'used_memory': info.get('used_memory_human'),
|
||||
'hit_rate': hits / (hits + misses) * 100 if hits + misses > 0 else 0,
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Django Redis](https://github.com/jazzband/django-redis)
|
||||
- [Redis Documentation](https://redis.io/documentation)
|
||||
- [Cache Invalidation Strategies](https://en.wikipedia.org/wiki/Cache_invalidation)
|
||||
216
docs/architecture/adr-005-authentication-approach.md
Normal file
216
docs/architecture/adr-005-authentication-approach.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# ADR-005: Authentication Approach
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki needs to authenticate users for:
|
||||
- Web browsing (session-based)
|
||||
- API access (token-based)
|
||||
- Social login (Google, Discord)
|
||||
|
||||
We needed an authentication approach that would:
|
||||
- Support multiple authentication methods
|
||||
- Provide secure token handling for API
|
||||
- Enable social authentication
|
||||
- Work seamlessly with Django + HTMX architecture
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented a **Hybrid Authentication System** using django-allauth for social auth and djangorestframework-simplejwt for API tokens.
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
| Context | Method | Library |
|
||||
|---------|--------|---------|
|
||||
| Web browsing | Session-based | Django sessions |
|
||||
| API access | JWT tokens | djangorestframework-simplejwt |
|
||||
| Social login | OAuth2 | django-allauth |
|
||||
| Password reset | Email tokens | Django built-in |
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Request │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Web Request │ │ API Request │ │ Social Auth │
|
||||
│ │ │ │ │ │
|
||||
│ Session Cookie │ │ Bearer Token │ │ OAuth Flow │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└───────────────┼───────────────┘
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Django User │
|
||||
│ (Authenticated) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### JWT Token Configuration
|
||||
|
||||
```python
|
||||
# backend/config/settings/rest_framework.py
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
}
|
||||
```
|
||||
|
||||
### Social Authentication
|
||||
|
||||
```python
|
||||
# backend/config/settings/third_party.py
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
'google': {
|
||||
'SCOPE': ['profile', 'email'],
|
||||
'AUTH_PARAMS': {'access_type': 'online'},
|
||||
},
|
||||
'discord': {
|
||||
'SCOPE': ['identify', 'email'],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Flexibility**: Multiple auth methods for different use cases
|
||||
2. **Security**: JWT with short-lived access tokens
|
||||
3. **User Experience**: Social login reduces friction
|
||||
4. **Standards-Based**: OAuth2 and JWT are industry standards
|
||||
5. **Django Integration**: Seamless with Django's user model
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Complexity**: Multiple auth systems to maintain
|
||||
2. **Token Management**: Must handle token refresh client-side
|
||||
3. **Social Provider Dependency**: Reliance on third-party OAuth providers
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
#### Web Session Authentication
|
||||
|
||||
```
|
||||
1. User visits /login/
|
||||
2. User submits credentials
|
||||
3. Django validates credentials
|
||||
4. Session created, cookie set
|
||||
5. Subsequent requests include session cookie
|
||||
```
|
||||
|
||||
#### API JWT Authentication
|
||||
|
||||
```
|
||||
1. Client POST /api/v1/auth/login/
|
||||
{username, password}
|
||||
2. Server validates, returns tokens
|
||||
{access: "...", refresh: "..."}
|
||||
3. Client includes in requests:
|
||||
Authorization: Bearer <access_token>
|
||||
4. On 401, client refreshes:
|
||||
POST /api/v1/auth/token/refresh/
|
||||
{refresh: "..."}
|
||||
```
|
||||
|
||||
#### Social Authentication
|
||||
|
||||
```
|
||||
1. User clicks "Login with Google"
|
||||
2. Redirect to Google OAuth
|
||||
3. User authorizes application
|
||||
4. Google redirects with auth code
|
||||
5. Server exchanges code for tokens
|
||||
6. Server creates/updates user
|
||||
7. Session created
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Session-Only Authentication
|
||||
|
||||
**Rejected because:**
|
||||
- Not suitable for mobile apps
|
||||
- Not RESTful for API access
|
||||
- CSRF complexity for API clients
|
||||
|
||||
### JWT-Only Authentication
|
||||
|
||||
**Rejected because:**
|
||||
- More complex for web browsing
|
||||
- Token storage in browser has security concerns
|
||||
- Session logout not immediate
|
||||
|
||||
### OAuth2 Server (Self-Hosted)
|
||||
|
||||
**Rejected because:**
|
||||
- Significant complexity for current needs
|
||||
- django-oauth-toolkit overkill
|
||||
- django-allauth sufficient for social auth
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Permission Classes
|
||||
|
||||
```python
|
||||
# API views use JWT or session authentication
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Custom User Model
|
||||
|
||||
```python
|
||||
# backend/apps/accounts/models.py
|
||||
class User(AbstractUser):
|
||||
email = models.EmailField(unique=True)
|
||||
display_name = models.CharField(max_length=50, blank=True)
|
||||
avatar_url = models.URLField(blank=True)
|
||||
email_verified = models.BooleanField(default=False)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
```
|
||||
|
||||
### Email Verification
|
||||
|
||||
```python
|
||||
# Required before full access
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_AUTHENTICATION_METHOD = 'email'
|
||||
```
|
||||
|
||||
### Security Measures
|
||||
|
||||
1. **Password Hashing**: Django's PBKDF2 with SHA256
|
||||
2. **Token Blacklisting**: Invalidated refresh tokens stored
|
||||
3. **Rate Limiting**: Login attempts limited
|
||||
4. **HTTPS Required**: Tokens only sent over secure connections
|
||||
|
||||
## References
|
||||
|
||||
- [django-allauth Documentation](https://django-allauth.readthedocs.io/)
|
||||
- [djangorestframework-simplejwt](https://django-rest-framework-simplejwt.readthedocs.io/)
|
||||
- [OAuth 2.0 Specification](https://oauth.net/2/)
|
||||
- [JWT Best Practices](https://auth0.com/blog/jwt-security-best-practices/)
|
||||
226
docs/architecture/adr-006-media-handling-cloudflare.md
Normal file
226
docs/architecture/adr-006-media-handling-cloudflare.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# ADR-006: Media Handling with Cloudflare Images
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki handles user-uploaded images for:
|
||||
- Park photos
|
||||
- Ride photos
|
||||
- User avatars
|
||||
|
||||
We needed a media handling solution that would:
|
||||
- Handle image uploads efficiently
|
||||
- Optimize images for different devices
|
||||
- Provide CDN delivery globally
|
||||
- Support image transformations (resizing, cropping)
|
||||
- Minimize storage costs
|
||||
|
||||
## Decision
|
||||
|
||||
We chose **Cloudflare Images** as our image hosting and transformation service.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Upload │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Django Backend │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ MediaService │ │
|
||||
│ │ - Validate upload │ │
|
||||
│ │ - Generate upload URL │ │
|
||||
│ │ - Store Cloudflare ID │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Cloudflare Images │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ - Image storage │ │
|
||||
│ │ - On-the-fly transformations │ │
|
||||
│ │ - Global CDN delivery │ │
|
||||
│ │ - Multiple variants │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Image Variants
|
||||
|
||||
| Variant | Dimensions | Use Case |
|
||||
|---------|------------|----------|
|
||||
| `thumbnail` | 150x150 | List views, avatars |
|
||||
| `card` | 400x300 | Card components |
|
||||
| `hero` | 1200x600 | Park/ride headers |
|
||||
| `public` | Original (max 2000px) | Full size view |
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
# Environment variables
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_ID = config('CLOUDFLARE_IMAGES_ACCOUNT_ID')
|
||||
CLOUDFLARE_IMAGES_API_TOKEN = config('CLOUDFLARE_IMAGES_API_TOKEN')
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_HASH = config('CLOUDFLARE_IMAGES_ACCOUNT_HASH')
|
||||
```
|
||||
|
||||
### Upload Flow
|
||||
|
||||
```python
|
||||
# backend/apps/core/services/media_service.py
|
||||
class CloudflareImagesService:
|
||||
"""Service for handling image uploads to Cloudflare Images."""
|
||||
|
||||
def get_direct_upload_url(self):
|
||||
"""Get a one-time upload URL for direct uploads."""
|
||||
response = self._api_request('POST', 'direct_upload')
|
||||
return {
|
||||
'upload_url': response['uploadURL'],
|
||||
'image_id': response['id'],
|
||||
}
|
||||
|
||||
def get_image_url(self, image_id, variant='public'):
|
||||
"""Get the delivery URL for an image."""
|
||||
return f"https://imagedelivery.net/{self.account_hash}/{image_id}/{variant}"
|
||||
|
||||
def delete_image(self, image_id):
|
||||
"""Delete an image from Cloudflare."""
|
||||
return self._api_request('DELETE', f'images/{image_id}')
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Global CDN**: Fast image delivery worldwide
|
||||
2. **Automatic Optimization**: WebP/AVIF conversion as appropriate
|
||||
3. **On-Demand Transformations**: Resize/crop without pre-generating
|
||||
4. **Cost Effective**: Pay per image stored, not per transformation
|
||||
5. **Reduced Server Load**: Direct uploads bypass our servers
|
||||
6. **Security**: Signed URLs for private content if needed
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Vendor Lock-in**: Cloudflare-specific API
|
||||
2. **External Dependency**: Service availability dependency
|
||||
3. **Cost Scaling**: Costs increase with storage volume
|
||||
4. **Migration Complexity**: Moving away requires re-uploading
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
https://imagedelivery.net/{account_hash}/{image_id}/{variant}
|
||||
|
||||
Examples:
|
||||
https://imagedelivery.net/abc123/img-xyz/thumbnail
|
||||
https://imagedelivery.net/abc123/img-xyz/hero
|
||||
https://imagedelivery.net/abc123/img-xyz/public
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Django + S3 + CloudFront
|
||||
|
||||
**Rejected because:**
|
||||
- Requires pre-generating all sizes
|
||||
- More complex infrastructure
|
||||
- Higher storage costs for variants
|
||||
- Manual CDN configuration
|
||||
|
||||
### Self-Hosted with ImageMagick
|
||||
|
||||
**Rejected because:**
|
||||
- Server CPU overhead for transformations
|
||||
- No built-in CDN
|
||||
- Storage management complexity
|
||||
- Scaling challenges
|
||||
|
||||
### Imgix
|
||||
|
||||
**Rejected because:**
|
||||
- Higher costs for our volume
|
||||
- Already using Cloudflare for other services
|
||||
- Similar feature set to Cloudflare Images
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Photo Model
|
||||
|
||||
```python
|
||||
class Photo(models.Model):
|
||||
"""Base photo model using Cloudflare Images."""
|
||||
|
||||
cloudflare_id = models.CharField(max_length=100, unique=True)
|
||||
original_filename = models.CharField(max_length=255)
|
||||
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
is_approved = models.BooleanField(default=False)
|
||||
caption = models.TextField(blank=True)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
return get_image_url(self.cloudflare_id, 'thumbnail')
|
||||
|
||||
@property
|
||||
def public_url(self):
|
||||
return get_image_url(self.cloudflare_id, 'public')
|
||||
```
|
||||
|
||||
### Direct Upload API
|
||||
|
||||
```python
|
||||
# backend/apps/api/v1/views/upload.py
|
||||
class DirectUploadView(APIView):
|
||||
"""Get a direct upload URL for Cloudflare Images."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
service = CloudflareImagesService()
|
||||
upload_data = service.get_direct_upload_url()
|
||||
return Response({
|
||||
'upload_url': upload_data['upload_url'],
|
||||
'image_id': upload_data['image_id'],
|
||||
})
|
||||
```
|
||||
|
||||
### Cleanup Task
|
||||
|
||||
```python
|
||||
# Celery task for cleaning up orphaned images
|
||||
@shared_task
|
||||
def cleanup_orphaned_images():
|
||||
"""Delete images not referenced by any model."""
|
||||
cutoff = timezone.now() - timedelta(hours=24)
|
||||
orphaned = CloudflareImage.objects.filter(
|
||||
created_at__lt=cutoff,
|
||||
park_photos__isnull=True,
|
||||
ride_photos__isnull=True,
|
||||
user_avatars__isnull=True,
|
||||
)
|
||||
for image in orphaned:
|
||||
service.delete_image(image.cloudflare_id)
|
||||
image.delete()
|
||||
```
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
```python
|
||||
def get_image_url(image_id, variant='public', fallback='/static/images/placeholder.jpg'):
|
||||
"""Get image URL with fallback for missing images."""
|
||||
if not image_id:
|
||||
return fallback
|
||||
return f"https://imagedelivery.net/{ACCOUNT_HASH}/{image_id}/{variant}"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Cloudflare Images Documentation](https://developers.cloudflare.com/images/)
|
||||
- [Direct Creator Upload](https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload/)
|
||||
- [Image Variants](https://developers.cloudflare.com/images/cloudflare-images/transform/flexible-variants/)
|
||||
259
docs/configuration/README.md
Normal file
259
docs/configuration/README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# ThrillWiki Configuration System
|
||||
|
||||
This document provides an overview of the ThrillWiki configuration system, including how settings are organized, how to configure different environments, and best practices for managing configuration.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
ThrillWiki uses a modular configuration architecture that separates concerns and makes settings easy to manage across different environments.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
backend/config/
|
||||
├── django/ # Environment-specific settings
|
||||
│ ├── base.py # Core Django settings (imports from settings/)
|
||||
│ ├── local.py # Local development settings
|
||||
│ ├── production.py # Production settings
|
||||
│ └── test.py # Test settings
|
||||
├── settings/ # Modular settings modules
|
||||
│ ├── __init__.py # Package documentation
|
||||
│ ├── cache.py # Redis caching configuration
|
||||
│ ├── database.py # Database and GeoDjango settings
|
||||
│ ├── email.py # Email backend configuration
|
||||
│ ├── logging.py # Logging formatters, handlers, loggers
|
||||
│ ├── rest_framework.py # DRF, JWT, CORS, API docs
|
||||
│ ├── secrets.py # Secret management utilities
|
||||
│ ├── security.py # Security headers and authentication
|
||||
│ ├── storage.py # Static/media files and WhiteNoise
|
||||
│ ├── third_party.py # Allauth, Celery, Cloudflare, etc.
|
||||
│ └── validation.py # Environment variable validation
|
||||
└── celery.py # Celery task queue configuration
|
||||
```
|
||||
|
||||
### How Settings Are Loaded
|
||||
|
||||
1. **base.py** defines core Django settings and imports all modular settings
|
||||
2. Environment-specific files (**local.py**, **production.py**, **test.py**) extend base.py
|
||||
3. The active settings module is determined by `DJANGO_SETTINGS_MODULE`
|
||||
4. Modular settings use **python-decouple** to read environment variables
|
||||
|
||||
### Environment Selection
|
||||
|
||||
The settings module is selected in this order:
|
||||
|
||||
1. `DJANGO_SETTINGS_MODULE` environment variable (explicit override)
|
||||
2. Test command detection (`manage.py test` → `config.django.test`)
|
||||
3. Production indicators (cloud provider environment variables)
|
||||
4. `DEBUG=False` → `config.django.production`
|
||||
5. Default → `config.django.local`
|
||||
|
||||
## Configuration Methods
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
Environment variables are the primary configuration method. Create a `.env` file from the template:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your values
|
||||
```
|
||||
|
||||
### python-decouple
|
||||
|
||||
All settings modules use python-decouple for consistent environment variable handling:
|
||||
|
||||
```python
|
||||
from decouple import config
|
||||
|
||||
# String with default
|
||||
DEBUG = config("DEBUG", default=True, cast=bool)
|
||||
|
||||
# Integer with validation
|
||||
PORT = config("PORT", default=8000, cast=int)
|
||||
|
||||
# List from comma-separated string
|
||||
ALLOWED_HOSTS = config(
|
||||
"ALLOWED_HOSTS",
|
||||
default="localhost",
|
||||
cast=lambda v: [s.strip() for s in v.split(",")]
|
||||
)
|
||||
```
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
### Local Development (`config.django.local`)
|
||||
|
||||
- Debug mode enabled
|
||||
- Local memory cache (no Redis required)
|
||||
- Console email backend
|
||||
- Development middleware (nplusone, debug toolbar)
|
||||
- Relaxed security settings
|
||||
|
||||
### Production (`config.django.production`)
|
||||
|
||||
- Debug mode disabled
|
||||
- Redis caching required
|
||||
- Strict security settings (HSTS, secure cookies)
|
||||
- JSON logging for log aggregation
|
||||
- Sentry integration (optional)
|
||||
|
||||
### Test (`config.django.test`)
|
||||
|
||||
- In-memory SpatiaLite database
|
||||
- In-memory cache
|
||||
- Fast password hashing
|
||||
- Logging disabled
|
||||
- Celery tasks run synchronously
|
||||
|
||||
## Modular Settings Reference
|
||||
|
||||
### database.py
|
||||
|
||||
- `DATABASES` - Database connection configuration
|
||||
- `GDAL_LIBRARY_PATH`, `GEOS_LIBRARY_PATH` - GeoDjango libraries
|
||||
- `CONN_MAX_AGE` - Connection pooling
|
||||
- `DATABASE_OPTIONS` - PostgreSQL-specific settings
|
||||
|
||||
### cache.py
|
||||
|
||||
- `CACHES` - Redis cache backends (default, sessions, api)
|
||||
- `SESSION_*` - Session storage settings
|
||||
- `CACHE_MIDDLEWARE_*` - Cache middleware settings
|
||||
|
||||
### security.py
|
||||
|
||||
- `SECURE_*` - Security header settings
|
||||
- `SESSION_COOKIE_*`, `CSRF_COOKIE_*` - Cookie security
|
||||
- `AUTH_PASSWORD_VALIDATORS` - Password validation rules
|
||||
- `PERMISSIONS_POLICY` - Browser feature permissions
|
||||
|
||||
### email.py
|
||||
|
||||
- `EMAIL_BACKEND` - Email sending backend
|
||||
- `FORWARD_EMAIL_*` - ForwardEmail configuration
|
||||
- SMTP settings for custom email servers
|
||||
|
||||
### rest_framework.py
|
||||
|
||||
- `REST_FRAMEWORK` - DRF configuration
|
||||
- `CORS_*` - Cross-origin settings
|
||||
- `SIMPLE_JWT` - JWT token settings
|
||||
- `REST_AUTH` - dj-rest-auth settings
|
||||
- `SPECTACULAR_SETTINGS` - OpenAPI documentation
|
||||
|
||||
### third_party.py
|
||||
|
||||
- `ACCOUNT_*`, `SOCIALACCOUNT_*` - Allauth settings
|
||||
- `CLOUDFLARE_IMAGES` - Image CDN configuration
|
||||
- `ROADTRIP_*` - Road trip service settings
|
||||
- `HEALTH_CHECK` - Health check thresholds
|
||||
|
||||
### storage.py
|
||||
|
||||
- `STATIC_*`, `MEDIA_*` - File serving configuration
|
||||
- `STORAGES` - Django 4.2+ storage backends
|
||||
- `WHITENOISE_*` - Static file optimization
|
||||
- `FILE_UPLOAD_*` - Upload security limits
|
||||
|
||||
### logging.py
|
||||
|
||||
- `LOGGING` - Complete logging configuration
|
||||
- Formatters: verbose, json, simple
|
||||
- Handlers: console, file, error_file, performance
|
||||
- Loggers for Django, application, and third-party
|
||||
|
||||
## Secret Management
|
||||
|
||||
### Validation
|
||||
|
||||
Use the management command to validate configuration:
|
||||
|
||||
```bash
|
||||
# Full validation
|
||||
python manage.py validate_settings
|
||||
|
||||
# Strict mode (warnings become errors)
|
||||
python manage.py validate_settings --strict
|
||||
|
||||
# JSON output
|
||||
python manage.py validate_settings --json
|
||||
|
||||
# Secrets only
|
||||
python manage.py validate_settings --secrets-only
|
||||
```
|
||||
|
||||
### Required Secrets
|
||||
|
||||
These secrets must be set in all environments:
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `SECRET_KEY` | Django cryptographic signing key (50+ chars) |
|
||||
| `DATABASE_URL` | Database connection URL |
|
||||
|
||||
### Secret Rotation
|
||||
|
||||
For production environments:
|
||||
|
||||
1. Enable rotation checking: `SECRET_ROTATION_ENABLED=True`
|
||||
2. Track versions: `SECRET_KEY_VERSION=1`
|
||||
3. Monitor expiry warnings in logs
|
||||
4. Rotate secrets before expiry
|
||||
|
||||
See [Secret Management Guide](./secret-management.md) for detailed procedures.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Settings module not found:**
|
||||
```
|
||||
ModuleNotFoundError: No module named 'config.django.local'
|
||||
```
|
||||
Ensure you're running commands from the `backend/` directory.
|
||||
|
||||
**Required environment variable missing:**
|
||||
```
|
||||
decouple.UndefinedValueError: SECRET_KEY not found
|
||||
```
|
||||
Create a `.env` file or set the environment variable.
|
||||
|
||||
**Database connection failed:**
|
||||
```
|
||||
django.db.utils.OperationalError: could not connect to server
|
||||
```
|
||||
Check `DATABASE_URL` format and database server status.
|
||||
|
||||
### Validation Errors
|
||||
|
||||
Run validation to identify configuration issues:
|
||||
|
||||
```bash
|
||||
python manage.py validate_settings
|
||||
```
|
||||
|
||||
### Debug Configuration
|
||||
|
||||
To debug configuration loading:
|
||||
|
||||
```python
|
||||
# In Django shell
|
||||
from django.conf import settings
|
||||
print(settings.DEBUG)
|
||||
print(settings.DATABASES)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Never commit secrets** - Use `.env` files or secret management services
|
||||
2. **Use environment variables** - Don't hardcode configuration values
|
||||
3. **Validate on startup** - Catch configuration errors early
|
||||
4. **Separate environments** - Use different settings for dev/staging/production
|
||||
5. **Document custom settings** - Add comments explaining non-obvious configuration
|
||||
6. **Use appropriate defaults** - Secure defaults for production, convenient for development
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Environment Variables Reference](./environment-variables.md)
|
||||
- [Secret Management Guide](./secret-management.md)
|
||||
- [Deployment Guide](./deployment.md)
|
||||
501
docs/configuration/deployment.md
Normal file
501
docs/configuration/deployment.md
Normal file
@@ -0,0 +1,501 @@
|
||||
# Deployment Guide
|
||||
|
||||
This guide covers deploying ThrillWiki to production environments, including configuration, security, and monitoring setup.
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
### Configuration
|
||||
|
||||
- [ ] All required environment variables set
|
||||
- [ ] `DEBUG=False`
|
||||
- [ ] Strong `SECRET_KEY` (50+ characters, randomly generated)
|
||||
- [ ] `ALLOWED_HOSTS` properly configured
|
||||
- [ ] `CSRF_TRUSTED_ORIGINS` set
|
||||
- [ ] Database URL points to production database
|
||||
- [ ] Redis URL configured for caching
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] `SECURE_SSL_REDIRECT=True`
|
||||
- [ ] `SESSION_COOKIE_SECURE=True`
|
||||
- [ ] `CSRF_COOKIE_SECURE=True`
|
||||
- [ ] `SECURE_HSTS_SECONDS` set (31536000 recommended)
|
||||
- [ ] `SECURE_HSTS_INCLUDE_SUBDOMAINS=True`
|
||||
- [ ] No debug tools enabled
|
||||
- [ ] Turnstile keys configured
|
||||
|
||||
### Validation
|
||||
|
||||
Run the configuration validator:
|
||||
|
||||
```bash
|
||||
python manage.py validate_settings --strict
|
||||
python manage.py check --deploy
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Core Django
|
||||
SECRET_KEY=<generate-secure-key>
|
||||
DEBUG=False
|
||||
DJANGO_SETTINGS_MODULE=config.django.production
|
||||
ALLOWED_HOSTS=thrillwiki.com,www.thrillwiki.com
|
||||
CSRF_TRUSTED_ORIGINS=https://thrillwiki.com,https://www.thrillwiki.com
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgis://user:pass@db-host:5432/thrillwiki
|
||||
|
||||
# Cache
|
||||
REDIS_URL=redis://:password@redis-host:6379/1
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND=django_forwardemail.backends.ForwardEmailBackend
|
||||
FORWARD_EMAIL_API_KEY=your-api-key
|
||||
FORWARD_EMAIL_DOMAIN=thrillwiki.com
|
||||
|
||||
# Security
|
||||
TURNSTILE_SITE_KEY=your-site-key
|
||||
TURNSTILE_SECRET_KEY=your-secret-key
|
||||
SECURE_SSL_REDIRECT=True
|
||||
SESSION_COOKIE_SECURE=True
|
||||
CSRF_COOKIE_SECURE=True
|
||||
|
||||
# Cloudflare Images
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
|
||||
CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_HASH=your-account-hash
|
||||
|
||||
# Road Trip Service
|
||||
ROADTRIP_USER_AGENT=ThrillWiki/1.0 (https://thrillwiki.com)
|
||||
|
||||
# Frontend
|
||||
FRONTEND_DOMAIN=https://thrillwiki.com
|
||||
```
|
||||
|
||||
### Optional Production Settings
|
||||
|
||||
```bash
|
||||
# Monitoring
|
||||
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
|
||||
# Secret rotation
|
||||
SECRET_ROTATION_ENABLED=True
|
||||
SECRET_KEY_VERSION=1
|
||||
|
||||
# Performance tuning
|
||||
DATABASE_CONN_MAX_AGE=600
|
||||
REDIS_MAX_CONNECTIONS=100
|
||||
```
|
||||
|
||||
## Deployment Platforms
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
Create a production Dockerfile:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
libgdal-dev \
|
||||
libgeos-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY backend/pyproject.toml backend/uv.lock ./
|
||||
RUN pip install uv && uv sync --frozen
|
||||
|
||||
# Copy application
|
||||
COPY backend/ .
|
||||
|
||||
# Collect static files
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
# Run with Gunicorn
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "thrillwiki.wsgi:application"]
|
||||
```
|
||||
|
||||
Docker Compose for production:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=config.django.production
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
db:
|
||||
image: postgis/postgis:14-3.3
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=thrillwiki
|
||||
- POSTGRES_USER=thrillwiki
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
celery:
|
||||
build: .
|
||||
command: celery -A config.celery worker -l info
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
celery-beat:
|
||||
build: .
|
||||
command: celery -A config.celery beat -l info
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
### Heroku Deployment
|
||||
|
||||
```bash
|
||||
# Create app
|
||||
heroku create thrillwiki
|
||||
|
||||
# Add buildpacks
|
||||
heroku buildpacks:add heroku/python
|
||||
heroku buildpacks:add https://github.com/heroku/heroku-geo-buildpack.git
|
||||
|
||||
# Add add-ons
|
||||
heroku addons:create heroku-postgresql:standard-0
|
||||
heroku addons:create heroku-redis:premium-0
|
||||
|
||||
# Set environment variables
|
||||
heroku config:set DJANGO_SETTINGS_MODULE=config.django.production
|
||||
heroku config:set SECRET_KEY=$(python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())")
|
||||
heroku config:set DEBUG=False
|
||||
# ... set other variables
|
||||
|
||||
# Deploy
|
||||
git push heroku main
|
||||
|
||||
# Run migrations
|
||||
heroku run python manage.py migrate
|
||||
```
|
||||
|
||||
### AWS ECS Deployment
|
||||
|
||||
Task definition example:
|
||||
|
||||
```json
|
||||
{
|
||||
"family": "thrillwiki",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": "web",
|
||||
"image": "your-ecr-repo/thrillwiki:latest",
|
||||
"portMappings": [
|
||||
{
|
||||
"containerPort": 8000,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"environment": [
|
||||
{
|
||||
"name": "DJANGO_SETTINGS_MODULE",
|
||||
"value": "config.django.production"
|
||||
}
|
||||
],
|
||||
"secrets": [
|
||||
{
|
||||
"name": "SECRET_KEY",
|
||||
"valueFrom": "arn:aws:secretsmanager:region:account:secret:thrillwiki/SECRET_KEY"
|
||||
},
|
||||
{
|
||||
"name": "DATABASE_URL",
|
||||
"valueFrom": "arn:aws:secretsmanager:region:account:secret:thrillwiki/DATABASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
### PostgreSQL with PostGIS
|
||||
|
||||
```sql
|
||||
-- Create database
|
||||
CREATE DATABASE thrillwiki;
|
||||
|
||||
-- Enable PostGIS
|
||||
\c thrillwiki
|
||||
CREATE EXTENSION postgis;
|
||||
CREATE EXTENSION postgis_topology;
|
||||
|
||||
-- Create user
|
||||
CREATE USER thrillwiki_app WITH ENCRYPTED PASSWORD 'secure-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki_app;
|
||||
```
|
||||
|
||||
### Run Migrations
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Static Files
|
||||
|
||||
### Collect Static Files
|
||||
|
||||
```bash
|
||||
python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### WhiteNoise Configuration
|
||||
|
||||
WhiteNoise is configured for production static file serving. No additional web server configuration needed for static files.
|
||||
|
||||
### CDN Integration (Optional)
|
||||
|
||||
For CDN, update `STATIC_URL`:
|
||||
|
||||
```bash
|
||||
STATIC_URL=https://cdn.thrillwiki.com/static/
|
||||
```
|
||||
|
||||
## Monitoring Setup
|
||||
|
||||
### Sentry Integration
|
||||
|
||||
```bash
|
||||
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
Health check endpoint: `/health/`
|
||||
|
||||
```bash
|
||||
# Test health check
|
||||
curl https://thrillwiki.com/health/
|
||||
```
|
||||
|
||||
Configure load balancer health checks to use this endpoint.
|
||||
|
||||
### Logging
|
||||
|
||||
Production logging outputs JSON to stdout (suitable for log aggregation):
|
||||
|
||||
```json
|
||||
{
|
||||
"levelname": "INFO",
|
||||
"asctime": "2024-01-15 10:30:00,000",
|
||||
"module": "views",
|
||||
"message": "Request processed"
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Database Connection Pooling
|
||||
|
||||
```bash
|
||||
DATABASE_CONN_MAX_AGE=600 # 10 minutes
|
||||
DATABASE_CONNECT_TIMEOUT=10
|
||||
DATABASE_STATEMENT_TIMEOUT=30000
|
||||
```
|
||||
|
||||
### Redis Connection Pooling
|
||||
|
||||
```bash
|
||||
REDIS_MAX_CONNECTIONS=100
|
||||
REDIS_CONNECTION_TIMEOUT=20
|
||||
```
|
||||
|
||||
### Gunicorn Configuration
|
||||
|
||||
```bash
|
||||
# gunicorn.conf.py
|
||||
workers = 4 # (2 * CPU cores) + 1
|
||||
worker_class = "gevent"
|
||||
worker_connections = 1000
|
||||
timeout = 30
|
||||
keepalive = 5
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
||||
```
|
||||
|
||||
## Celery Workers
|
||||
|
||||
### Start Workers
|
||||
|
||||
```bash
|
||||
# Main worker
|
||||
celery -A config.celery worker -l info
|
||||
|
||||
# Beat scheduler
|
||||
celery -A config.celery beat -l info
|
||||
|
||||
# With specific queues
|
||||
celery -A config.celery worker -l info -Q default,trending,analytics,cache
|
||||
```
|
||||
|
||||
### Supervisor Configuration
|
||||
|
||||
```ini
|
||||
[program:celery]
|
||||
command=/app/.venv/bin/celery -A config.celery worker -l info
|
||||
directory=/app
|
||||
user=www-data
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/celery/worker.log
|
||||
stderr_logfile=/var/log/celery/worker-error.log
|
||||
|
||||
[program:celery-beat]
|
||||
command=/app/.venv/bin/celery -A config.celery beat -l info
|
||||
directory=/app
|
||||
user=www-data
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/celery/beat.log
|
||||
stderr_logfile=/var/log/celery/beat-error.log
|
||||
```
|
||||
|
||||
## SSL/TLS Configuration
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name thrillwiki.com www.thrillwiki.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/thrillwiki.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/thrillwiki.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name thrillwiki.com www.thrillwiki.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Deployment Verification
|
||||
|
||||
### Functional Tests
|
||||
|
||||
```bash
|
||||
# API health
|
||||
curl -s https://thrillwiki.com/api/health/ | jq
|
||||
|
||||
# Static files
|
||||
curl -I https://thrillwiki.com/static/css/tailwind.css
|
||||
|
||||
# API endpoint
|
||||
curl -s https://thrillwiki.com/api/v1/parks/ | jq
|
||||
```
|
||||
|
||||
### Security Headers
|
||||
|
||||
```bash
|
||||
# Check security headers
|
||||
curl -I https://thrillwiki.com/ | grep -E "(Strict-Transport|X-Frame|X-Content|Content-Security)"
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
```bash
|
||||
# Response time
|
||||
time curl -s https://thrillwiki.com/api/v1/parks/ > /dev/null
|
||||
```
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If deployment fails:
|
||||
|
||||
1. **Immediate rollback:**
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose down
|
||||
docker-compose -f docker-compose.previous.yml up -d
|
||||
|
||||
# Heroku
|
||||
heroku rollback
|
||||
|
||||
# Kubernetes
|
||||
kubectl rollout undo deployment/thrillwiki
|
||||
```
|
||||
|
||||
2. **Database rollback (if needed):**
|
||||
```bash
|
||||
python manage.py migrate app_name migration_number
|
||||
```
|
||||
|
||||
3. **Investigate and fix:**
|
||||
- Check error logs
|
||||
- Review configuration differences
|
||||
- Test in staging environment
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
|
||||
- **Daily:** Review error logs, check health endpoints
|
||||
- **Weekly:** Review performance metrics, check disk usage
|
||||
- **Monthly:** Rotate secrets, review security updates
|
||||
- **Quarterly:** Full security audit, dependency updates
|
||||
|
||||
### Backup Procedures
|
||||
|
||||
```bash
|
||||
# Database backup
|
||||
pg_dump thrillwiki > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Media files backup
|
||||
aws s3 sync /app/media s3://thrillwiki-backups/media/
|
||||
```
|
||||
338
docs/configuration/environment-variables.md
Normal file
338
docs/configuration/environment-variables.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# Environment Variables Reference
|
||||
|
||||
Complete reference for all environment variables used in ThrillWiki.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Copy the example file: `cp .env.example .env`
|
||||
2. Edit `.env` with your values
|
||||
3. Run validation: `python manage.py validate_settings`
|
||||
|
||||
## Required Variables
|
||||
|
||||
These must be set in all environments.
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `SECRET_KEY` | string | Django secret key (minimum 50 characters) |
|
||||
| `DATABASE_URL` | url | Database connection URL |
|
||||
|
||||
## Production-Required Variables
|
||||
|
||||
These variables **must** be explicitly set for production deployments. The application will not function correctly in production without them.
|
||||
|
||||
> **Important:** See [`.env.example`](../../.env.example) for example values and [`PRODUCTION_CHECKLIST.md`](../PRODUCTION_CHECKLIST.md) for deployment verification steps.
|
||||
|
||||
| Variable | Type | Format/Example | Description |
|
||||
|----------|------|----------------|-------------|
|
||||
| `DEBUG` | bool | `False` | **Must be `False` in production.** Enables detailed error pages and debug toolbar when `True`. |
|
||||
| `DJANGO_SETTINGS_MODULE` | string | `config.django.production` | **Must use `config.django.production` for production.** Controls which settings module Django loads. |
|
||||
| `ALLOWED_HOSTS` | list | `thrillwiki.com,www.thrillwiki.com` | **Required.** Comma-separated list of valid hostnames. No default in production - requests will fail without this. |
|
||||
| `CSRF_TRUSTED_ORIGINS` | list | `https://thrillwiki.com,https://www.thrillwiki.com` | **Required.** Must include `https://` prefix. CSRF validation fails without proper configuration. |
|
||||
| `REDIS_URL` | url | `redis://[:password@]host:6379/0` | **Required for caching and sessions.** Production uses Redis for caching, session storage, and Celery broker. |
|
||||
|
||||
### Production Settings Behavior
|
||||
|
||||
When `DJANGO_SETTINGS_MODULE=config.django.production`:
|
||||
- `DEBUG` is always `False`
|
||||
- `ALLOWED_HOSTS` has **no default** - must be explicitly set
|
||||
- `CSRF_TRUSTED_ORIGINS` has **no default** - must be explicitly set
|
||||
- SSL redirect is enforced (`SECURE_SSL_REDIRECT=True`)
|
||||
- Secure cookies are enforced (`SESSION_COOKIE_SECURE=True`, `CSRF_COOKIE_SECURE=True`)
|
||||
- HSTS is enabled with 1-year max-age
|
||||
- Redis cache is required (application will fail if `REDIS_URL` is not set)
|
||||
|
||||
### Validation
|
||||
|
||||
Run the following to validate your production configuration:
|
||||
|
||||
```bash
|
||||
DJANGO_SETTINGS_MODULE=config.django.production python manage.py check --deploy
|
||||
```
|
||||
|
||||
## Core Django Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `DEBUG` | bool | `True` | Enable debug mode |
|
||||
| `DJANGO_SETTINGS_MODULE` | string | `config.django.local` | Settings module to use |
|
||||
| `ALLOWED_HOSTS` | list | `localhost,127.0.0.1` | Comma-separated allowed hosts |
|
||||
| `CSRF_TRUSTED_ORIGINS` | list | `` | Comma-separated trusted origins |
|
||||
|
||||
## Database Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `DATABASE_URL` | url | (required) | Database connection URL |
|
||||
| `DATABASE_CONN_MAX_AGE` | int | `600` | Connection pool timeout (seconds) |
|
||||
| `DATABASE_CONNECT_TIMEOUT` | int | `10` | Connection timeout (seconds) |
|
||||
| `DATABASE_STATEMENT_TIMEOUT` | int | `30000` | Query timeout (milliseconds) |
|
||||
| `DATABASE_READ_REPLICA_URL` | url | `` | Optional read replica URL |
|
||||
|
||||
### Database URL Formats
|
||||
|
||||
```bash
|
||||
# PostgreSQL with PostGIS
|
||||
DATABASE_URL=postgis://user:password@host:5432/dbname
|
||||
|
||||
# PostgreSQL without PostGIS
|
||||
DATABASE_URL=postgres://user:password@host:5432/dbname
|
||||
|
||||
# SQLite (development only)
|
||||
DATABASE_URL=sqlite:///path/to/db.sqlite3
|
||||
```
|
||||
|
||||
## GeoDjango Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `GDAL_LIBRARY_PATH` | path | `/opt/homebrew/lib/libgdal.dylib` | Path to GDAL library |
|
||||
| `GEOS_LIBRARY_PATH` | path | `/opt/homebrew/lib/libgeos_c.dylib` | Path to GEOS library |
|
||||
|
||||
### Platform-Specific Paths
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
|
||||
GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
|
||||
|
||||
# Linux (Ubuntu/Debian)
|
||||
GDAL_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgdal.so
|
||||
GEOS_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgeos_c.so
|
||||
```
|
||||
|
||||
## Cache Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `REDIS_URL` | url | `redis://127.0.0.1:6379/1` | Redis connection URL |
|
||||
| `REDIS_SESSIONS_URL` | url | (from REDIS_URL) | Redis URL for sessions |
|
||||
| `REDIS_API_URL` | url | (from REDIS_URL) | Redis URL for API cache |
|
||||
| `REDIS_MAX_CONNECTIONS` | int | `100` | Maximum Redis connections |
|
||||
| `REDIS_CONNECTION_TIMEOUT` | int | `20` | Redis connection timeout |
|
||||
| `REDIS_IGNORE_EXCEPTIONS` | bool | `True` | Graceful degradation |
|
||||
| `CACHE_MIDDLEWARE_SECONDS` | int | `300` | Cache middleware timeout |
|
||||
| `CACHE_MIDDLEWARE_KEY_PREFIX` | string | `thrillwiki` | Cache key prefix |
|
||||
| `CACHE_KEY_PREFIX` | string | `thrillwiki` | Default cache key prefix |
|
||||
|
||||
## Email Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `EMAIL_BACKEND` | string | `django_forwardemail.backends.ForwardEmailBackend` | Email backend class |
|
||||
| `SERVER_EMAIL` | email | `django_webmaster@thrillwiki.com` | Server email address |
|
||||
| `DEFAULT_FROM_EMAIL` | string | `ThrillWiki <noreply@thrillwiki.com>` | Default from address |
|
||||
| `EMAIL_SUBJECT_PREFIX` | string | `[ThrillWiki]` | Subject prefix |
|
||||
| `EMAIL_TIMEOUT` | int | `30` | Email operation timeout |
|
||||
|
||||
### ForwardEmail Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `FORWARD_EMAIL_BASE_URL` | url | `https://api.forwardemail.net` | API base URL |
|
||||
| `FORWARD_EMAIL_API_KEY` | string | `` | API key |
|
||||
| `FORWARD_EMAIL_DOMAIN` | string | `` | Sending domain |
|
||||
|
||||
### SMTP Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `EMAIL_HOST` | string | `localhost` | SMTP server |
|
||||
| `EMAIL_PORT` | int | `587` | SMTP port |
|
||||
| `EMAIL_USE_TLS` | bool | `True` | Use TLS |
|
||||
| `EMAIL_USE_SSL` | bool | `False` | Use SSL |
|
||||
| `EMAIL_HOST_USER` | string | `` | SMTP username |
|
||||
| `EMAIL_HOST_PASSWORD` | string | `` | SMTP password |
|
||||
|
||||
## Security Settings
|
||||
|
||||
### SSL/HTTPS
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SECURE_SSL_REDIRECT` | bool | `False` | Redirect HTTP to HTTPS |
|
||||
| `SECURE_PROXY_SSL_HEADER` | tuple | `` | Proxy SSL header |
|
||||
|
||||
### HSTS (HTTP Strict Transport Security)
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SECURE_HSTS_SECONDS` | int | `31536000` | HSTS max-age |
|
||||
| `SECURE_HSTS_INCLUDE_SUBDOMAINS` | bool | `True` | Include subdomains |
|
||||
| `SECURE_HSTS_PRELOAD` | bool | `False` | Allow preload |
|
||||
| `SECURE_REDIRECT_EXEMPT` | list | `` | URLs exempt from redirect |
|
||||
|
||||
### Security Headers
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SECURE_BROWSER_XSS_FILTER` | bool | `True` | XSS filter header |
|
||||
| `SECURE_CONTENT_TYPE_NOSNIFF` | bool | `True` | Nosniff header |
|
||||
| `X_FRAME_OPTIONS` | string | `DENY` | Frame options |
|
||||
| `SECURE_REFERRER_POLICY` | string | `strict-origin-when-cross-origin` | Referrer policy |
|
||||
| `SECURE_CROSS_ORIGIN_OPENER_POLICY` | string | `same-origin` | COOP header |
|
||||
|
||||
### Cookie Security
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SESSION_COOKIE_SECURE` | bool | `False` | HTTPS-only session cookie |
|
||||
| `SESSION_COOKIE_HTTPONLY` | bool | `True` | No JS access |
|
||||
| `SESSION_COOKIE_SAMESITE` | string | `Lax` | SameSite attribute |
|
||||
| `SESSION_COOKIE_AGE` | int | `3600` | Cookie age (seconds) |
|
||||
| `CSRF_COOKIE_SECURE` | bool | `False` | HTTPS-only CSRF cookie |
|
||||
| `CSRF_COOKIE_HTTPONLY` | bool | `True` | No JS access |
|
||||
| `CSRF_COOKIE_SAMESITE` | string | `Lax` | SameSite attribute |
|
||||
|
||||
### Cloudflare Turnstile
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `TURNSTILE_SITE_KEY` | string | `` | Turnstile site key |
|
||||
| `TURNSTILE_SECRET_KEY` | string | `` | Turnstile secret key |
|
||||
| `TURNSTILE_VERIFY_URL` | url | `https://challenges.cloudflare.com/turnstile/v0/siteverify` | Verification URL |
|
||||
|
||||
## API Configuration
|
||||
|
||||
### CORS
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `CORS_ALLOWED_ORIGINS` | list | `` | Allowed origins |
|
||||
| `CORS_ALLOW_ALL_ORIGINS` | bool | `False` | Allow all origins |
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `API_RATE_LIMIT_PER_MINUTE` | int | `60` | Requests per minute |
|
||||
| `API_RATE_LIMIT_PER_HOUR` | int | `1000` | Requests per hour |
|
||||
| `API_RATE_LIMIT_ANON_PER_MINUTE` | int | `60` | Anonymous rate limit |
|
||||
| `API_RATE_LIMIT_USER_PER_HOUR` | int | `1000` | Authenticated rate limit |
|
||||
|
||||
### Pagination
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `API_PAGE_SIZE` | int | `20` | Default page size |
|
||||
| `API_MAX_PAGE_SIZE` | int | `100` | Maximum page size |
|
||||
| `API_VERSION` | string | `1.0.0` | API version string |
|
||||
|
||||
## JWT Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `JWT_ACCESS_TOKEN_LIFETIME_MINUTES` | int | `15` | Access token lifetime |
|
||||
| `JWT_REFRESH_TOKEN_LIFETIME_DAYS` | int | `7` | Refresh token lifetime |
|
||||
| `JWT_ISSUER` | string | `thrillwiki` | Token issuer |
|
||||
|
||||
## Cloudflare Images
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `CLOUDFLARE_IMAGES_ACCOUNT_ID` | string | `` | Account ID |
|
||||
| `CLOUDFLARE_IMAGES_API_TOKEN` | string | `` | API token |
|
||||
| `CLOUDFLARE_IMAGES_ACCOUNT_HASH` | string | `` | Account hash |
|
||||
| `CLOUDFLARE_IMAGES_WEBHOOK_SECRET` | string | `` | Webhook secret |
|
||||
| `CLOUDFLARE_IMAGES_DEFAULT_VARIANT` | string | `public` | Default variant |
|
||||
| `CLOUDFLARE_IMAGES_UPLOAD_TIMEOUT` | int | `300` | Upload timeout |
|
||||
| `CLOUDFLARE_IMAGES_CLEANUP_HOURS` | int | `24` | Cleanup interval |
|
||||
| `CLOUDFLARE_IMAGES_MAX_FILE_SIZE` | int | `10485760` | Max file size (bytes) |
|
||||
| `CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS` | bool | `False` | Require signed URLs |
|
||||
|
||||
## Road Trip Service
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `ROADTRIP_USER_AGENT` | string | `ThrillWiki/1.0` | OSM API user agent |
|
||||
| `ROADTRIP_CACHE_TIMEOUT` | int | `86400` | Geocoding cache timeout |
|
||||
| `ROADTRIP_ROUTE_CACHE_TIMEOUT` | int | `21600` | Route cache timeout |
|
||||
| `ROADTRIP_MAX_REQUESTS_PER_SECOND` | int | `1` | Rate limit |
|
||||
| `ROADTRIP_REQUEST_TIMEOUT` | int | `10` | Request timeout |
|
||||
| `ROADTRIP_MAX_RETRIES` | int | `3` | Max retries |
|
||||
| `ROADTRIP_BACKOFF_FACTOR` | int | `2` | Retry backoff |
|
||||
|
||||
## Logging Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `LOG_DIR` | path | `logs` | Log directory |
|
||||
| `ROOT_LOG_LEVEL` | string | `INFO` | Root log level |
|
||||
| `DJANGO_LOG_LEVEL` | string | `WARNING` | Django log level |
|
||||
| `DB_LOG_LEVEL` | string | `WARNING` | Database log level |
|
||||
| `APP_LOG_LEVEL` | string | `INFO` | Application log level |
|
||||
| `PERFORMANCE_LOG_LEVEL` | string | `INFO` | Performance log level |
|
||||
| `QUERY_LOG_LEVEL` | string | `WARNING` | Query log level |
|
||||
| `NPLUSONE_LOG_LEVEL` | string | `WARNING` | N+1 detection level |
|
||||
| `REQUEST_LOG_LEVEL` | string | `INFO` | Request log level |
|
||||
| `CELERY_LOG_LEVEL` | string | `INFO` | Celery log level |
|
||||
| `CONSOLE_LOG_LEVEL` | string | `INFO` | Console output level |
|
||||
| `FILE_LOG_LEVEL` | string | `INFO` | File output level |
|
||||
| `FILE_LOG_FORMATTER` | string | `json` | File log format |
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Sentry
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SENTRY_DSN` | url | `` | Sentry DSN |
|
||||
| `SENTRY_ENVIRONMENT` | string | `production` | Sentry environment |
|
||||
| `SENTRY_TRACES_SAMPLE_RATE` | float | `0.1` | Trace sampling rate |
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `HEALTH_CHECK_DISK_USAGE_MAX` | int | `90` | Max disk usage % |
|
||||
| `HEALTH_CHECK_MEMORY_MIN` | int | `100` | Min available memory MB |
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `ENABLE_DEBUG_TOOLBAR` | bool | `True` | Enable debug toolbar |
|
||||
| `ENABLE_SILK_PROFILER` | bool | `False` | Enable Silk profiler |
|
||||
| `TEMPLATES_ENABLED` | bool | `True` | Enable Django templates |
|
||||
| `AUTOCOMPLETE_BLOCK_UNAUTHENTICATED` | bool | `False` | Require auth for autocomplete |
|
||||
|
||||
## File Upload Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `FILE_UPLOAD_MAX_MEMORY_SIZE` | int | `2621440` | Max in-memory upload (bytes) |
|
||||
| `DATA_UPLOAD_MAX_MEMORY_SIZE` | int | `10485760` | Max request size (bytes) |
|
||||
| `DATA_UPLOAD_MAX_NUMBER_FIELDS` | int | `1000` | Max form fields |
|
||||
|
||||
## WhiteNoise Settings
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `WHITENOISE_COMPRESSION_QUALITY` | int | `90` | Compression quality |
|
||||
| `WHITENOISE_MAX_AGE` | int | `31536000` | Cache max-age |
|
||||
| `WHITENOISE_MANIFEST_STRICT` | bool | `False` | Strict manifest mode |
|
||||
|
||||
## Secret Management
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `SECRET_ROTATION_ENABLED` | bool | `False` | Enable rotation checks |
|
||||
| `SECRET_KEY_VERSION` | string | `1` | Current secret version |
|
||||
| `SECRET_EXPIRY_WARNING_DAYS` | int | `30` | Warning threshold |
|
||||
| `PASSWORD_MIN_LENGTH` | int | `8` | Minimum password length |
|
||||
|
||||
## Celery Configuration
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `CELERY_TASK_ALWAYS_EAGER` | bool | `False` | Run tasks synchronously |
|
||||
| `CELERY_TASK_EAGER_PROPAGATES` | bool | `False` | Propagate task errors |
|
||||
|
||||
## Third-Party Integrations
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `FRONTEND_DOMAIN` | url | `https://thrillwiki.com` | Frontend URL |
|
||||
| `LOGIN_REDIRECT_URL` | path | `/` | Post-login redirect |
|
||||
| `ACCOUNT_LOGOUT_REDIRECT_URL` | path | `/` | Post-logout redirect |
|
||||
| `ACCOUNT_EMAIL_VERIFICATION` | string | `mandatory` | Email verification mode |
|
||||
330
docs/configuration/secret-management.md
Normal file
330
docs/configuration/secret-management.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# Secret Management Guide
|
||||
|
||||
This guide covers best practices for managing secrets in ThrillWiki, including rotation procedures, integration with secret management services, and emergency procedures.
|
||||
|
||||
## Overview
|
||||
|
||||
Secrets are sensitive configuration values that must be protected:
|
||||
|
||||
- Database credentials
|
||||
- API keys and tokens
|
||||
- Encryption keys
|
||||
- Service passwords
|
||||
|
||||
ThrillWiki provides a flexible secret management system that can work with environment variables (development) or integrate with enterprise secret management services (production).
|
||||
|
||||
## Secret Classification
|
||||
|
||||
### Critical Secrets (Immediate rotation required if compromised)
|
||||
|
||||
| Secret | Impact | Rotation Frequency |
|
||||
|--------|--------|-------------------|
|
||||
| `SECRET_KEY` | All cryptographic operations | 90 days recommended |
|
||||
| `DATABASE_URL` | Database access | On suspected breach |
|
||||
| `SENTRY_DSN` | Error tracking access | On suspected breach |
|
||||
|
||||
### High-Priority Secrets
|
||||
|
||||
| Secret | Impact | Rotation Frequency |
|
||||
|--------|--------|-------------------|
|
||||
| `CLOUDFLARE_IMAGES_API_TOKEN` | Image CDN access | 180 days |
|
||||
| `FORWARD_EMAIL_API_KEY` | Email sending | 180 days |
|
||||
| `TURNSTILE_SECRET_KEY` | CAPTCHA bypass | 365 days |
|
||||
|
||||
### Service Secrets
|
||||
|
||||
| Secret | Impact | Rotation Frequency |
|
||||
|--------|--------|-------------------|
|
||||
| `CLOUDFLARE_IMAGES_WEBHOOK_SECRET` | Webhook verification | 365 days |
|
||||
| `REDIS_URL` (with password) | Cache access | On suspected breach |
|
||||
|
||||
## Development Setup
|
||||
|
||||
For development, use a `.env` file:
|
||||
|
||||
```bash
|
||||
# Copy the template
|
||||
cp .env.example .env
|
||||
|
||||
# Generate a secure SECRET_KEY
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
|
||||
# Edit .env with your values
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Validate your configuration:
|
||||
|
||||
```bash
|
||||
python manage.py validate_settings
|
||||
```
|
||||
|
||||
## Production Secret Management
|
||||
|
||||
### Option 1: Environment Variables (Simple)
|
||||
|
||||
Set secrets as environment variables in your deployment platform:
|
||||
|
||||
```bash
|
||||
# Heroku
|
||||
heroku config:set SECRET_KEY="your-secret-key"
|
||||
|
||||
# Docker
|
||||
docker run -e SECRET_KEY="your-secret-key" ...
|
||||
|
||||
# Kubernetes (from secret)
|
||||
kubectl create secret generic thrillwiki-secrets \
|
||||
--from-literal=SECRET_KEY="your-secret-key"
|
||||
```
|
||||
|
||||
### Option 2: AWS Secrets Manager
|
||||
|
||||
For AWS deployments, integrate with Secrets Manager:
|
||||
|
||||
```python
|
||||
# In config/settings/secrets.py, implement:
|
||||
from config.settings.secrets import SecretProvider
|
||||
|
||||
class AWSSecretsProvider(SecretProvider):
|
||||
def __init__(self, secret_name: str):
|
||||
import boto3
|
||||
self.client = boto3.client('secretsmanager')
|
||||
self.secret_name = secret_name
|
||||
self._cache = {}
|
||||
|
||||
def get_secret(self, name: str) -> str:
|
||||
if not self._cache:
|
||||
response = self.client.get_secret_value(
|
||||
SecretId=self.secret_name
|
||||
)
|
||||
import json
|
||||
self._cache = json.loads(response['SecretString'])
|
||||
return self._cache.get(name)
|
||||
|
||||
# Usage in settings
|
||||
from config.settings.secrets import set_secret_provider
|
||||
set_secret_provider(AWSSecretsProvider('thrillwiki/production'))
|
||||
```
|
||||
|
||||
### Option 3: HashiCorp Vault
|
||||
|
||||
For Vault integration:
|
||||
|
||||
```python
|
||||
class VaultSecretsProvider(SecretProvider):
|
||||
def __init__(self, vault_addr: str, token: str, path: str):
|
||||
import hvac
|
||||
self.client = hvac.Client(url=vault_addr, token=token)
|
||||
self.path = path
|
||||
|
||||
def get_secret(self, name: str) -> str:
|
||||
response = self.client.secrets.kv.read_secret_version(
|
||||
path=self.path
|
||||
)
|
||||
return response['data']['data'].get(name)
|
||||
```
|
||||
|
||||
## Secret Rotation Procedures
|
||||
|
||||
### Rotating SECRET_KEY
|
||||
|
||||
**Impact:** All sessions will be invalidated. Users will need to log in again.
|
||||
|
||||
1. **Prepare the new key:**
|
||||
```bash
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
```
|
||||
|
||||
2. **Update deployment:**
|
||||
```bash
|
||||
# Update environment variable
|
||||
export SECRET_KEY="new-secret-key-here"
|
||||
export SECRET_KEY_VERSION="2" # Increment version
|
||||
```
|
||||
|
||||
3. **Deploy during low-traffic period**
|
||||
|
||||
4. **Clear session cache:**
|
||||
```bash
|
||||
python manage.py clearsessions
|
||||
```
|
||||
|
||||
5. **Monitor for issues:**
|
||||
- Check error rates
|
||||
- Monitor login success rates
|
||||
- Watch for authentication errors
|
||||
|
||||
### Rotating Database Credentials
|
||||
|
||||
**Impact:** Application downtime if not done carefully.
|
||||
|
||||
1. **Create new database user:**
|
||||
```sql
|
||||
CREATE USER thrillwiki_new WITH PASSWORD 'new-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki_new;
|
||||
```
|
||||
|
||||
2. **Update application:**
|
||||
```bash
|
||||
export DATABASE_URL="postgis://thrillwiki_new:new-password@host:5432/thrillwiki"
|
||||
```
|
||||
|
||||
3. **Deploy and verify**
|
||||
|
||||
4. **Remove old user:**
|
||||
```sql
|
||||
DROP USER thrillwiki_old;
|
||||
```
|
||||
|
||||
### Rotating API Keys
|
||||
|
||||
For third-party API keys:
|
||||
|
||||
1. **Generate new key** in the service's dashboard
|
||||
2. **Update environment variable**
|
||||
3. **Deploy**
|
||||
4. **Revoke old key** in the service's dashboard
|
||||
|
||||
## Automated Rotation
|
||||
|
||||
### Enable Rotation Monitoring
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
SECRET_ROTATION_ENABLED=True
|
||||
SECRET_KEY_VERSION=1
|
||||
SECRET_EXPIRY_WARNING_DAYS=30
|
||||
```
|
||||
|
||||
### Check Rotation Status
|
||||
|
||||
```bash
|
||||
python manage.py validate_settings --secrets-only
|
||||
```
|
||||
|
||||
### Automated Alerts
|
||||
|
||||
Configure logging to alert on expiry warnings:
|
||||
|
||||
```python
|
||||
# In your monitoring setup, watch for:
|
||||
# Logger: security
|
||||
# Level: WARNING
|
||||
# Message contains: "Secret expiry"
|
||||
```
|
||||
|
||||
## Emergency Procedures
|
||||
|
||||
### Suspected Credential Compromise
|
||||
|
||||
1. **Immediate Actions:**
|
||||
- Rotate the compromised credential immediately
|
||||
- Check access logs for unauthorized use
|
||||
- Review related systems for compromise
|
||||
|
||||
2. **Investigation:**
|
||||
- Identify the source of the leak
|
||||
- Review access patterns
|
||||
- Document the incident
|
||||
|
||||
3. **Remediation:**
|
||||
- Fix the vulnerability that caused the leak
|
||||
- Update security procedures
|
||||
- Conduct a post-mortem
|
||||
|
||||
### Complete Secret Rotation (Breach Response)
|
||||
|
||||
If a major breach is suspected, rotate all secrets:
|
||||
|
||||
```bash
|
||||
# 1. Generate new SECRET_KEY
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
|
||||
# 2. Create new database credentials
|
||||
# 3. Regenerate all API keys in respective dashboards
|
||||
# 4. Update all environment variables
|
||||
# 5. Deploy
|
||||
# 6. Clear all caches
|
||||
python manage.py clear_cache # If you have this command
|
||||
redis-cli FLUSHALL # Or clear Redis directly
|
||||
|
||||
# 7. Invalidate all sessions
|
||||
python manage.py clearsessions
|
||||
|
||||
# 8. Revoke all old credentials
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- ✅ Use different secrets for each environment
|
||||
- ✅ Rotate secrets regularly (90-180 days)
|
||||
- ✅ Use secret management services in production
|
||||
- ✅ Monitor for secret exposure (git-secrets, truffleHog)
|
||||
- ✅ Log secret access for audit trails
|
||||
- ✅ Use strong, randomly generated secrets
|
||||
|
||||
### Don'ts
|
||||
|
||||
- ❌ Never commit secrets to version control
|
||||
- ❌ Never share secrets via chat or email
|
||||
- ❌ Never use the same secret across environments
|
||||
- ❌ Never use default or example secrets in production
|
||||
- ❌ Never log secret values (even partially)
|
||||
|
||||
## Audit and Compliance
|
||||
|
||||
### Access Logging
|
||||
|
||||
Secret access is logged to the security logger:
|
||||
|
||||
```python
|
||||
# Logged events:
|
||||
# - Secret validation failures
|
||||
# - Secret rotation warnings
|
||||
# - Invalid secret format detections
|
||||
```
|
||||
|
||||
### Regular Audits
|
||||
|
||||
Monthly security checklist:
|
||||
|
||||
- [ ] Review secret rotation schedule
|
||||
- [ ] Check for exposed secrets in logs
|
||||
- [ ] Verify secret strength requirements
|
||||
- [ ] Audit secret access patterns
|
||||
- [ ] Test secret rotation procedures
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Secret validation failed"
|
||||
|
||||
```bash
|
||||
# Check the specific error
|
||||
python manage.py validate_settings
|
||||
|
||||
# Common issues:
|
||||
# - Secret too short
|
||||
# - Placeholder value detected
|
||||
# - Missing required secret
|
||||
```
|
||||
|
||||
### "SECRET_KEY does not meet requirements"
|
||||
|
||||
```bash
|
||||
# Generate a proper key
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||
```
|
||||
|
||||
### Secret provider errors
|
||||
|
||||
```bash
|
||||
# Test secret provider connectivity
|
||||
python -c "
|
||||
from config.settings.secrets import get_secret_provider
|
||||
provider = get_secret_provider()
|
||||
print(provider.list_secrets())
|
||||
"
|
||||
```
|
||||
Reference in New Issue
Block a user