feat: Refactor rides app with unique constraints, mixins, and enhanced documentation

- Added migration to convert unique_together constraints to UniqueConstraint for RideModel.
- Introduced RideFormMixin for handling entity suggestions in ride forms.
- Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements.
- Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling.
- Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples.
- Implemented a benchmarking script for query performance analysis and optimization.
- Developed security documentation detailing measures, configurations, and a security checklist.
- Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
This commit is contained in:
pacnpal
2025-12-22 11:17:31 -05:00
parent 45d97b6e68
commit 2e35f8c5d9
71 changed files with 8036 additions and 1462 deletions

View File

@@ -0,0 +1,261 @@
# Code Standards
This document defines the code quality standards for the ThrillWiki backend.
## Formatting & Style
### PEP 8 Compliance
All Python code must comply with PEP 8, verified using:
- **black**: Code formatting (line length: 88)
- **flake8**: Style checking (max-line-length: 88, max-complexity: 10)
- **ruff**: Fast linting and import sorting
### Running Formatters
```bash
# Format code
uv run black backend/
# Check style
uv run flake8 backend/ --max-line-length=88 --max-complexity=10
# Lint and fix
uv run ruff check backend/ --fix
```
## Docstring Requirements
### Coverage
- 100% coverage for public classes and methods
- 100% coverage for all functions
- Optional for private methods (but encouraged)
### Style
Follow Google-style docstrings:
```python
def function_name(arg1: Type1, arg2: Type2) -> ReturnType:
"""
Brief description of what this function does.
Longer description if needed, explaining the purpose,
behavior, and any important details.
Args:
arg1: Description of arg1
arg2: Description of arg2
Returns:
Description of return value
Raises:
ExceptionType: When this exception is raised
Example:
>>> function_name("value1", "value2")
"result"
"""
```
### Class Docstrings
```python
class ClassName:
"""
Brief description of what this class does.
Longer description if needed.
Attributes:
attr1: Description of attr1
attr2: Description of attr2
Example:
instance = ClassName()
instance.method()
"""
```
### View Docstrings
Views should include URL patterns and permissions:
```python
class MyView(DetailView):
"""
Brief description of what this view does.
View Type: CBV (DetailView)
URL Pattern: /resource/<slug>/
Template: app/resource_detail.html
Permissions: LoginRequired
"""
```
## Complexity Guidelines
### Limits
- **Maximum McCabe complexity**: 10
- **Maximum method length**: 50 lines
- **Maximum nesting depth**: 3 levels
### Checking Complexity
```bash
# Check McCabe complexity
uv run flake8 backend/ --max-complexity=10 --select=C901
# Get complexity metrics
uv run radon cc backend/apps/ -a
```
### Refactoring Strategies
1. **Extract helper methods** for distinct responsibilities:
```python
# Before
def process_data(self, data):
# Validate data (10 lines)
# Transform data (10 lines)
# Save data (10 lines)
# Send notifications (10 lines)
pass
# After
def process_data(self, data):
self._validate_data(data)
transformed = self._transform_data(data)
result = self._save_data(transformed)
self._send_notifications(result)
return result
```
2. **Use early returns** to reduce nesting:
```python
# Before
def process(self, data):
if data:
if data.get('field1'):
if data.get('field2'):
return result
return None
# After
def process(self, data):
if not data:
return None
if not data.get('field1'):
return None
if not data.get('field2'):
return None
return result
```
3. **Move complex logic to service layer**
## Service Layer Patterns
### Service Method Signature
Always use keyword-only arguments for service methods:
```python
class MyService:
@staticmethod
def create_entity(
*, # Force keyword-only arguments
name: str,
description: str = "",
created_by: Optional[User] = None,
) -> Entity:
"""Create a new entity."""
pass
```
### Validation Pattern
Always call `full_clean()` before save:
```python
@staticmethod
def create_park(*, name: str, ...) -> Park:
with transaction.atomic():
park = Park(name=name, ...)
park.full_clean() # Validate before save
park.save()
return park
```
## Import Organization
Imports should be organized in this order:
1. Standard library
2. Third-party packages
3. Django imports
4. Local app imports
```python
import logging
from typing import Any, Dict, Optional
from django.contrib.auth import get_user_model
from django.db import transaction
from rest_framework import status
from apps.core.exceptions import ServiceError
from .models import MyModel
```
## Type Hints
Use type hints for all function signatures:
```python
def process_data(
data: Dict[str, Any],
user: Optional[User] = None,
) -> ProcessResult:
"""Process data and return result."""
pass
```
## Testing Requirements
- Maintain or improve test coverage with changes
- Add tests for new service methods
- Add tests for new mixins and base classes
- Run tests before committing:
```bash
pytest backend/tests/ --cov=backend/apps --cov-report=html
```
## Pre-commit Configuration
The following pre-commit hooks are configured:
```yaml
repos:
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
args: [--max-line-length=88, --max-complexity=10]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.10
hooks:
- id: ruff
args: [--fix]
```

