mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 11:31:09 -05:00
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:
261
backend/docs/code_standards.md
Normal file
261
backend/docs/code_standards.md
Normal 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]
|
||||
```
|
||||
219
backend/docs/error_handling.md
Normal file
219
backend/docs/error_handling.md
Normal 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)
|
||||
```
|
||||
158
backend/docs/view_guidelines.md
Normal file
158
backend/docs/view_guidelines.md
Normal 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
|
||||
Reference in New Issue
Block a user