View File

@@ -0,0 +1,219 @@
# Error Handling Guidelines
This document provides guidelines for handling errors consistently across the ThrillWiki backend.
## Exception Hierarchy
ThrillWiki uses a structured exception hierarchy defined in `apps/core/exceptions.py`:
```
ThrillWikiException (base)
├── ValidationException (400)
├── NotFoundError (404)
├── PermissionDeniedError (403)
├── BusinessLogicError (400)
├── ServiceError (500)
├── ExternalServiceError (502)
├── CacheError (500)
├── Domain-specific exceptions:
│ ├── ParkError
│ │ ├── ParkNotFoundError
│ │ └── ParkOperationError
│ ├── RideError
│ │ ├── RideNotFoundError
│ │ └── RideOperationError
│ ├── LocationError
│ │ ├── InvalidCoordinatesError
│ │ └── GeolocationError
│ ├── ReviewError
│ │ ├── ReviewModerationError
│ │ └── DuplicateReviewError
│ └── AccountError
│ ├── InsufficientPermissionsError
│ └── EmailError
```
## Using ErrorHandler
The `ErrorHandler` class in `apps/core/utils/error_handling.py` provides standardized error handling.
### Template Views
```python
from apps.core.utils.error_handling import ErrorHandler
from apps.core.exceptions import ServiceError
def my_view(request):
try:
result = SomeService.do_operation(...)
except ServiceError as e:
ErrorHandler.handle_view_error(
request,
e,
user_message="The operation failed. Please try again.",
log_message=f"Service operation failed for user {request.user.id}"
)
return redirect("some-fallback")
except ValidationError as e:
ErrorHandler.handle_view_error(
request,
e,
user_message="Invalid data provided",
level="warning"
)
return redirect("form-view")
```
### API Views
```python
from apps.core.utils.error_handling import ErrorHandler
from apps.core.exceptions import ServiceError
from rest_framework import status
class MyAPIView(APIView):
def post(self, request):
try:
result = SomeService.do_operation(...)
return ErrorHandler.api_success_response(
data=result,
message="Operation completed successfully"
)
except ServiceError as e:
return ErrorHandler.handle_api_error(
e,
user_message="Failed to complete operation",
status_code=status.HTTP_400_BAD_REQUEST
)
```
## Best Practices
### 1. Always Catch Specific Exceptions
```python
# Good
try:
park = ParkService.create_park(...)
except ParkOperationError as e:
# Handle park-specific error
pass
except ValidationException as e:
# Handle validation error
pass
# Bad
try:
park = ParkService.create_park(...)
except Exception as e:
# Too broad - loses error context
pass
```
### 2. Log with Appropriate Context
```python
# Good
logger.error(
f"Park creation failed for user {user.id}: {error}",
exc_info=True,
extra={"user_id": user.id, "park_name": name}
)
# Bad
logger.error(f"Error: {error}")
```
### 3. Provide Clear User Messages
```python
# Good - User-friendly and actionable
ErrorHandler.handle_view_error(
request,
error,
user_message="Unable to save your changes. Please check your input and try again."
)
# Bad - Technical details exposed to user
ErrorHandler.handle_view_error(
request,
error,
user_message=f"IntegrityError: UNIQUE constraint failed: parks_park.slug"
)
```
### 4. Use Appropriate HTTP Status Codes
| Error Type | Status Code | When to Use |
|------------|-------------|-------------|
| ValidationException | 400 | Invalid user input |
| NotFoundError | 404 | Resource doesn't exist |
| PermissionDeniedError | 403 | User lacks permission |
| BusinessLogicError | 400 | Business rule violation |
| ServiceError | 500 | Internal service failure |
| ExternalServiceError | 502 | Third-party service failure |
### 5. Never Use Bare `except:` Clauses
```python
# Never do this
try:
something()
except:
pass
# Always specify exception type
try:
something()
except SpecificException:
handle_error()
```
## Error Response Format
### API Error Response
```json
{
"error": "User-friendly error message",
"detail": "Technical error details",
"error_code": "SPECIFIC_ERROR_CODE",
"details": {
"field": "Additional context"
}
}
```
### API Success Response
```json
{
"status": "success",
"message": "Operation completed successfully",
"data": {
// Response data
}
}
```
## Creating Custom Exceptions
When creating domain-specific exceptions:
```python
from apps.core.exceptions import BusinessLogicError
class MyDomainError(BusinessLogicError):
"""Raised when my domain operation fails."""
default_message = "My domain operation failed"
error_code = "MY_DOMAIN_ERROR"
status_code = 400
def __init__(self, context_value: str = None, **kwargs):
if context_value:
kwargs["details"] = {"context": context_value}
kwargs["message"] = f"Operation failed for: {context_value}"
super().__init__(**kwargs)
```

View File

@@ -0,0 +1,158 @@
# View Pattern Guidelines
This document provides guidelines for implementing views in the ThrillWiki backend.
## When to Use CBVs
Use Class-Based Views for:
- CRUD operations (CreateView, UpdateView, DetailView, ListView, DeleteView)
- Complex views with multiple methods
- Views that benefit from inheritance and mixins
- Views with shared context or queryset logic
### CBV Examples
```python
class ParkDetailView(OptimizedDetailView):
"""Display park details with related data."""
model = Park
template_name = "parks/park_detail.html"
select_related_fields = ["location", "operator"]
prefetch_related_fields = ["photos", "rides"]
```
## When to Use FBVs
Use Function-Based Views for:
- Simple HTMX partial renders
- Single-purpose utility endpoints
- Search/autocomplete endpoints
- Status badge/action endpoints
### FBV Examples
```python
def search_parks(request: HttpRequest) -> HttpResponse:
"""
HTMX endpoint for park search autocomplete.
View Type: FBV (HTMX Partial)
URL Pattern: /parks/search/
Returns: HTML partial
"""
query = request.GET.get("q", "").strip()
parks = Park.objects.filter(name__icontains=query)[:10]
return render(request, "parks/partials/search_results.html", {"parks": parks})
```
## When to Use ViewSets
Use DRF ViewSets for:
- REST API endpoints
- Resources with standard CRUD operations
- Resources requiring nested routing
### ViewSet Examples
```python
class ParkPhotoViewSet(ModelViewSet):
"""ViewSet for managing park photos via API."""
queryset = ParkPhoto.objects.all()
serializer_class = ParkPhotoSerializer
permission_classes = [IsAuthenticated]
```
## View Inventory
### Parks App
- **CBVs**: ParkListView, ParkCreateView, ParkUpdateView, ParkDetailView, ParkAreaDetailView, OperatorListView
- **FBVs**: geocode_location, reverse_geocode, search_parks, roadtrip_* (HTMX)
### Rides App
- **CBVs**: RideDetailView, RideCreateView, RideUpdateView, RideListView, SingleCategoryListView, RideRankingsView, RideRankingDetailView, ManufacturerListView, DesignerListView
- **FBVs**: show_coaster_fields, ride_status_actions, ride_header_badge, search_companies, search_ride_models, get_search_suggestions, ranking_history_chart, ranking_comparisons
### Accounts App
- **CBVs**: ProfileView, SettingsView, CustomLoginView, CustomSignupView
- **FBVs**: user_redirect_view, email_required, request_password_reset, reset_password
### Moderation App
- **ViewSets**: ModerationReportViewSet, ModerationQueueViewSet, ModerationActionViewSet, BulkOperationViewSet, UserModerationViewSet
## View Type Indicators
Always include view type information in docstrings:
```python
class ParkDetailView(DetailView):
"""
Display park details with related data.
View Type: CBV (DetailView)
URL Pattern: /parks/<slug>/
Template: parks/park_detail.html
Permissions: Public
"""
def search_parks(request: HttpRequest) -> HttpResponse:
"""
HTMX endpoint for park search autocomplete.
View Type: FBV (HTMX Partial)
URL Pattern: /parks/search/
Returns: HTML partial
"""
```
## Base Classes
### OptimizedListView
Automatically applies select_related and prefetch_related based on class attributes.
```python
from apps.core.views.base import OptimizedListView
class RideListView(OptimizedListView):
model = Ride
select_related_fields = ['park', 'manufacturer']
prefetch_related_fields = ['photos']
```
### OptimizedDetailView
Automatically applies select_related and prefetch_related based on class attributes.
```python
from apps.core.views.base import OptimizedDetailView
class RideDetailView(OptimizedDetailView):
model = Ride
select_related_fields = ['park', 'park__location', 'manufacturer']
prefetch_related_fields = ['photos', 'coaster_stats']
```
## Mixins
### RideFormMixin
Handles entity suggestions in ride forms (manufacturers, designers, models).
```python
from apps.rides.mixins import RideFormMixin
class RideCreateView(RideFormMixin, CreateView):
def form_valid(self, form):
self.handle_entity_suggestions(form)
return super().form_valid(form)
```
## Best Practices
1. **Prefer CBVs for CRUD** - Use Django's built-in generic views
2. **Prefer FBVs for HTMX** - Simple endpoints are easier to understand as functions
3. **Document view types** - Include View Type in all docstrings
4. **Use mixins for shared logic** - Avoid code duplication
5. **Use base classes for query optimization** - OptimizedListView, OptimizedDetailView