"""
if not value:
- return ''
+ return ""
return sanitize_attribute_value(str(value))
@@ -187,36 +190,36 @@ def attr_safe_filter(value):
# Predefined safe SVG icons
# These are trusted and can be rendered without sanitization
BUILTIN_ICONS = {
- 'check': '''
''',
- 'x': '''
''',
- 'plus': '''
''',
- 'minus': '''
''',
- 'chevron-down': '''
''',
- 'chevron-up': '''
''',
- 'chevron-left': '''
''',
- 'chevron-right': '''
''',
- 'search': '''
''',
- 'menu': '''
''',
- 'user': '''
''',
- 'cog': '''
''',
- 'trash': '''
''',
- 'pencil': '''
''',
- 'eye': '''
''',
- 'eye-slash': '''
''',
- 'arrow-left': '''
''',
- 'arrow-right': '''
''',
- 'info': '''
''',
- 'warning': '''
''',
- 'error': '''
''',
- 'success': '''
''',
- 'loading': '''
''',
- 'external-link': '''
''',
- 'download': '''
''',
- 'upload': '''
''',
- 'star': '''
''',
- 'star-filled': '''
''',
- 'heart': '''
''',
- 'heart-filled': '''
''',
+ "check": """
""",
+ "x": """
""",
+ "plus": """
""",
+ "minus": """
""",
+ "chevron-down": """
""",
+ "chevron-up": """
""",
+ "chevron-left": """
""",
+ "chevron-right": """
""",
+ "search": """
""",
+ "menu": """
""",
+ "user": """
""",
+ "cog": """
""",
+ "trash": """
""",
+ "pencil": """
""",
+ "eye": """
""",
+ "eye-slash": """
""",
+ "arrow-left": """
""",
+ "arrow-right": """
""",
+ "info": """
""",
+ "warning": """
""",
+ "error": """
""",
+ "success": """
""",
+ "loading": """
""",
+ "external-link": """
""",
+ "download": """
""",
+ "upload": """
""",
+ "star": """
""",
+ "star-filled": """
""",
+ "heart": """
""",
+ "heart-filled": """
""",
}
@@ -243,18 +246,18 @@ def icon(name, **kwargs):
if not svg_template:
# Return empty string for unknown icons (fail silently)
- return ''
+ return ""
# Build attributes string
attrs_list = []
for key, value in kwargs.items():
# Convert underscore to hyphen for HTML attributes (e.g., aria_hidden -> aria-hidden)
- attr_name = key.replace('_', '-')
+ attr_name = key.replace("_", "-")
# Escape attribute values to prevent XSS
safe_value = sanitize_attribute_value(str(value))
attrs_list.append(f'{attr_name}="{safe_value}"')
- attrs_str = ' '.join(attrs_list)
+ attrs_str = " ".join(attrs_list)
# Substitute attributes into template
svg = svg_template.format(attrs=attrs_str)
@@ -263,7 +266,7 @@ def icon(name, **kwargs):
@register.simple_tag
-def icon_class(name, size='w-5 h-5', extra_class=''):
+def icon_class(name, size="w-5 h-5", extra_class=""):
"""
Render a trusted SVG icon with common class presets.
@@ -278,5 +281,5 @@ def icon_class(name, size='w-5 h-5', extra_class=''):
Returns:
Safe HTML for the icon SVG
"""
- classes = f'{size} {extra_class}'.strip()
- return icon(name, **{'class': classes})
+ classes = f"{size} {extra_class}".strip()
+ return icon(name, **{"class": classes})
diff --git a/backend/apps/core/tests/test_history.py b/backend/apps/core/tests/test_history.py
index 79a71f7e..4811d33a 100644
--- a/backend/apps/core/tests/test_history.py
+++ b/backend/apps/core/tests/test_history.py
@@ -1,4 +1,3 @@
-
import pghistory
import pytest
from django.contrib.auth import get_user_model
@@ -7,6 +6,7 @@ from apps.parks.models import Company, Park
User = get_user_model()
+
@pytest.mark.django_db
class TestTrackedModel:
"""
@@ -20,10 +20,7 @@ class TestTrackedModel:
with pghistory.context(user=user.id):
park = Park.objects.create(
- name="History Test Park",
- description="Testing history",
- operating_season="Summer",
- operator=company
+ name="History Test Park", description="Testing history", operating_season="Summer", operator=company
)
# Verify history using the helper method from TrackedModel
@@ -50,6 +47,5 @@ class TestTrackedModel:
park.save()
assert park.get_history().count() == 2
- latest = park.get_history().first() # Ordered by -pgh_created_at
+ latest = park.get_history().first() # Ordered by -pgh_created_at
assert latest.name == "Updated"
-
diff --git a/backend/apps/core/urls/__init__.py b/backend/apps/core/urls/__init__.py
index f70fe166..4b413933 100644
--- a/backend/apps/core/urls/__init__.py
+++ b/backend/apps/core/urls/__init__.py
@@ -17,9 +17,7 @@ app_name = "core"
entity_patterns = [
path("search/", EntityFuzzySearchView.as_view(), name="entity_fuzzy_search"),
path("not-found/", EntityNotFoundView.as_view(), name="entity_not_found"),
- path(
- "suggestions/", QuickEntitySuggestionView.as_view(), name="entity_suggestions"
- ),
+ path("suggestions/", QuickEntitySuggestionView.as_view(), name="entity_suggestions"),
]
# FSM transition endpoints
diff --git a/backend/apps/core/utils/cloudflare.py b/backend/apps/core/utils/cloudflare.py
index 54136ddc..187898ef 100644
--- a/backend/apps/core/utils/cloudflare.py
+++ b/backend/apps/core/utils/cloudflare.py
@@ -6,6 +6,7 @@ from django.core.exceptions import ImproperlyConfigured
logger = logging.getLogger(__name__)
+
def get_direct_upload_url(user_id=None):
"""
Generates a direct upload URL for Cloudflare Images.
@@ -20,13 +21,11 @@ def get_direct_upload_url(user_id=None):
ImproperlyConfigured: If Cloudflare settings are missing.
requests.RequestException: If the Cloudflare API request fails.
"""
- account_id = getattr(settings, 'CLOUDFLARE_IMAGES_ACCOUNT_ID', None)
- api_token = getattr(settings, 'CLOUDFLARE_IMAGES_API_TOKEN', None)
+ account_id = getattr(settings, "CLOUDFLARE_IMAGES_ACCOUNT_ID", None)
+ api_token = getattr(settings, "CLOUDFLARE_IMAGES_API_TOKEN", None)
if not account_id or not api_token:
- raise ImproperlyConfigured(
- "CLOUDFLARE_IMAGES_ACCOUNT_ID and CLOUDFLARE_IMAGES_API_TOKEN must be set."
- )
+ raise ImproperlyConfigured("CLOUDFLARE_IMAGES_ACCOUNT_ID and CLOUDFLARE_IMAGES_API_TOKEN must be set.")
url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v2/direct_upload"
diff --git a/backend/apps/core/utils/file_scanner.py b/backend/apps/core/utils/file_scanner.py
index 0954a651..414dcd4c 100644
--- a/backend/apps/core/utils/file_scanner.py
+++ b/backend/apps/core/utils/file_scanner.py
@@ -37,6 +37,7 @@ from django.core.files.uploadedfile import UploadedFile
class FileValidationError(ValidationError):
"""Custom exception for file validation errors."""
+
pass
@@ -47,41 +48,49 @@ class FileValidationError(ValidationError):
# Magic number signatures for common image formats
# Format: (magic_bytes, offset, description)
IMAGE_SIGNATURES = {
- 'jpeg': [
- (b'\xFF\xD8\xFF\xE0', 0, 'JPEG (JFIF)'),
- (b'\xFF\xD8\xFF\xE1', 0, 'JPEG (EXIF)'),
- (b'\xFF\xD8\xFF\xE2', 0, 'JPEG (ICC)'),
- (b'\xFF\xD8\xFF\xE3', 0, 'JPEG (Samsung)'),
- (b'\xFF\xD8\xFF\xE8', 0, 'JPEG (SPIFF)'),
- (b'\xFF\xD8\xFF\xDB', 0, 'JPEG (Raw)'),
+ "jpeg": [
+ (b"\xff\xd8\xff\xe0", 0, "JPEG (JFIF)"),
+ (b"\xff\xd8\xff\xe1", 0, "JPEG (EXIF)"),
+ (b"\xff\xd8\xff\xe2", 0, "JPEG (ICC)"),
+ (b"\xff\xd8\xff\xe3", 0, "JPEG (Samsung)"),
+ (b"\xff\xd8\xff\xe8", 0, "JPEG (SPIFF)"),
+ (b"\xff\xd8\xff\xdb", 0, "JPEG (Raw)"),
],
- 'png': [
- (b'\x89PNG\r\n\x1a\n', 0, 'PNG'),
+ "png": [
+ (b"\x89PNG\r\n\x1a\n", 0, "PNG"),
],
- 'gif': [
- (b'GIF87a', 0, 'GIF87a'),
- (b'GIF89a', 0, 'GIF89a'),
+ "gif": [
+ (b"GIF87a", 0, "GIF87a"),
+ (b"GIF89a", 0, "GIF89a"),
],
- 'webp': [
- (b'RIFF', 0, 'RIFF'), # WebP starts with RIFF header
+ "webp": [
+ (b"RIFF", 0, "RIFF"), # WebP starts with RIFF header
],
- 'bmp': [
- (b'BM', 0, 'BMP'),
+ "bmp": [
+ (b"BM", 0, "BMP"),
],
}
# All allowed MIME types
-ALLOWED_IMAGE_MIME_TYPES: set[str] = frozenset({
- 'image/jpeg',
- 'image/png',
- 'image/gif',
- 'image/webp',
-})
+ALLOWED_IMAGE_MIME_TYPES: set[str] = frozenset(
+ {
+ "image/jpeg",
+ "image/png",
+ "image/gif",
+ "image/webp",
+ }
+)
# Allowed file extensions
-ALLOWED_IMAGE_EXTENSIONS: set[str] = frozenset({
- '.jpg', '.jpeg', '.png', '.gif', '.webp',
-})
+ALLOWED_IMAGE_EXTENSIONS: set[str] = frozenset(
+ {
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".webp",
+ }
+)
# Maximum file size (10MB)
MAX_FILE_SIZE = 10 * 1024 * 1024
@@ -94,6 +103,7 @@ MIN_FILE_SIZE = 100 # 100 bytes
# File Validation Functions
# =============================================================================
+
def validate_image_upload(
file: UploadedFile,
max_size: int = MAX_FILE_SIZE,
@@ -133,39 +143,29 @@ def validate_image_upload(
# 2. Check file size
if file.size > max_size:
- raise FileValidationError(
- f"File too large. Maximum size is {max_size // (1024 * 1024)}MB"
- )
+ raise FileValidationError(f"File too large. Maximum size is {max_size // (1024 * 1024)}MB")
if file.size < MIN_FILE_SIZE:
raise FileValidationError("File too small or empty")
# 3. Check file extension
- filename = file.name or ''
+ filename = file.name or ""
ext = os.path.splitext(filename)[1].lower()
if ext not in allowed_extensions:
- raise FileValidationError(
- f"Invalid file extension '{ext}'. Allowed: {', '.join(allowed_extensions)}"
- )
+ raise FileValidationError(f"Invalid file extension '{ext}'. Allowed: {', '.join(allowed_extensions)}")
# 4. Check Content-Type header
- content_type = getattr(file, 'content_type', '')
+ content_type = getattr(file, "content_type", "")
if content_type and content_type not in allowed_types:
- raise FileValidationError(
- f"Invalid file type '{content_type}'. Allowed: {', '.join(allowed_types)}"
- )
+ raise FileValidationError(f"Invalid file type '{content_type}'. Allowed: {', '.join(allowed_types)}")
# 5. Validate magic numbers (actual file content)
if not _validate_magic_number(file):
- raise FileValidationError(
- "File content doesn't match file extension. File may be corrupted or malicious."
- )
+ raise FileValidationError("File content doesn't match file extension. File may be corrupted or malicious.")
# 6. Validate image integrity using PIL
if not _validate_image_integrity(file):
- raise FileValidationError(
- "Invalid or corrupted image file"
- )
+ raise FileValidationError("Invalid or corrupted image file")
return True
@@ -191,10 +191,10 @@ def _validate_magic_number(file: UploadedFile) -> bool:
# Check against known signatures
for format_name, signatures in IMAGE_SIGNATURES.items():
for magic, offset, _description in signatures:
- if len(header) >= offset + len(magic) and header[offset:offset + len(magic)] == magic:
+ if len(header) >= offset + len(magic) and header[offset : offset + len(magic)] == magic:
# Special handling for WebP (must also have WEBP marker)
- if format_name == 'webp':
- if len(header) >= 12 and header[8:12] == b'WEBP':
+ if format_name == "webp":
+ if len(header) >= 12 and header[8:12] == b"WEBP":
return True
else:
return True
@@ -233,9 +233,7 @@ def _validate_image_integrity(file: UploadedFile) -> bool:
# Prevent decompression bombs
max_dimension = 10000
if img2.width > max_dimension or img2.height > max_dimension:
- raise FileValidationError(
- f"Image dimensions too large. Maximum is {max_dimension}x{max_dimension}"
- )
+ raise FileValidationError(f"Image dimensions too large. Maximum is {max_dimension}x{max_dimension}")
# Check for very small dimensions (might be suspicious)
if img2.width < 1 or img2.height < 1:
@@ -253,6 +251,7 @@ def _validate_image_integrity(file: UploadedFile) -> bool:
# Filename Sanitization
# =============================================================================
+
def sanitize_filename(filename: str, max_length: int = 100) -> str:
"""
Sanitize a filename to prevent directory traversal and other attacks.
@@ -281,13 +280,13 @@ def sanitize_filename(filename: str, max_length: int = 100) -> str:
# Remove or replace dangerous characters from name
# Allow alphanumeric, hyphens, underscores, dots
- name = re.sub(r'[^\w\-.]', '_', name)
+ name = re.sub(r"[^\w\-.]", "_", name)
# Remove leading dots and underscores (hidden file prevention)
- name = name.lstrip('._')
+ name = name.lstrip("._")
# Collapse multiple underscores
- name = re.sub(r'_+', '_', name)
+ name = re.sub(r"_+", "_", name)
# Ensure name is not empty
if not name:
@@ -295,7 +294,7 @@ def sanitize_filename(filename: str, max_length: int = 100) -> str:
# Sanitize extension
ext = ext.lower()
- ext = re.sub(r'[^\w.]', '', ext)
+ ext = re.sub(r"[^\w.]", "", ext)
# Combine and truncate
result = f"{name[:max_length - len(ext)]}{ext}"
@@ -303,7 +302,7 @@ def sanitize_filename(filename: str, max_length: int = 100) -> str:
return result
-def generate_unique_filename(original_filename: str, prefix: str = '') -> str:
+def generate_unique_filename(original_filename: str, prefix: str = "") -> str:
"""
Generate a unique filename using UUID while preserving extension.
@@ -317,7 +316,7 @@ def generate_unique_filename(original_filename: str, prefix: str = '') -> str:
ext = os.path.splitext(original_filename)[1].lower()
# Sanitize extension
- ext = re.sub(r'[^\w.]', '', ext)
+ ext = re.sub(r"[^\w.]", "", ext)
# Generate unique filename
unique_id = uuid.uuid4().hex[:12]
@@ -332,9 +331,9 @@ def generate_unique_filename(original_filename: str, prefix: str = '') -> str:
# Rate limiting configuration
UPLOAD_RATE_LIMITS = {
- 'per_minute': 10,
- 'per_hour': 100,
- 'per_day': 500,
+ "per_minute": 10,
+ "per_hour": 100,
+ "per_day": 500,
}
@@ -351,24 +350,25 @@ def check_upload_rate_limit(user_id: int, cache_backend=None) -> tuple[bool, str
"""
if cache_backend is None:
from django.core.cache import cache
+
cache_backend = cache
# Check per-minute limit
minute_key = f"upload_rate:{user_id}:minute"
minute_count = cache_backend.get(minute_key, 0)
- if minute_count >= UPLOAD_RATE_LIMITS['per_minute']:
+ if minute_count >= UPLOAD_RATE_LIMITS["per_minute"]:
return False, "Upload rate limit exceeded. Please wait a minute."
# Check per-hour limit
hour_key = f"upload_rate:{user_id}:hour"
hour_count = cache_backend.get(hour_key, 0)
- if hour_count >= UPLOAD_RATE_LIMITS['per_hour']:
+ if hour_count >= UPLOAD_RATE_LIMITS["per_hour"]:
return False, "Hourly upload limit exceeded. Please try again later."
# Check per-day limit
day_key = f"upload_rate:{user_id}:day"
day_count = cache_backend.get(day_key, 0)
- if day_count >= UPLOAD_RATE_LIMITS['per_day']:
+ if day_count >= UPLOAD_RATE_LIMITS["per_day"]:
return False, "Daily upload limit exceeded. Please try again tomorrow."
return True, ""
@@ -384,6 +384,7 @@ def increment_upload_count(user_id: int, cache_backend=None) -> None:
"""
if cache_backend is None:
from django.core.cache import cache
+
cache_backend = cache
# Increment per-minute counter (expires in 60 seconds)
@@ -412,6 +413,7 @@ def increment_upload_count(user_id: int, cache_backend=None) -> None:
# Antivirus Integration Point
# =============================================================================
+
def scan_file_for_malware(file: UploadedFile) -> tuple[bool, str]:
"""
Placeholder for antivirus/malware scanning integration.
diff --git a/backend/apps/core/utils/html_sanitizer.py b/backend/apps/core/utils/html_sanitizer.py
index c37c510f..5c992458 100644
--- a/backend/apps/core/utils/html_sanitizer.py
+++ b/backend/apps/core/utils/html_sanitizer.py
@@ -26,6 +26,7 @@ from typing import Any
try:
import bleach
+
BLEACH_AVAILABLE = True
except ImportError:
BLEACH_AVAILABLE = False
@@ -36,71 +37,135 @@ except ImportError:
# =============================================================================
# Default allowed HTML tags for user-generated content
-ALLOWED_TAGS = frozenset([
- # Text formatting
- 'p', 'br', 'hr',
- 'strong', 'b', 'em', 'i', 'u', 's', 'strike',
- 'sub', 'sup', 'small', 'mark',
-
- # Headers
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
-
- # Lists
- 'ul', 'ol', 'li',
-
- # Links (with restrictions on attributes)
- 'a',
-
- # Block elements
- 'blockquote', 'pre', 'code',
- 'div', 'span',
-
- # Tables
- 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
-])
+ALLOWED_TAGS = frozenset(
+ [
+ # Text formatting
+ "p",
+ "br",
+ "hr",
+ "strong",
+ "b",
+ "em",
+ "i",
+ "u",
+ "s",
+ "strike",
+ "sub",
+ "sup",
+ "small",
+ "mark",
+ # Headers
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ # Lists
+ "ul",
+ "ol",
+ "li",
+ # Links (with restrictions on attributes)
+ "a",
+ # Block elements
+ "blockquote",
+ "pre",
+ "code",
+ "div",
+ "span",
+ # Tables
+ "table",
+ "thead",
+ "tbody",
+ "tfoot",
+ "tr",
+ "th",
+ "td",
+ ]
+)
# Allowed attributes for each tag
ALLOWED_ATTRIBUTES = {
- 'a': ['href', 'title', 'rel', 'target'],
- 'img': ['src', 'alt', 'title', 'width', 'height'],
- 'div': ['class'],
- 'span': ['class'],
- 'p': ['class'],
- 'table': ['class'],
- 'th': ['class', 'colspan', 'rowspan'],
- 'td': ['class', 'colspan', 'rowspan'],
- '*': ['class'], # Allow class on all elements
+ "a": ["href", "title", "rel", "target"],
+ "img": ["src", "alt", "title", "width", "height"],
+ "div": ["class"],
+ "span": ["class"],
+ "p": ["class"],
+ "table": ["class"],
+ "th": ["class", "colspan", "rowspan"],
+ "td": ["class", "colspan", "rowspan"],
+ "*": ["class"], # Allow class on all elements
}
# Allowed URL protocols
-ALLOWED_PROTOCOLS = frozenset([
- 'http', 'https', 'mailto', 'tel',
-])
+ALLOWED_PROTOCOLS = frozenset(
+ [
+ "http",
+ "https",
+ "mailto",
+ "tel",
+ ]
+)
# Minimal tags for comments and short text
-MINIMAL_TAGS = frozenset([
- 'p', 'br', 'strong', 'b', 'em', 'i', 'a',
-])
+MINIMAL_TAGS = frozenset(
+ [
+ "p",
+ "br",
+ "strong",
+ "b",
+ "em",
+ "i",
+ "a",
+ ]
+)
# Tags allowed in icon SVGs (for icon template rendering)
-SVG_TAGS = frozenset([
- 'svg', 'path', 'g', 'circle', 'rect', 'line', 'polyline', 'polygon',
- 'ellipse', 'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath',
- 'mask', 'linearGradient', 'radialGradient', 'stop', 'title',
-])
+SVG_TAGS = frozenset(
+ [
+ "svg",
+ "path",
+ "g",
+ "circle",
+ "rect",
+ "line",
+ "polyline",
+ "polygon",
+ "ellipse",
+ "text",
+ "tspan",
+ "defs",
+ "use",
+ "symbol",
+ "clipPath",
+ "mask",
+ "linearGradient",
+ "radialGradient",
+ "stop",
+ "title",
+ ]
+)
SVG_ATTRIBUTES = {
- 'svg': ['viewBox', 'width', 'height', 'fill', 'stroke', 'class',
- 'xmlns', 'aria-hidden', 'role'],
- 'path': ['d', 'fill', 'stroke', 'stroke-width', 'stroke-linecap',
- 'stroke-linejoin', 'class', 'fill-rule', 'clip-rule'],
- 'g': ['fill', 'stroke', 'transform', 'class'],
- 'circle': ['cx', 'cy', 'r', 'fill', 'stroke', 'class'],
- 'rect': ['x', 'y', 'width', 'height', 'rx', 'ry', 'fill', 'stroke', 'class'],
- 'line': ['x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width', 'class'],
- 'polyline': ['points', 'fill', 'stroke', 'class'],
- 'polygon': ['points', 'fill', 'stroke', 'class'],
- '*': ['class', 'fill', 'stroke'],
+ "svg": ["viewBox", "width", "height", "fill", "stroke", "class", "xmlns", "aria-hidden", "role"],
+ "path": [
+ "d",
+ "fill",
+ "stroke",
+ "stroke-width",
+ "stroke-linecap",
+ "stroke-linejoin",
+ "class",
+ "fill-rule",
+ "clip-rule",
+ ],
+ "g": ["fill", "stroke", "transform", "class"],
+ "circle": ["cx", "cy", "r", "fill", "stroke", "class"],
+ "rect": ["x", "y", "width", "height", "rx", "ry", "fill", "stroke", "class"],
+ "line": ["x1", "y1", "x2", "y2", "stroke", "stroke-width", "class"],
+ "polyline": ["points", "fill", "stroke", "class"],
+ "polygon": ["points", "fill", "stroke", "class"],
+ "*": ["class", "fill", "stroke"],
}
@@ -108,6 +173,7 @@ SVG_ATTRIBUTES = {
# Sanitization Functions
# =============================================================================
+
def sanitize_html(
html: str | None,
allowed_tags: frozenset | None = None,
@@ -133,7 +199,7 @@ def sanitize_html(
'
Hello
'
"""
if not html:
- return ''
+ return ""
if not isinstance(html, str):
html = str(html)
@@ -170,7 +236,7 @@ def sanitize_minimal(html: str | None) -> str:
return sanitize_html(
html,
allowed_tags=MINIMAL_TAGS,
- allowed_attributes={'a': ['href', 'title']},
+ allowed_attributes={"a": ["href", "title"]},
)
@@ -188,7 +254,7 @@ def sanitize_svg(svg: str | None) -> str:
Sanitized SVG string safe for inline rendering
"""
if not svg:
- return ''
+ return ""
if not isinstance(svg, str):
svg = str(svg)
@@ -218,7 +284,7 @@ def strip_html(html: str | None) -> str:
Plain text with all HTML tags removed
"""
if not html:
- return ''
+ return ""
if not isinstance(html, str):
html = str(html)
@@ -227,13 +293,14 @@ def strip_html(html: str | None) -> str:
return bleach.clean(html, tags=[], strip=True)
else:
# Fallback: use regex to strip tags
- return re.sub(r'<[^>]+>', '', html)
+ return re.sub(r"<[^>]+>", "", html)
# =============================================================================
# JSON/JavaScript Context Sanitization
# =============================================================================
+
def sanitize_for_json(data: Any) -> str:
"""
Safely serialize data for embedding in JavaScript/JSON contexts.
@@ -251,14 +318,12 @@ def sanitize_for_json(data: Any) -> str:
'{"name": "\\u003c/script\\u003e\\u003cscript\\u003ealert(\\"xss\\")"}'
"""
# JSON encode with safe characters escaped
- return json.dumps(data, ensure_ascii=False).replace(
- '<', '\\u003c'
- ).replace(
- '>', '\\u003e'
- ).replace(
- '&', '\\u0026'
- ).replace(
- "'", '\\u0027'
+ return (
+ json.dumps(data, ensure_ascii=False)
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("&", "\\u0026")
+ .replace("'", "\\u0027")
)
@@ -273,26 +338,21 @@ def escape_js_string(s: str | None) -> str:
Escaped string safe for JavaScript contexts
"""
if not s:
- return ''
+ return ""
if not isinstance(s, str):
s = str(s)
# Escape backslashes first, then other special characters
- return s.replace('\\', '\\\\').replace(
- "'", "\\'"
- ).replace(
- '"', '\\"'
- ).replace(
- '\n', '\\n'
- ).replace(
- '\r', '\\r'
- ).replace(
- '<', '\\u003c'
- ).replace(
- '>', '\\u003e'
- ).replace(
- '&', '\\u0026'
+ return (
+ s.replace("\\", "\\\\")
+ .replace("'", "\\'")
+ .replace('"', '\\"')
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("&", "\\u0026")
)
@@ -300,6 +360,7 @@ def escape_js_string(s: str | None) -> str:
# URL Sanitization
# =============================================================================
+
def sanitize_url(url: str | None, allowed_protocols: frozenset | None = None) -> str:
"""
Sanitize a URL to prevent javascript: and other dangerous protocols.
@@ -312,7 +373,7 @@ def sanitize_url(url: str | None, allowed_protocols: frozenset | None = None) ->
Sanitized URL or empty string if unsafe
"""
if not url:
- return ''
+ return ""
if not isinstance(url, str):
url = str(url)
@@ -320,7 +381,7 @@ def sanitize_url(url: str | None, allowed_protocols: frozenset | None = None) ->
url = url.strip()
if not url:
- return ''
+ return ""
protocols = allowed_protocols if allowed_protocols is not None else ALLOWED_PROTOCOLS
@@ -328,12 +389,12 @@ def sanitize_url(url: str | None, allowed_protocols: frozenset | None = None) ->
url_lower = url.lower()
# Check for javascript:, data:, vbscript:, etc.
- if ':' in url_lower:
- protocol = url_lower.split(':')[0]
- if protocol not in protocols:
+ if ":" in url_lower:
+ protocol = url_lower.split(":")[0]
+ if protocol not in protocols: # noqa: SIM102
# Allow relative URLs and anchor links
- if not (url.startswith('/') or url.startswith('#') or url.startswith('?')):
- return ''
+ if not (url.startswith("/") or url.startswith("#") or url.startswith("?")):
+ return ""
return url
@@ -342,6 +403,7 @@ def sanitize_url(url: str | None, allowed_protocols: frozenset | None = None) ->
# Attribute Sanitization
# =============================================================================
+
def sanitize_attribute_value(value: str | None) -> str:
"""
Sanitize a value for use in HTML attributes.
@@ -353,7 +415,7 @@ def sanitize_attribute_value(value: str | None) -> str:
Sanitized value safe for HTML attribute contexts
"""
if not value:
- return ''
+ return ""
if not isinstance(value, str):
value = str(value)
@@ -373,10 +435,10 @@ def sanitize_class_name(name: str | None) -> str:
Sanitized class name containing only safe characters
"""
if not name:
- return ''
+ return ""
if not isinstance(name, str):
name = str(name)
# Only allow alphanumeric, hyphens, and underscores
- return re.sub(r'[^a-zA-Z0-9_-]', '', name)
+ return re.sub(r"[^a-zA-Z0-9_-]", "", name)
diff --git a/backend/apps/core/utils/query_optimization.py b/backend/apps/core/utils/query_optimization.py
index c021e194..63b132ac 100644
--- a/backend/apps/core/utils/query_optimization.py
+++ b/backend/apps/core/utils/query_optimization.py
@@ -16,9 +16,7 @@ logger = logging.getLogger("query_optimization")
@contextmanager
-def track_queries(
- operation_name: str, warn_threshold: int = 10, time_threshold: float = 1.0
-):
+def track_queries(operation_name: str, warn_threshold: int = 10, time_threshold: float = 1.0):
"""
Context manager to track database queries for specific operations
@@ -47,15 +45,9 @@ def track_queries(
recent_queries = connection.queries[-total_queries:]
query_details = [
{
- "sql": (
- query["sql"][:500] + "..."
- if len(query["sql"]) > 500
- else query["sql"]
- ),
+ "sql": (query["sql"][:500] + "..." if len(query["sql"]) > 500 else query["sql"]),
"time": float(query["time"]),
- "duplicate_count": sum(
- 1 for q in recent_queries if q["sql"] == query["sql"]
- ),
+ "duplicate_count": sum(1 for q in recent_queries if q["sql"] == query["sql"]),
}
for query in recent_queries
]
@@ -65,22 +57,18 @@ def track_queries(
"query_count": total_queries,
"execution_time": execution_time,
"queries": query_details if settings.DEBUG else [],
- "slow_queries": [
- q for q in query_details if q["time"] > 0.1
- ], # Queries slower than 100ms
+ "slow_queries": [q for q in query_details if q["time"] > 0.1], # Queries slower than 100ms
}
# Log warnings for performance issues
if total_queries > warn_threshold or execution_time > time_threshold:
logger.warning(
- f"Performance concern in {operation_name}: "
- f"{total_queries} queries, {execution_time:.2f}s",
+ f"Performance concern in {operation_name}: " f"{total_queries} queries, {execution_time:.2f}s",
extra=performance_data,
)
else:
logger.debug(
- f"Query tracking for {operation_name}: "
- f"{total_queries} queries, {execution_time:.2f}s",
+ f"Query tracking for {operation_name}: " f"{total_queries} queries, {execution_time:.2f}s",
extra=performance_data,
)
@@ -109,9 +97,7 @@ class QueryOptimizer:
Optimize Ride queryset with proper relationships
"""
return (
- queryset.select_related(
- "park", "park__location", "manufacturer", "created_by"
- )
+ queryset.select_related("park", "park__location", "manufacturer", "created_by")
.prefetch_related("reviews__user", "media_items")
.annotate(
review_count=Count("reviews"),
@@ -158,9 +144,7 @@ class QueryCache:
"""Caching utilities for expensive queries"""
@staticmethod
- def cache_queryset_result(
- cache_key: str, queryset_func, timeout: int = 3600, **kwargs
- ):
+ def cache_queryset_result(cache_key: str, queryset_func, timeout: int = 3600, **kwargs):
"""
Cache the result of an expensive queryset operation
@@ -202,13 +186,9 @@ class QueryCache:
# For Redis cache backends that support pattern deletion
if hasattr(cache, "delete_pattern"):
deleted_count = cache.delete_pattern(pattern)
- logger.info(
- f"Invalidated {deleted_count} cache keys for pattern: {pattern}"
- )
+ logger.info(f"Invalidated {deleted_count} cache keys for pattern: {pattern}")
else:
- logger.warning(
- f"Cache backend does not support pattern deletion: {pattern}"
- )
+ logger.warning(f"Cache backend does not support pattern deletion: {pattern}")
except Exception as e:
logger.error(f"Error invalidating cache pattern {pattern}: {e}")
@@ -249,10 +229,7 @@ class IndexAnalyzer:
sql_upper = sql.upper()
analysis = {
"has_where_clause": "WHERE" in sql_upper,
- "has_join": any(
- join in sql_upper
- for join in ["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN"]
- ),
+ "has_join": any(join in sql_upper for join in ["JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN"]),
"has_order_by": "ORDER BY" in sql_upper,
"has_group_by": "GROUP BY" in sql_upper,
"has_like": "LIKE" in sql_upper,
@@ -266,19 +243,13 @@ class IndexAnalyzer:
# Suggest indexes based on patterns
if analysis["has_where_clause"] and not analysis["has_join"]:
- analysis["suggestions"].append(
- "Consider adding indexes on WHERE clause columns"
- )
+ analysis["suggestions"].append("Consider adding indexes on WHERE clause columns")
if analysis["has_order_by"]:
- analysis["suggestions"].append(
- "Consider adding indexes on ORDER BY columns"
- )
+ analysis["suggestions"].append("Consider adding indexes on ORDER BY columns")
if analysis["has_like"] and "%" not in sql[: sql.find("LIKE") + 10]:
- analysis["suggestions"].append(
- "LIKE queries with leading wildcards cannot use indexes efficiently"
- )
+ analysis["suggestions"].append("LIKE queries with leading wildcards cannot use indexes efficiently")
return analysis
@@ -294,28 +265,16 @@ class IndexAnalyzer:
# automatically)
for field in opts.fields:
if isinstance(field, models.ForeignKey):
- suggestions.append(
- f"Index on {field.name} (automatically created by Django)"
- )
+ suggestions.append(f"Index on {field.name} (automatically created by Django)")
# Suggest composite indexes for common query patterns
- date_fields = [
- f.name
- for f in opts.fields
- if isinstance(f, (models.DateField, models.DateTimeField))
- ]
- status_fields = [
- f.name
- for f in opts.fields
- if f.name in ["status", "is_active", "is_published"]
- ]
+ date_fields = [f.name for f in opts.fields if isinstance(f, models.DateField | models.DateTimeField)]
+ status_fields = [f.name for f in opts.fields if f.name in ["status", "is_active", "is_published"]]
if date_fields and status_fields:
for date_field in date_fields:
for status_field in status_fields:
- suggestions.append(
- f"Composite index on ({status_field}, {date_field}) for filtered date queries"
- )
+ suggestions.append(f"Composite index on ({status_field}, {date_field}) for filtered date queries")
# Suggest indexes for fields commonly used in WHERE clauses
common_filter_fields = ["slug", "name", "created_at", "updated_at"]
@@ -340,9 +299,7 @@ def log_query_performance():
return decorator
-def optimize_queryset_for_serialization(
- queryset: QuerySet, fields: list[str]
-) -> QuerySet:
+def optimize_queryset_for_serialization(queryset: QuerySet, fields: list[str]) -> QuerySet:
"""
Optimize a queryset for API serialization by only selecting needed fields
@@ -362,9 +319,7 @@ def optimize_queryset_for_serialization(
field = opts.get_field(field_name)
if isinstance(field, models.ForeignKey):
select_related_fields.append(field_name)
- elif isinstance(
- field, (models.ManyToManyField, models.reverse.ManyToManyRel)
- ):
+ elif isinstance(field, models.ManyToManyField | models.reverse.ManyToManyRel):
prefetch_related_fields.append(field_name)
except models.FieldDoesNotExist:
# Field might be a property or method, skip optimization
@@ -421,7 +376,6 @@ def monitor_db_performance(operation_name: str):
)
else:
logger.debug(
- f"DB performance for {operation_name}: "
- f"{duration:.3f}s, {total_queries} queries",
+ f"DB performance for {operation_name}: " f"{duration:.3f}s, {total_queries} queries",
extra=performance_data,
)
diff --git a/backend/apps/core/utils/turnstile.py b/backend/apps/core/utils/turnstile.py
index 5e7b071b..196d8d6e 100644
--- a/backend/apps/core/utils/turnstile.py
+++ b/backend/apps/core/utils/turnstile.py
@@ -4,6 +4,7 @@ Cloudflare Turnstile validation utilities.
This module provides a function to validate Turnstile tokens
on the server side before processing form submissions.
"""
+
import requests
from django.conf import settings
@@ -20,45 +21,41 @@ def validate_turnstile_token(token: str, ip: str = None) -> dict:
dict with 'success' boolean and optional 'error' message
"""
# Skip validation if configured (dev mode)
- if getattr(settings, 'TURNSTILE_SKIP_VALIDATION', False):
- return {'success': True}
+ if getattr(settings, "TURNSTILE_SKIP_VALIDATION", False):
+ return {"success": True}
- secret = getattr(settings, 'TURNSTILE_SECRET', '')
+ secret = getattr(settings, "TURNSTILE_SECRET", "")
if not secret:
- return {'success': True} # Skip if no secret configured
+ return {"success": True} # Skip if no secret configured
if not token:
- return {'success': False, 'error': 'Captcha verification required'}
+ return {"success": False, "error": "Captcha verification required"}
try:
response = requests.post(
- 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
+ "https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={
- 'secret': secret,
- 'response': token,
- 'remoteip': ip,
+ "secret": secret,
+ "response": token,
+ "remoteip": ip,
},
- timeout=10
+ timeout=10,
)
result = response.json()
- if result.get('success'):
- return {'success': True}
+ if result.get("success"):
+ return {"success": True}
else:
- error_codes = result.get('error-codes', [])
- return {
- 'success': False,
- 'error': 'Captcha verification failed',
- 'error_codes': error_codes
- }
+ error_codes = result.get("error-codes", [])
+ return {"success": False, "error": "Captcha verification failed", "error_codes": error_codes}
except requests.RequestException:
# Log error but don't block user on network issues
- return {'success': True} # Fail open to avoid blocking legitimate users
+ return {"success": True} # Fail open to avoid blocking legitimate users
def get_client_ip(request):
"""Extract client IP from request, handling proxies."""
- x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
- return x_forwarded_for.split(',')[0].strip()
- return request.META.get('REMOTE_ADDR')
+ return x_forwarded_for.split(",")[0].strip()
+ return request.META.get("REMOTE_ADDR")
diff --git a/backend/apps/core/views/base.py b/backend/apps/core/views/base.py
index 4e4cb4bc..8f1a3c00 100644
--- a/backend/apps/core/views/base.py
+++ b/backend/apps/core/views/base.py
@@ -5,7 +5,6 @@ This module provides base view classes that implement common patterns
such as automatic query optimization with select_related and prefetch_related.
"""
-
from django.db.models import QuerySet
from django.views.generic import DetailView, ListView
diff --git a/backend/apps/core/views/entity_search.py b/backend/apps/core/views/entity_search.py
index bed79b66..4bb3e7fe 100644
--- a/backend/apps/core/views/entity_search.py
+++ b/backend/apps/core/views/entity_search.py
@@ -2,7 +2,6 @@
Entity search views with fuzzy matching and authentication prompts.
"""
-
import contextlib
from rest_framework import status
@@ -67,9 +66,7 @@ class EntityFuzzySearchView(APIView):
try:
# Parse request data
query = request.data.get("query", "").strip()
- entity_types_raw = request.data.get(
- "entity_types", ["park", "ride", "company"]
- )
+ entity_types_raw = request.data.get("entity_types", ["park", "ride", "company"])
include_suggestions = request.data.get("include_suggestions", True)
# Validate query
@@ -105,9 +102,7 @@ class EntityFuzzySearchView(APIView):
"query": query,
"matches": [match.to_dict() for match in matches],
"user_authenticated": (
- request.user.is_authenticated
- if hasattr(request.user, "is_authenticated")
- else False
+ request.user.is_authenticated if hasattr(request.user, "is_authenticated") else False
),
}
@@ -211,9 +206,7 @@ class EntityNotFoundView(APIView):
"context": context,
"matches": [match.to_dict() for match in matches],
"user_authenticated": (
- request.user.is_authenticated
- if hasattr(request.user, "is_authenticated")
- else False
+ request.user.is_authenticated if hasattr(request.user, "is_authenticated") else False
),
"has_matches": len(matches) > 0,
}
@@ -267,9 +260,7 @@ class QuickEntitySuggestionView(APIView):
limit = min(int(request.GET.get("limit", 5)), 10) # Cap at 10
if not query or len(query) < 2:
- return Response(
- {"suggestions": [], "query": query}, status=status.HTTP_200_OK
- )
+ return Response({"suggestions": [], "query": query}, status=status.HTTP_200_OK)
# Parse entity types
entity_types = []
@@ -282,9 +273,7 @@ class QuickEntitySuggestionView(APIView):
entity_types = [EntityType.PARK, EntityType.RIDE, EntityType.COMPANY]
# Get fuzzy matches
- matches, _ = entity_fuzzy_matcher.find_entity(
- query=query, entity_types=entity_types, user=request.user
- )
+ matches, _ = entity_fuzzy_matcher.find_entity(query=query, entity_types=entity_types, user=request.user)
# Format as simple suggestions
suggestions = []
@@ -313,9 +302,7 @@ class QuickEntitySuggestionView(APIView):
# Utility function for other views to use
-def get_entity_suggestions(
- query: str, entity_types: list[str] | None = None, user=None
-):
+def get_entity_suggestions(query: str, entity_types: list[str] | None = None, user=None):
"""
Utility function for other Django views to get entity suggestions.
@@ -340,8 +327,6 @@ def get_entity_suggestions(
if not parsed_types:
parsed_types = [EntityType.PARK, EntityType.RIDE, EntityType.COMPANY]
- return entity_fuzzy_matcher.find_entity(
- query=query, entity_types=parsed_types, user=user
- )
+ return entity_fuzzy_matcher.find_entity(query=query, entity_types=parsed_types, user=user)
except Exception:
return [], None
diff --git a/backend/apps/core/views/map_views.py b/backend/apps/core/views/map_views.py
index 4318f98b..2ec2a16e 100644
--- a/backend/apps/core/views/map_views.py
+++ b/backend/apps/core/views/map_views.py
@@ -86,7 +86,7 @@ class MapAPIView(View):
return bounds
return None
except (ValueError, TypeError) as e:
- raise ValidationError(f"Invalid bounds parameters: {e}")
+ raise ValidationError(f"Invalid bounds parameters: {e}") from None
def _parse_pagination(self, request: HttpRequest) -> dict[str, int]:
"""Parse pagination parameters from request."""
@@ -125,11 +125,7 @@ class MapAPIView(View):
if location_types_param:
type_strings = location_types_param.split(",")
valid_types = {lt.value for lt in LocationType}
- filters.location_types = {
- LocationType(t.strip())
- for t in type_strings
- if t.strip() in valid_types
- }
+ filters.location_types = {LocationType(t.strip()) for t in type_strings if t.strip() in valid_types}
# Park status
park_status_param = request.GET.get("park_status")
@@ -199,7 +195,7 @@ class MapAPIView(View):
)
except (ValueError, TypeError) as e:
- raise ValidationError(f"Invalid filter parameters: {e}")
+ raise ValidationError(f"Invalid filter parameters: {e}") from None
def _parse_zoom_level(self, request: HttpRequest) -> int:
"""Parse zoom level from request with default."""
@@ -218,9 +214,7 @@ class MapAPIView(View):
request: HttpRequest,
) -> dict[str, Any]:
"""Create paginated response with metadata."""
- total_pages = (total_count + pagination["page_size"] - 1) // pagination[
- "page_size"
- ]
+ total_pages = (total_count + pagination["page_size"] - 1) // pagination["page_size"]
# Build pagination URLs
base_url = request.build_absolute_uri(request.path)
@@ -278,9 +272,7 @@ class MapAPIView(View):
return JsonResponse(response_data, status=status)
- def _success_response(
- self, data: Any, message: str = None, metadata: dict[str, Any] = None
- ) -> JsonResponse:
+ def _success_response(self, data: Any, message: str = None, metadata: dict[str, Any] = None) -> JsonResponse:
"""Return standardized success response."""
response_data = {
"status": "success",
@@ -397,9 +389,7 @@ class MapLocationDetailView(MapAPIView):
"""
@method_decorator(cache_page(600)) # Cache for 10 minutes
- def get(
- self, request: HttpRequest, location_type: str, location_id: int
- ) -> JsonResponse:
+ def get(self, request: HttpRequest, location_type: str, location_id: int) -> JsonResponse:
"""Get detailed information for a specific location."""
try:
# Validate location type
@@ -422,9 +412,7 @@ class MapLocationDetailView(MapAPIView):
)
# Get location details
- location = unified_map_service.get_location_details(
- location_type, location_id
- )
+ location = unified_map_service.get_location_details(location_type, location_id)
if not location:
return self._error_response(
@@ -499,9 +487,7 @@ class MapSearchView(MapAPIView):
try:
valid_types = {lt.value for lt in LocationType}
location_types = {
- LocationType(t.strip())
- for t in types_param.split(",")
- if t.strip() in valid_types
+ LocationType(t.strip()) for t in types_param.split(",") if t.strip() in valid_types
}
except ValueError:
return self._error_response(
@@ -569,9 +555,7 @@ class MapBoundsView(MapAPIView):
# Parse required bounds
bounds = self._parse_bounds(request)
if not bounds:
- return self._error_response(
- "Bounds parameters required: north, south, east, west", 400
- )
+ return self._error_response("Bounds parameters required: north, south, east, west", 400)
# Parse optional filters
location_types = None
diff --git a/backend/apps/core/views/maps.py b/backend/apps/core/views/maps.py
index bd30125a..1001881b 100644
--- a/backend/apps/core/views/maps.py
+++ b/backend/apps/core/views/maps.py
@@ -74,9 +74,7 @@ class UniversalMapView(MapViewMixin, TemplateView):
)
# Handle initial bounds from query parameters
- if all(
- param in self.request.GET for param in ["north", "south", "east", "west"]
- ):
+ if all(param in self.request.GET for param in ["north", "south", "east", "west"]):
with contextlib.suppress(ValueError, TypeError):
context["initial_bounds"] = {
"north": float(self.request.GET["north"]),
@@ -243,9 +241,7 @@ class LocationSearchView(MapViewMixin, View):
limit = min(20, max(5, int(request.GET.get("limit", "10"))))
# Perform search
- results = unified_map_service.search_locations(
- query=query, location_types=location_types, limit=limit
- )
+ results = unified_map_service.search_locations(query=query, location_types=location_types, limit=limit)
return render(
request,
@@ -285,11 +281,7 @@ class MapBoundsUpdateView(MapViewMixin, View):
zoom_level = int(data.get("zoom", 10))
location_types = None
if "types" in data:
- location_types = {
- LocationType(t)
- for t in data["types"]
- if t in [lt.value for lt in LocationType]
- }
+ location_types = {LocationType(t) for t in data["types"] if t in [lt.value for lt in LocationType]}
# Location types are used directly in the service call
@@ -324,9 +316,7 @@ class LocationDetailModalView(MapViewMixin, View):
URL: /maps/htmx/location/
//
"""
- def get(
- self, request: HttpRequest, location_type: str, location_id: int
- ) -> HttpResponse:
+ def get(self, request: HttpRequest, location_type: str, location_id: int) -> HttpResponse:
"""Return location detail modal content."""
try:
# Validate location type
@@ -338,9 +328,7 @@ class LocationDetailModalView(MapViewMixin, View):
)
# Get location details
- location = unified_map_service.get_location_details(
- location_type, location_id
- )
+ location = unified_map_service.get_location_details(location_type, location_id)
if not location:
return render(
@@ -356,9 +344,7 @@ class LocationDetailModalView(MapViewMixin, View):
)
except Exception as e:
- return render(
- request, "maps/partials/location_modal.html", {"error": str(e)}
- )
+ return render(request, "maps/partials/location_modal.html", {"error": str(e)})
class LocationListView(MapViewMixin, TemplateView):
@@ -392,9 +378,7 @@ class LocationListView(MapViewMixin, TemplateView):
)
# Get locations without clustering
- map_response = unified_map_service.get_map_data(
- filters=filters, cluster=False, use_cache=True
- )
+ map_response = unified_map_service.get_map_data(filters=filters, cluster=False, use_cache=True)
# Paginate results
paginator = Paginator(map_response.locations, self.paginate_by)
diff --git a/backend/apps/core/views/performance_dashboard.py b/backend/apps/core/views/performance_dashboard.py
index f629b29c..31c95fda 100644
--- a/backend/apps/core/views/performance_dashboard.py
+++ b/backend/apps/core/views/performance_dashboard.py
@@ -83,13 +83,15 @@ class PerformanceDashboardView(TemplateView):
try:
client = cache._cache.get_client()
info = client.info()
- cache_stats.update({
- "connected_clients": info.get("connected_clients"),
- "used_memory_human": info.get("used_memory_human"),
- "keyspace_hits": info.get("keyspace_hits", 0),
- "keyspace_misses": info.get("keyspace_misses", 0),
- "total_commands": info.get("total_commands_processed"),
- })
+ cache_stats.update(
+ {
+ "connected_clients": info.get("connected_clients"),
+ "used_memory_human": info.get("used_memory_human"),
+ "keyspace_hits": info.get("keyspace_hits", 0),
+ "keyspace_misses": info.get("keyspace_misses", 0),
+ "total_commands": info.get("total_commands_processed"),
+ }
+ )
# Calculate hit rate
hits = info.get("keyspace_hits", 0)
@@ -127,8 +129,7 @@ class PerformanceDashboardView(TemplateView):
# Get connection count (PostgreSQL specific)
try:
cursor.execute(
- "SELECT count(*) FROM pg_stat_activity WHERE datname = %s;",
- [db_settings.get("NAME")]
+ "SELECT count(*) FROM pg_stat_activity WHERE datname = %s;", [db_settings.get("NAME")]
)
stats["active_connections"] = cursor.fetchone()[0]
except Exception:
@@ -244,16 +245,18 @@ class CacheStatsAPIView(View):
client = cache._cache.get_client()
info = client.info()
- cache_info.update({
- "used_memory": info.get("used_memory_human"),
- "connected_clients": info.get("connected_clients"),
- "keyspace_hits": info.get("keyspace_hits", 0),
- "keyspace_misses": info.get("keyspace_misses", 0),
- "expired_keys": info.get("expired_keys", 0),
- "evicted_keys": info.get("evicted_keys", 0),
- "total_connections_received": info.get("total_connections_received"),
- "total_commands_processed": info.get("total_commands_processed"),
- })
+ cache_info.update(
+ {
+ "used_memory": info.get("used_memory_human"),
+ "connected_clients": info.get("connected_clients"),
+ "keyspace_hits": info.get("keyspace_hits", 0),
+ "keyspace_misses": info.get("keyspace_misses", 0),
+ "expired_keys": info.get("expired_keys", 0),
+ "evicted_keys": info.get("evicted_keys", 0),
+ "total_connections_received": info.get("total_connections_received"),
+ "total_commands_processed": info.get("total_commands_processed"),
+ }
+ )
# Calculate metrics
hits = info.get("keyspace_hits", 0)
diff --git a/backend/apps/core/views/search.py b/backend/apps/core/views/search.py
index ad59e123..6da3b57a 100644
--- a/backend/apps/core/views/search.py
+++ b/backend/apps/core/views/search.py
@@ -18,11 +18,7 @@ class AdaptiveSearchView(TemplateView):
"""
Get the base queryset, optimized with select_related and prefetch_related
"""
- return (
- Park.objects.select_related("operator", "property_owner")
- .prefetch_related("location", "photos")
- .all()
- )
+ return Park.objects.select_related("operator", "property_owner").prefetch_related("location", "photos").all()
def get_filterset(self):
"""
@@ -46,9 +42,7 @@ class AdaptiveSearchView(TemplateView):
{
"results": filterset.qs,
"filters": filterset,
- "applied_filters": bool(
- self.request.GET
- ), # Check if any filters are applied
+ "applied_filters": bool(self.request.GET), # Check if any filters are applied
"is_location_search": bool(location_search or near_location),
"location_search_query": location_search or near_location,
}
diff --git a/backend/apps/core/views/views.py b/backend/apps/core/views/views.py
index 8b204933..93db646f 100644
--- a/backend/apps/core/views/views.py
+++ b/backend/apps/core/views/views.py
@@ -54,9 +54,7 @@ class SlugRedirectMixin(View):
# Build kwargs for reverse()
reverse_kwargs = self.get_redirect_url_kwargs()
# Redirect to the current slug URL
- return redirect(
- reverse(url_pattern, kwargs=reverse_kwargs), permanent=True
- )
+ return redirect(reverse(url_pattern, kwargs=reverse_kwargs), permanent=True)
return super().dispatch(request, *args, **kwargs)
except Exception: # pylint: disable=broad-exception-caught
# Fallback to default dispatch on any error (e.g. object not found)
@@ -67,9 +65,7 @@ class SlugRedirectMixin(View):
Get the URL pattern name for redirects.
Should be overridden by subclasses.
"""
- raise NotImplementedError(
- "Subclasses must implement get_redirect_url_pattern()"
- )
+ raise NotImplementedError("Subclasses must implement get_redirect_url_pattern()")
def get_redirect_url_kwargs(self) -> dict[str, Any]:
"""
@@ -202,9 +198,7 @@ def get_transition_metadata(transition_name: str) -> dict[str, Any]:
return TRANSITION_METADATA["default"].copy()
-def add_toast_trigger(
- response: HttpResponse, message: str, toast_type: str = "success"
-) -> HttpResponse:
+def add_toast_trigger(response: HttpResponse, message: str, toast_type: str = "success") -> HttpResponse:
"""
Add HX-Trigger header to trigger Alpine.js toast.
@@ -256,16 +250,12 @@ class FSMTransitionView(View):
The model class or None if not found
"""
try:
- content_type = ContentType.objects.get(
- app_label=app_label, model=model_name
- )
+ content_type = ContentType.objects.get(app_label=app_label, model=model_name)
return content_type.model_class()
except ContentType.DoesNotExist:
return None
- def get_object(
- self, model_class: type[Model], pk: Any, slug: str | None = None
- ) -> Model:
+ def get_object(self, model_class: type[Model], pk: Any, slug: str | None = None) -> Model:
"""
Get the model instance.
@@ -297,9 +287,7 @@ class FSMTransitionView(View):
"""
return getattr(obj, transition_name, None)
- def validate_transition(
- self, obj: Model, transition_name: str, user
- ) -> tuple[bool, str | None]:
+ def validate_transition(self, obj: Model, transition_name: str, user) -> tuple[bool, str | None]:
"""
Validate that the transition can proceed.
@@ -331,9 +319,7 @@ class FSMTransitionView(View):
return True, None
- def execute_transition(
- self, obj: Model, transition_name: str, user, **kwargs
- ) -> None:
+ def execute_transition(self, obj: Model, transition_name: str, user, **kwargs) -> None:
"""
Execute the transition on the object.
@@ -355,9 +341,7 @@ class FSMTransitionView(View):
def get_success_message(self, obj: Model, transition_name: str) -> str:
"""Generate a success message for the transition."""
# Clean up transition name for display
- display_name = (
- transition_name.replace("transition_to_", "").replace("_", " ").title()
- )
+ display_name = transition_name.replace("transition_to_", "").replace("_", " ").title()
model_name = obj._meta.verbose_name.title()
return f"{model_name} has been {display_name.lower()}d successfully."
@@ -404,9 +388,7 @@ class FSMTransitionView(View):
except TemplateDoesNotExist:
return "htmx/updated_row.html"
- def format_success_response(
- self, request: HttpRequest, obj: Model, transition_name: str
- ) -> HttpResponse:
+ def format_success_response(self, request: HttpRequest, obj: Model, transition_name: str) -> HttpResponse:
"""
Format a successful transition response.
@@ -443,17 +425,11 @@ class FSMTransitionView(View):
{
"success": True,
"message": message,
- "new_state": (
- getattr(obj, obj.state_field_name, None)
- if hasattr(obj, "state_field_name")
- else None
- ),
+ "new_state": (getattr(obj, obj.state_field_name, None) if hasattr(obj, "state_field_name") else None),
}
)
- def format_error_response(
- self, request: HttpRequest, error: Exception, status_code: int = 400
- ) -> HttpResponse:
+ def format_error_response(self, request: HttpRequest, error: Exception, status_code: int = 400) -> HttpResponse:
"""
Format an error response.
@@ -489,36 +465,26 @@ class FSMTransitionView(View):
if not all([app_label, model_name, transition_name]):
return self.format_error_response(
request,
- ValueError(
- "Missing required parameters: app_label, model_name, and transition_name"
- ),
+ ValueError("Missing required parameters: app_label, model_name, and transition_name"),
400,
)
if not pk and not slug:
- return self.format_error_response(
- request, ValueError("Missing required parameter: pk or slug"), 400
- )
+ return self.format_error_response(request, ValueError("Missing required parameter: pk or slug"), 400)
# Get the model class
model_class = self.get_model_class(app_label, model_name)
if model_class is None:
- return self.format_error_response(
- request, ValueError(f"Model '{app_label}.{model_name}' not found"), 404
- )
+ return self.format_error_response(request, ValueError(f"Model '{app_label}.{model_name}' not found"), 404)
# Get the object
try:
obj = self.get_object(model_class, pk, slug)
except ObjectDoesNotExist:
- return self.format_error_response(
- request, ValueError(f"Object not found: {model_name} with pk={pk}"), 404
- )
+ return self.format_error_response(request, ValueError(f"Object not found: {model_name} with pk={pk}"), 404)
# Validate the transition
- can_execute, error_msg = self.validate_transition(
- obj, transition_name, request.user
- )
+ can_execute, error_msg = self.validate_transition(obj, transition_name, request.user)
if not can_execute:
return self.format_error_response(
request,
@@ -561,15 +527,11 @@ class FSMTransitionView(View):
return self.format_error_response(request, e, 400)
except TransitionNotAllowed as e:
- logger.warning(
- f"Transition not allowed: '{transition_name}' on {model_class.__name__}(pk={obj.pk}): {e}"
- )
+ logger.warning(f"Transition not allowed: '{transition_name}' on {model_class.__name__}(pk={obj.pk}): {e}")
return self.format_error_response(request, e, 400)
except Exception as e:
logger.exception(
f"Unexpected error during transition '{transition_name}' on {model_class.__name__}(pk={obj.pk})"
)
- return self.format_error_response(
- request, ValueError(f"An unexpected error occurred: {str(e)}"), 500
- )
+ return self.format_error_response(request, ValueError(f"An unexpected error occurred: {str(e)}"), 500)
diff --git a/backend/apps/lists/admin.py b/backend/apps/lists/admin.py
index 28964510..19d4cf3d 100644
--- a/backend/apps/lists/admin.py
+++ b/backend/apps/lists/admin.py
@@ -14,6 +14,7 @@ from .models import ListItem, UserList
class ListItemInline(admin.TabularInline):
"""Inline admin for ListItem within UserList admin."""
+
model = ListItem
extra = 1
fields = ("content_type", "object_id", "rank", "notes")
@@ -24,6 +25,7 @@ class ListItemInline(admin.TabularInline):
@admin.register(UserList)
class UserListAdmin(QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMixin, BaseModelAdmin):
"""Admin interface for UserList."""
+
list_display = (
"title",
"user_link",
@@ -65,6 +67,7 @@ class UserListAdmin(QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMi
def user_link(self, obj):
if obj.user:
from django.urls import reverse
+
url = reverse("admin:accounts_customuser_change", args=[obj.user.pk])
return format_html('{}', url, obj.user.username)
return "-"
@@ -82,6 +85,7 @@ class UserListAdmin(QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMi
@admin.register(ListItem)
class ListItemAdmin(QueryOptimizationMixin, BaseModelAdmin):
"""Admin interface for ListItem."""
+
list_display = (
"user_list",
"content_type",
diff --git a/backend/apps/lists/views.py b/backend/apps/lists/views.py
index 01a9a883..b6fd682f 100644
--- a/backend/apps/lists/views.py
+++ b/backend/apps/lists/views.py
@@ -28,4 +28,6 @@ class ListItemViewSet(viewsets.ModelViewSet):
lookup_field = "id"
def get_queryset(self):
- return ListItem.objects.filter(user_list__is_public=True) | ListItem.objects.filter(user_list__user=self.request.user)
+ return ListItem.objects.filter(user_list__is_public=True) | ListItem.objects.filter(
+ user_list__user=self.request.user
+ )
diff --git a/backend/apps/media/commands/download_photos.py b/backend/apps/media/commands/download_photos.py
index 09d9df86..a6853d2f 100644
--- a/backend/apps/media/commands/download_photos.py
+++ b/backend/apps/media/commands/download_photos.py
@@ -52,9 +52,7 @@ class Command(BaseCommand):
park.name}: {
photo.image.name}"
)
- self.stdout.write(
- f"Database record created with ID: {photo.id}"
- )
+ self.stdout.write(f"Database record created with ID: {photo.id}")
else:
self.stdout.write(
f"Error downloading image. Status code: {
@@ -112,9 +110,7 @@ class Command(BaseCommand):
)
except Exception as e:
- self.stdout.write(
- f"Error downloading ride photo: {str(e)}"
- )
+ self.stdout.write(f"Error downloading ride photo: {str(e)}")
except Ride.DoesNotExist:
self.stdout.write(
diff --git a/backend/apps/media/commands/fix_photo_paths.py b/backend/apps/media/commands/fix_photo_paths.py
index 138d0042..891fbb50 100644
--- a/backend/apps/media/commands/fix_photo_paths.py
+++ b/backend/apps/media/commands/fix_photo_paths.py
@@ -49,9 +49,7 @@ class Command(BaseCommand):
if files:
# Get the first file and update the database
# record
- file_path = os.path.join(
- content_type, identifier, files[0]
- )
+ file_path = os.path.join(content_type, identifier, files[0])
if os.path.exists(os.path.join("media", file_path)):
photo.image.name = file_path
photo.save()
@@ -111,9 +109,7 @@ class Command(BaseCommand):
if files:
# Get the first file and update the database
# record
- file_path = os.path.join(
- content_type, identifier, files[0]
- )
+ file_path = os.path.join(content_type, identifier, files[0])
if os.path.exists(os.path.join("media", file_path)):
photo.image.name = file_path
photo.save()
diff --git a/backend/apps/media/commands/move_photos.py b/backend/apps/media/commands/move_photos.py
index f99edf17..e840c75b 100644
--- a/backend/apps/media/commands/move_photos.py
+++ b/backend/apps/media/commands/move_photos.py
@@ -37,9 +37,7 @@ class Command(BaseCommand):
identifier = photo.park.slug
# Look for any files in that directory
- old_dir = os.path.join(
- settings.MEDIA_ROOT, content_type, identifier
- )
+ old_dir = os.path.join(settings.MEDIA_ROOT, content_type, identifier)
if os.path.exists(old_dir):
files = [
f
@@ -83,9 +81,7 @@ class Command(BaseCommand):
# Move the file
if current_path != new_full_path:
- shutil.copy2(
- current_path, new_full_path
- ) # Use copy2 to preserve metadata
+ shutil.copy2(current_path, new_full_path) # Use copy2 to preserve metadata
processed_files.add(current_path)
else:
processed_files.add(current_path)
@@ -116,9 +112,7 @@ class Command(BaseCommand):
identifier = parts[1] # e.g., 'alton-towers'
# Look for any files in that directory
- old_dir = os.path.join(
- settings.MEDIA_ROOT, content_type, identifier
- )
+ old_dir = os.path.join(settings.MEDIA_ROOT, content_type, identifier)
if os.path.exists(old_dir):
files = [
f
@@ -162,9 +156,7 @@ class Command(BaseCommand):
# Move the file
if current_path != new_full_path:
- shutil.copy2(
- current_path, new_full_path
- ) # Use copy2 to preserve metadata
+ shutil.copy2(current_path, new_full_path) # Use copy2 to preserve metadata
processed_files.add(current_path)
else:
processed_files.add(current_path)
@@ -192,8 +184,6 @@ class Command(BaseCommand):
os.remove(file_path)
self.stdout.write(f"Removed old file: {file_path}")
except Exception as e:
- self.stdout.write(
- f"Error removing {file_path}: {str(e)}"
- )
+ self.stdout.write(f"Error removing {file_path}: {str(e)}")
self.stdout.write("Finished moving photo files and cleaning up")
diff --git a/backend/apps/media/models.py b/backend/apps/media/models.py
index 8ebb387a..d4af84cb 100644
--- a/backend/apps/media/models.py
+++ b/backend/apps/media/models.py
@@ -23,10 +23,7 @@ class Photo(TrackedModel):
# The actual image
image = models.ForeignKey(
- CloudflareImage,
- on_delete=models.CASCADE,
- related_name="photos_usage",
- help_text="Cloudflare Image reference"
+ CloudflareImage, on_delete=models.CASCADE, related_name="photos_usage", help_text="Cloudflare Image reference"
)
# Generic relation to target object (Park, Ride, etc.)
@@ -40,10 +37,7 @@ class Photo(TrackedModel):
# Metadata
caption = models.CharField(max_length=255, blank=True, help_text="Photo caption")
- is_public = models.BooleanField(
- default=True,
- help_text="Whether this photo is visible to others"
- )
+ is_public = models.BooleanField(default=True, help_text="Whether this photo is visible to others")
# We might want credit/source info if not taken by user
source = models.CharField(max_length=100, blank=True, help_text="Source/Credit if applicable")
diff --git a/backend/apps/media/serializers.py b/backend/apps/media/serializers.py
index e0bfdf9c..c6691d69 100644
--- a/backend/apps/media/serializers.py
+++ b/backend/apps/media/serializers.py
@@ -14,6 +14,7 @@ class CloudflareImageSerializer(serializers.ModelSerializer):
model = CloudflareImage
fields = ["id", "cloudflare_id", "variants"]
+
class PhotoSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
image = CloudflareImageSerializer(read_only=True)
@@ -56,10 +57,10 @@ class PhotoSerializer(serializers.ModelSerializer):
# Return public variant or default
if obj.image:
# Check if get_url method exists or we construct strictly
- return getattr(obj.image, 'get_url', lambda x: None)('public')
+ return getattr(obj.image, "get_url", lambda x: None)("public")
return None
def get_thumbnail(self, obj):
if obj.image:
- return getattr(obj.image, 'get_url', lambda x: None)('thumbnail')
+ return getattr(obj.image, "get_url", lambda x: None)("thumbnail")
return None
diff --git a/backend/apps/moderation/admin.py b/backend/apps/moderation/admin.py
index 1ca3ba2a..d0577989 100644
--- a/backend/apps/moderation/admin.py
+++ b/backend/apps/moderation/admin.py
@@ -51,20 +51,16 @@ class ModerationAdminSite(AdminSite):
extra_context = extra_context or {}
# Get pending counts
- extra_context["pending_edits"] = EditSubmission.objects.filter(
- status="PENDING"
- ).count()
- extra_context["pending_photos"] = PhotoSubmission.objects.filter(
- status="PENDING"
- ).count()
+ extra_context["pending_edits"] = EditSubmission.objects.filter(status="PENDING").count()
+ extra_context["pending_photos"] = PhotoSubmission.objects.filter(status="PENDING").count()
# Get recent activity
- extra_context["recent_edits"] = EditSubmission.objects.select_related(
- "user", "handled_by"
- ).order_by("-created_at")[:5]
- extra_context["recent_photos"] = PhotoSubmission.objects.select_related(
- "user", "handled_by"
- ).order_by("-created_at")[:5]
+ extra_context["recent_edits"] = EditSubmission.objects.select_related("user", "handled_by").order_by(
+ "-created_at"
+ )[:5]
+ extra_context["recent_photos"] = PhotoSubmission.objects.select_related("user", "handled_by").order_by(
+ "-created_at"
+ )[:5]
return super().index(request, extra_context)
@@ -639,9 +635,7 @@ class StateLogAdmin(admin.ModelAdmin):
output = StringIO()
writer = csv.writer(output)
- writer.writerow(
- ["ID", "Timestamp", "Model", "Object ID", "State", "Transition", "User"]
- )
+ writer.writerow(["ID", "Timestamp", "Model", "Object ID", "State", "Transition", "User"])
for log in queryset:
writer.writerow(
diff --git a/backend/apps/moderation/apps.py b/backend/apps/moderation/apps.py
index b4317f14..ad4511b3 100644
--- a/backend/apps/moderation/apps.py
+++ b/backend/apps/moderation/apps.py
@@ -82,82 +82,31 @@ class ModerationConfig(AppConfig):
)
# EditSubmission callbacks (transitions from CLAIMED state)
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'APPROVED',
- SubmissionApprovedNotification()
- )
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'APPROVED',
- ModerationCacheInvalidation()
- )
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'REJECTED',
- SubmissionRejectedNotification()
- )
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'REJECTED',
- ModerationCacheInvalidation()
- )
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'ESCALATED',
- SubmissionEscalatedNotification()
- )
- register_callback(
- EditSubmission, 'status', 'CLAIMED', 'ESCALATED',
- ModerationCacheInvalidation()
- )
+ register_callback(EditSubmission, "status", "CLAIMED", "APPROVED", SubmissionApprovedNotification())
+ register_callback(EditSubmission, "status", "CLAIMED", "APPROVED", ModerationCacheInvalidation())
+ register_callback(EditSubmission, "status", "CLAIMED", "REJECTED", SubmissionRejectedNotification())
+ register_callback(EditSubmission, "status", "CLAIMED", "REJECTED", ModerationCacheInvalidation())
+ register_callback(EditSubmission, "status", "CLAIMED", "ESCALATED", SubmissionEscalatedNotification())
+ register_callback(EditSubmission, "status", "CLAIMED", "ESCALATED", ModerationCacheInvalidation())
# PhotoSubmission callbacks (transitions from CLAIMED state)
- register_callback(
- PhotoSubmission, 'status', 'CLAIMED', 'APPROVED',
- SubmissionApprovedNotification()
- )
- register_callback(
- PhotoSubmission, 'status', 'CLAIMED', 'APPROVED',
- ModerationCacheInvalidation()
- )
- register_callback(
- PhotoSubmission, 'status', 'CLAIMED', 'REJECTED',
- SubmissionRejectedNotification()
- )
- register_callback(
- PhotoSubmission, 'status', 'CLAIMED', 'REJECTED',
- ModerationCacheInvalidation()
- )
- register_callback(
- PhotoSubmission, 'status', 'CLAIMED', 'ESCALATED',
- SubmissionEscalatedNotification()
- )
+ register_callback(PhotoSubmission, "status", "CLAIMED", "APPROVED", SubmissionApprovedNotification())
+ register_callback(PhotoSubmission, "status", "CLAIMED", "APPROVED", ModerationCacheInvalidation())
+ register_callback(PhotoSubmission, "status", "CLAIMED", "REJECTED", SubmissionRejectedNotification())
+ register_callback(PhotoSubmission, "status", "CLAIMED", "REJECTED", ModerationCacheInvalidation())
+ register_callback(PhotoSubmission, "status", "CLAIMED", "ESCALATED", SubmissionEscalatedNotification())
# ModerationReport callbacks
- register_callback(
- ModerationReport, 'status', '*', '*',
- ModerationNotificationCallback()
- )
- register_callback(
- ModerationReport, 'status', '*', '*',
- ModerationCacheInvalidation()
- )
+ register_callback(ModerationReport, "status", "*", "*", ModerationNotificationCallback())
+ register_callback(ModerationReport, "status", "*", "*", ModerationCacheInvalidation())
# ModerationQueue callbacks
- register_callback(
- ModerationQueue, 'status', '*', '*',
- ModerationNotificationCallback()
- )
- register_callback(
- ModerationQueue, 'status', '*', '*',
- ModerationCacheInvalidation()
- )
+ register_callback(ModerationQueue, "status", "*", "*", ModerationNotificationCallback())
+ register_callback(ModerationQueue, "status", "*", "*", ModerationCacheInvalidation())
# BulkOperation callbacks
- register_callback(
- BulkOperation, 'status', '*', '*',
- ModerationNotificationCallback()
- )
- register_callback(
- BulkOperation, 'status', '*', '*',
- ModerationCacheInvalidation()
- )
+ register_callback(BulkOperation, "status", "*", "*", ModerationNotificationCallback())
+ register_callback(BulkOperation, "status", "*", "*", ModerationCacheInvalidation())
logger.debug("Registered moderation transition callbacks")
diff --git a/backend/apps/moderation/choices.py b/backend/apps/moderation/choices.py
index 289215b6..bc09a8cc 100644
--- a/backend/apps/moderation/choices.py
+++ b/backend/apps/moderation/choices.py
@@ -18,80 +18,80 @@ EDIT_SUBMISSION_STATUSES = [
label="Pending",
description="Submission awaiting moderator review",
metadata={
- 'color': 'yellow',
- 'icon': 'clock',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'can_transition_to': ['CLAIMED'], # Must be claimed before any action
- 'requires_moderator': True,
- 'is_actionable': True
+ "color": "yellow",
+ "icon": "clock",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "can_transition_to": ["CLAIMED"], # Must be claimed before any action
+ "requires_moderator": True,
+ "is_actionable": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLAIMED",
label="Claimed",
description="Submission has been claimed by a moderator for review",
metadata={
- 'color': 'blue',
- 'icon': 'user-check',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 2,
+ "color": "blue",
+ "icon": "user-check",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 2,
# Note: PENDING not included to avoid cycle - unclaim uses direct status update
- 'can_transition_to': ['APPROVED', 'REJECTED', 'ESCALATED'],
- 'requires_moderator': True,
- 'is_actionable': True,
- 'is_locked': True # Indicates this submission is locked for editing by others
+ "can_transition_to": ["APPROVED", "REJECTED", "ESCALATED"],
+ "requires_moderator": True,
+ "is_actionable": True,
+ "is_locked": True, # Indicates this submission is locked for editing by others
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="APPROVED",
label="Approved",
description="Submission has been approved and changes applied",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 3,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_actionable': False,
- 'is_final': True
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 3,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="REJECTED",
label="Rejected",
description="Submission has been rejected and will not be applied",
metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 4,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_actionable': False,
- 'is_final': True
+ "color": "red",
+ "icon": "x-circle",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 4,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="ESCALATED",
label="Escalated",
description="Submission has been escalated for higher-level review",
metadata={
- 'color': 'purple',
- 'icon': 'arrow-up',
- 'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
- 'sort_order': 5,
- 'can_transition_to': ['APPROVED', 'REJECTED'],
- 'requires_moderator': True,
- 'is_actionable': True,
- 'escalation_level': 'admin'
+ "color": "purple",
+ "icon": "arrow-up",
+ "css_class": "bg-purple-100 text-purple-800 border-purple-200",
+ "sort_order": 5,
+ "can_transition_to": ["APPROVED", "REJECTED"],
+ "requires_moderator": True,
+ "is_actionable": True,
+ "escalation_level": "admin",
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -101,28 +101,28 @@ SUBMISSION_TYPES = [
label="Edit Existing",
description="Modification to existing content",
metadata={
- 'color': 'blue',
- 'icon': 'pencil',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 1,
- 'requires_existing_object': True,
- 'complexity_level': 'medium'
+ "color": "blue",
+ "icon": "pencil",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 1,
+ "requires_existing_object": True,
+ "complexity_level": "medium",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CREATE",
label="Create New",
description="Creation of new content",
metadata={
- 'color': 'green',
- 'icon': 'plus-circle',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 2,
- 'requires_existing_object': False,
- 'complexity_level': 'high'
+ "color": "green",
+ "icon": "plus-circle",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 2,
+ "requires_existing_object": False,
+ "complexity_level": "high",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -136,62 +136,62 @@ MODERATION_REPORT_STATUSES = [
label="Pending Review",
description="Report awaiting initial moderator review",
metadata={
- 'color': 'yellow',
- 'icon': 'clock',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'can_transition_to': ['UNDER_REVIEW', 'DISMISSED'],
- 'requires_assignment': False,
- 'is_actionable': True
+ "color": "yellow",
+ "icon": "clock",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "can_transition_to": ["UNDER_REVIEW", "DISMISSED"],
+ "requires_assignment": False,
+ "is_actionable": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="UNDER_REVIEW",
label="Under Review",
description="Report is actively being investigated by a moderator",
metadata={
- 'color': 'blue',
- 'icon': 'eye',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 2,
- 'can_transition_to': ['RESOLVED', 'DISMISSED'],
- 'requires_assignment': True,
- 'is_actionable': True
+ "color": "blue",
+ "icon": "eye",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 2,
+ "can_transition_to": ["RESOLVED", "DISMISSED"],
+ "requires_assignment": True,
+ "is_actionable": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="RESOLVED",
label="Resolved",
description="Report has been resolved with appropriate action taken",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 3,
- 'can_transition_to': [],
- 'requires_assignment': True,
- 'is_actionable': False,
- 'is_final': True
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 3,
+ "can_transition_to": [],
+ "requires_assignment": True,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="DISMISSED",
label="Dismissed",
description="Report was reviewed but no action was necessary",
metadata={
- 'color': 'gray',
- 'icon': 'x-circle',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 4,
- 'can_transition_to': [],
- 'requires_assignment': True,
- 'is_actionable': False,
- 'is_final': True
+ "color": "gray",
+ "icon": "x-circle",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 4,
+ "can_transition_to": [],
+ "requires_assignment": True,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -201,61 +201,61 @@ PRIORITY_LEVELS = [
label="Low",
description="Low priority - can be handled in regular workflow",
metadata={
- 'color': 'green',
- 'icon': 'arrow-down',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 1,
- 'sla_hours': 168, # 7 days
- 'escalation_threshold': 240, # 10 days
- 'urgency_level': 1
+ "color": "green",
+ "icon": "arrow-down",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 1,
+ "sla_hours": 168, # 7 days
+ "escalation_threshold": 240, # 10 days
+ "urgency_level": 1,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="MEDIUM",
label="Medium",
description="Medium priority - standard response time expected",
metadata={
- 'color': 'yellow',
- 'icon': 'minus',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 2,
- 'sla_hours': 72, # 3 days
- 'escalation_threshold': 120, # 5 days
- 'urgency_level': 2
+ "color": "yellow",
+ "icon": "minus",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 2,
+ "sla_hours": 72, # 3 days
+ "escalation_threshold": 120, # 5 days
+ "urgency_level": 2,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="HIGH",
label="High",
description="High priority - requires prompt attention",
metadata={
- 'color': 'orange',
- 'icon': 'arrow-up',
- 'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
- 'sort_order': 3,
- 'sla_hours': 24, # 1 day
- 'escalation_threshold': 48, # 2 days
- 'urgency_level': 3
+ "color": "orange",
+ "icon": "arrow-up",
+ "css_class": "bg-orange-100 text-orange-800 border-orange-200",
+ "sort_order": 3,
+ "sla_hours": 24, # 1 day
+ "escalation_threshold": 48, # 2 days
+ "urgency_level": 3,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="URGENT",
label="Urgent",
description="Urgent priority - immediate attention required",
metadata={
- 'color': 'red',
- 'icon': 'exclamation',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 4,
- 'sla_hours': 4, # 4 hours
- 'escalation_threshold': 8, # 8 hours
- 'urgency_level': 4,
- 'requires_immediate_notification': True
+ "color": "red",
+ "icon": "exclamation",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 4,
+ "sla_hours": 4, # 4 hours
+ "escalation_threshold": 8, # 8 hours
+ "urgency_level": 4,
+ "requires_immediate_notification": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -265,145 +265,145 @@ REPORT_TYPES = [
label="Spam",
description="Unwanted or repetitive content",
metadata={
- 'color': 'yellow',
- 'icon': 'ban',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'default_priority': 'MEDIUM',
- 'auto_actions': ['content_review'],
- 'severity_level': 2
+ "color": "yellow",
+ "icon": "ban",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "default_priority": "MEDIUM",
+ "auto_actions": ["content_review"],
+ "severity_level": 2,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="HARASSMENT",
label="Harassment",
description="Targeted harassment or bullying behavior",
metadata={
- 'color': 'red',
- 'icon': 'shield-exclamation',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 2,
- 'default_priority': 'HIGH',
- 'auto_actions': ['user_review', 'content_review'],
- 'severity_level': 4,
- 'requires_user_action': True
+ "color": "red",
+ "icon": "shield-exclamation",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 2,
+ "default_priority": "HIGH",
+ "auto_actions": ["user_review", "content_review"],
+ "severity_level": 4,
+ "requires_user_action": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="INAPPROPRIATE_CONTENT",
label="Inappropriate Content",
description="Content that violates community guidelines",
metadata={
- 'color': 'orange',
- 'icon': 'exclamation-triangle',
- 'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
- 'sort_order': 3,
- 'default_priority': 'HIGH',
- 'auto_actions': ['content_review'],
- 'severity_level': 3
+ "color": "orange",
+ "icon": "exclamation-triangle",
+ "css_class": "bg-orange-100 text-orange-800 border-orange-200",
+ "sort_order": 3,
+ "default_priority": "HIGH",
+ "auto_actions": ["content_review"],
+ "severity_level": 3,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="MISINFORMATION",
label="Misinformation",
description="False or misleading information",
metadata={
- 'color': 'purple',
- 'icon': 'information-circle',
- 'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
- 'sort_order': 4,
- 'default_priority': 'HIGH',
- 'auto_actions': ['content_review', 'fact_check'],
- 'severity_level': 3,
- 'requires_expert_review': True
+ "color": "purple",
+ "icon": "information-circle",
+ "css_class": "bg-purple-100 text-purple-800 border-purple-200",
+ "sort_order": 4,
+ "default_priority": "HIGH",
+ "auto_actions": ["content_review", "fact_check"],
+ "severity_level": 3,
+ "requires_expert_review": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="COPYRIGHT",
label="Copyright Violation",
description="Unauthorized use of copyrighted material",
metadata={
- 'color': 'indigo',
- 'icon': 'document-duplicate',
- 'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
- 'sort_order': 5,
- 'default_priority': 'HIGH',
- 'auto_actions': ['content_review', 'legal_review'],
- 'severity_level': 4,
- 'requires_legal_review': True
+ "color": "indigo",
+ "icon": "document-duplicate",
+ "css_class": "bg-indigo-100 text-indigo-800 border-indigo-200",
+ "sort_order": 5,
+ "default_priority": "HIGH",
+ "auto_actions": ["content_review", "legal_review"],
+ "severity_level": 4,
+ "requires_legal_review": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="PRIVACY",
label="Privacy Violation",
description="Unauthorized sharing of private information",
metadata={
- 'color': 'pink',
- 'icon': 'lock-closed',
- 'css_class': 'bg-pink-100 text-pink-800 border-pink-200',
- 'sort_order': 6,
- 'default_priority': 'URGENT',
- 'auto_actions': ['content_removal', 'user_review'],
- 'severity_level': 5,
- 'requires_immediate_action': True
+ "color": "pink",
+ "icon": "lock-closed",
+ "css_class": "bg-pink-100 text-pink-800 border-pink-200",
+ "sort_order": 6,
+ "default_priority": "URGENT",
+ "auto_actions": ["content_removal", "user_review"],
+ "severity_level": 5,
+ "requires_immediate_action": True,
},
- category=ChoiceCategory.SECURITY
+ category=ChoiceCategory.SECURITY,
),
RichChoice(
value="HATE_SPEECH",
label="Hate Speech",
description="Content promoting hatred or discrimination",
metadata={
- 'color': 'red',
- 'icon': 'fire',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 7,
- 'default_priority': 'URGENT',
- 'auto_actions': ['content_removal', 'user_suspension'],
- 'severity_level': 5,
- 'requires_immediate_action': True,
- 'zero_tolerance': True
+ "color": "red",
+ "icon": "fire",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 7,
+ "default_priority": "URGENT",
+ "auto_actions": ["content_removal", "user_suspension"],
+ "severity_level": 5,
+ "requires_immediate_action": True,
+ "zero_tolerance": True,
},
- category=ChoiceCategory.SECURITY
+ category=ChoiceCategory.SECURITY,
),
RichChoice(
value="VIOLENCE",
label="Violence or Threats",
description="Content containing violence or threatening behavior",
metadata={
- 'color': 'red',
- 'icon': 'exclamation',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 8,
- 'default_priority': 'URGENT',
- 'auto_actions': ['content_removal', 'user_ban', 'law_enforcement_notification'],
- 'severity_level': 5,
- 'requires_immediate_action': True,
- 'zero_tolerance': True,
- 'requires_law_enforcement': True
+ "color": "red",
+ "icon": "exclamation",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 8,
+ "default_priority": "URGENT",
+ "auto_actions": ["content_removal", "user_ban", "law_enforcement_notification"],
+ "severity_level": 5,
+ "requires_immediate_action": True,
+ "zero_tolerance": True,
+ "requires_law_enforcement": True,
},
- category=ChoiceCategory.SECURITY
+ category=ChoiceCategory.SECURITY,
),
RichChoice(
value="OTHER",
label="Other",
description="Other issues not covered by specific categories",
metadata={
- 'color': 'gray',
- 'icon': 'dots-horizontal',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 9,
- 'default_priority': 'MEDIUM',
- 'auto_actions': ['manual_review'],
- 'severity_level': 1,
- 'requires_manual_categorization': True
+ "color": "gray",
+ "icon": "dots-horizontal",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 9,
+ "default_priority": "MEDIUM",
+ "auto_actions": ["manual_review"],
+ "severity_level": 1,
+ "requires_manual_categorization": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -417,62 +417,62 @@ MODERATION_QUEUE_STATUSES = [
label="Pending",
description="Queue item awaiting assignment or action",
metadata={
- 'color': 'yellow',
- 'icon': 'clock',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'can_transition_to': ['IN_PROGRESS', 'CANCELLED'],
- 'requires_assignment': False,
- 'is_actionable': True
+ "color": "yellow",
+ "icon": "clock",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "can_transition_to": ["IN_PROGRESS", "CANCELLED"],
+ "requires_assignment": False,
+ "is_actionable": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="IN_PROGRESS",
label="In Progress",
description="Queue item is actively being worked on",
metadata={
- 'color': 'blue',
- 'icon': 'play',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 2,
- 'can_transition_to': ['COMPLETED', 'CANCELLED'],
- 'requires_assignment': True,
- 'is_actionable': True
+ "color": "blue",
+ "icon": "play",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 2,
+ "can_transition_to": ["COMPLETED", "CANCELLED"],
+ "requires_assignment": True,
+ "is_actionable": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="COMPLETED",
label="Completed",
description="Queue item has been successfully completed",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 3,
- 'can_transition_to': [],
- 'requires_assignment': True,
- 'is_actionable': False,
- 'is_final': True
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 3,
+ "can_transition_to": [],
+ "requires_assignment": True,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CANCELLED",
label="Cancelled",
description="Queue item was cancelled and will not be completed",
metadata={
- 'color': 'gray',
- 'icon': 'x-circle',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 4,
- 'can_transition_to': [],
- 'requires_assignment': False,
- 'is_actionable': False,
- 'is_final': True
+ "color": "gray",
+ "icon": "x-circle",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 4,
+ "can_transition_to": [],
+ "requires_assignment": False,
+ "is_actionable": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -482,90 +482,90 @@ QUEUE_ITEM_TYPES = [
label="Content Review",
description="Review of user-submitted content for policy compliance",
metadata={
- 'color': 'blue',
- 'icon': 'document-text',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 1,
- 'estimated_time_minutes': 15,
- 'required_permissions': ['content_moderation'],
- 'complexity_level': 'medium'
+ "color": "blue",
+ "icon": "document-text",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 1,
+ "estimated_time_minutes": 15,
+ "required_permissions": ["content_moderation"],
+ "complexity_level": "medium",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="USER_REVIEW",
label="User Review",
description="Review of user account or behavior",
metadata={
- 'color': 'purple',
- 'icon': 'user',
- 'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
- 'sort_order': 2,
- 'estimated_time_minutes': 30,
- 'required_permissions': ['user_moderation'],
- 'complexity_level': 'high'
+ "color": "purple",
+ "icon": "user",
+ "css_class": "bg-purple-100 text-purple-800 border-purple-200",
+ "sort_order": 2,
+ "estimated_time_minutes": 30,
+ "required_permissions": ["user_moderation"],
+ "complexity_level": "high",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="BULK_ACTION",
label="Bulk Action",
description="Large-scale administrative operation",
metadata={
- 'color': 'indigo',
- 'icon': 'collection',
- 'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
- 'sort_order': 3,
- 'estimated_time_minutes': 60,
- 'required_permissions': ['bulk_operations'],
- 'complexity_level': 'high'
+ "color": "indigo",
+ "icon": "collection",
+ "css_class": "bg-indigo-100 text-indigo-800 border-indigo-200",
+ "sort_order": 3,
+ "estimated_time_minutes": 60,
+ "required_permissions": ["bulk_operations"],
+ "complexity_level": "high",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="POLICY_VIOLATION",
label="Policy Violation",
description="Investigation of potential policy violations",
metadata={
- 'color': 'red',
- 'icon': 'shield-exclamation',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 4,
- 'estimated_time_minutes': 45,
- 'required_permissions': ['policy_enforcement'],
- 'complexity_level': 'high'
+ "color": "red",
+ "icon": "shield-exclamation",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 4,
+ "estimated_time_minutes": 45,
+ "required_permissions": ["policy_enforcement"],
+ "complexity_level": "high",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="APPEAL",
label="Appeal",
description="Review of user appeal against moderation action",
metadata={
- 'color': 'orange',
- 'icon': 'scale',
- 'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
- 'sort_order': 5,
- 'estimated_time_minutes': 30,
- 'required_permissions': ['appeal_review'],
- 'complexity_level': 'high'
+ "color": "orange",
+ "icon": "scale",
+ "css_class": "bg-orange-100 text-orange-800 border-orange-200",
+ "sort_order": 5,
+ "estimated_time_minutes": 30,
+ "required_permissions": ["appeal_review"],
+ "complexity_level": "high",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="OTHER",
label="Other",
description="Other moderation tasks not covered by specific types",
metadata={
- 'color': 'gray',
- 'icon': 'dots-horizontal',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 6,
- 'estimated_time_minutes': 20,
- 'required_permissions': ['general_moderation'],
- 'complexity_level': 'medium'
+ "color": "gray",
+ "icon": "dots-horizontal",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 6,
+ "estimated_time_minutes": 20,
+ "required_permissions": ["general_moderation"],
+ "complexity_level": "medium",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -579,133 +579,133 @@ MODERATION_ACTION_TYPES = [
label="Warning",
description="Formal warning issued to user",
metadata={
- 'color': 'yellow',
- 'icon': 'exclamation-triangle',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'severity_level': 1,
- 'is_temporary': False,
- 'affects_privileges': False,
- 'escalation_path': ['USER_SUSPENSION']
+ "color": "yellow",
+ "icon": "exclamation-triangle",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "severity_level": 1,
+ "is_temporary": False,
+ "affects_privileges": False,
+ "escalation_path": ["USER_SUSPENSION"],
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="USER_SUSPENSION",
label="User Suspension",
description="Temporary suspension of user account",
metadata={
- 'color': 'orange',
- 'icon': 'pause',
- 'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
- 'sort_order': 2,
- 'severity_level': 3,
- 'is_temporary': True,
- 'affects_privileges': True,
- 'requires_duration': True,
- 'escalation_path': ['USER_BAN']
+ "color": "orange",
+ "icon": "pause",
+ "css_class": "bg-orange-100 text-orange-800 border-orange-200",
+ "sort_order": 2,
+ "severity_level": 3,
+ "is_temporary": True,
+ "affects_privileges": True,
+ "requires_duration": True,
+ "escalation_path": ["USER_BAN"],
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="USER_BAN",
label="User Ban",
description="Permanent ban of user account",
metadata={
- 'color': 'red',
- 'icon': 'ban',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 3,
- 'severity_level': 5,
- 'is_temporary': False,
- 'affects_privileges': True,
- 'is_permanent': True,
- 'requires_admin_approval': True
+ "color": "red",
+ "icon": "ban",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 3,
+ "severity_level": 5,
+ "is_temporary": False,
+ "affects_privileges": True,
+ "is_permanent": True,
+ "requires_admin_approval": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CONTENT_REMOVAL",
label="Content Removal",
description="Removal of specific content",
metadata={
- 'color': 'red',
- 'icon': 'trash',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 4,
- 'severity_level': 2,
- 'is_temporary': False,
- 'affects_privileges': False,
- 'is_content_action': True
+ "color": "red",
+ "icon": "trash",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 4,
+ "severity_level": 2,
+ "is_temporary": False,
+ "affects_privileges": False,
+ "is_content_action": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CONTENT_EDIT",
label="Content Edit",
description="Modification of content to comply with policies",
metadata={
- 'color': 'blue',
- 'icon': 'pencil',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 5,
- 'severity_level': 1,
- 'is_temporary': False,
- 'affects_privileges': False,
- 'is_content_action': True,
- 'preserves_content': True
+ "color": "blue",
+ "icon": "pencil",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 5,
+ "severity_level": 1,
+ "is_temporary": False,
+ "affects_privileges": False,
+ "is_content_action": True,
+ "preserves_content": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CONTENT_RESTRICTION",
label="Content Restriction",
description="Restriction of content visibility or access",
metadata={
- 'color': 'purple',
- 'icon': 'eye-off',
- 'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
- 'sort_order': 6,
- 'severity_level': 2,
- 'is_temporary': True,
- 'affects_privileges': False,
- 'is_content_action': True,
- 'requires_duration': True
+ "color": "purple",
+ "icon": "eye-off",
+ "css_class": "bg-purple-100 text-purple-800 border-purple-200",
+ "sort_order": 6,
+ "severity_level": 2,
+ "is_temporary": True,
+ "affects_privileges": False,
+ "is_content_action": True,
+ "requires_duration": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="ACCOUNT_RESTRICTION",
label="Account Restriction",
description="Restriction of specific account privileges",
metadata={
- 'color': 'indigo',
- 'icon': 'lock-closed',
- 'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
- 'sort_order': 7,
- 'severity_level': 3,
- 'is_temporary': True,
- 'affects_privileges': True,
- 'requires_duration': True,
- 'escalation_path': ['USER_SUSPENSION']
+ "color": "indigo",
+ "icon": "lock-closed",
+ "css_class": "bg-indigo-100 text-indigo-800 border-indigo-200",
+ "sort_order": 7,
+ "severity_level": 3,
+ "is_temporary": True,
+ "affects_privileges": True,
+ "requires_duration": True,
+ "escalation_path": ["USER_SUSPENSION"],
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="OTHER",
label="Other",
description="Other moderation actions not covered by specific types",
metadata={
- 'color': 'gray',
- 'icon': 'dots-horizontal',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 8,
- 'severity_level': 1,
- 'is_temporary': False,
- 'affects_privileges': False,
- 'requires_manual_review': True
+ "color": "gray",
+ "icon": "dots-horizontal",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 8,
+ "severity_level": 1,
+ "is_temporary": False,
+ "affects_privileges": False,
+ "requires_manual_review": True,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -719,80 +719,80 @@ BULK_OPERATION_STATUSES = [
label="Pending",
description="Operation is queued and waiting to start",
metadata={
- 'color': 'yellow',
- 'icon': 'clock',
- 'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
- 'sort_order': 1,
- 'can_transition_to': ['RUNNING', 'CANCELLED'],
- 'is_actionable': True,
- 'can_cancel': True
+ "color": "yellow",
+ "icon": "clock",
+ "css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
+ "sort_order": 1,
+ "can_transition_to": ["RUNNING", "CANCELLED"],
+ "is_actionable": True,
+ "can_cancel": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="RUNNING",
label="Running",
description="Operation is currently executing",
metadata={
- 'color': 'blue',
- 'icon': 'play',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 2,
- 'can_transition_to': ['COMPLETED', 'FAILED', 'CANCELLED'],
- 'is_actionable': True,
- 'can_cancel': True,
- 'shows_progress': True
+ "color": "blue",
+ "icon": "play",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 2,
+ "can_transition_to": ["COMPLETED", "FAILED", "CANCELLED"],
+ "is_actionable": True,
+ "can_cancel": True,
+ "shows_progress": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="COMPLETED",
label="Completed",
description="Operation completed successfully",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 3,
- 'can_transition_to': [],
- 'is_actionable': False,
- 'can_cancel': False,
- 'is_final': True
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 3,
+ "can_transition_to": [],
+ "is_actionable": False,
+ "can_cancel": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="FAILED",
label="Failed",
description="Operation failed with errors",
metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 4,
- 'can_transition_to': [],
- 'is_actionable': False,
- 'can_cancel': False,
- 'is_final': True,
- 'requires_investigation': True
+ "color": "red",
+ "icon": "x-circle",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 4,
+ "can_transition_to": [],
+ "is_actionable": False,
+ "can_cancel": False,
+ "is_final": True,
+ "requires_investigation": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CANCELLED",
label="Cancelled",
description="Operation was cancelled before completion",
metadata={
- 'color': 'gray',
- 'icon': 'stop',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 5,
- 'can_transition_to': [],
- 'is_actionable': False,
- 'can_cancel': False,
- 'is_final': True
+ "color": "gray",
+ "icon": "stop",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 5,
+ "can_transition_to": [],
+ "is_actionable": False,
+ "can_cancel": False,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -802,128 +802,128 @@ BULK_OPERATION_TYPES = [
label="Update Parks",
description="Bulk update operations on park data",
metadata={
- 'color': 'green',
- 'icon': 'map',
- 'css_class': 'bg-green-100 text-green-800 border-green-200',
- 'sort_order': 1,
- 'estimated_duration_minutes': 30,
- 'required_permissions': ['bulk_park_operations'],
- 'affects_data': ['parks'],
- 'risk_level': 'medium'
+ "color": "green",
+ "icon": "map",
+ "css_class": "bg-green-100 text-green-800 border-green-200",
+ "sort_order": 1,
+ "estimated_duration_minutes": 30,
+ "required_permissions": ["bulk_park_operations"],
+ "affects_data": ["parks"],
+ "risk_level": "medium",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="UPDATE_RIDES",
label="Update Rides",
description="Bulk update operations on ride data",
metadata={
- 'color': 'blue',
- 'icon': 'cog',
- 'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
- 'sort_order': 2,
- 'estimated_duration_minutes': 45,
- 'required_permissions': ['bulk_ride_operations'],
- 'affects_data': ['rides'],
- 'risk_level': 'medium'
+ "color": "blue",
+ "icon": "cog",
+ "css_class": "bg-blue-100 text-blue-800 border-blue-200",
+ "sort_order": 2,
+ "estimated_duration_minutes": 45,
+ "required_permissions": ["bulk_ride_operations"],
+ "affects_data": ["rides"],
+ "risk_level": "medium",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="IMPORT_DATA",
label="Import Data",
description="Import data from external sources",
metadata={
- 'color': 'purple',
- 'icon': 'download',
- 'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
- 'sort_order': 3,
- 'estimated_duration_minutes': 60,
- 'required_permissions': ['data_import'],
- 'affects_data': ['parks', 'rides', 'users'],
- 'risk_level': 'high'
+ "color": "purple",
+ "icon": "download",
+ "css_class": "bg-purple-100 text-purple-800 border-purple-200",
+ "sort_order": 3,
+ "estimated_duration_minutes": 60,
+ "required_permissions": ["data_import"],
+ "affects_data": ["parks", "rides", "users"],
+ "risk_level": "high",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="EXPORT_DATA",
label="Export Data",
description="Export data for backup or analysis",
metadata={
- 'color': 'indigo',
- 'icon': 'upload',
- 'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
- 'sort_order': 4,
- 'estimated_duration_minutes': 20,
- 'required_permissions': ['data_export'],
- 'affects_data': [],
- 'risk_level': 'low'
+ "color": "indigo",
+ "icon": "upload",
+ "css_class": "bg-indigo-100 text-indigo-800 border-indigo-200",
+ "sort_order": 4,
+ "estimated_duration_minutes": 20,
+ "required_permissions": ["data_export"],
+ "affects_data": [],
+ "risk_level": "low",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="MODERATE_CONTENT",
label="Moderate Content",
description="Bulk moderation actions on content",
metadata={
- 'color': 'orange',
- 'icon': 'shield-check',
- 'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
- 'sort_order': 5,
- 'estimated_duration_minutes': 40,
- 'required_permissions': ['bulk_moderation'],
- 'affects_data': ['content', 'users'],
- 'risk_level': 'high'
+ "color": "orange",
+ "icon": "shield-check",
+ "css_class": "bg-orange-100 text-orange-800 border-orange-200",
+ "sort_order": 5,
+ "estimated_duration_minutes": 40,
+ "required_permissions": ["bulk_moderation"],
+ "affects_data": ["content", "users"],
+ "risk_level": "high",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="USER_ACTIONS",
label="User Actions",
description="Bulk actions on user accounts",
metadata={
- 'color': 'red',
- 'icon': 'users',
- 'css_class': 'bg-red-100 text-red-800 border-red-200',
- 'sort_order': 6,
- 'estimated_duration_minutes': 50,
- 'required_permissions': ['bulk_user_operations'],
- 'affects_data': ['users'],
- 'risk_level': 'high'
+ "color": "red",
+ "icon": "users",
+ "css_class": "bg-red-100 text-red-800 border-red-200",
+ "sort_order": 6,
+ "estimated_duration_minutes": 50,
+ "required_permissions": ["bulk_user_operations"],
+ "affects_data": ["users"],
+ "risk_level": "high",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="CLEANUP",
label="Cleanup",
description="System cleanup and maintenance operations",
metadata={
- 'color': 'gray',
- 'icon': 'trash',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 7,
- 'estimated_duration_minutes': 25,
- 'required_permissions': ['system_maintenance'],
- 'affects_data': ['system'],
- 'risk_level': 'low'
+ "color": "gray",
+ "icon": "trash",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 7,
+ "estimated_duration_minutes": 25,
+ "required_permissions": ["system_maintenance"],
+ "affects_data": ["system"],
+ "risk_level": "low",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="OTHER",
label="Other",
description="Other bulk operations not covered by specific types",
metadata={
- 'color': 'gray',
- 'icon': 'dots-horizontal',
- 'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
- 'sort_order': 8,
- 'estimated_duration_minutes': 30,
- 'required_permissions': ['general_operations'],
- 'affects_data': [],
- 'risk_level': 'medium'
+ "color": "gray",
+ "icon": "dots-horizontal",
+ "css_class": "bg-gray-100 text-gray-800 border-gray-200",
+ "sort_order": 8,
+ "estimated_duration_minutes": 30,
+ "required_permissions": ["general_operations"],
+ "affects_data": [],
+ "risk_level": "medium",
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
]
@@ -941,12 +941,20 @@ PHOTO_SUBMISSION_STATUSES = EDIT_SUBMISSION_STATUSES
# Register all choice groups with the global registry
register_choices("edit_submission_statuses", EDIT_SUBMISSION_STATUSES, "moderation", "Edit submission status options")
register_choices("submission_types", SUBMISSION_TYPES, "moderation", "Submission type classifications")
-register_choices("moderation_report_statuses", MODERATION_REPORT_STATUSES, "moderation", "Moderation report status options")
+register_choices(
+ "moderation_report_statuses", MODERATION_REPORT_STATUSES, "moderation", "Moderation report status options"
+)
register_choices("priority_levels", PRIORITY_LEVELS, "moderation", "Priority level classifications")
register_choices("report_types", REPORT_TYPES, "moderation", "Report type classifications")
-register_choices("moderation_queue_statuses", MODERATION_QUEUE_STATUSES, "moderation", "Moderation queue status options")
+register_choices(
+ "moderation_queue_statuses", MODERATION_QUEUE_STATUSES, "moderation", "Moderation queue status options"
+)
register_choices("queue_item_types", QUEUE_ITEM_TYPES, "moderation", "Queue item type classifications")
-register_choices("moderation_action_types", MODERATION_ACTION_TYPES, "moderation", "Moderation action type classifications")
+register_choices(
+ "moderation_action_types", MODERATION_ACTION_TYPES, "moderation", "Moderation action type classifications"
+)
register_choices("bulk_operation_statuses", BULK_OPERATION_STATUSES, "moderation", "Bulk operation status options")
register_choices("bulk_operation_types", BULK_OPERATION_TYPES, "moderation", "Bulk operation type classifications")
-register_choices("photo_submission_statuses", PHOTO_SUBMISSION_STATUSES, "moderation", "Photo submission status options")
+register_choices(
+ "photo_submission_statuses", PHOTO_SUBMISSION_STATUSES, "moderation", "Photo submission status options"
+)
diff --git a/backend/apps/moderation/context_processors.py b/backend/apps/moderation/context_processors.py
index 5d5d99a9..327caf42 100644
--- a/backend/apps/moderation/context_processors.py
+++ b/backend/apps/moderation/context_processors.py
@@ -11,14 +11,9 @@ def moderation_access(request):
context["user_role"] = request.user.role
# Check both role-based and Django's built-in superuser status
context["has_moderation_access"] = (
- request.user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]
- or request.user.is_superuser
- )
- context["has_admin_access"] = (
- request.user.role in ["ADMIN", "SUPERUSER"] or request.user.is_superuser
- )
- context["has_superuser_access"] = (
- request.user.role == "SUPERUSER" or request.user.is_superuser
+ request.user.role in ["MODERATOR", "ADMIN", "SUPERUSER"] or request.user.is_superuser
)
+ context["has_admin_access"] = request.user.role in ["ADMIN", "SUPERUSER"] or request.user.is_superuser
+ context["has_superuser_access"] = request.user.role == "SUPERUSER" or request.user.is_superuser
return context
diff --git a/backend/apps/moderation/filters.py b/backend/apps/moderation/filters.py
index cb297038..d60e282d 100644
--- a/backend/apps/moderation/filters.py
+++ b/backend/apps/moderation/filters.py
@@ -29,20 +29,22 @@ class ModerationReportFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
- choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_report_statuses", "moderation")],
- help_text="Filter by report status"
+ choices=lambda: [
+ (choice.value, choice.label) for choice in get_choices("moderation_report_statuses", "moderation")
+ ],
+ help_text="Filter by report status",
)
# Priority filters
priority = django_filters.ChoiceFilter(
choices=lambda: [(choice.value, choice.label) for choice in get_choices("priority_levels", "moderation")],
- help_text="Filter by report priority"
+ help_text="Filter by report priority",
)
# Report type filters
report_type = django_filters.ChoiceFilter(
choices=lambda: [(choice.value, choice.label) for choice in get_choices("report_types", "moderation")],
- help_text="Filter by report type"
+ help_text="Filter by report type",
)
# User filters
@@ -87,13 +89,9 @@ class ModerationReportFilter(django_filters.FilterSet):
)
# Special filters
- unassigned = django_filters.BooleanFilter(
- method="filter_unassigned", help_text="Filter for unassigned reports"
- )
+ unassigned = django_filters.BooleanFilter(method="filter_unassigned", help_text="Filter for unassigned reports")
- overdue = django_filters.BooleanFilter(
- method="filter_overdue", help_text="Filter for overdue reports based on SLA"
- )
+ overdue = django_filters.BooleanFilter(method="filter_overdue", help_text="Filter for overdue reports based on SLA")
has_resolution = django_filters.BooleanFilter(
method="filter_has_resolution",
@@ -143,12 +141,8 @@ class ModerationReportFilter(django_filters.FilterSet):
def filter_has_resolution(self, queryset, name, value):
"""Filter reports with/without resolution."""
if value:
- return queryset.exclude(
- resolution_action__isnull=True, resolution_action=""
- )
- return queryset.filter(
- Q(resolution_action__isnull=True) | Q(resolution_action="")
- )
+ return queryset.exclude(resolution_action__isnull=True, resolution_action="")
+ return queryset.filter(Q(resolution_action__isnull=True) | Q(resolution_action=""))
class ModerationQueueFilter(django_filters.FilterSet):
@@ -156,8 +150,10 @@ class ModerationQueueFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
- choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_queue_statuses", "moderation")],
- help_text="Filter by queue item status"
+ choices=lambda: [
+ (choice.value, choice.label) for choice in get_choices("moderation_queue_statuses", "moderation")
+ ],
+ help_text="Filter by queue item status",
)
# Priority filters
@@ -169,7 +165,7 @@ class ModerationQueueFilter(django_filters.FilterSet):
# Item type filters
item_type = django_filters.ChoiceFilter(
choices=lambda: [(choice.value, choice.label) for choice in get_choices("queue_item_types", "moderation")],
- help_text="Filter by queue item type"
+ help_text="Filter by queue item type",
)
# Assignment filters
@@ -178,9 +174,7 @@ class ModerationQueueFilter(django_filters.FilterSet):
help_text="Filter by assigned moderator",
)
- unassigned = django_filters.BooleanFilter(
- method="filter_unassigned", help_text="Filter for unassigned queue items"
- )
+ unassigned = django_filters.BooleanFilter(method="filter_unassigned", help_text="Filter for unassigned queue items")
# Date filters
created_after = django_filters.DateTimeFilter(
@@ -208,9 +202,7 @@ class ModerationQueueFilter(django_filters.FilterSet):
)
# Content type filters
- content_type = django_filters.CharFilter(
- field_name="content_type__model", help_text="Filter by content type"
- )
+ content_type = django_filters.CharFilter(field_name="content_type__model", help_text="Filter by content type")
# Related report filters
has_related_report = django_filters.BooleanFilter(
@@ -248,8 +240,10 @@ class ModerationActionFilter(django_filters.FilterSet):
# Action type filters
action_type = django_filters.ChoiceFilter(
- choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_action_types", "moderation")],
- help_text="Filter by action type"
+ choices=lambda: [
+ (choice.value, choice.label) for choice in get_choices("moderation_action_types", "moderation")
+ ],
+ help_text="Filter by action type",
)
# User filters
@@ -258,9 +252,7 @@ class ModerationActionFilter(django_filters.FilterSet):
help_text="Filter by moderator who took the action",
)
- target_user = django_filters.ModelChoiceFilter(
- queryset=User.objects.all(), help_text="Filter by target user"
- )
+ target_user = django_filters.ModelChoiceFilter(queryset=User.objects.all(), help_text="Filter by target user")
# Status filters
is_active = django_filters.BooleanFilter(help_text="Filter by active status")
@@ -291,9 +283,7 @@ class ModerationActionFilter(django_filters.FilterSet):
)
# Special filters
- expired = django_filters.BooleanFilter(
- method="filter_expired", help_text="Filter for expired actions"
- )
+ expired = django_filters.BooleanFilter(method="filter_expired", help_text="Filter for expired actions")
expiring_soon = django_filters.BooleanFilter(
method="filter_expiring_soon",
@@ -345,8 +335,10 @@ class BulkOperationFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
- choices=lambda: [(choice.value, choice.label) for choice in get_choices("bulk_operation_statuses", "moderation")],
- help_text="Filter by operation status"
+ choices=lambda: [
+ (choice.value, choice.label) for choice in get_choices("bulk_operation_statuses", "moderation")
+ ],
+ help_text="Filter by operation status",
)
# Operation type filters
@@ -358,7 +350,7 @@ class BulkOperationFilter(django_filters.FilterSet):
# Priority filters
priority = django_filters.ChoiceFilter(
choices=lambda: [(choice.value, choice.label) for choice in get_choices("priority_levels", "moderation")],
- help_text="Filter by operation priority"
+ help_text="Filter by operation priority",
)
# User filters
@@ -405,9 +397,7 @@ class BulkOperationFilter(django_filters.FilterSet):
)
# Special filters
- can_cancel = django_filters.BooleanFilter(
- help_text="Filter by cancellation capability"
- )
+ can_cancel = django_filters.BooleanFilter(help_text="Filter by cancellation capability")
has_failures = django_filters.BooleanFilter(
method="filter_has_failures",
diff --git a/backend/apps/moderation/management/commands/analyze_transitions.py b/backend/apps/moderation/management/commands/analyze_transitions.py
index 53115741..78f4e3ff 100644
--- a/backend/apps/moderation/management/commands/analyze_transitions.py
+++ b/backend/apps/moderation/management/commands/analyze_transitions.py
@@ -16,36 +16,25 @@ from django_fsm_log.models import StateLog
class Command(BaseCommand):
- help = 'Analyze state transition patterns and generate statistics'
+ help = "Analyze state transition patterns and generate statistics"
def add_arguments(self, parser):
+ parser.add_argument("--days", type=int, default=30, help="Number of days to analyze (default: 30)")
+ parser.add_argument("--model", type=str, help="Specific model to analyze (e.g., editsubmission)")
parser.add_argument(
- '--days',
- type=int,
- default=30,
- help='Number of days to analyze (default: 30)'
- )
- parser.add_argument(
- '--model',
+ "--output",
type=str,
- help='Specific model to analyze (e.g., editsubmission)'
- )
- parser.add_argument(
- '--output',
- type=str,
- choices=['console', 'json', 'csv'],
- default='console',
- help='Output format (default: console)'
+ choices=["console", "json", "csv"],
+ default="console",
+ help="Output format (default: console)",
)
def handle(self, *args, **options):
- days = options['days']
- model_filter = options['model']
- output_format = options['output']
+ days = options["days"]
+ model_filter = options["model"]
+ output_format = options["output"]
- self.stdout.write(
- self.style.SUCCESS(f'\n=== State Transition Analysis (Last {days} days) ===\n')
- )
+ self.stdout.write(self.style.SUCCESS(f"\n=== State Transition Analysis (Last {days} days) ===\n"))
# Filter by date range
start_date = timezone.now() - timedelta(days=days)
@@ -56,173 +45,134 @@ class Command(BaseCommand):
try:
content_type = ContentType.objects.get(model=model_filter.lower())
queryset = queryset.filter(content_type=content_type)
- self.stdout.write(f'Filtering for model: {model_filter}\n')
+ self.stdout.write(f"Filtering for model: {model_filter}\n")
except ContentType.DoesNotExist:
- self.stdout.write(
- self.style.ERROR(f'Model "{model_filter}" not found')
- )
+ self.stdout.write(self.style.ERROR(f'Model "{model_filter}" not found'))
return
# Total transitions
total_transitions = queryset.count()
- self.stdout.write(
- self.style.SUCCESS(f'Total Transitions: {total_transitions}\n')
- )
+ self.stdout.write(self.style.SUCCESS(f"Total Transitions: {total_transitions}\n"))
if total_transitions == 0:
- self.stdout.write(
- self.style.WARNING('No transitions found in the specified period.')
- )
+ self.stdout.write(self.style.WARNING("No transitions found in the specified period."))
return
# Most common transitions
- self.stdout.write(self.style.SUCCESS('\n--- Most Common Transitions ---'))
+ self.stdout.write(self.style.SUCCESS("\n--- Most Common Transitions ---"))
common_transitions = (
- queryset.values('transition', 'content_type__model')
- .annotate(count=Count('id'))
- .order_by('-count')[:10]
+ queryset.values("transition", "content_type__model").annotate(count=Count("id")).order_by("-count")[:10]
)
for t in common_transitions:
- model_name = t['content_type__model']
- transition_name = t['transition'] or 'N/A'
- count = t['count']
+ model_name = t["content_type__model"]
+ transition_name = t["transition"] or "N/A"
+ count = t["count"]
percentage = (count / total_transitions) * 100
- self.stdout.write(
- f" {model_name}.{transition_name}: {count} ({percentage:.1f}%)"
- )
+ self.stdout.write(f" {model_name}.{transition_name}: {count} ({percentage:.1f}%)")
# Transitions by model
- self.stdout.write(self.style.SUCCESS('\n--- Transitions by Model ---'))
- by_model = (
- queryset.values('content_type__model')
- .annotate(count=Count('id'))
- .order_by('-count')
- )
+ self.stdout.write(self.style.SUCCESS("\n--- Transitions by Model ---"))
+ by_model = queryset.values("content_type__model").annotate(count=Count("id")).order_by("-count")
for m in by_model:
- model_name = m['content_type__model']
- count = m['count']
+ model_name = m["content_type__model"]
+ count = m["count"]
percentage = (count / total_transitions) * 100
- self.stdout.write(
- f" {model_name}: {count} ({percentage:.1f}%)"
- )
+ self.stdout.write(f" {model_name}: {count} ({percentage:.1f}%)")
# Transitions by state
- self.stdout.write(self.style.SUCCESS('\n--- Final States Distribution ---'))
- by_state = (
- queryset.values('state')
- .annotate(count=Count('id'))
- .order_by('-count')
- )
+ self.stdout.write(self.style.SUCCESS("\n--- Final States Distribution ---"))
+ by_state = queryset.values("state").annotate(count=Count("id")).order_by("-count")
for s in by_state:
- state_name = s['state']
- count = s['count']
+ state_name = s["state"]
+ count = s["count"]
percentage = (count / total_transitions) * 100
- self.stdout.write(
- f" {state_name}: {count} ({percentage:.1f}%)"
- )
+ self.stdout.write(f" {state_name}: {count} ({percentage:.1f}%)")
# Most active users
- self.stdout.write(self.style.SUCCESS('\n--- Most Active Users ---'))
+ self.stdout.write(self.style.SUCCESS("\n--- Most Active Users ---"))
active_users = (
queryset.exclude(by__isnull=True)
- .values('by__username', 'by__id')
- .annotate(count=Count('id'))
- .order_by('-count')[:10]
+ .values("by__username", "by__id")
+ .annotate(count=Count("id"))
+ .order_by("-count")[:10]
)
for u in active_users:
- username = u['by__username']
- user_id = u['by__id']
- count = u['count']
- self.stdout.write(
- f" {username} (ID: {user_id}): {count} transitions"
- )
+ username = u["by__username"]
+ user_id = u["by__id"]
+ count = u["count"]
+ self.stdout.write(f" {username} (ID: {user_id}): {count} transitions")
# System vs User transitions
system_count = queryset.filter(by__isnull=True).count()
user_count = queryset.exclude(by__isnull=True).count()
- self.stdout.write(self.style.SUCCESS('\n--- Transition Attribution ---'))
+ self.stdout.write(self.style.SUCCESS("\n--- Transition Attribution ---"))
self.stdout.write(f" User-initiated: {user_count} ({(user_count/total_transitions)*100:.1f}%)")
self.stdout.write(f" System-initiated: {system_count} ({(system_count/total_transitions)*100:.1f}%)")
# Daily transition volume
# Security: Using Django ORM functions instead of raw SQL .extra() to prevent SQL injection
- self.stdout.write(self.style.SUCCESS('\n--- Daily Transition Volume ---'))
+ self.stdout.write(self.style.SUCCESS("\n--- Daily Transition Volume ---"))
daily_stats = (
- queryset.annotate(day=TruncDate('timestamp'))
- .values('day')
- .annotate(count=Count('id'))
- .order_by('-day')[:7]
+ queryset.annotate(day=TruncDate("timestamp")).values("day").annotate(count=Count("id")).order_by("-day")[:7]
)
for day in daily_stats:
- date = day['day']
- count = day['count']
+ date = day["day"]
+ count = day["count"]
self.stdout.write(f" {date}: {count} transitions")
# Busiest hours
# Security: Using Django ORM functions instead of raw SQL .extra() to prevent SQL injection
- self.stdout.write(self.style.SUCCESS('\n--- Busiest Hours (UTC) ---'))
+ self.stdout.write(self.style.SUCCESS("\n--- Busiest Hours (UTC) ---"))
hourly_stats = (
- queryset.annotate(hour=ExtractHour('timestamp'))
- .values('hour')
- .annotate(count=Count('id'))
- .order_by('-count')[:5]
+ queryset.annotate(hour=ExtractHour("timestamp"))
+ .values("hour")
+ .annotate(count=Count("id"))
+ .order_by("-count")[:5]
)
for hour in hourly_stats:
- hour_val = int(hour['hour'])
- count = hour['count']
+ hour_val = int(hour["hour"])
+ count = hour["count"]
self.stdout.write(f" Hour {hour_val:02d}:00: {count} transitions")
# Transition patterns (common sequences)
- self.stdout.write(self.style.SUCCESS('\n--- Common Transition Patterns ---'))
- self.stdout.write(' Analyzing transition sequences...')
+ self.stdout.write(self.style.SUCCESS("\n--- Common Transition Patterns ---"))
+ self.stdout.write(" Analyzing transition sequences...")
# Get recent objects and their transition sequences
- recent_objects = (
- queryset.values('content_type', 'object_id')
- .distinct()[:100]
- )
+ recent_objects = queryset.values("content_type", "object_id").distinct()[:100]
pattern_counts = {}
for obj in recent_objects:
transitions = list(
- StateLog.objects.filter(
- content_type=obj['content_type'],
- object_id=obj['object_id']
- )
- .order_by('timestamp')
- .values_list('transition', flat=True)
+ StateLog.objects.filter(content_type=obj["content_type"], object_id=obj["object_id"])
+ .order_by("timestamp")
+ .values_list("transition", flat=True)
)
# Create pattern from consecutive transitions
if len(transitions) >= 2:
- pattern = ' → '.join([t or 'N/A' for t in transitions[:3]])
+ pattern = " → ".join([t or "N/A" for t in transitions[:3]])
pattern_counts[pattern] = pattern_counts.get(pattern, 0) + 1
# Display top patterns
- sorted_patterns = sorted(
- pattern_counts.items(),
- key=lambda x: x[1],
- reverse=True
- )[:5]
+ sorted_patterns = sorted(pattern_counts.items(), key=lambda x: x[1], reverse=True)[:5]
for pattern, count in sorted_patterns:
self.stdout.write(f" {pattern}: {count} occurrences")
- self.stdout.write(
- self.style.SUCCESS('\n=== Analysis Complete ===\n')
- )
+ self.stdout.write(self.style.SUCCESS("\n=== Analysis Complete ===\n"))
# Export options
- if output_format == 'json':
+ if output_format == "json":
self._export_json(queryset, days)
- elif output_format == 'csv':
+ elif output_format == "csv":
self._export_csv(queryset, days)
def _export_json(self, queryset, days):
@@ -231,24 +181,21 @@ class Command(BaseCommand):
from datetime import datetime
data = {
- 'analysis_date': datetime.now().isoformat(),
- 'period_days': days,
- 'total_transitions': queryset.count(),
- 'transitions': list(
+ "analysis_date": datetime.now().isoformat(),
+ "period_days": days,
+ "total_transitions": queryset.count(),
+ "transitions": list(
queryset.values(
- 'id', 'timestamp', 'state', 'transition',
- 'content_type__model', 'object_id', 'by__username'
+ "id", "timestamp", "state", "transition", "content_type__model", "object_id", "by__username"
)
- )
+ ),
}
filename = f'transition_analysis_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
- with open(filename, 'w') as f:
+ with open(filename, "w") as f:
json.dump(data, f, indent=2, default=str)
- self.stdout.write(
- self.style.SUCCESS(f'Exported to {filename}')
- )
+ self.stdout.write(self.style.SUCCESS(f"Exported to {filename}"))
def _export_csv(self, queryset, days):
"""Export analysis results as CSV."""
@@ -257,24 +204,21 @@ class Command(BaseCommand):
filename = f'transition_analysis_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
- with open(filename, 'w', newline='') as f:
+ with open(filename, "w", newline="") as f:
writer = csv.writer(f)
- writer.writerow([
- 'ID', 'Timestamp', 'Model', 'Object ID',
- 'State', 'Transition', 'User'
- ])
+ writer.writerow(["ID", "Timestamp", "Model", "Object ID", "State", "Transition", "User"])
- for log in queryset.select_related('content_type', 'by'):
- writer.writerow([
- log.id,
- log.timestamp,
- log.content_type.model,
- log.object_id,
- log.state,
- log.transition or 'N/A',
- log.by.username if log.by else 'System'
- ])
+ for log in queryset.select_related("content_type", "by"):
+ writer.writerow(
+ [
+ log.id,
+ log.timestamp,
+ log.content_type.model,
+ log.object_id,
+ log.state,
+ log.transition or "N/A",
+ log.by.username if log.by else "System",
+ ]
+ )
- self.stdout.write(
- self.style.SUCCESS(f'Exported to {filename}')
- )
+ self.stdout.write(self.style.SUCCESS(f"Exported to {filename}"))
diff --git a/backend/apps/moderation/management/commands/seed_submissions.py b/backend/apps/moderation/management/commands/seed_submissions.py
index bb1b11db..246cd3fc 100644
--- a/backend/apps/moderation/management/commands/seed_submissions.py
+++ b/backend/apps/moderation/management/commands/seed_submissions.py
@@ -17,9 +17,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
# Ensure we have a test user
- user, created = User.objects.get_or_create(
- username="test_user", email="test@example.com"
- )
+ user, created = User.objects.get_or_create(username="test_user", email="test@example.com")
if created:
user.set_password("testpass123")
user.save()
@@ -215,9 +213,7 @@ class Command(BaseCommand):
"audio system, and increased capacity due to improved loading efficiency."
),
source=(
- "Park operations manual\n"
- "Maintenance records\n"
- "Personal observation and timing of new ride cycle"
+ "Park operations manual\n" "Maintenance records\n" "Personal observation and timing of new ride cycle"
),
status="PENDING",
)
@@ -225,10 +221,10 @@ class Command(BaseCommand):
# Create PhotoSubmissions with detailed captions
# Park photo submission
- image_data = b"GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"
- dummy_image = SimpleUploadedFile(
- "park_entrance.gif", image_data, content_type="image/gif"
+ image_data = (
+ b"GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"
)
+ dummy_image = SimpleUploadedFile("park_entrance.gif", image_data, content_type="image/gif")
PhotoSubmission.objects.create(
user=user,
@@ -244,9 +240,7 @@ class Command(BaseCommand):
)
# Ride photo submission
- dummy_image2 = SimpleUploadedFile(
- "coaster_track.gif", image_data, content_type="image/gif"
- )
+ dummy_image2 = SimpleUploadedFile("coaster_track.gif", image_data, content_type="image/gif")
PhotoSubmission.objects.create(
user=user,
content_type=ride_ct,
diff --git a/backend/apps/moderation/management/commands/validate_state_machines.py b/backend/apps/moderation/management/commands/validate_state_machines.py
index 17c0dee2..1f6188a6 100644
--- a/backend/apps/moderation/management/commands/validate_state_machines.py
+++ b/backend/apps/moderation/management/commands/validate_state_machines.py
@@ -1,4 +1,5 @@
"""Management command to validate state machine configurations for moderation models."""
+
from django.core.management import CommandError
from django.core.management.base import BaseCommand
@@ -76,18 +77,15 @@ class Command(BaseCommand):
model_key = model_name.lower()
if model_key not in models_to_validate:
raise CommandError(
- f"Unknown model: {model_name}. "
- f"Valid options: {', '.join(models_to_validate.keys())}"
+ f"Unknown model: {model_name}. " f"Valid options: {', '.join(models_to_validate.keys())}"
)
models_to_validate = {model_key: models_to_validate[model_key]}
- self.stdout.write(
- self.style.SUCCESS("\nValidating State Machine Configurations\n")
- )
+ self.stdout.write(self.style.SUCCESS("\nValidating State Machine Configurations\n"))
self.stdout.write("=" * 60 + "\n")
all_valid = True
- for model_key, (
+ for _model_key, (
model_class,
choice_group,
domain,
@@ -101,61 +99,34 @@ class Command(BaseCommand):
result = validator.validate_choice_group()
if result.is_valid:
- self.stdout.write(
- self.style.SUCCESS(
- f" ✓ {model_class.__name__} validation passed"
- )
- )
+ self.stdout.write(self.style.SUCCESS(f" ✓ {model_class.__name__} validation passed"))
if verbose:
self._show_transition_graph(choice_group, domain)
else:
all_valid = False
- self.stdout.write(
- self.style.ERROR(
- f" ✗ {model_class.__name__} validation failed"
- )
- )
+ self.stdout.write(self.style.ERROR(f" ✗ {model_class.__name__} validation failed"))
for error in result.errors:
- self.stdout.write(
- self.style.ERROR(f" - {error.message}")
- )
+ self.stdout.write(self.style.ERROR(f" - {error.message}"))
# Check FSM field
if not self._check_fsm_field(model_class):
all_valid = False
- self.stdout.write(
- self.style.ERROR(
- f" - FSM field 'status' not found on "
- f"{model_class.__name__}"
- )
- )
+ self.stdout.write(self.style.ERROR(f" - FSM field 'status' not found on " f"{model_class.__name__}"))
# Check mixin
if not self._check_state_machine_mixin(model_class):
all_valid = False
self.stdout.write(
- self.style.WARNING(
- f" - StateMachineMixin not found on "
- f"{model_class.__name__}"
- )
+ self.style.WARNING(f" - StateMachineMixin not found on " f"{model_class.__name__}")
)
self.stdout.write("\n" + "=" * 60)
if all_valid:
- self.stdout.write(
- self.style.SUCCESS(
- "\n✓ All validations passed successfully!\n"
- )
- )
+ self.stdout.write(self.style.SUCCESS("\n✓ All validations passed successfully!\n"))
else:
- self.stdout.write(
- self.style.ERROR(
- "\n✗ Some validations failed. "
- "Please review the errors above.\n"
- )
- )
+ self.stdout.write(self.style.ERROR("\n✗ Some validations failed. " "Please review the errors above.\n"))
raise CommandError("State machine validation failed")
def _check_fsm_field(self, model_class):
@@ -177,9 +148,7 @@ class Command(BaseCommand):
self.stdout.write("\n Transition Graph:")
- graph = registry_instance.export_transition_graph(
- choice_group, domain
- )
+ graph = registry_instance.export_transition_graph(choice_group, domain)
for source, targets in sorted(graph.items()):
if targets:
diff --git a/backend/apps/moderation/migrations/0001_initial.py b/backend/apps/moderation/migrations/0001_initial.py
index 0553be8b..3f8ceff6 100644
--- a/backend/apps/moderation/migrations/0001_initial.py
+++ b/backend/apps/moderation/migrations/0001_initial.py
@@ -47,9 +47,7 @@ class Migration(migrations.Migration):
),
(
"changes",
- models.JSONField(
- help_text="JSON representation of the changes or new object data"
- ),
+ models.JSONField(help_text="JSON representation of the changes or new object data"),
),
(
"moderator_changes",
@@ -150,9 +148,7 @@ class Migration(migrations.Migration):
),
(
"changes",
- models.JSONField(
- help_text="JSON representation of the changes or new object data"
- ),
+ models.JSONField(help_text="JSON representation of the changes or new object data"),
),
(
"moderator_changes",
diff --git a/backend/apps/moderation/migrations/0003_bulkoperation_bulkoperationevent_moderationaction_and_more.py b/backend/apps/moderation/migrations/0003_bulkoperation_bulkoperationevent_moderationaction_and_more.py
index bb5c8a84..65aff7f1 100644
--- a/backend/apps/moderation/migrations/0003_bulkoperation_bulkoperationevent_moderationaction_and_more.py
+++ b/backend/apps/moderation/migrations/0003_bulkoperation_bulkoperationevent_moderationaction_and_more.py
@@ -812,21 +812,15 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="bulkoperation",
- index=models.Index(
- fields=["status", "priority"], name="moderation__status_f11ee8_idx"
- ),
+ index=models.Index(fields=["status", "priority"], name="moderation__status_f11ee8_idx"),
),
migrations.AddIndex(
model_name="bulkoperation",
- index=models.Index(
- fields=["created_by"], name="moderation__created_4fe5d2_idx"
- ),
+ index=models.Index(fields=["created_by"], name="moderation__created_4fe5d2_idx"),
),
migrations.AddIndex(
model_name="bulkoperation",
- index=models.Index(
- fields=["operation_type"], name="moderation__operati_bc84d9_idx"
- ),
+ index=models.Index(fields=["operation_type"], name="moderation__operati_bc84d9_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="bulkoperation",
@@ -859,9 +853,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="moderationreport",
- index=models.Index(
- fields=["status", "priority"], name="moderation__status_6aa18c_idx"
- ),
+ index=models.Index(fields=["status", "priority"], name="moderation__status_6aa18c_idx"),
),
migrations.AddIndex(
model_name="moderationreport",
@@ -872,9 +864,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="moderationreport",
- index=models.Index(
- fields=["assigned_moderator"], name="moderation__assigne_c43cdf_idx"
- ),
+ index=models.Index(fields=["assigned_moderator"], name="moderation__assigne_c43cdf_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="moderationreport",
@@ -907,9 +897,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="moderationqueue",
- index=models.Index(
- fields=["status", "priority"], name="moderation__status_6f2a75_idx"
- ),
+ index=models.Index(fields=["status", "priority"], name="moderation__status_6f2a75_idx"),
),
migrations.AddIndex(
model_name="moderationqueue",
@@ -920,15 +908,11 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="moderationqueue",
- index=models.Index(
- fields=["assigned_to"], name="moderation__assigne_2fc958_idx"
- ),
+ index=models.Index(fields=["assigned_to"], name="moderation__assigne_2fc958_idx"),
),
migrations.AddIndex(
model_name="moderationqueue",
- index=models.Index(
- fields=["flagged_by"], name="moderation__flagged_169834_idx"
- ),
+ index=models.Index(fields=["flagged_by"], name="moderation__flagged_169834_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="moderationqueue",
@@ -975,9 +959,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="moderationaction",
- index=models.Index(
- fields=["expires_at"], name="moderation__expires_963efb_idx"
- ),
+ index=models.Index(fields=["expires_at"], name="moderation__expires_963efb_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="moderationaction",
diff --git a/backend/apps/moderation/migrations/0004_alter_moderationqueue_options_and_more.py b/backend/apps/moderation/migrations/0004_alter_moderationqueue_options_and_more.py
index f4159efd..b9f8041c 100644
--- a/backend/apps/moderation/migrations/0004_alter_moderationqueue_options_and_more.py
+++ b/backend/apps/moderation/migrations/0004_alter_moderationqueue_options_and_more.py
@@ -55,9 +55,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="can_cancel",
- field=models.BooleanField(
- default=True, help_text="Whether this operation can be cancelled"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this operation can be cancelled"),
),
migrations.AlterField(
model_name="bulkoperation",
@@ -67,23 +65,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="estimated_duration_minutes",
- field=models.PositiveIntegerField(
- blank=True, help_text="Estimated duration in minutes", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Estimated duration in minutes", null=True),
),
migrations.AlterField(
model_name="bulkoperation",
name="failed_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of items that failed"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of items that failed"),
),
migrations.AlterField(
model_name="bulkoperation",
name="id",
- field=models.BigAutoField(
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
- ),
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
),
migrations.AlterField(
model_name="bulkoperation",
@@ -105,9 +97,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="parameters",
- field=models.JSONField(
- default=dict, help_text="Parameters for the operation"
- ),
+ field=models.JSONField(default=dict, help_text="Parameters for the operation"),
),
migrations.AlterField(
model_name="bulkoperation",
@@ -126,9 +116,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="processed_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of items processed"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of items processed"),
),
migrations.AlterField(
model_name="bulkoperation",
@@ -142,23 +130,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="schedule_for",
- field=models.DateTimeField(
- blank=True, help_text="When to run this operation", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When to run this operation", null=True),
),
migrations.AlterField(
model_name="bulkoperation",
name="total_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Total number of items to process"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Total number of items to process"),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="can_cancel",
- field=models.BooleanField(
- default=True, help_text="Whether this operation can be cancelled"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this operation can be cancelled"),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -168,16 +150,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="estimated_duration_minutes",
- field=models.PositiveIntegerField(
- blank=True, help_text="Estimated duration in minutes", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Estimated duration in minutes", null=True),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="failed_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of items that failed"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of items that failed"),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -204,9 +182,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="parameters",
- field=models.JSONField(
- default=dict, help_text="Parameters for the operation"
- ),
+ field=models.JSONField(default=dict, help_text="Parameters for the operation"),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -225,9 +201,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="processed_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of items processed"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of items processed"),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -241,16 +215,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="schedule_for",
- field=models.DateTimeField(
- blank=True, help_text="When to run this operation", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When to run this operation", null=True),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="total_items",
- field=models.PositiveIntegerField(
- default=0, help_text="Total number of items to process"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Total number of items to process"),
),
migrations.AlterField(
model_name="moderationaction",
@@ -286,23 +256,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationaction",
name="expires_at",
- field=models.DateTimeField(
- blank=True, help_text="When this action expires", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this action expires", null=True),
),
migrations.AlterField(
model_name="moderationaction",
name="is_active",
- field=models.BooleanField(
- default=True, help_text="Whether this action is currently active"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this action is currently active"),
),
migrations.AlterField(
model_name="moderationaction",
name="reason",
- field=models.CharField(
- help_text="Brief reason for the action", max_length=200
- ),
+ field=models.CharField(help_text="Brief reason for the action", max_length=200),
),
migrations.AlterField(
model_name="moderationactionevent",
@@ -338,44 +302,32 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationactionevent",
name="expires_at",
- field=models.DateTimeField(
- blank=True, help_text="When this action expires", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this action expires", null=True),
),
migrations.AlterField(
model_name="moderationactionevent",
name="is_active",
- field=models.BooleanField(
- default=True, help_text="Whether this action is currently active"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this action is currently active"),
),
migrations.AlterField(
model_name="moderationactionevent",
name="reason",
- field=models.CharField(
- help_text="Brief reason for the action", max_length=200
- ),
+ field=models.CharField(help_text="Brief reason for the action", max_length=200),
),
migrations.AlterField(
model_name="moderationqueue",
name="description",
- field=models.TextField(
- help_text="Detailed description of what needs to be done"
- ),
+ field=models.TextField(help_text="Detailed description of what needs to be done"),
),
migrations.AlterField(
model_name="moderationqueue",
name="entity_id",
- field=models.PositiveIntegerField(
- blank=True, help_text="ID of the related entity", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="ID of the related entity", null=True),
),
migrations.AlterField(
model_name="moderationqueue",
name="entity_preview",
- field=models.JSONField(
- blank=True, default=dict, help_text="Preview data for the entity"
- ),
+ field=models.JSONField(blank=True, default=dict, help_text="Preview data for the entity"),
),
migrations.AlterField(
model_name="moderationqueue",
@@ -389,9 +341,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueue",
name="estimated_review_time",
- field=models.PositiveIntegerField(
- default=30, help_text="Estimated time in minutes"
- ),
+ field=models.PositiveIntegerField(default=30, help_text="Estimated time in minutes"),
),
migrations.AlterField(
model_name="moderationqueue",
@@ -436,37 +386,27 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueue",
name="tags",
- field=models.JSONField(
- blank=True, default=list, help_text="Tags for categorization"
- ),
+ field=models.JSONField(blank=True, default=list, help_text="Tags for categorization"),
),
migrations.AlterField(
model_name="moderationqueue",
name="title",
- field=models.CharField(
- help_text="Brief title for the queue item", max_length=200
- ),
+ field=models.CharField(help_text="Brief title for the queue item", max_length=200),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="description",
- field=models.TextField(
- help_text="Detailed description of what needs to be done"
- ),
+ field=models.TextField(help_text="Detailed description of what needs to be done"),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="entity_id",
- field=models.PositiveIntegerField(
- blank=True, help_text="ID of the related entity", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="ID of the related entity", null=True),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="entity_preview",
- field=models.JSONField(
- blank=True, default=dict, help_text="Preview data for the entity"
- ),
+ field=models.JSONField(blank=True, default=dict, help_text="Preview data for the entity"),
),
migrations.AlterField(
model_name="moderationqueueevent",
@@ -480,9 +420,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueueevent",
name="estimated_review_time",
- field=models.PositiveIntegerField(
- default=30, help_text="Estimated time in minutes"
- ),
+ field=models.PositiveIntegerField(default=30, help_text="Estimated time in minutes"),
),
migrations.AlterField(
model_name="moderationqueueevent",
@@ -529,16 +467,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueueevent",
name="tags",
- field=models.JSONField(
- blank=True, default=list, help_text="Tags for categorization"
- ),
+ field=models.JSONField(blank=True, default=list, help_text="Tags for categorization"),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="title",
- field=models.CharField(
- help_text="Brief title for the queue item", max_length=200
- ),
+ field=models.CharField(help_text="Brief title for the queue item", max_length=200),
),
migrations.AlterField(
model_name="moderationreport",
@@ -557,9 +491,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreport",
name="reason",
- field=models.CharField(
- help_text="Brief reason for the report", max_length=200
- ),
+ field=models.CharField(help_text="Brief reason for the report", max_length=200),
),
migrations.AlterField(
model_name="moderationreport",
@@ -582,9 +514,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreport",
name="reported_entity_id",
- field=models.PositiveIntegerField(
- help_text="ID of the entity being reported"
- ),
+ field=models.PositiveIntegerField(help_text="ID of the entity being reported"),
),
migrations.AlterField(
model_name="moderationreport",
@@ -641,9 +571,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreportevent",
name="reason",
- field=models.CharField(
- help_text="Brief reason for the report", max_length=200
- ),
+ field=models.CharField(help_text="Brief reason for the report", max_length=200),
),
migrations.AlterField(
model_name="moderationreportevent",
@@ -666,9 +594,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreportevent",
name="reported_entity_id",
- field=models.PositiveIntegerField(
- help_text="ID of the entity being reported"
- ),
+ field=models.PositiveIntegerField(help_text="ID of the entity being reported"),
),
migrations.AlterField(
model_name="moderationreportevent",
@@ -710,45 +636,31 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="bulkoperation",
- index=models.Index(
- fields=["schedule_for"], name="moderation__schedul_350704_idx"
- ),
+ index=models.Index(fields=["schedule_for"], name="moderation__schedul_350704_idx"),
),
migrations.AddIndex(
model_name="bulkoperation",
- index=models.Index(
- fields=["created_at"], name="moderation__created_b705f4_idx"
- ),
+ index=models.Index(fields=["created_at"], name="moderation__created_b705f4_idx"),
),
migrations.AddIndex(
model_name="moderationaction",
- index=models.Index(
- fields=["moderator"], name="moderation__moderat_1c19b0_idx"
- ),
+ index=models.Index(fields=["moderator"], name="moderation__moderat_1c19b0_idx"),
),
migrations.AddIndex(
model_name="moderationaction",
- index=models.Index(
- fields=["created_at"], name="moderation__created_6378e6_idx"
- ),
+ index=models.Index(fields=["created_at"], name="moderation__created_6378e6_idx"),
),
migrations.AddIndex(
model_name="moderationqueue",
- index=models.Index(
- fields=["created_at"], name="moderation__created_fe6dd0_idx"
- ),
+ index=models.Index(fields=["created_at"], name="moderation__created_fe6dd0_idx"),
),
migrations.AddIndex(
model_name="moderationreport",
- index=models.Index(
- fields=["reported_by"], name="moderation__reporte_81af56_idx"
- ),
+ index=models.Index(fields=["reported_by"], name="moderation__reporte_81af56_idx"),
),
migrations.AddIndex(
model_name="moderationreport",
- index=models.Index(
- fields=["created_at"], name="moderation__created_ae337c_idx"
- ),
+ index=models.Index(fields=["created_at"], name="moderation__created_ae337c_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="moderationqueue",
diff --git a/backend/apps/moderation/migrations/0008_alter_bulkoperation_options_and_more.py b/backend/apps/moderation/migrations/0008_alter_bulkoperation_options_and_more.py
index e0893669..a7ee7398 100644
--- a/backend/apps/moderation/migrations/0008_alter_bulkoperation_options_and_more.py
+++ b/backend/apps/moderation/migrations/0008_alter_bulkoperation_options_and_more.py
@@ -67,9 +67,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="completed_at",
- field=models.DateTimeField(
- blank=True, help_text="When this operation completed", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this operation completed", null=True),
),
migrations.AlterField(
model_name="bulkoperation",
@@ -84,23 +82,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperation",
name="started_at",
- field=models.DateTimeField(
- blank=True, help_text="When this operation started", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this operation started", null=True),
),
migrations.AlterField(
model_name="bulkoperation",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this operation was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this operation was last updated"),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="completed_at",
- field=models.DateTimeField(
- blank=True, help_text="When this operation completed", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this operation completed", null=True),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -117,9 +109,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="started_at",
- field=models.DateTimeField(
- blank=True, help_text="When this operation started", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this operation started", null=True),
),
migrations.AlterField(
model_name="bulkoperationevent",
@@ -142,9 +132,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="bulkoperationevent",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this operation was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this operation was last updated"),
),
migrations.AlterField(
model_name="editsubmission",
@@ -158,9 +146,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="editsubmission",
name="handled_at",
- field=models.DateTimeField(
- blank=True, help_text="When this submission was handled", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this submission was handled", null=True),
),
migrations.AlterField(
model_name="editsubmission",
@@ -208,9 +194,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="editsubmissionevent",
name="handled_at",
- field=models.DateTimeField(
- blank=True, help_text="When this submission was handled", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this submission was handled", null=True),
),
migrations.AlterField(
model_name="editsubmissionevent",
@@ -267,9 +251,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationaction",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this action was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this action was created"),
),
migrations.AlterField(
model_name="moderationaction",
@@ -306,16 +288,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationaction",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this action was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this action was last updated"),
),
migrations.AlterField(
model_name="moderationactionevent",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this action was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this action was created"),
),
migrations.AlterField(
model_name="moderationactionevent",
@@ -358,16 +336,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationactionevent",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this action was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this action was last updated"),
),
migrations.AlterField(
model_name="moderationqueue",
name="assigned_at",
- field=models.DateTimeField(
- blank=True, help_text="When this item was assigned", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this item was assigned", null=True),
),
migrations.AlterField(
model_name="moderationqueue",
@@ -384,9 +358,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueue",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this item was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this item was created"),
),
migrations.AlterField(
model_name="moderationqueue",
@@ -415,16 +387,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueue",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this item was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this item was last updated"),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="assigned_at",
- field=models.DateTimeField(
- blank=True, help_text="When this item was assigned", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this item was assigned", null=True),
),
migrations.AlterField(
model_name="moderationqueueevent",
@@ -443,9 +411,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueueevent",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this item was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this item was created"),
),
migrations.AlterField(
model_name="moderationqueueevent",
@@ -495,9 +461,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationqueueevent",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this item was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this item was last updated"),
),
migrations.AlterField(
model_name="moderationreport",
@@ -514,9 +478,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreport",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this report was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this report was created"),
),
migrations.AlterField(
model_name="moderationreport",
@@ -531,16 +493,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreport",
name="resolved_at",
- field=models.DateTimeField(
- blank=True, help_text="When this report was resolved", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this report was resolved", null=True),
),
migrations.AlterField(
model_name="moderationreport",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this report was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this report was last updated"),
),
migrations.AlterField(
model_name="moderationreportevent",
@@ -559,9 +517,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreportevent",
name="created_at",
- field=models.DateTimeField(
- auto_now_add=True, help_text="When this report was created"
- ),
+ field=models.DateTimeField(auto_now_add=True, help_text="When this report was created"),
),
migrations.AlterField(
model_name="moderationreportevent",
@@ -578,9 +534,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreportevent",
name="resolved_at",
- field=models.DateTimeField(
- blank=True, help_text="When this report was resolved", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this report was resolved", null=True),
),
migrations.AlterField(
model_name="moderationreportevent",
@@ -602,16 +556,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="moderationreportevent",
name="updated_at",
- field=models.DateTimeField(
- auto_now=True, help_text="When this report was last updated"
- ),
+ field=models.DateTimeField(auto_now=True, help_text="When this report was last updated"),
),
migrations.AlterField(
model_name="photosubmission",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Photo caption", max_length=255),
),
migrations.AlterField(
model_name="photosubmission",
@@ -625,16 +575,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="photosubmission",
name="date_taken",
- field=models.DateField(
- blank=True, help_text="Date the photo was taken", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date the photo was taken", null=True),
),
migrations.AlterField(
model_name="photosubmission",
name="handled_at",
- field=models.DateTimeField(
- blank=True, help_text="When this submission was handled", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this submission was handled", null=True),
),
migrations.AlterField(
model_name="photosubmission",
@@ -651,9 +597,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="photosubmission",
name="object_id",
- field=models.PositiveIntegerField(
- help_text="ID of object this photo is for"
- ),
+ field=models.PositiveIntegerField(help_text="ID of object this photo is for"),
),
migrations.AlterField(
model_name="photosubmission",
@@ -668,9 +612,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="photosubmissionevent",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Photo caption", max_length=255),
),
migrations.AlterField(
model_name="photosubmissionevent",
@@ -687,16 +629,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="photosubmissionevent",
name="date_taken",
- field=models.DateField(
- blank=True, help_text="Date the photo was taken", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date the photo was taken", null=True),
),
migrations.AlterField(
model_name="photosubmissionevent",
name="handled_at",
- field=models.DateTimeField(
- blank=True, help_text="When this submission was handled", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this submission was handled", null=True),
),
migrations.AlterField(
model_name="photosubmissionevent",
@@ -715,9 +653,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="photosubmissionevent",
name="object_id",
- field=models.PositiveIntegerField(
- help_text="ID of object this photo is for"
- ),
+ field=models.PositiveIntegerField(help_text="ID of object this photo is for"),
),
migrations.AlterField(
model_name="photosubmissionevent",
diff --git a/backend/apps/moderation/mixins.py b/backend/apps/moderation/mixins.py
index c9544149..80a92d79 100644
--- a/backend/apps/moderation/mixins.py
+++ b/backend/apps/moderation/mixins.py
@@ -132,9 +132,7 @@ class EditSubmissionMixin(DetailView):
status=400,
)
- return self.handle_edit_submission(
- request, changes, reason, source, submission_type
- )
+ return self.handle_edit_submission(request, changes, reason, source, submission_type)
except json.JSONDecodeError:
return JsonResponse(
@@ -169,9 +167,7 @@ class PhotoSubmissionMixin(DetailView):
try:
obj = self.get_object()
except (AttributeError, self.model.DoesNotExist):
- return JsonResponse(
- {"status": "error", "message": "Invalid object."}, status=400
- )
+ return JsonResponse({"status": "error", "message": "Invalid object."}, status=400)
if not request.FILES.get("photo"):
return JsonResponse(
diff --git a/backend/apps/moderation/models.py b/backend/apps/moderation/models.py
index ac01631d..79bab15b 100644
--- a/backend/apps/moderation/models.py
+++ b/backend/apps/moderation/models.py
@@ -17,7 +17,7 @@ are registered via the callback configuration defined in each model's Meta class
"""
from datetime import timedelta
-from typing import Any, Union
+from typing import Any
import pghistory
from django.conf import settings
@@ -33,7 +33,7 @@ from apps.core.choices.fields import RichChoiceField
from apps.core.history import TrackedModel
from apps.core.state_machine import RichFSMField, StateMachineMixin
-UserType = Union[AbstractBaseUser, AnonymousUser]
+UserType = AbstractBaseUser | AnonymousUser
# Lazy callback imports to avoid circular dependencies
@@ -45,11 +45,12 @@ def _get_notification_callbacks():
SubmissionEscalatedNotification,
SubmissionRejectedNotification,
)
+
return {
- 'approved': SubmissionApprovedNotification,
- 'rejected': SubmissionRejectedNotification,
- 'escalated': SubmissionEscalatedNotification,
- 'moderation': ModerationNotificationCallback,
+ "approved": SubmissionApprovedNotification,
+ "rejected": SubmissionRejectedNotification,
+ "escalated": SubmissionEscalatedNotification,
+ "moderation": ModerationNotificationCallback,
}
@@ -59,9 +60,10 @@ def _get_cache_callbacks():
CacheInvalidationCallback,
ModerationCacheInvalidation,
)
+
return {
- 'generic': CacheInvalidationCallback,
- 'moderation': ModerationCacheInvalidation,
+ "generic": CacheInvalidationCallback,
+ "moderation": ModerationCacheInvalidation,
}
@@ -69,6 +71,7 @@ def _get_cache_callbacks():
# Original EditSubmission Model (Preserved)
# ============================================================================
+
@pghistory.track() # Track all changes by default
class EditSubmission(StateMachineMixin, TrackedModel):
"""Edit submission model with FSM-managed status transitions."""
@@ -98,16 +101,11 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# Type of submission
submission_type = RichChoiceField(
- choice_group="submission_types",
- domain="moderation",
- max_length=10,
- default="EDIT"
+ choice_group="submission_types", domain="moderation", max_length=10, default="EDIT"
)
# The actual changes/data
- changes = models.JSONField(
- help_text="JSON representation of the changes or new object data"
- )
+ changes = models.JSONField(help_text="JSON representation of the changes or new object data")
# Moderator's edited version of changes before approval
moderator_changes = models.JSONField(
@@ -118,14 +116,9 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# Metadata
reason = models.TextField(help_text="Why this edit/addition is needed")
- source = models.TextField(
- blank=True, help_text="Source of information (if applicable)"
- )
+ source = models.TextField(blank=True, help_text="Source of information (if applicable)")
status = RichFSMField(
- choice_group="edit_submission_statuses",
- domain="moderation",
- max_length=20,
- default="PENDING"
+ choice_group="edit_submission_statuses", domain="moderation", max_length=20, default="PENDING"
)
created_at = models.DateTimeField(auto_now_add=True)
@@ -138,12 +131,8 @@ class EditSubmission(StateMachineMixin, TrackedModel):
related_name="handled_submissions",
help_text="Moderator who handled this submission",
)
- handled_at = models.DateTimeField(
- null=True, blank=True, help_text="When this submission was handled"
- )
- notes = models.TextField(
- blank=True, help_text="Notes from the moderator about this submission"
- )
+ handled_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was handled")
+ notes = models.TextField(blank=True, help_text="Notes from the moderator about this submission")
# Claim tracking for concurrency control
claimed_by = models.ForeignKey(
@@ -154,9 +143,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
related_name="claimed_edit_submissions",
help_text="Moderator who has claimed this submission for review",
)
- claimed_at = models.DateTimeField(
- null=True, blank=True, help_text="When this submission was claimed"
- )
+ claimed_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was claimed")
class Meta(TrackedModel.Meta):
verbose_name = "Edit Submission"
@@ -187,12 +174,12 @@ class EditSubmission(StateMachineMixin, TrackedModel):
field = model_class._meta.get_field(field_name)
if isinstance(field, models.ForeignKey) and value is not None:
try:
- related_obj = field.related_model.objects.get(pk=value) # type: ignore
+ related_obj = field.related_model.objects.get(pk=value) # type: ignore
resolved_data[field_name] = related_obj
except ObjectDoesNotExist:
raise ValueError(
- f"Related object {field.related_model.__name__} with pk={value} does not exist" # type: ignore
- )
+ f"Related object {field.related_model.__name__} with pk={value} does not exist" # type: ignore
+ ) from None
except FieldDoesNotExist:
# Field doesn't exist on model, skip it
continue
@@ -217,9 +204,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
from django.core.exceptions import ValidationError
if self.status != "PENDING":
- raise ValidationError(
- f"Cannot claim submission: current status is {self.status}, expected PENDING"
- )
+ raise ValidationError(f"Cannot claim submission: current status is {self.status}, expected PENDING")
self.transition_to_claimed(user=user)
self.claimed_by = user
@@ -240,9 +225,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
from django.core.exceptions import ValidationError
if self.status != "CLAIMED":
- raise ValidationError(
- f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED"
- )
+ raise ValidationError(f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED")
# Set status directly (not via FSM transition to avoid cycle)
# This is intentional - the unclaim action is a special "rollback" operation
@@ -274,9 +257,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# Validate state - must be CLAIMED before approval
if self.status != "CLAIMED":
- raise ValidationError(
- f"Cannot approve submission: must be CLAIMED first (current status: {self.status})"
- )
+ raise ValidationError(f"Cannot approve submission: must be CLAIMED first (current status: {self.status})")
model_class = self.content_type.model_class()
if not model_class:
@@ -341,9 +322,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# Validate state - must be CLAIMED before rejection
if self.status != "CLAIMED":
- raise ValidationError(
- f"Cannot reject submission: must be CLAIMED first (current status: {self.status})"
- )
+ raise ValidationError(f"Cannot reject submission: must be CLAIMED first (current status: {self.status})")
# Use FSM transition to update status
self.transition_to_rejected(user=rejecter)
@@ -369,9 +348,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# Validate state - must be CLAIMED before escalation
if self.status != "CLAIMED":
- raise ValidationError(
- f"Cannot escalate submission: must be CLAIMED first (current status: {self.status})"
- )
+ raise ValidationError(f"Cannot escalate submission: must be CLAIMED first (current status: {self.status})")
# Use FSM transition to update status
self.transition_to_escalated(user=escalator)
@@ -395,6 +372,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
# New Moderation System Models
# ============================================================================
+
@pghistory.track()
class ModerationReport(StateMachineMixin, TrackedModel):
"""
@@ -407,43 +385,29 @@ class ModerationReport(StateMachineMixin, TrackedModel):
state_field_name = "status"
# Report details
- report_type = RichChoiceField(
- choice_group="report_types",
- domain="moderation",
- max_length=50
- )
+ report_type = RichChoiceField(choice_group="report_types", domain="moderation", max_length=50)
status = RichFSMField(
- choice_group="moderation_report_statuses",
- domain="moderation",
- max_length=20,
- default='PENDING'
- )
- priority = RichChoiceField(
- choice_group="priority_levels",
- domain="moderation",
- max_length=10,
- default='MEDIUM'
+ choice_group="moderation_report_statuses", domain="moderation", max_length=20, default="PENDING"
)
+ priority = RichChoiceField(choice_group="priority_levels", domain="moderation", max_length=10, default="MEDIUM")
# What is being reported
reported_entity_type = models.CharField(
- max_length=50, help_text="Type of entity being reported (park, ride, user, etc.)")
- reported_entity_id = models.PositiveIntegerField(
- help_text="ID of the entity being reported")
- content_type = models.ForeignKey(
- ContentType, on_delete=models.CASCADE, null=True, blank=True)
+ max_length=50, help_text="Type of entity being reported (park, ride, user, etc.)"
+ )
+ reported_entity_id = models.PositiveIntegerField(help_text="ID of the entity being reported")
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
# Report content
reason = models.CharField(max_length=200, help_text="Brief reason for the report")
description = models.TextField(help_text="Detailed description of the issue")
- evidence_urls = models.JSONField(
- default=list, blank=True, help_text="URLs to evidence (screenshots, etc.)")
+ evidence_urls = models.JSONField(default=list, blank=True, help_text="URLs to evidence (screenshots, etc.)")
# Users involved
reported_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
- related_name='moderation_reports_made',
+ related_name="moderation_reports_made",
help_text="User who made this report",
)
assigned_moderator = models.ForeignKey(
@@ -451,40 +415,32 @@ class ModerationReport(StateMachineMixin, TrackedModel):
on_delete=models.SET_NULL,
null=True,
blank=True,
- related_name='assigned_moderation_reports',
+ related_name="assigned_moderation_reports",
help_text="Moderator assigned to handle this report",
)
# Resolution
- resolution_action = models.CharField(
- max_length=100, blank=True, help_text="Action taken to resolve")
- resolution_notes = models.TextField(
- blank=True, help_text="Notes about the resolution")
- resolved_at = models.DateTimeField(
- null=True, blank=True, help_text="When this report was resolved"
- )
+ resolution_action = models.CharField(max_length=100, blank=True, help_text="Action taken to resolve")
+ resolution_notes = models.TextField(blank=True, help_text="Notes about the resolution")
+ resolved_at = models.DateTimeField(null=True, blank=True, help_text="When this report was resolved")
# Timestamps
- created_at = models.DateTimeField(
- auto_now_add=True, help_text="When this report was created"
- )
- updated_at = models.DateTimeField(
- auto_now=True, help_text="When this report was last updated"
- )
+ created_at = models.DateTimeField(auto_now_add=True, help_text="When this report was created")
+ updated_at = models.DateTimeField(auto_now=True, help_text="When this report was last updated")
class Meta(TrackedModel.Meta):
verbose_name = "Moderation Report"
verbose_name_plural = "Moderation Reports"
- ordering = ['-created_at']
+ ordering = ["-created_at"]
indexes = [
- models.Index(fields=['status', 'priority']),
- models.Index(fields=['reported_by']),
- models.Index(fields=['assigned_moderator']),
- models.Index(fields=['created_at']),
+ models.Index(fields=["status", "priority"]),
+ models.Index(fields=["reported_by"]),
+ models.Index(fields=["assigned_moderator"]),
+ models.Index(fields=["created_at"]),
]
def __str__(self):
- return f"{self.get_report_type_display()} report by {self.reported_by.username}" # type: ignore
+ return f"{self.get_report_type_display()} report by {self.reported_by.username}" # type: ignore
@pghistory.track()
@@ -499,37 +455,20 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
state_field_name = "status"
# Queue item details
- item_type = RichChoiceField(
- choice_group="queue_item_types",
- domain="moderation",
- max_length=50
- )
+ item_type = RichChoiceField(choice_group="queue_item_types", domain="moderation", max_length=50)
status = RichFSMField(
- choice_group="moderation_queue_statuses",
- domain="moderation",
- max_length=20,
- default='PENDING'
- )
- priority = RichChoiceField(
- choice_group="priority_levels",
- domain="moderation",
- max_length=10,
- default='MEDIUM'
+ choice_group="moderation_queue_statuses", domain="moderation", max_length=20, default="PENDING"
)
+ priority = RichChoiceField(choice_group="priority_levels", domain="moderation", max_length=10, default="MEDIUM")
title = models.CharField(max_length=200, help_text="Brief title for the queue item")
- description = models.TextField(
- help_text="Detailed description of what needs to be done")
+ description = models.TextField(help_text="Detailed description of what needs to be done")
# What entity this relates to
- entity_type = models.CharField(
- max_length=50, blank=True, help_text="Type of entity (park, ride, user, etc.)")
- entity_id = models.PositiveIntegerField(
- null=True, blank=True, help_text="ID of the related entity")
- entity_preview = models.JSONField(
- default=dict, blank=True, help_text="Preview data for the entity")
- content_type = models.ForeignKey(
- ContentType, on_delete=models.CASCADE, null=True, blank=True)
+ entity_type = models.CharField(max_length=50, blank=True, help_text="Type of entity (park, ride, user, etc.)")
+ entity_id = models.PositiveIntegerField(null=True, blank=True, help_text="ID of the related entity")
+ entity_preview = models.JSONField(default=dict, blank=True, help_text="Preview data for the entity")
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
# Assignment and timing
assigned_to = models.ForeignKey(
@@ -537,14 +476,11 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
on_delete=models.SET_NULL,
null=True,
blank=True,
- related_name='assigned_queue_items',
+ related_name="assigned_queue_items",
help_text="Moderator assigned to this item",
)
- assigned_at = models.DateTimeField(
- null=True, blank=True, help_text="When this item was assigned"
- )
- estimated_review_time = models.PositiveIntegerField(
- default=30, help_text="Estimated time in minutes")
+ assigned_at = models.DateTimeField(null=True, blank=True, help_text="When this item was assigned")
+ estimated_review_time = models.PositiveIntegerField(default=30, help_text="Estimated time in minutes")
# Metadata
flagged_by = models.ForeignKey(
@@ -552,11 +488,10 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
on_delete=models.SET_NULL,
null=True,
blank=True,
- related_name='flagged_queue_items',
+ related_name="flagged_queue_items",
help_text="User who flagged this item",
)
- tags = models.JSONField(default=list, blank=True,
- help_text="Tags for categorization")
+ tags = models.JSONField(default=list, blank=True, help_text="Tags for categorization")
# Related objects
related_report = models.ForeignKey(
@@ -564,30 +499,26 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
on_delete=models.CASCADE,
null=True,
blank=True,
- related_name='queue_items',
+ related_name="queue_items",
help_text="Related moderation report",
)
# Timestamps
- created_at = models.DateTimeField(
- auto_now_add=True, help_text="When this item was created"
- )
- updated_at = models.DateTimeField(
- auto_now=True, help_text="When this item was last updated"
- )
+ created_at = models.DateTimeField(auto_now_add=True, help_text="When this item was created")
+ updated_at = models.DateTimeField(auto_now=True, help_text="When this item was last updated")
class Meta(TrackedModel.Meta):
verbose_name = "Moderation Queue Item"
verbose_name_plural = "Moderation Queue Items"
- ordering = ['priority', 'created_at']
+ ordering = ["priority", "created_at"]
indexes = [
- models.Index(fields=['status', 'priority']),
- models.Index(fields=['assigned_to']),
- models.Index(fields=['created_at']),
+ models.Index(fields=["status", "priority"]),
+ models.Index(fields=["assigned_to"]),
+ models.Index(fields=["created_at"]),
]
def __str__(self):
- return f"{self.get_item_type_display()}: {self.title}" # type: ignore
+ return f"{self.get_item_type_display()}: {self.title}" # type: ignore
@pghistory.track()
@@ -600,36 +531,28 @@ class ModerationAction(TrackedModel):
"""
# Action details
- action_type = RichChoiceField(
- choice_group="moderation_action_types",
- domain="moderation",
- max_length=50
- )
+ action_type = RichChoiceField(choice_group="moderation_action_types", domain="moderation", max_length=50)
reason = models.CharField(max_length=200, help_text="Brief reason for the action")
details = models.TextField(help_text="Detailed explanation of the action")
# Duration (for temporary actions)
duration_hours = models.PositiveIntegerField(
- null=True,
- blank=True,
- help_text="Duration in hours for temporary actions"
+ null=True, blank=True, help_text="Duration in hours for temporary actions"
)
- expires_at = models.DateTimeField(
- null=True, blank=True, help_text="When this action expires")
- is_active = models.BooleanField(
- default=True, help_text="Whether this action is currently active")
+ expires_at = models.DateTimeField(null=True, blank=True, help_text="When this action expires")
+ is_active = models.BooleanField(default=True, help_text="Whether this action is currently active")
# Users involved
moderator = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
- related_name='moderation_actions_taken',
+ related_name="moderation_actions_taken",
help_text="Moderator who took this action",
)
target_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
- related_name='moderation_actions_received',
+ related_name="moderation_actions_received",
help_text="User this action was taken against",
)
@@ -639,31 +562,27 @@ class ModerationAction(TrackedModel):
on_delete=models.SET_NULL,
null=True,
blank=True,
- related_name='actions_taken',
+ related_name="actions_taken",
help_text="Related moderation report",
)
# Timestamps
- created_at = models.DateTimeField(
- auto_now_add=True, help_text="When this action was created"
- )
- updated_at = models.DateTimeField(
- auto_now=True, help_text="When this action was last updated"
- )
+ created_at = models.DateTimeField(auto_now_add=True, help_text="When this action was created")
+ updated_at = models.DateTimeField(auto_now=True, help_text="When this action was last updated")
class Meta(TrackedModel.Meta):
verbose_name = "Moderation Action"
verbose_name_plural = "Moderation Actions"
- ordering = ['-created_at']
+ ordering = ["-created_at"]
indexes = [
- models.Index(fields=['target_user', 'is_active']),
- models.Index(fields=['moderator']),
- models.Index(fields=['expires_at']),
- models.Index(fields=['created_at']),
+ models.Index(fields=["target_user", "is_active"]),
+ models.Index(fields=["moderator"]),
+ models.Index(fields=["expires_at"]),
+ models.Index(fields=["created_at"]),
]
def __str__(self):
- return f"{self.get_action_type_display()} against {self.target_user.username} by {self.moderator.username}" # type: ignore
+ return f"{self.get_action_type_display()} against {self.target_user.username} by {self.moderator.username}" # type: ignore
def save(self, *args, **kwargs):
# Set expiration time if duration is provided
@@ -684,85 +603,56 @@ class BulkOperation(StateMachineMixin, TrackedModel):
state_field_name = "status"
# Operation details
- operation_type = RichChoiceField(
- choice_group="bulk_operation_types",
- domain="moderation",
- max_length=50
- )
- status = RichFSMField(
- choice_group="bulk_operation_statuses",
- domain="moderation",
- max_length=20,
- default='PENDING'
- )
- priority = RichChoiceField(
- choice_group="priority_levels",
- domain="moderation",
- max_length=10,
- default='MEDIUM'
- )
+ operation_type = RichChoiceField(choice_group="bulk_operation_types", domain="moderation", max_length=50)
+ status = RichFSMField(choice_group="bulk_operation_statuses", domain="moderation", max_length=20, default="PENDING")
+ priority = RichChoiceField(choice_group="priority_levels", domain="moderation", max_length=10, default="MEDIUM")
description = models.TextField(help_text="Description of what this operation does")
# Operation parameters and results
- parameters = models.JSONField(
- default=dict, help_text="Parameters for the operation")
- results = models.JSONField(default=dict, blank=True,
- help_text="Results and output from the operation")
+ parameters = models.JSONField(default=dict, help_text="Parameters for the operation")
+ results = models.JSONField(default=dict, blank=True, help_text="Results and output from the operation")
# Progress tracking
- total_items = models.PositiveIntegerField(
- default=0, help_text="Total number of items to process")
- processed_items = models.PositiveIntegerField(
- default=0, help_text="Number of items processed")
- failed_items = models.PositiveIntegerField(
- default=0, help_text="Number of items that failed")
+ total_items = models.PositiveIntegerField(default=0, help_text="Total number of items to process")
+ processed_items = models.PositiveIntegerField(default=0, help_text="Number of items processed")
+ failed_items = models.PositiveIntegerField(default=0, help_text="Number of items that failed")
# Timing
estimated_duration_minutes = models.PositiveIntegerField(
- null=True,
- blank=True,
- help_text="Estimated duration in minutes"
+ null=True, blank=True, help_text="Estimated duration in minutes"
)
- schedule_for = models.DateTimeField(
- null=True, blank=True, help_text="When to run this operation")
+ schedule_for = models.DateTimeField(null=True, blank=True, help_text="When to run this operation")
# Control
- can_cancel = models.BooleanField(
- default=True, help_text="Whether this operation can be cancelled")
+ can_cancel = models.BooleanField(default=True, help_text="Whether this operation can be cancelled")
# User who created the operation
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
- related_name='bulk_operations_created',
+ related_name="bulk_operations_created",
help_text="User who created this operation",
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
- started_at = models.DateTimeField(
- null=True, blank=True, help_text="When this operation started"
- )
- completed_at = models.DateTimeField(
- null=True, blank=True, help_text="When this operation completed"
- )
- updated_at = models.DateTimeField(
- auto_now=True, help_text="When this operation was last updated"
- )
+ started_at = models.DateTimeField(null=True, blank=True, help_text="When this operation started")
+ completed_at = models.DateTimeField(null=True, blank=True, help_text="When this operation completed")
+ updated_at = models.DateTimeField(auto_now=True, help_text="When this operation was last updated")
class Meta(TrackedModel.Meta):
verbose_name = "Bulk Operation"
verbose_name_plural = "Bulk Operations"
- ordering = ['-created_at']
+ ordering = ["-created_at"]
indexes = [
- models.Index(fields=['status', 'priority']),
- models.Index(fields=['created_by']),
- models.Index(fields=['schedule_for']),
- models.Index(fields=['created_at']),
+ models.Index(fields=["status", "priority"]),
+ models.Index(fields=["created_by"]),
+ models.Index(fields=["schedule_for"]),
+ models.Index(fields=["created_at"]),
]
def __str__(self):
- return f"{self.get_operation_type_display()}: {self.description[:50]}" # type: ignore
+ return f"{self.get_operation_type_display()}: {self.description[:50]}" # type: ignore
@property
def progress_percentage(self):
@@ -792,28 +682,21 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
on_delete=models.CASCADE,
help_text="Type of object this photo is for",
)
- object_id = models.PositiveIntegerField(
- help_text="ID of object this photo is for"
- )
+ object_id = models.PositiveIntegerField(help_text="ID of object this photo is for")
content_object = GenericForeignKey("content_type", "object_id")
# The photo itself
photo = models.ForeignKey(
- 'django_cloudflareimages_toolkit.CloudflareImage',
+ "django_cloudflareimages_toolkit.CloudflareImage",
on_delete=models.CASCADE,
- help_text="Photo submission stored on Cloudflare Images"
+ help_text="Photo submission stored on Cloudflare Images",
)
caption = models.CharField(max_length=255, blank=True, help_text="Photo caption")
- date_taken = models.DateField(
- null=True, blank=True, help_text="Date the photo was taken"
- )
+ date_taken = models.DateField(null=True, blank=True, help_text="Date the photo was taken")
# Metadata
status = RichFSMField(
- choice_group="photo_submission_statuses",
- domain="moderation",
- max_length=20,
- default="PENDING"
+ choice_group="photo_submission_statuses", domain="moderation", max_length=20, default="PENDING"
)
created_at = models.DateTimeField(auto_now_add=True)
@@ -826,9 +709,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
related_name="handled_photos",
help_text="Moderator who handled this submission",
)
- handled_at = models.DateTimeField(
- null=True, blank=True, help_text="When this submission was handled"
- )
+ handled_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was handled")
notes = models.TextField(
blank=True,
help_text="Notes from the moderator about this photo submission",
@@ -843,9 +724,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
related_name="claimed_photo_submissions",
help_text="Moderator who has claimed this submission for review",
)
- claimed_at = models.DateTimeField(
- null=True, blank=True, help_text="When this submission was claimed"
- )
+ claimed_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was claimed")
class Meta(TrackedModel.Meta):
verbose_name = "Photo Submission"
@@ -873,9 +752,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
from django.core.exceptions import ValidationError
if self.status != "PENDING":
- raise ValidationError(
- f"Cannot claim submission: current status is {self.status}, expected PENDING"
- )
+ raise ValidationError(f"Cannot claim submission: current status is {self.status}, expected PENDING")
self.transition_to_claimed(user=user)
self.claimed_by = user
@@ -896,9 +773,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
from django.core.exceptions import ValidationError
if self.status != "CLAIMED":
- raise ValidationError(
- f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED"
- )
+ raise ValidationError(f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED")
# Set status directly (not via FSM transition to avoid cycle)
# This is intentional - the unclaim action is a special "rollback" operation
diff --git a/backend/apps/moderation/permissions.py b/backend/apps/moderation/permissions.py
index 0f83a6fa..23db0bb1 100644
--- a/backend/apps/moderation/permissions.py
+++ b/backend/apps/moderation/permissions.py
@@ -88,7 +88,7 @@ class PermissionGuardAdapter:
return False
# Check object permission if available
- if hasattr(permission, "has_object_permission"):
+ if hasattr(permission, "has_object_permission"): # noqa: SIM102
if not permission.has_object_permission(mock_request, None, instance):
self._last_error_code = "OBJECT_PERMISSION_DENIED"
return False
@@ -318,9 +318,7 @@ class CanAssignModerationTasks(GuardMixin, permissions.BasePermission):
# Moderators can only assign to themselves
if user_role == "MODERATOR":
# Check if they're trying to assign to themselves
- assignee_id = request.data.get("moderator_id") or request.data.get(
- "assigned_to"
- )
+ assignee_id = request.data.get("moderator_id") or request.data.get("assigned_to")
if assignee_id:
return str(assignee_id) == str(request.user.id)
return True
@@ -362,7 +360,7 @@ class CanPerformBulkOperations(GuardMixin, permissions.BasePermission):
# Add any admin-specific restrictions for bulk operations here
# For example, admins might not be able to perform certain destructive operations
operation_type = getattr(obj, "operation_type", None)
- if operation_type in ["DELETE_USERS", "PURGE_DATA"]:
+ if operation_type in ["DELETE_USERS", "PURGE_DATA"]: # noqa: SIM103
return False # Only superusers can perform these operations
return True
diff --git a/backend/apps/moderation/selectors.py b/backend/apps/moderation/selectors.py
index 18cb08e3..cab642b5 100644
--- a/backend/apps/moderation/selectors.py
+++ b/backend/apps/moderation/selectors.py
@@ -14,9 +14,7 @@ from django.utils import timezone
from .models import EditSubmission
-def pending_submissions_for_review(
- *, content_type: str | None = None, limit: int = 50
-) -> QuerySet[EditSubmission]:
+def pending_submissions_for_review(*, content_type: str | None = None, limit: int = 50) -> QuerySet[EditSubmission]:
"""
Get pending submissions that need moderation review.
@@ -39,9 +37,7 @@ def pending_submissions_for_review(
return queryset.order_by("created_at")[:limit]
-def submissions_by_user(
- *, user_id: int, status: str | None = None
-) -> QuerySet[EditSubmission]:
+def submissions_by_user(*, user_id: int, status: str | None = None) -> QuerySet[EditSubmission]:
"""
Get submissions created by a specific user.
@@ -52,9 +48,7 @@ def submissions_by_user(
Returns:
QuerySet of user's submissions
"""
- queryset = EditSubmission.objects.filter(user_id=user_id).select_related(
- "content_type", "handled_by"
- )
+ queryset = EditSubmission.objects.filter(user_id=user_id).select_related("content_type", "handled_by")
if status:
queryset = queryset.filter(status=status)
@@ -62,9 +56,7 @@ def submissions_by_user(
return queryset.order_by("-created_at")
-def submissions_handled_by_moderator(
- *, moderator_id: int, days: int = 30
-) -> QuerySet[EditSubmission]:
+def submissions_handled_by_moderator(*, moderator_id: int, days: int = 30) -> QuerySet[EditSubmission]:
"""
Get submissions handled by a specific moderator in the last N days.
@@ -78,9 +70,7 @@ def submissions_handled_by_moderator(
cutoff_date = timezone.now() - timedelta(days=days)
return (
- EditSubmission.objects.filter(
- handled_by_id=moderator_id, handled_at__gte=cutoff_date
- )
+ EditSubmission.objects.filter(handled_by_id=moderator_id, handled_at__gte=cutoff_date)
.select_related("user", "content_type")
.order_by("-handled_at")
)
@@ -105,9 +95,7 @@ def recent_submissions(*, days: int = 7) -> QuerySet[EditSubmission]:
)
-def submissions_by_content_type(
- *, content_type: str, status: str | None = None
-) -> QuerySet[EditSubmission]:
+def submissions_by_content_type(*, content_type: str, status: str | None = None) -> QuerySet[EditSubmission]:
"""
Get submissions for a specific content type.
@@ -118,9 +106,9 @@ def submissions_by_content_type(
Returns:
QuerySet of submissions for the content type
"""
- queryset = EditSubmission.objects.filter(
- content_type__model=content_type.lower()
- ).select_related("user", "handled_by")
+ queryset = EditSubmission.objects.filter(content_type__model=content_type.lower()).select_related(
+ "user", "handled_by"
+ )
if status:
queryset = queryset.filter(status=status)
@@ -136,12 +124,8 @@ def moderation_queue_summary() -> dict[str, Any]:
Dictionary containing queue statistics
"""
pending_count = EditSubmission.objects.filter(status="PENDING").count()
- approved_today = EditSubmission.objects.filter(
- status="APPROVED", handled_at__date=timezone.now().date()
- ).count()
- rejected_today = EditSubmission.objects.filter(
- status="REJECTED", handled_at__date=timezone.now().date()
- ).count()
+ approved_today = EditSubmission.objects.filter(status="APPROVED", handled_at__date=timezone.now().date()).count()
+ rejected_today = EditSubmission.objects.filter(status="REJECTED", handled_at__date=timezone.now().date()).count()
# Submissions by content type
submissions_by_type = (
@@ -159,9 +143,7 @@ def moderation_queue_summary() -> dict[str, Any]:
}
-def moderation_statistics_summary(
- *, days: int = 30, moderator: User | None = None
-) -> dict[str, Any]:
+def moderation_statistics_summary(*, days: int = 30, moderator: User | None = None) -> dict[str, Any]:
"""
Get comprehensive moderation statistics for a time period.
@@ -189,8 +171,7 @@ def moderation_statistics_summary(
handled_queryset.exclude(handled_at__isnull=True)
.annotate(
response_hours=ExpressionWrapper(
- Extract(F('handled_at') - F('created_at'), 'epoch') / 3600.0,
- output_field=FloatField()
+ Extract(F("handled_at") - F("created_at"), "epoch") / 3600.0, output_field=FloatField()
)
)
.values_list("response_hours", flat=True)
diff --git a/backend/apps/moderation/serializers.py b/backend/apps/moderation/serializers.py
index 8f97a425..fb49a627 100644
--- a/backend/apps/moderation/serializers.py
+++ b/backend/apps/moderation/serializers.py
@@ -68,9 +68,7 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
submitted_by = UserBasicSerializer(source="user", read_only=True)
claimed_by = UserBasicSerializer(read_only=True)
- content_type_name = serializers.CharField(
- source="content_type.model", read_only=True
- )
+ content_type_name = serializers.CharField(source="content_type.model", read_only=True)
# UI Metadata fields for Nuxt rendering
status_color = serializers.SerializerMethodField()
@@ -117,10 +115,10 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
def get_status_color(self, obj) -> str:
"""Return hex color based on status for UI badges."""
colors = {
- "PENDING": "#f59e0b", # Amber
- "CLAIMED": "#3b82f6", # Blue
- "APPROVED": "#10b981", # Emerald
- "REJECTED": "#ef4444", # Red
+ "PENDING": "#f59e0b", # Amber
+ "CLAIMED": "#3b82f6", # Blue
+ "APPROVED": "#10b981", # Emerald
+ "REJECTED": "#ef4444", # Red
"ESCALATED": "#8b5cf6", # Violet
}
return colors.get(obj.status, "#6b7280") # Default gray
@@ -154,15 +152,9 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
class EditSubmissionListSerializer(serializers.ModelSerializer):
"""Optimized serializer for EditSubmission lists."""
- submitted_by_username = serializers.CharField(
- source="user.username", read_only=True
- )
- claimed_by_username = serializers.CharField(
- source="claimed_by.username", read_only=True, allow_null=True
- )
- content_type_name = serializers.CharField(
- source="content_type.model", read_only=True
- )
+ submitted_by_username = serializers.CharField(source="user.username", read_only=True)
+ claimed_by_username = serializers.CharField(source="claimed_by.username", read_only=True, allow_null=True)
+ content_type_name = serializers.CharField(source="content_type.model", read_only=True)
status_color = serializers.SerializerMethodField()
status_icon = serializers.SerializerMethodField()
@@ -218,13 +210,9 @@ class ModerationReportSerializer(serializers.ModelSerializer):
# Computed fields
is_overdue = serializers.SerializerMethodField()
time_since_created = serializers.SerializerMethodField()
- priority_display = serializers.CharField(
- source="get_priority_display", read_only=True
- )
+ priority_display = serializers.CharField(source="get_priority_display", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
- report_type_display = serializers.CharField(
- source="get_report_type_display", read_only=True
- )
+ report_type_display = serializers.CharField(source="get_report_type_display", read_only=True)
class Meta:
model = ModerationReport
@@ -318,17 +306,13 @@ class CreateModerationReportSerializer(serializers.ModelSerializer):
valid_entity_types = ["park", "ride", "review", "photo", "user", "comment"]
if attrs["reported_entity_type"] not in valid_entity_types:
raise serializers.ValidationError(
- {
- "reported_entity_type": f'Must be one of: {", ".join(valid_entity_types)}'
- }
+ {"reported_entity_type": f'Must be one of: {", ".join(valid_entity_types)}'}
)
# Validate evidence URLs
evidence_urls = attrs.get("evidence_urls", [])
if not isinstance(evidence_urls, list):
- raise serializers.ValidationError(
- {"evidence_urls": "Must be a list of URLs"}
- )
+ raise serializers.ValidationError({"evidence_urls": "Must be a list of URLs"})
return attrs
@@ -351,9 +335,7 @@ class CreateModerationReportSerializer(serializers.ModelSerializer):
if entity_type in app_label_map:
try:
- content_type = ContentType.objects.get(
- app_label=app_label_map[entity_type], model=entity_type
- )
+ content_type = ContentType.objects.get(app_label=app_label_map[entity_type], model=entity_type)
validated_data["content_type"] = content_type
except ContentType.DoesNotExist:
pass
@@ -377,9 +359,7 @@ class UpdateModerationReportSerializer(serializers.ModelSerializer):
def validate_status(self, value):
"""Validate status transitions."""
if self.instance and self.instance.status == "RESOLVED" and value != "RESOLVED":
- raise serializers.ValidationError(
- "Cannot change status of resolved report"
- )
+ raise serializers.ValidationError("Cannot change status of resolved report")
return value
def update(self, instance, validated_data):
@@ -462,13 +442,9 @@ class ModerationQueueSerializer(serializers.ModelSerializer):
def get_estimated_completion(self, obj) -> str:
"""Estimated completion time."""
if obj.assigned_at:
- completion_time = obj.assigned_at + timedelta(
- minutes=obj.estimated_review_time
- )
+ completion_time = obj.assigned_at + timedelta(minutes=obj.estimated_review_time)
else:
- completion_time = timezone.now() + timedelta(
- minutes=obj.estimated_review_time
- )
+ completion_time = timezone.now() + timedelta(minutes=obj.estimated_review_time)
return completion_time.isoformat()
@@ -484,12 +460,10 @@ class AssignQueueItemSerializer(serializers.Serializer):
user = User.objects.get(id=value)
user_role = getattr(user, "role", "USER")
if user_role not in ["MODERATOR", "ADMIN", "SUPERUSER"]:
- raise serializers.ValidationError(
- "User must be a moderator, admin, or superuser"
- )
+ raise serializers.ValidationError("User must be a moderator, admin, or superuser")
return value
except User.DoesNotExist:
- raise serializers.ValidationError("Moderator not found")
+ raise serializers.ValidationError("Moderator not found") from None
class CompleteQueueItemSerializer(serializers.Serializer):
@@ -514,9 +488,7 @@ class CompleteQueueItemSerializer(serializers.Serializer):
# Require notes for certain actions
if action in ["USER_WARNING", "USER_SUSPENDED", "USER_BANNED"] and not notes:
- raise serializers.ValidationError(
- {"notes": f"Notes are required for action: {action}"}
- )
+ raise serializers.ValidationError({"notes": f"Notes are required for action: {action}"})
return attrs
@@ -536,9 +508,7 @@ class ModerationActionSerializer(serializers.ModelSerializer):
# Computed fields
is_expired = serializers.SerializerMethodField()
time_remaining = serializers.SerializerMethodField()
- action_type_display = serializers.CharField(
- source="get_action_type_display", read_only=True
- )
+ action_type_display = serializers.CharField(source="get_action_type_display", read_only=True)
class Meta:
model = ModerationAction
@@ -620,7 +590,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
User.objects.get(id=value)
return value
except User.DoesNotExist:
- raise serializers.ValidationError("Target user not found")
+ raise serializers.ValidationError("Target user not found") from None
def validate_related_report_id(self, value):
"""Validate related report exists."""
@@ -629,7 +599,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
ModerationReport.objects.get(id=value)
return value
except ModerationReport.DoesNotExist:
- raise serializers.ValidationError("Related report not found")
+ raise serializers.ValidationError("Related report not found") from None
return value
def validate(self, attrs):
@@ -640,17 +610,11 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
# Validate duration for temporary actions
temporary_actions = ["USER_SUSPENSION", "CONTENT_RESTRICTION"]
if action_type in temporary_actions and not duration_hours:
- raise serializers.ValidationError(
- {"duration_hours": f"Duration is required for {action_type}"}
- )
+ raise serializers.ValidationError({"duration_hours": f"Duration is required for {action_type}"})
# Validate duration range
- if duration_hours and (
- duration_hours < 1 or duration_hours > 8760
- ): # 1 hour to 1 year
- raise serializers.ValidationError(
- {"duration_hours": "Duration must be between 1 and 8760 hours (1 year)"}
- )
+ if duration_hours and (duration_hours < 1 or duration_hours > 8760): # 1 hour to 1 year
+ raise serializers.ValidationError({"duration_hours": "Duration must be between 1 and 8760 hours (1 year)"})
return attrs
@@ -668,9 +632,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
# Set expiration time for temporary actions
if validated_data.get("duration_hours"):
- validated_data["expires_at"] = timezone.now() + timedelta(
- hours=validated_data["duration_hours"]
- )
+ validated_data["expires_at"] = timezone.now() + timedelta(hours=validated_data["duration_hours"])
return super().create(validated_data)
@@ -688,9 +650,7 @@ class BulkOperationSerializer(serializers.ModelSerializer):
# Computed fields
progress_percentage = serializers.SerializerMethodField()
estimated_completion = serializers.SerializerMethodField()
- operation_type_display = serializers.CharField(
- source="get_operation_type_display", read_only=True
- )
+ operation_type_display = serializers.CharField(source="get_operation_type_display", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
@@ -741,17 +701,13 @@ class BulkOperationSerializer(serializers.ModelSerializer):
if obj.status == "COMPLETED":
return obj.completed_at.isoformat() if obj.completed_at else None
- if obj.status == "RUNNING" and obj.started_at:
+ if obj.status == "RUNNING" and obj.started_at: # noqa: SIM102
# Calculate based on current progress
if obj.processed_items > 0:
elapsed_minutes = (timezone.now() - obj.started_at).total_seconds() / 60
rate = obj.processed_items / elapsed_minutes
remaining_items = obj.total_items - obj.processed_items
- remaining_minutes = (
- remaining_items / rate
- if rate > 0
- else obj.estimated_duration_minutes
- )
+ remaining_minutes = remaining_items / rate if rate > 0 else obj.estimated_duration_minutes
completion_time = timezone.now() + timedelta(minutes=remaining_minutes)
return completion_time.isoformat()
@@ -759,9 +715,7 @@ class BulkOperationSerializer(serializers.ModelSerializer):
if obj.schedule_for:
return obj.schedule_for.isoformat()
elif obj.estimated_duration_minutes:
- completion_time = timezone.now() + timedelta(
- minutes=obj.estimated_duration_minutes
- )
+ completion_time = timezone.now() + timedelta(minutes=obj.estimated_duration_minutes)
return completion_time.isoformat()
return None
@@ -801,9 +755,7 @@ class CreateBulkOperationSerializer(serializers.ModelSerializer):
if operation_type in required_params:
for param in required_params[operation_type]:
if param not in value:
- raise serializers.ValidationError(
- f'Parameter "{param}" is required for {operation_type}'
- )
+ raise serializers.ValidationError(f'Parameter "{param}" is required for {operation_type}')
return value
@@ -902,27 +854,28 @@ class UserModerationProfileSerializer(serializers.Serializer):
class StateLogSerializer(serializers.ModelSerializer):
"""Serializer for FSM transition history."""
- user = serializers.CharField(source='by.username', read_only=True)
- model = serializers.CharField(source='content_type.model', read_only=True)
- from_state = serializers.CharField(source='source_state', read_only=True)
- to_state = serializers.CharField(source='state', read_only=True)
- reason = serializers.CharField(source='description', read_only=True)
+ user = serializers.CharField(source="by.username", read_only=True)
+ model = serializers.CharField(source="content_type.model", read_only=True)
+ from_state = serializers.CharField(source="source_state", read_only=True)
+ to_state = serializers.CharField(source="state", read_only=True)
+ reason = serializers.CharField(source="description", read_only=True)
class Meta:
from django_fsm_log.models import StateLog
+
model = StateLog
fields = [
- 'id',
- 'timestamp',
- 'model',
- 'object_id',
- 'state',
- 'from_state',
- 'to_state',
- 'transition',
- 'user',
- 'description',
- 'reason',
+ "id",
+ "timestamp",
+ "model",
+ "object_id",
+ "state",
+ "from_state",
+ "to_state",
+ "transition",
+ "user",
+ "description",
+ "reason",
]
read_only_fields = fields
@@ -931,9 +884,7 @@ class PhotoSubmissionSerializer(serializers.ModelSerializer):
"""Serializer for PhotoSubmission."""
submitted_by = UserBasicSerializer(source="user", read_only=True)
- content_type_name = serializers.CharField(
- source="content_type.model", read_only=True
- )
+ content_type_name = serializers.CharField(source="content_type.model", read_only=True)
photo_url = serializers.SerializerMethodField()
# UI Metadata
@@ -1012,4 +963,3 @@ class PhotoSubmissionSerializer(serializers.ModelSerializer):
else:
minutes = diff.seconds // 60
return f"{minutes} minutes ago"
-
diff --git a/backend/apps/moderation/services.py b/backend/apps/moderation/services.py
index 46778b0f..bead54d6 100644
--- a/backend/apps/moderation/services.py
+++ b/backend/apps/moderation/services.py
@@ -19,9 +19,7 @@ class ModerationService:
"""Service for handling content moderation workflows."""
@staticmethod
- def approve_submission(
- *, submission_id: int, moderator: User, notes: str | None = None
- ) -> object | None:
+ def approve_submission(*, submission_id: int, moderator: User, notes: str | None = None) -> object | None:
"""
Approve a content submission and apply changes.
@@ -39,9 +37,7 @@ class ModerationService:
ValueError: If submission cannot be processed
"""
with transaction.atomic():
- submission = EditSubmission.objects.select_for_update().get(
- id=submission_id
- )
+ submission = EditSubmission.objects.select_for_update().get(id=submission_id)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending approval")
@@ -75,9 +71,7 @@ class ModerationService:
raise
@staticmethod
- def reject_submission(
- *, submission_id: int, moderator: User, reason: str
- ) -> EditSubmission:
+ def reject_submission(*, submission_id: int, moderator: User, reason: str) -> EditSubmission:
"""
Reject a content submission.
@@ -94,9 +88,7 @@ class ModerationService:
ValueError: If submission cannot be rejected
"""
with transaction.atomic():
- submission = EditSubmission.objects.select_for_update().get(
- id=submission_id
- )
+ submission = EditSubmission.objects.select_for_update().get(id=submission_id)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending review")
@@ -175,9 +167,7 @@ class ModerationService:
ValueError: If submission cannot be modified
"""
with transaction.atomic():
- submission = EditSubmission.objects.select_for_update().get(
- id=submission_id
- )
+ submission = EditSubmission.objects.select_for_update().get(id=submission_id)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending review")
@@ -220,9 +210,7 @@ class ModerationService:
return pending_submissions_for_review(content_type=content_type, limit=limit)
@staticmethod
- def get_submission_statistics(
- *, days: int = 30, moderator: User | None = None
- ) -> dict[str, Any]:
+ def get_submission_statistics(*, days: int = 30, moderator: User | None = None) -> dict[str, Any]:
"""
Get moderation statistics for a time period.
@@ -248,7 +236,7 @@ class ModerationService:
Returns:
True if user is MODERATOR, ADMIN, or SUPERUSER
"""
- return user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
+ return user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]
@staticmethod
def create_edit_submission_with_queue(
@@ -297,33 +285,32 @@ class ModerationService:
try:
created_object = submission.approve(submitter)
return {
- 'submission': submission,
- 'status': 'auto_approved',
- 'created_object': created_object,
- 'queue_item': None,
- 'message': 'Submission auto-approved for moderator'
+ "submission": submission,
+ "status": "auto_approved",
+ "created_object": created_object,
+ "queue_item": None,
+ "message": "Submission auto-approved for moderator",
}
except Exception as e:
return {
- 'submission': submission,
- 'status': 'failed',
- 'created_object': None,
- 'queue_item': None,
- 'message': f'Auto-approval failed: {str(e)}'
+ "submission": submission,
+ "status": "failed",
+ "created_object": None,
+ "queue_item": None,
+ "message": f"Auto-approval failed: {str(e)}",
}
else:
# Create queue item for regular users
queue_item = ModerationService._create_queue_item_for_submission(
- submission=submission,
- submitter=submitter
+ submission=submission, submitter=submitter
)
return {
- 'submission': submission,
- 'status': 'queued',
- 'created_object': None,
- 'queue_item': queue_item,
- 'message': 'Submission added to moderation queue'
+ "submission": submission,
+ "status": "queued",
+ "created_object": None,
+ "queue_item": queue_item,
+ "message": "Submission added to moderation queue",
}
@staticmethod
@@ -370,36 +357,33 @@ class ModerationService:
try:
submission.auto_approve()
return {
- 'submission': submission,
- 'status': 'auto_approved',
- 'queue_item': None,
- 'message': 'Photo submission auto-approved for moderator'
+ "submission": submission,
+ "status": "auto_approved",
+ "queue_item": None,
+ "message": "Photo submission auto-approved for moderator",
}
except Exception as e:
return {
- 'submission': submission,
- 'status': 'failed',
- 'queue_item': None,
- 'message': f'Auto-approval failed: {str(e)}'
+ "submission": submission,
+ "status": "failed",
+ "queue_item": None,
+ "message": f"Auto-approval failed: {str(e)}",
}
else:
# Create queue item for regular users
queue_item = ModerationService._create_queue_item_for_photo_submission(
- submission=submission,
- submitter=submitter
+ submission=submission, submitter=submitter
)
return {
- 'submission': submission,
- 'status': 'queued',
- 'queue_item': queue_item,
- 'message': 'Photo submission added to moderation queue'
+ "submission": submission,
+ "status": "queued",
+ "queue_item": queue_item,
+ "message": "Photo submission added to moderation queue",
}
@staticmethod
- def _create_queue_item_for_submission(
- *, submission: EditSubmission, submitter: User
- ) -> ModerationQueue:
+ def _create_queue_item_for_submission(*, submission: EditSubmission, submitter: User) -> ModerationQueue:
"""
Create a moderation queue item for an edit submission.
@@ -417,13 +401,13 @@ class ModerationService:
# Create preview data
entity_preview = {
- 'submission_type': submission.submission_type,
- 'changes_count': len(submission.changes) if submission.changes else 0,
- 'reason': submission.reason[:100] if submission.reason else "",
+ "submission_type": submission.submission_type,
+ "changes_count": len(submission.changes) if submission.changes else 0,
+ "reason": submission.reason[:100] if submission.reason else "",
}
if submission.content_object:
- entity_preview['object_name'] = str(submission.content_object)
+ entity_preview["object_name"] = str(submission.content_object)
# Determine title and description
action = "creation" if submission.submission_type == "CREATE" else "edit"
@@ -435,7 +419,7 @@ class ModerationService:
# Create queue item
queue_item = ModerationQueue(
- item_type='CONTENT_REVIEW',
+ item_type="CONTENT_REVIEW",
title=title,
description=description,
entity_type=entity_type,
@@ -443,9 +427,9 @@ class ModerationService:
entity_preview=entity_preview,
content_type=content_type,
flagged_by=submitter,
- priority='MEDIUM',
+ priority="MEDIUM",
estimated_review_time=15, # 15 minutes default
- tags=['edit_submission', submission.submission_type.lower()],
+ tags=["edit_submission", submission.submission_type.lower()],
)
queue_item.full_clean()
@@ -454,9 +438,7 @@ class ModerationService:
return queue_item
@staticmethod
- def _create_queue_item_for_photo_submission(
- *, submission: PhotoSubmission, submitter: User
- ) -> ModerationQueue:
+ def _create_queue_item_for_photo_submission(*, submission: PhotoSubmission, submitter: User) -> ModerationQueue:
"""
Create a moderation queue item for a photo submission.
@@ -474,13 +456,13 @@ class ModerationService:
# Create preview data
entity_preview = {
- 'caption': submission.caption,
- 'date_taken': submission.date_taken.isoformat() if submission.date_taken else None,
- 'photo_url': submission.photo.url if submission.photo else None,
+ "caption": submission.caption,
+ "date_taken": submission.date_taken.isoformat() if submission.date_taken else None,
+ "photo_url": submission.photo.url if submission.photo else None,
}
if submission.content_object:
- entity_preview['object_name'] = str(submission.content_object)
+ entity_preview["object_name"] = str(submission.content_object)
# Create title and description
title = f"Photo submission for {entity_type} by {submitter.username}"
@@ -490,7 +472,7 @@ class ModerationService:
# Create queue item
queue_item = ModerationQueue(
- item_type='CONTENT_REVIEW',
+ item_type="CONTENT_REVIEW",
title=title,
description=description,
entity_type=entity_type,
@@ -498,9 +480,9 @@ class ModerationService:
entity_preview=entity_preview,
content_type=content_type,
flagged_by=submitter,
- priority='LOW', # Photos typically lower priority
+ priority="LOW", # Photos typically lower priority
estimated_review_time=5, # 5 minutes default for photos
- tags=['photo_submission'],
+ tags=["photo_submission"],
)
queue_item.full_clean()
@@ -525,11 +507,9 @@ class ModerationService:
Dictionary with processing results
"""
with transaction.atomic():
- queue_item = ModerationQueue.objects.select_for_update().get(
- id=queue_item_id
- )
+ queue_item = ModerationQueue.objects.select_for_update().get(id=queue_item_id)
- if queue_item.status != 'PENDING':
+ if queue_item.status != "PENDING":
raise ValueError(f"Queue item {queue_item_id} is not pending")
# Transition queue item into an active state before processing
@@ -542,7 +522,7 @@ class ModerationService:
pass
except AttributeError:
# Fallback for environments without the generated transition method
- queue_item.status = 'IN_PROGRESS'
+ queue_item.status = "IN_PROGRESS"
moved_to_in_progress = True
if moved_to_in_progress:
@@ -554,116 +534,94 @@ class ModerationService:
try:
queue_item.transition_to_completed(user=moderator)
except TransitionNotAllowed:
- queue_item.status = 'COMPLETED'
+ queue_item.status = "COMPLETED"
except AttributeError:
- queue_item.status = 'COMPLETED'
+ queue_item.status = "COMPLETED"
# Find related submission
- if 'edit_submission' in queue_item.tags:
+ if "edit_submission" in queue_item.tags:
# Find EditSubmission
submissions = EditSubmission.objects.filter(
user=queue_item.flagged_by,
content_type=queue_item.content_type,
object_id=queue_item.entity_id,
- status='PENDING'
- ).order_by('-created_at')
+ status="PENDING",
+ ).order_by("-created_at")
if not submissions.exists():
- raise ValueError(
- "No pending edit submission found for this queue item")
+ raise ValueError("No pending edit submission found for this queue item")
submission = submissions.first()
- if action == 'approve':
+ if action == "approve":
try:
created_object = submission.approve(moderator)
# Use FSM transition for queue status
_complete_queue_item()
result = {
- 'status': 'approved',
- 'created_object': created_object,
- 'message': 'Submission approved successfully'
+ "status": "approved",
+ "created_object": created_object,
+ "message": "Submission approved successfully",
}
except Exception as e:
# Use FSM transition for queue status
_complete_queue_item()
- result = {
- 'status': 'failed',
- 'created_object': None,
- 'message': f'Approval failed: {str(e)}'
- }
- elif action == 'reject':
+ result = {"status": "failed", "created_object": None, "message": f"Approval failed: {str(e)}"}
+ elif action == "reject":
submission.reject(moderator, notes or "Rejected by moderator")
# Use FSM transition for queue status
_complete_queue_item()
- result = {
- 'status': 'rejected',
- 'created_object': None,
- 'message': 'Submission rejected'
- }
- elif action == 'escalate':
+ result = {"status": "rejected", "created_object": None, "message": "Submission rejected"}
+ elif action == "escalate":
submission.escalate(moderator, notes or "Escalated for review")
- queue_item.priority = 'HIGH'
+ queue_item.priority = "HIGH"
# Keep status as PENDING for escalation
- result = {
- 'status': 'escalated',
- 'created_object': None,
- 'message': 'Submission escalated'
- }
+ result = {"status": "escalated", "created_object": None, "message": "Submission escalated"}
else:
raise ValueError(f"Unknown action: {action}")
- elif 'photo_submission' in queue_item.tags:
+ elif "photo_submission" in queue_item.tags:
# Find PhotoSubmission
submissions = PhotoSubmission.objects.filter(
user=queue_item.flagged_by,
content_type=queue_item.content_type,
object_id=queue_item.entity_id,
- status='PENDING'
- ).order_by('-created_at')
+ status="PENDING",
+ ).order_by("-created_at")
if not submissions.exists():
- raise ValueError(
- "No pending photo submission found for this queue item")
+ raise ValueError("No pending photo submission found for this queue item")
submission = submissions.first()
- if action == 'approve':
+ if action == "approve":
try:
submission.approve(moderator, notes or "")
# Use FSM transition for queue status
_complete_queue_item()
result = {
- 'status': 'approved',
- 'created_object': None,
- 'message': 'Photo submission approved successfully'
+ "status": "approved",
+ "created_object": None,
+ "message": "Photo submission approved successfully",
}
except Exception as e:
# Use FSM transition for queue status
_complete_queue_item()
result = {
- 'status': 'failed',
- 'created_object': None,
- 'message': f'Photo approval failed: {str(e)}'
+ "status": "failed",
+ "created_object": None,
+ "message": f"Photo approval failed: {str(e)}",
}
- elif action == 'reject':
+ elif action == "reject":
submission.reject(moderator, notes or "Rejected by moderator")
# Use FSM transition for queue status
_complete_queue_item()
- result = {
- 'status': 'rejected',
- 'created_object': None,
- 'message': 'Photo submission rejected'
- }
- elif action == 'escalate':
+ result = {"status": "rejected", "created_object": None, "message": "Photo submission rejected"}
+ elif action == "escalate":
submission.escalate(moderator, notes or "Escalated for review")
- queue_item.priority = 'HIGH'
+ queue_item.priority = "HIGH"
# Keep status as PENDING for escalation
- result = {
- 'status': 'escalated',
- 'created_object': None,
- 'message': 'Photo submission escalated'
- }
+ result = {"status": "escalated", "created_object": None, "message": "Photo submission escalated"}
else:
raise ValueError(f"Unknown action: {action}")
else:
@@ -678,5 +636,5 @@ class ModerationService:
queue_item.full_clean()
queue_item.save()
- result['queue_item'] = queue_item
+ result["queue_item"] = queue_item
return result
diff --git a/backend/apps/moderation/signals.py b/backend/apps/moderation/signals.py
index 91dddb22..5e8bb60b 100644
--- a/backend/apps/moderation/signals.py
+++ b/backend/apps/moderation/signals.py
@@ -48,12 +48,10 @@ def handle_submission_claimed(instance, source, target, user, context=None, **kw
user: The user who claimed.
context: Optional TransitionContext.
"""
- if target != 'CLAIMED':
+ if target != "CLAIMED":
return
- logger.info(
- f"Submission {instance.pk} claimed by {user.username if user else 'system'}"
- )
+ logger.info(f"Submission {instance.pk} claimed by {user.username if user else 'system'}")
# Broadcast for real-time dashboard updates
_broadcast_submission_status_change(instance, source, target, user)
@@ -72,12 +70,10 @@ def handle_submission_unclaimed(instance, source, target, user, context=None, **
user: The user who unclaimed.
context: Optional TransitionContext.
"""
- if source != 'CLAIMED' or target != 'PENDING':
+ if source != "CLAIMED" or target != "PENDING":
return
- logger.info(
- f"Submission {instance.pk} unclaimed by {user.username if user else 'system'}"
- )
+ logger.info(f"Submission {instance.pk} unclaimed by {user.username if user else 'system'}")
# Broadcast for real-time dashboard updates
_broadcast_submission_status_change(instance, source, target, user)
@@ -96,25 +92,21 @@ def handle_submission_approved(instance, source, target, user, context=None, **k
user: The user who approved.
context: Optional TransitionContext.
"""
- if target != 'APPROVED':
+ if target != "APPROVED":
return
- logger.info(
- f"Submission {instance.pk} approved by {user if user else 'system'}"
- )
+ logger.info(f"Submission {instance.pk} approved by {user if user else 'system'}")
# Trigger notification (handled by NotificationCallback)
# Invalidate cache (handled by CacheInvalidationCallback)
# Apply the submission changes if applicable
- if hasattr(instance, 'apply_changes'):
+ if hasattr(instance, "apply_changes"):
try:
instance.apply_changes()
logger.info(f"Applied changes for submission {instance.pk}")
except Exception as e:
- logger.exception(
- f"Failed to apply changes for submission {instance.pk}: {e}"
- )
+ logger.exception(f"Failed to apply changes for submission {instance.pk}: {e}")
def handle_submission_rejected(instance, source, target, user, context=None, **kwargs):
@@ -130,13 +122,12 @@ def handle_submission_rejected(instance, source, target, user, context=None, **k
user: The user who rejected.
context: Optional TransitionContext.
"""
- if target != 'REJECTED':
+ if target != "REJECTED":
return
- reason = context.extra_data.get('reason', '') if context else ''
+ reason = context.extra_data.get("reason", "") if context else ""
logger.info(
- f"Submission {instance.pk} rejected by {user if user else 'system'}"
- f"{f': {reason}' if reason else ''}"
+ f"Submission {instance.pk} rejected by {user if user else 'system'}" f"{f': {reason}' if reason else ''}"
)
@@ -153,13 +144,12 @@ def handle_submission_escalated(instance, source, target, user, context=None, **
user: The user who escalated.
context: Optional TransitionContext.
"""
- if target != 'ESCALATED':
+ if target != "ESCALATED":
return
- reason = context.extra_data.get('reason', '') if context else ''
+ reason = context.extra_data.get("reason", "") if context else ""
logger.info(
- f"Submission {instance.pk} escalated by {user if user else 'system'}"
- f"{f': {reason}' if reason else ''}"
+ f"Submission {instance.pk} escalated by {user if user else 'system'}" f"{f': {reason}' if reason else ''}"
)
# Create escalation task if task system is available
@@ -179,15 +169,13 @@ def handle_report_resolved(instance, source, target, user, context=None, **kwarg
user: The user who resolved.
context: Optional TransitionContext.
"""
- if target != 'RESOLVED':
+ if target != "RESOLVED":
return
- logger.info(
- f"ModerationReport {instance.pk} resolved by {user if user else 'system'}"
- )
+ logger.info(f"ModerationReport {instance.pk} resolved by {user if user else 'system'}")
# Update related queue items
- _update_related_queue_items(instance, 'COMPLETED')
+ _update_related_queue_items(instance, "COMPLETED")
def handle_queue_completed(instance, source, target, user, context=None, **kwargs):
@@ -203,12 +191,10 @@ def handle_queue_completed(instance, source, target, user, context=None, **kwarg
user: The user who completed.
context: Optional TransitionContext.
"""
- if target != 'COMPLETED':
+ if target != "COMPLETED":
return
- logger.info(
- f"ModerationQueue {instance.pk} completed by {user if user else 'system'}"
- )
+ logger.info(f"ModerationQueue {instance.pk} completed by {user if user else 'system'}")
# Update moderation statistics
_update_moderation_stats(instance, user)
@@ -227,18 +213,17 @@ def handle_bulk_operation_status(instance, source, target, user, context=None, *
user: The user who initiated the change.
context: Optional TransitionContext.
"""
- logger.info(
- f"BulkOperation {instance.pk} transitioned: {source} → {target}"
- )
+ logger.info(f"BulkOperation {instance.pk} transitioned: {source} → {target}")
- if target == 'COMPLETED':
+ if target == "COMPLETED":
_finalize_bulk_operation(instance, success=True)
- elif target == 'FAILED':
+ elif target == "FAILED":
_finalize_bulk_operation(instance, success=False)
# Helper functions
+
def _create_escalation_task(instance, user, reason):
"""Create an escalation task for admin review."""
try:
@@ -247,7 +232,7 @@ def _create_escalation_task(instance, user, reason):
# Create a queue item for the escalated submission
ModerationQueue.objects.create(
content_object=instance,
- priority='HIGH',
+ priority="HIGH",
reason=f"Escalated: {reason}" if reason else "Escalated for review",
created_by=user,
)
@@ -287,10 +272,10 @@ def _update_moderation_stats(instance, user):
try:
# Update user's moderation count if they have a profile
- profile = getattr(user, 'profile', None)
- if profile and hasattr(profile, 'moderation_count'):
+ profile = getattr(user, "profile", None)
+ if profile and hasattr(profile, "moderation_count"):
profile.moderation_count += 1
- profile.save(update_fields=['moderation_count'])
+ profile.save(update_fields=["moderation_count"])
logger.debug(f"Updated moderation count for {user}")
except Exception as e:
logger.warning(f"Failed to update moderation stats: {e}")
@@ -302,7 +287,7 @@ def _finalize_bulk_operation(instance, success):
from django.utils import timezone
instance.completed_at = timezone.now()
- instance.save(update_fields=['completed_at'])
+ instance.save(update_fields=["completed_at"])
if success:
logger.info(
@@ -312,8 +297,7 @@ def _finalize_bulk_operation(instance, success):
)
else:
logger.warning(
- f"BulkOperation {instance.pk} failed: "
- f"{getattr(instance, 'error_message', 'Unknown error')}"
+ f"BulkOperation {instance.pk} failed: " f"{getattr(instance, 'error_message', 'Unknown error')}"
)
except Exception as e:
logger.warning(f"Failed to finalize bulk operation: {e}")
@@ -355,9 +339,9 @@ def _broadcast_submission_status_change(instance, source, target, user):
}
# Add claim information if available
- if hasattr(instance, 'claimed_by') and instance.claimed_by:
+ if hasattr(instance, "claimed_by") and instance.claimed_by:
payload["locked_by"] = instance.claimed_by.username
- if hasattr(instance, 'claimed_at') and instance.claimed_at:
+ if hasattr(instance, "claimed_at") and instance.claimed_at:
payload["locked_at"] = instance.claimed_at.isoformat()
# Emit the signal for downstream notification handlers
@@ -371,16 +355,14 @@ def _broadcast_submission_status_change(instance, source, target, user):
payload=payload,
)
- logger.debug(
- f"Broadcast status change: {submission_type}#{instance.pk} "
- f"{source} -> {target}"
- )
+ logger.debug(f"Broadcast status change: {submission_type}#{instance.pk} " f"{source} -> {target}")
except Exception as e:
logger.warning(f"Failed to broadcast submission status change: {e}")
# Signal handler registration
+
def register_moderation_signal_handlers():
"""
Register all moderation signal handlers.
@@ -399,70 +381,31 @@ def register_moderation_signal_handlers():
)
# EditSubmission handlers
- register_transition_handler(
- EditSubmission, '*', 'APPROVED',
- handle_submission_approved, stage='post'
- )
- register_transition_handler(
- EditSubmission, '*', 'REJECTED',
- handle_submission_rejected, stage='post'
- )
- register_transition_handler(
- EditSubmission, '*', 'ESCALATED',
- handle_submission_escalated, stage='post'
- )
+ register_transition_handler(EditSubmission, "*", "APPROVED", handle_submission_approved, stage="post")
+ register_transition_handler(EditSubmission, "*", "REJECTED", handle_submission_rejected, stage="post")
+ register_transition_handler(EditSubmission, "*", "ESCALATED", handle_submission_escalated, stage="post")
# PhotoSubmission handlers
- register_transition_handler(
- PhotoSubmission, '*', 'APPROVED',
- handle_submission_approved, stage='post'
- )
- register_transition_handler(
- PhotoSubmission, '*', 'REJECTED',
- handle_submission_rejected, stage='post'
- )
- register_transition_handler(
- PhotoSubmission, '*', 'ESCALATED',
- handle_submission_escalated, stage='post'
- )
+ register_transition_handler(PhotoSubmission, "*", "APPROVED", handle_submission_approved, stage="post")
+ register_transition_handler(PhotoSubmission, "*", "REJECTED", handle_submission_rejected, stage="post")
+ register_transition_handler(PhotoSubmission, "*", "ESCALATED", handle_submission_escalated, stage="post")
# ModerationReport handlers
- register_transition_handler(
- ModerationReport, '*', 'RESOLVED',
- handle_report_resolved, stage='post'
- )
+ register_transition_handler(ModerationReport, "*", "RESOLVED", handle_report_resolved, stage="post")
# ModerationQueue handlers
- register_transition_handler(
- ModerationQueue, '*', 'COMPLETED',
- handle_queue_completed, stage='post'
- )
+ register_transition_handler(ModerationQueue, "*", "COMPLETED", handle_queue_completed, stage="post")
# BulkOperation handlers
- register_transition_handler(
- BulkOperation, '*', '*',
- handle_bulk_operation_status, stage='post'
- )
+ register_transition_handler(BulkOperation, "*", "*", handle_bulk_operation_status, stage="post")
# Claim/Unclaim handlers for EditSubmission
- register_transition_handler(
- EditSubmission, 'PENDING', 'CLAIMED',
- handle_submission_claimed, stage='post'
- )
- register_transition_handler(
- EditSubmission, 'CLAIMED', 'PENDING',
- handle_submission_unclaimed, stage='post'
- )
+ register_transition_handler(EditSubmission, "PENDING", "CLAIMED", handle_submission_claimed, stage="post")
+ register_transition_handler(EditSubmission, "CLAIMED", "PENDING", handle_submission_unclaimed, stage="post")
# Claim/Unclaim handlers for PhotoSubmission
- register_transition_handler(
- PhotoSubmission, 'PENDING', 'CLAIMED',
- handle_submission_claimed, stage='post'
- )
- register_transition_handler(
- PhotoSubmission, 'CLAIMED', 'PENDING',
- handle_submission_unclaimed, stage='post'
- )
+ register_transition_handler(PhotoSubmission, "PENDING", "CLAIMED", handle_submission_claimed, stage="post")
+ register_transition_handler(PhotoSubmission, "CLAIMED", "PENDING", handle_submission_unclaimed, stage="post")
logger.info("Registered moderation signal handlers")
@@ -471,14 +414,14 @@ def register_moderation_signal_handlers():
__all__ = [
- 'submission_status_changed',
- 'register_moderation_signal_handlers',
- 'handle_submission_approved',
- 'handle_submission_rejected',
- 'handle_submission_escalated',
- 'handle_submission_claimed',
- 'handle_submission_unclaimed',
- 'handle_report_resolved',
- 'handle_queue_completed',
- 'handle_bulk_operation_status',
+ "submission_status_changed",
+ "register_moderation_signal_handlers",
+ "handle_submission_approved",
+ "handle_submission_rejected",
+ "handle_submission_escalated",
+ "handle_submission_claimed",
+ "handle_submission_unclaimed",
+ "handle_report_resolved",
+ "handle_queue_completed",
+ "handle_bulk_operation_status",
]
diff --git a/backend/apps/moderation/sse.py b/backend/apps/moderation/sse.py
index 17567a8a..63ea15f7 100644
--- a/backend/apps/moderation/sse.py
+++ b/backend/apps/moderation/sse.py
@@ -4,6 +4,7 @@ Server-Sent Events (SSE) endpoint for real-time moderation dashboard updates.
This module provides a streaming HTTP response that broadcasts submission status
changes to connected moderators in real-time.
"""
+
import json
import logging
import queue
@@ -103,6 +104,7 @@ class ModerationSSEView(APIView):
Sends a heartbeat every 30 seconds to keep the connection alive.
"""
+
def event_stream() -> Generator[str]:
client_queue = sse_broadcaster.subscribe()
@@ -124,13 +126,10 @@ class ModerationSSEView(APIView):
finally:
sse_broadcaster.unsubscribe(client_queue)
- response = StreamingHttpResponse(
- event_stream(),
- content_type='text/event-stream'
- )
- response['Cache-Control'] = 'no-cache'
- response['X-Accel-Buffering'] = 'no' # Disable nginx buffering
- response['Connection'] = 'keep-alive'
+ response = StreamingHttpResponse(event_stream(), content_type="text/event-stream")
+ response["Cache-Control"] = "no-cache"
+ response["X-Accel-Buffering"] = "no" # Disable nginx buffering
+ response["Connection"] = "keep-alive"
return response
@@ -168,15 +167,17 @@ class ModerationSSETestView(APIView):
sse_broadcaster.broadcast(test_payload)
- return JsonResponse({
- "status": "ok",
- "message": f"Test event broadcast to {len(sse_broadcaster._subscribers)} clients",
- "payload": test_payload,
- })
+ return JsonResponse(
+ {
+ "status": "ok",
+ "message": f"Test event broadcast to {len(sse_broadcaster._subscribers)} clients",
+ "payload": test_payload,
+ }
+ )
__all__ = [
- 'ModerationSSEView',
- 'ModerationSSETestView',
- 'sse_broadcaster',
+ "ModerationSSEView",
+ "ModerationSSETestView",
+ "sse_broadcaster",
]
diff --git a/backend/apps/moderation/templatetags/moderation_tags.py b/backend/apps/moderation/templatetags/moderation_tags.py
index f7f39017..7f0a927a 100644
--- a/backend/apps/moderation/templatetags/moderation_tags.py
+++ b/backend/apps/moderation/templatetags/moderation_tags.py
@@ -14,9 +14,7 @@ def get_object_name(value: int | None, model_path: str) -> str | None:
app_label, model = model_path.split(".")
try:
- content_type = ContentType.objects.get(
- app_label=app_label.lower(), model=model.lower()
- )
+ content_type = ContentType.objects.get(app_label=app_label.lower(), model=model.lower())
model_class = content_type.model_class()
if not model_class:
return None
@@ -60,9 +58,7 @@ def get_park_area_name(value: int | None, park_id: int | None) -> str | None:
@register.filter
-def get_item(
- dictionary: dict[str, Any] | None, key: str | int | None
-) -> list[Any]:
+def get_item(dictionary: dict[str, Any] | None, key: str | int | None) -> list[Any]:
"""Get item from dictionary by key."""
if not dictionary or not isinstance(dictionary, dict) or not key:
return []
diff --git a/backend/apps/moderation/tests.py b/backend/apps/moderation/tests.py
index d9870b03..f0b6cf1d 100644
--- a/backend/apps/moderation/tests.py
+++ b/backend/apps/moderation/tests.py
@@ -147,9 +147,7 @@ class ModerationMixinsTests(TestCase):
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
changes = {"name": "New Name"}
- response = view.handle_edit_submission(
- request, changes, "Test reason", "Test source"
- )
+ response = view.handle_edit_submission(request, changes, "Test reason", "Test source")
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
@@ -163,9 +161,7 @@ class ModerationMixinsTests(TestCase):
view.setup(request, pk=self.operator.pk)
view.kwargs = {"pk": self.operator.pk}
changes = {"name": "New Name"}
- response = view.handle_edit_submission(
- request, changes, "Test reason", "Test source"
- )
+ response = view.handle_edit_submission(request, changes, "Test reason", "Test source")
self.assertIsInstance(response, JsonResponse)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
@@ -177,9 +173,7 @@ class ModerationMixinsTests(TestCase):
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
- request = self.factory.post(
- f"/test/{self.operator.pk}/", data={}, format="multipart"
- )
+ request = self.factory.post(f"/test/{self.operator.pk}/", data={}, format="multipart")
request.user = AnonymousUser()
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
@@ -192,9 +186,7 @@ class ModerationMixinsTests(TestCase):
view.kwargs = {"pk": self.operator.pk}
view.object = self.operator
- request = self.factory.post(
- f"/test/{self.operator.pk}/", data={}, format="multipart"
- )
+ request = self.factory.post(f"/test/{self.operator.pk}/", data={}, format="multipart")
request.user = self.user
view.setup(request, pk=self.operator.pk)
response = view.handle_photo_submission(request)
@@ -384,45 +376,33 @@ class EditSubmissionTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123',
- role='USER'
+ username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
- )
- self.operator = Operator.objects.create(
- name='Test Operator',
- description='Test Description'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
+ self.operator = Operator.objects.create(name="Test Operator", description="Test Description")
self.content_type = ContentType.objects.get_for_model(Operator)
- def _create_submission(self, status='PENDING'):
+ def _create_submission(self, status="PENDING"):
"""Helper to create an EditSubmission."""
return EditSubmission.objects.create(
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
status=status,
- reason='Test reason'
+ reason="Test reason",
)
def test_pending_to_approved_transition(self):
"""Test transition from PENDING to APPROVED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
@@ -430,43 +410,43 @@ class EditSubmissionTransitionTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
def test_pending_to_rejected_transition(self):
"""Test transition from PENDING to REJECTED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
- submission.notes = 'Rejected: Insufficient evidence'
+ submission.notes = "Rejected: Insufficient evidence"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
+ self.assertEqual(submission.status, "REJECTED")
self.assertEqual(submission.handled_by, self.moderator)
- self.assertIn('Rejected', submission.notes)
+ self.assertIn("Rejected", submission.notes)
def test_pending_to_escalated_transition(self):
"""Test transition from PENDING to ESCALATED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_escalated(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
- submission.notes = 'Escalated: Needs admin review'
+ submission.notes = "Escalated: Needs admin review"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'ESCALATED')
+ self.assertEqual(submission.status, "ESCALATED")
def test_escalated_to_approved_transition(self):
"""Test transition from ESCALATED to APPROVED."""
- submission = self._create_submission(status='ESCALATED')
+ submission = self._create_submission(status="ESCALATED")
submission.transition_to_approved(user=self.admin)
submission.handled_by = self.admin
@@ -474,25 +454,25 @@ class EditSubmissionTransitionTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.admin)
def test_escalated_to_rejected_transition(self):
"""Test transition from ESCALATED to REJECTED."""
- submission = self._create_submission(status='ESCALATED')
+ submission = self._create_submission(status="ESCALATED")
submission.transition_to_rejected(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
- submission.notes = 'Rejected by admin'
+ submission.notes = "Rejected by admin"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
+ self.assertEqual(submission.status, "REJECTED")
def test_invalid_transition_from_approved(self):
"""Test that transitions from APPROVED state fail."""
- submission = self._create_submission(status='APPROVED')
+ submission = self._create_submission(status="APPROVED")
# Attempting to transition from APPROVED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
@@ -500,7 +480,7 @@ class EditSubmissionTransitionTests(TestCase):
def test_invalid_transition_from_rejected(self):
"""Test that transitions from REJECTED state fail."""
- submission = self._create_submission(status='REJECTED')
+ submission = self._create_submission(status="REJECTED")
# Attempting to transition from REJECTED should raise TransitionNotAllowed
with self.assertRaises(TransitionNotAllowed):
@@ -513,7 +493,7 @@ class EditSubmissionTransitionTests(TestCase):
submission.approve(self.moderator)
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
@@ -521,21 +501,21 @@ class EditSubmissionTransitionTests(TestCase):
"""Test the reject() wrapper method."""
submission = self._create_submission()
- submission.reject(self.moderator, reason='Not enough evidence')
+ submission.reject(self.moderator, reason="Not enough evidence")
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
- self.assertIn('Not enough evidence', submission.notes)
+ self.assertEqual(submission.status, "REJECTED")
+ self.assertIn("Not enough evidence", submission.notes)
def test_escalate_wrapper_method(self):
"""Test the escalate() wrapper method."""
submission = self._create_submission()
- submission.escalate(self.moderator, reason='Needs admin approval')
+ submission.escalate(self.moderator, reason="Needs admin approval")
submission.refresh_from_db()
- self.assertEqual(submission.status, 'ESCALATED')
- self.assertIn('Needs admin approval', submission.notes)
+ self.assertEqual(submission.status, "ESCALATED")
+ self.assertIn("Needs admin approval", submission.notes)
# ============================================================================
@@ -549,90 +529,81 @@ class ModerationReportTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='reporter',
- email='reporter@example.com',
- password='testpass123',
- role='USER'
+ username="reporter", email="reporter@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
- )
- self.operator = Operator.objects.create(
- name='Test Operator',
- description='Test Description'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
+ self.operator = Operator.objects.create(name="Test Operator", description="Test Description")
self.content_type = ContentType.objects.get_for_model(Operator)
- def _create_report(self, status='PENDING'):
+ def _create_report(self, status="PENDING"):
"""Helper to create a ModerationReport."""
return ModerationReport.objects.create(
- report_type='CONTENT',
+ report_type="CONTENT",
status=status,
- priority='MEDIUM',
- reported_entity_type='company',
+ priority="MEDIUM",
+ reported_entity_type="company",
reported_entity_id=self.operator.id,
content_type=self.content_type,
- reason='Inaccurate information',
- description='The company information is incorrect',
- reported_by=self.user
+ reason="Inaccurate information",
+ description="The company information is incorrect",
+ reported_by=self.user,
)
def test_pending_to_under_review_transition(self):
"""Test transition from PENDING to UNDER_REVIEW."""
report = self._create_report()
- self.assertEqual(report.status, 'PENDING')
+ self.assertEqual(report.status, "PENDING")
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
report.refresh_from_db()
- self.assertEqual(report.status, 'UNDER_REVIEW')
+ self.assertEqual(report.status, "UNDER_REVIEW")
self.assertEqual(report.assigned_moderator, self.moderator)
def test_under_review_to_resolved_transition(self):
"""Test transition from UNDER_REVIEW to RESOLVED."""
- report = self._create_report(status='UNDER_REVIEW')
+ report = self._create_report(status="UNDER_REVIEW")
report.assigned_moderator = self.moderator
report.save()
report.transition_to_resolved(user=self.moderator)
- report.resolution_action = 'Content updated'
- report.resolution_notes = 'Fixed the incorrect information'
+ report.resolution_action = "Content updated"
+ report.resolution_notes = "Fixed the incorrect information"
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
- self.assertEqual(report.status, 'RESOLVED')
+ self.assertEqual(report.status, "RESOLVED")
self.assertIsNotNone(report.resolved_at)
def test_under_review_to_dismissed_transition(self):
"""Test transition from UNDER_REVIEW to DISMISSED."""
- report = self._create_report(status='UNDER_REVIEW')
+ report = self._create_report(status="UNDER_REVIEW")
report.assigned_moderator = self.moderator
report.save()
report.transition_to_dismissed(user=self.moderator)
- report.resolution_notes = 'Report is not valid'
+ report.resolution_notes = "Report is not valid"
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
- self.assertEqual(report.status, 'DISMISSED')
+ self.assertEqual(report.status, "DISMISSED")
def test_invalid_transition_from_resolved(self):
"""Test that transitions from RESOLVED state fail."""
- report = self._create_report(status='RESOLVED')
+ report = self._create_report(status="RESOLVED")
with self.assertRaises(TransitionNotAllowed):
report.transition_to_dismissed(user=self.moderator)
def test_invalid_transition_from_dismissed(self):
"""Test that transitions from DISMISSED state fail."""
- report = self._create_report(status='DISMISSED')
+ report = self._create_report(status="DISMISSED")
with self.assertRaises(TransitionNotAllowed):
report.transition_to_resolved(user=self.moderator)
@@ -649,27 +620,24 @@ class ModerationQueueTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
- def _create_queue_item(self, status='PENDING'):
+ def _create_queue_item(self, status="PENDING"):
"""Helper to create a ModerationQueue item."""
return ModerationQueue.objects.create(
- item_type='EDIT_SUBMISSION',
+ item_type="EDIT_SUBMISSION",
status=status,
- priority='MEDIUM',
- title='Review edit submission',
- description='User submitted an edit that needs review',
- flagged_by=self.moderator
+ priority="MEDIUM",
+ title="Review edit submission",
+ description="User submitted an edit that needs review",
+ flagged_by=self.moderator,
)
def test_pending_to_in_progress_transition(self):
"""Test transition from PENDING to IN_PROGRESS."""
item = self._create_queue_item()
- self.assertEqual(item.status, 'PENDING')
+ self.assertEqual(item.status, "PENDING")
item.transition_to_in_progress(user=self.moderator)
item.assigned_to = self.moderator
@@ -677,12 +645,12 @@ class ModerationQueueTransitionTests(TestCase):
item.save()
item.refresh_from_db()
- self.assertEqual(item.status, 'IN_PROGRESS')
+ self.assertEqual(item.status, "IN_PROGRESS")
self.assertEqual(item.assigned_to, self.moderator)
def test_in_progress_to_completed_transition(self):
"""Test transition from IN_PROGRESS to COMPLETED."""
- item = self._create_queue_item(status='IN_PROGRESS')
+ item = self._create_queue_item(status="IN_PROGRESS")
item.assigned_to = self.moderator
item.save()
@@ -690,11 +658,11 @@ class ModerationQueueTransitionTests(TestCase):
item.save()
item.refresh_from_db()
- self.assertEqual(item.status, 'COMPLETED')
+ self.assertEqual(item.status, "COMPLETED")
def test_in_progress_to_cancelled_transition(self):
"""Test transition from IN_PROGRESS to CANCELLED."""
- item = self._create_queue_item(status='IN_PROGRESS')
+ item = self._create_queue_item(status="IN_PROGRESS")
item.assigned_to = self.moderator
item.save()
@@ -702,7 +670,7 @@ class ModerationQueueTransitionTests(TestCase):
item.save()
item.refresh_from_db()
- self.assertEqual(item.status, 'CANCELLED')
+ self.assertEqual(item.status, "CANCELLED")
def test_pending_to_cancelled_transition(self):
"""Test transition from PENDING to CANCELLED."""
@@ -712,11 +680,11 @@ class ModerationQueueTransitionTests(TestCase):
item.save()
item.refresh_from_db()
- self.assertEqual(item.status, 'CANCELLED')
+ self.assertEqual(item.status, "CANCELLED")
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
- item = self._create_queue_item(status='COMPLETED')
+ item = self._create_queue_item(status="COMPLETED")
with self.assertRaises(TransitionNotAllowed):
item.transition_to_in_progress(user=self.moderator)
@@ -733,40 +701,37 @@ class BulkOperationTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
- def _create_bulk_operation(self, status='PENDING'):
+ def _create_bulk_operation(self, status="PENDING"):
"""Helper to create a BulkOperation."""
return BulkOperation.objects.create(
- operation_type='BULK_UPDATE',
+ operation_type="BULK_UPDATE",
status=status,
- priority='MEDIUM',
- description='Bulk update park statuses',
- parameters={'target': 'parks', 'action': 'update_status'},
+ priority="MEDIUM",
+ description="Bulk update park statuses",
+ parameters={"target": "parks", "action": "update_status"},
created_by=self.admin,
- total_items=100
+ total_items=100,
)
def test_pending_to_running_transition(self):
"""Test transition from PENDING to RUNNING."""
operation = self._create_bulk_operation()
- self.assertEqual(operation.status, 'PENDING')
+ self.assertEqual(operation.status, "PENDING")
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'RUNNING')
+ self.assertEqual(operation.status, "RUNNING")
self.assertIsNotNone(operation.started_at)
def test_running_to_completed_transition(self):
"""Test transition from RUNNING to COMPLETED."""
- operation = self._create_bulk_operation(status='RUNNING')
+ operation = self._create_bulk_operation(status="RUNNING")
operation.started_at = timezone.now()
operation.save()
@@ -776,24 +741,24 @@ class BulkOperationTransitionTests(TestCase):
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'COMPLETED')
+ self.assertEqual(operation.status, "COMPLETED")
self.assertIsNotNone(operation.completed_at)
self.assertEqual(operation.processed_items, 100)
def test_running_to_failed_transition(self):
"""Test transition from RUNNING to FAILED."""
- operation = self._create_bulk_operation(status='RUNNING')
+ operation = self._create_bulk_operation(status="RUNNING")
operation.started_at = timezone.now()
operation.save()
operation.transition_to_failed(user=self.admin)
operation.completed_at = timezone.now()
- operation.results = {'error': 'Database connection failed'}
+ operation.results = {"error": "Database connection failed"}
operation.failed_items = 50
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'FAILED')
+ self.assertEqual(operation.status, "FAILED")
self.assertEqual(operation.failed_items, 50)
def test_pending_to_cancelled_transition(self):
@@ -804,11 +769,11 @@ class BulkOperationTransitionTests(TestCase):
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'CANCELLED')
+ self.assertEqual(operation.status, "CANCELLED")
def test_running_to_cancelled_transition(self):
"""Test transition from RUNNING to CANCELLED when cancellable."""
- operation = self._create_bulk_operation(status='RUNNING')
+ operation = self._create_bulk_operation(status="RUNNING")
operation.can_cancel = True
operation.save()
@@ -816,18 +781,18 @@ class BulkOperationTransitionTests(TestCase):
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'CANCELLED')
+ self.assertEqual(operation.status, "CANCELLED")
def test_invalid_transition_from_completed(self):
"""Test that transitions from COMPLETED state fail."""
- operation = self._create_bulk_operation(status='COMPLETED')
+ operation = self._create_bulk_operation(status="COMPLETED")
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_running(user=self.admin)
def test_invalid_transition_from_failed(self):
"""Test that transitions from FAILED state fail."""
- operation = self._create_bulk_operation(status='FAILED')
+ operation = self._create_bulk_operation(status="FAILED")
with self.assertRaises(TransitionNotAllowed):
operation.transition_to_completed(user=self.admin)
@@ -858,21 +823,12 @@ class TransitionLoggingTestCase(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123',
- role='USER'
+ username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
- )
- self.operator = Operator.objects.create(
- name='Test Operator',
- description='Test Description'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
+ self.operator = Operator.objects.create(name="Test Operator", description="Test Description")
self.content_type = ContentType.objects.get_for_model(Operator)
def test_transition_creates_log(self):
@@ -884,10 +840,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
# Perform transition
@@ -896,15 +852,12 @@ class TransitionLoggingTestCase(TestCase):
# Check log was created
submission_ct = ContentType.objects.get_for_model(submission)
- log = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).first()
+ log = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).first()
self.assertIsNotNone(log, "StateLog entry should be created")
- self.assertEqual(log.state, 'APPROVED')
+ self.assertEqual(log.state, "APPROVED")
self.assertEqual(log.by, self.moderator)
- self.assertIn('approved', log.transition.lower())
+ self.assertIn("approved", log.transition.lower())
def test_multiple_transitions_logged(self):
"""Test that multiple transitions are all logged."""
@@ -914,10 +867,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
submission_ct = ContentType.objects.get_for_model(submission)
@@ -931,14 +884,11 @@ class TransitionLoggingTestCase(TestCase):
submission.save()
# Check multiple logs created
- logs = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).order_by("timestamp")
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
- self.assertEqual(logs[0].state, 'ESCALATED')
- self.assertEqual(logs[1].state, 'APPROVED')
+ self.assertEqual(logs[0].state, "ESCALATED")
+ self.assertEqual(logs[1].state, "APPROVED")
def test_history_endpoint_returns_logs(self):
"""Test history API endpoint returns transition logs."""
@@ -951,10 +901,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
# Perform transition to create log
@@ -963,7 +913,7 @@ class TransitionLoggingTestCase(TestCase):
# Note: This assumes EditSubmission has a history endpoint
# Adjust URL pattern based on actual implementation
- response = api_client.get('/api/moderation/reports/all_history/')
+ response = api_client.get("/api/moderation/reports/all_history/")
self.assertEqual(response.status_code, 200)
@@ -975,10 +925,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
# Perform transition without user
@@ -987,13 +937,10 @@ class TransitionLoggingTestCase(TestCase):
# Check log was created even without user
submission_ct = ContentType.objects.get_for_model(submission)
- log = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).first()
+ log = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).first()
self.assertIsNotNone(log)
- self.assertEqual(log.state, 'REJECTED')
+ self.assertEqual(log.state, "REJECTED")
self.assertIsNone(log.by, "System transitions should have no user")
def test_transition_log_includes_description(self):
@@ -1004,10 +951,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
# Perform transition
@@ -1016,14 +963,11 @@ class TransitionLoggingTestCase(TestCase):
# Check log
submission_ct = ContentType.objects.get_for_model(submission)
- log = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).first()
+ log = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).first()
self.assertIsNotNone(log)
# Description field exists and can be used for audit trails
- self.assertTrue(hasattr(log, 'description'))
+ self.assertTrue(hasattr(log, "description"))
def test_log_ordering_by_timestamp(self):
"""Test that logs are properly ordered by timestamp."""
@@ -1034,10 +978,10 @@ class TransitionLoggingTestCase(TestCase):
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- submission_type='EDIT',
- changes={'name': 'Updated Name'},
- status='PENDING',
- reason='Test reason'
+ submission_type="EDIT",
+ changes={"name": "Updated Name"},
+ status="PENDING",
+ reason="Test reason",
)
submission_ct = ContentType.objects.get_for_model(submission)
@@ -1050,10 +994,7 @@ class TransitionLoggingTestCase(TestCase):
submission.save()
# Get logs ordered by timestamp
- logs = list(StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).order_by('timestamp'))
+ logs = list(StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).order_by("timestamp"))
# Verify ordering
self.assertEqual(len(logs), 2)
@@ -1071,27 +1012,21 @@ class ModerationActionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.target_user = User.objects.create_user(
- username='target',
- email='target@example.com',
- password='testpass123',
- role='USER'
+ username="target", email="target@example.com", password="testpass123", role="USER"
)
def test_create_action_with_duration(self):
"""Test creating an action with duration sets expires_at."""
action = ModerationAction.objects.create(
- action_type='TEMPORARY_BAN',
- reason='Spam',
- details='User was spamming the forums',
+ action_type="TEMPORARY_BAN",
+ reason="Spam",
+ details="User was spamming the forums",
duration_hours=24,
moderator=self.moderator,
- target_user=self.target_user
+ target_user=self.target_user,
)
self.assertIsNotNone(action.expires_at)
@@ -1102,11 +1037,11 @@ class ModerationActionTests(TestCase):
def test_create_action_without_duration(self):
"""Test creating an action without duration has no expires_at."""
action = ModerationAction.objects.create(
- action_type='WARNING',
- reason='First offense',
- details='Warning issued for minor violation',
+ action_type="WARNING",
+ reason="First offense",
+ details="Warning issued for minor violation",
moderator=self.moderator,
- target_user=self.target_user
+ target_user=self.target_user,
)
self.assertIsNone(action.expires_at)
@@ -1114,11 +1049,11 @@ class ModerationActionTests(TestCase):
def test_action_is_active_by_default(self):
"""Test that new actions are active by default."""
action = ModerationAction.objects.create(
- action_type='WARNING',
- reason='Test',
- details='Test warning',
+ action_type="WARNING",
+ reason="Test",
+ details="Test warning",
moderator=self.moderator,
- target_user=self.target_user
+ target_user=self.target_user,
)
self.assertTrue(action.is_active)
@@ -1135,56 +1070,46 @@ class PhotoSubmissionTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123',
- role='USER'
+ username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
self.operator = Operator.objects.create(
- name='Test Operator',
- description='Test Description',
- roles=['OPERATOR']
+ name="Test Operator", description="Test Description", roles=["OPERATOR"]
)
self.content_type = ContentType.objects.get_for_model(Operator)
def _create_mock_photo(self):
"""Create a mock CloudflareImage for testing."""
from unittest.mock import Mock
+
mock_photo = Mock()
mock_photo.pk = 1
mock_photo.id = 1
return mock_photo
- def _create_submission(self, status='PENDING'):
+ def _create_submission(self, status="PENDING"):
"""Helper to create a PhotoSubmission."""
# Create using direct database creation to bypass FK validation
from unittest.mock import Mock, patch
- with patch.object(PhotoSubmission, 'photo', Mock()):
+ with patch.object(PhotoSubmission, "photo", Mock()):
submission = PhotoSubmission(
user=self.user,
content_type=self.content_type,
object_id=self.operator.id,
- caption='Test Photo',
+ caption="Test Photo",
status=status,
)
# Bypass model save to avoid FK constraint on photo
submission.photo_id = 1
submission.save(update_fields=None)
# Force status after creation for non-PENDING states
- if status != 'PENDING':
+ if status != "PENDING":
PhotoSubmission.objects.filter(pk=submission.pk).update(status=status)
submission.refresh_from_db()
return submission
@@ -1192,7 +1117,7 @@ class PhotoSubmissionTransitionTests(TestCase):
def test_pending_to_approved_transition(self):
"""Test transition from PENDING to APPROVED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_approved(user=self.moderator)
submission.handled_by = self.moderator
@@ -1200,43 +1125,43 @@ class PhotoSubmissionTransitionTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
def test_pending_to_rejected_transition(self):
"""Test transition from PENDING to REJECTED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
- submission.notes = 'Rejected: Image quality too low'
+ submission.notes = "Rejected: Image quality too low"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
+ self.assertEqual(submission.status, "REJECTED")
self.assertEqual(submission.handled_by, self.moderator)
- self.assertIn('Rejected', submission.notes)
+ self.assertIn("Rejected", submission.notes)
def test_pending_to_escalated_transition(self):
"""Test transition from PENDING to ESCALATED."""
submission = self._create_submission()
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
submission.transition_to_escalated(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
- submission.notes = 'Escalated: Copyright concerns'
+ submission.notes = "Escalated: Copyright concerns"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'ESCALATED')
+ self.assertEqual(submission.status, "ESCALATED")
def test_escalated_to_approved_transition(self):
"""Test transition from ESCALATED to APPROVED."""
- submission = self._create_submission(status='ESCALATED')
+ submission = self._create_submission(status="ESCALATED")
submission.transition_to_approved(user=self.admin)
submission.handled_by = self.admin
@@ -1244,32 +1169,32 @@ class PhotoSubmissionTransitionTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.admin)
def test_escalated_to_rejected_transition(self):
"""Test transition from ESCALATED to REJECTED."""
- submission = self._create_submission(status='ESCALATED')
+ submission = self._create_submission(status="ESCALATED")
submission.transition_to_rejected(user=self.admin)
submission.handled_by = self.admin
submission.handled_at = timezone.now()
- submission.notes = 'Rejected by admin after review'
+ submission.notes = "Rejected by admin after review"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
+ self.assertEqual(submission.status, "REJECTED")
def test_invalid_transition_from_approved(self):
"""Test that transitions from APPROVED state fail."""
- submission = self._create_submission(status='APPROVED')
+ submission = self._create_submission(status="APPROVED")
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_rejected(user=self.moderator)
def test_invalid_transition_from_rejected(self):
"""Test that transitions from REJECTED state fail."""
- submission = self._create_submission(status='REJECTED')
+ submission = self._create_submission(status="REJECTED")
with self.assertRaises(TransitionNotAllowed):
submission.transition_to_approved(user=self.moderator)
@@ -1281,12 +1206,12 @@ class PhotoSubmissionTransitionTests(TestCase):
submission = self._create_submission()
# Mock the photo creation part since we don't have actual photos
- with patch.object(submission, 'transition_to_rejected'):
- submission.reject(self.moderator, notes='Not suitable')
+ with patch.object(submission, "transition_to_rejected"):
+ submission.reject(self.moderator, notes="Not suitable")
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
- self.assertIn('Not suitable', submission.notes)
+ self.assertEqual(submission.status, "REJECTED")
+ self.assertIn("Not suitable", submission.notes)
def test_escalate_wrapper_method(self):
"""Test the escalate() wrapper method."""
@@ -1294,12 +1219,12 @@ class PhotoSubmissionTransitionTests(TestCase):
submission = self._create_submission()
- with patch.object(submission, 'transition_to_escalated'):
- submission.escalate(self.moderator, notes='Needs admin review')
+ with patch.object(submission, "transition_to_escalated"):
+ submission.escalate(self.moderator, notes="Needs admin review")
submission.refresh_from_db()
- self.assertEqual(submission.status, 'ESCALATED')
- self.assertIn('Needs admin review', submission.notes)
+ self.assertEqual(submission.status, "ESCALATED")
+ self.assertIn("Needs admin review", submission.notes)
def test_transition_creates_state_log(self):
"""Test that transitions create StateLog entries."""
@@ -1313,13 +1238,10 @@ class PhotoSubmissionTransitionTests(TestCase):
# Check log was created
submission_ct = ContentType.objects.get_for_model(submission)
- log = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).first()
+ log = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).first()
self.assertIsNotNone(log, "StateLog entry should be created")
- self.assertEqual(log.state, 'APPROVED')
+ self.assertEqual(log.state, "APPROVED")
self.assertEqual(log.by, self.moderator)
def test_multiple_transitions_logged(self):
@@ -1338,14 +1260,11 @@ class PhotoSubmissionTransitionTests(TestCase):
submission.save()
# Check multiple logs created
- logs = StateLog.objects.filter(
- content_type=submission_ct,
- object_id=submission.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=submission_ct, object_id=submission.id).order_by("timestamp")
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
- self.assertEqual(logs[0].state, 'ESCALATED')
- self.assertEqual(logs[1].state, 'APPROVED')
+ self.assertEqual(logs[0].state, "ESCALATED")
+ self.assertEqual(logs[1].state, "APPROVED")
def test_handled_by_and_handled_at_updated(self):
"""Test that handled_by and handled_at are properly updated."""
@@ -1369,7 +1288,7 @@ class PhotoSubmissionTransitionTests(TestCase):
def test_notes_field_updated_on_rejection(self):
"""Test that notes field is updated with rejection reason."""
submission = self._create_submission()
- rejection_reason = 'Image contains watermarks'
+ rejection_reason = "Image contains watermarks"
submission.transition_to_rejected(user=self.moderator)
submission.notes = rejection_reason
@@ -1381,7 +1300,7 @@ class PhotoSubmissionTransitionTests(TestCase):
def test_notes_field_updated_on_escalation(self):
"""Test that notes field is updated with escalation reason."""
submission = self._create_submission()
- escalation_reason = 'Potentially copyrighted content'
+ escalation_reason = "Potentially copyrighted content"
submission.transition_to_escalated(user=self.moderator)
submission.notes = escalation_reason
diff --git a/backend/apps/moderation/tests/test_admin.py b/backend/apps/moderation/tests/test_admin.py
index 9d9cf1e4..944aba6b 100644
--- a/backend/apps/moderation/tests/test_admin.py
+++ b/backend/apps/moderation/tests/test_admin.py
@@ -43,24 +43,15 @@ class TestModerationAdminSite(TestCase):
assert moderation_site.has_permission(request) is False
# Regular user
- request.user = type("obj", (object,), {
- "is_authenticated": True,
- "role": "USER"
- })()
+ request.user = type("obj", (object,), {"is_authenticated": True, "role": "USER"})()
assert moderation_site.has_permission(request) is False
# Moderator
- request.user = type("obj", (object,), {
- "is_authenticated": True,
- "role": "MODERATOR"
- })()
+ request.user = type("obj", (object,), {"is_authenticated": True, "role": "MODERATOR"})()
assert moderation_site.has_permission(request) is True
# Admin
- request.user = type("obj", (object,), {
- "is_authenticated": True,
- "role": "ADMIN"
- })()
+ request.user = type("obj", (object,), {"is_authenticated": True, "role": "ADMIN"})()
assert moderation_site.has_permission(request) is True
@@ -146,6 +137,7 @@ class TestStateLogAdmin(TestCase):
self.site = AdminSite()
# Note: StateLog is from django_fsm_log
from django_fsm_log.models import StateLog
+
self.admin = StateLogAdmin(model=StateLog, admin_site=self.site)
def test_readonly_permissions(self):
@@ -215,4 +207,5 @@ class TestRegisteredModels(TestCase):
def test_state_log_registered(self):
"""Verify StateLog is registered with moderation site."""
from django_fsm_log.models import StateLog
+
assert StateLog in moderation_site._registry
diff --git a/backend/apps/moderation/tests/test_workflows.py b/backend/apps/moderation/tests/test_workflows.py
index 7ff807ce..5c98da11 100644
--- a/backend/apps/moderation/tests/test_workflows.py
+++ b/backend/apps/moderation/tests/test_workflows.py
@@ -9,7 +9,6 @@ This module tests end-to-end moderation workflows including:
- Bulk operation workflow
"""
-
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
@@ -25,22 +24,13 @@ class SubmissionApprovalWorkflowTests(TestCase):
def setUpTestData(cls):
"""Set up test data for all tests."""
cls.regular_user = User.objects.create_user(
- username='regular_user',
- email='user@example.com',
- password='testpass123',
- role='USER'
+ username="regular_user", email="user@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='moderator',
- email='mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="mod@example.com", password="testpass123", role="MODERATOR"
)
cls.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
def test_edit_submission_approval_workflow(self):
@@ -53,10 +43,7 @@ class SubmissionApprovalWorkflowTests(TestCase):
from apps.parks.models import Company
# Create target object
- company = Company.objects.create(
- name='Test Company',
- description='Original description'
- )
+ company = Company.objects.create(name="Test Company", description="Original description")
# User submits an edit
content_type = ContentType.objects.get_for_model(company)
@@ -64,13 +51,13 @@ class SubmissionApprovalWorkflowTests(TestCase):
user=self.regular_user,
content_type=content_type,
object_id=company.id,
- submission_type='EDIT',
- changes={'description': 'Updated description'},
- status='PENDING',
- reason='Fixing typo'
+ submission_type="EDIT",
+ changes={"description": "Updated description"},
+ status="PENDING",
+ reason="Fixing typo",
)
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
self.assertIsNone(submission.handled_by)
self.assertIsNone(submission.handled_at)
@@ -81,7 +68,7 @@ class SubmissionApprovalWorkflowTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.moderator)
self.assertIsNotNone(submission.handled_at)
@@ -95,16 +82,9 @@ class SubmissionApprovalWorkflowTests(TestCase):
from apps.parks.models import Company, Park
# Create target park
- operator = Company.objects.create(
- name='Test Operator',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name="Test Operator", roles=["OPERATOR"])
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ name="Test Park", slug="test-park", operator=operator, status="OPERATING", timezone="America/New_York"
)
# User submits a photo
@@ -113,12 +93,12 @@ class SubmissionApprovalWorkflowTests(TestCase):
user=self.regular_user,
content_type=content_type,
object_id=park.id,
- status='PENDING',
- photo_type='GENERAL',
- description='Beautiful park entrance'
+ status="PENDING",
+ photo_type="GENERAL",
+ description="Beautiful park entrance",
)
- self.assertEqual(submission.status, 'PENDING')
+ self.assertEqual(submission.status, "PENDING")
# Moderator approves
submission.transition_to_approved(user=self.moderator)
@@ -127,7 +107,7 @@ class SubmissionApprovalWorkflowTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
class SubmissionRejectionWorkflowTests(TestCase):
@@ -136,16 +116,10 @@ class SubmissionRejectionWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.regular_user = User.objects.create_user(
- username='user_rej',
- email='user_rej@example.com',
- password='testpass123',
- role='USER'
+ username="user_rej", email="user_rej@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='mod_rej',
- email='mod_rej@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="mod_rej", email="mod_rej@example.com", password="testpass123", role="MODERATOR"
)
def test_edit_submission_rejection_with_reason(self):
@@ -157,32 +131,29 @@ class SubmissionRejectionWorkflowTests(TestCase):
from apps.moderation.models import EditSubmission
from apps.parks.models import Company
- company = Company.objects.create(
- name='Test Company',
- description='Original'
- )
+ company = Company.objects.create(name="Test Company", description="Original")
content_type = ContentType.objects.get_for_model(company)
submission = EditSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=company.id,
- submission_type='EDIT',
- changes={'name': 'Spam Content'},
- status='PENDING',
- reason='Name change request'
+ submission_type="EDIT",
+ changes={"name": "Spam Content"},
+ status="PENDING",
+ reason="Name change request",
)
# Moderator rejects
submission.transition_to_rejected(user=self.moderator)
submission.handled_by = self.moderator
submission.handled_at = timezone.now()
- submission.notes = 'Rejected: Content appears to be spam'
+ submission.notes = "Rejected: Content appears to be spam"
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'REJECTED')
- self.assertIn('spam', submission.notes.lower())
+ self.assertEqual(submission.status, "REJECTED")
+ self.assertIn("spam", submission.notes.lower())
class SubmissionEscalationWorkflowTests(TestCase):
@@ -191,22 +162,13 @@ class SubmissionEscalationWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.regular_user = User.objects.create_user(
- username='user_esc',
- email='user_esc@example.com',
- password='testpass123',
- role='USER'
+ username="user_esc", email="user_esc@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='mod_esc',
- email='mod_esc@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="mod_esc", email="mod_esc@example.com", password="testpass123", role="MODERATOR"
)
cls.admin = User.objects.create_user(
- username='admin_esc',
- email='admin_esc@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin_esc", email="admin_esc@example.com", password="testpass123", role="ADMIN"
)
def test_escalation_workflow(self):
@@ -218,28 +180,25 @@ class SubmissionEscalationWorkflowTests(TestCase):
from apps.moderation.models import EditSubmission
from apps.parks.models import Company
- company = Company.objects.create(
- name='Sensitive Company',
- description='Original'
- )
+ company = Company.objects.create(name="Sensitive Company", description="Original")
content_type = ContentType.objects.get_for_model(company)
submission = EditSubmission.objects.create(
user=self.regular_user,
content_type=content_type,
object_id=company.id,
- submission_type='EDIT',
- changes={'name': 'New Sensitive Name'},
- status='PENDING',
- reason='Major name change'
+ submission_type="EDIT",
+ changes={"name": "New Sensitive Name"},
+ status="PENDING",
+ reason="Major name change",
)
# Moderator escalates
submission.transition_to_escalated(user=self.moderator)
- submission.notes = 'Escalated: Major change needs admin review'
+ submission.notes = "Escalated: Major change needs admin review"
submission.save()
- self.assertEqual(submission.status, 'ESCALATED')
+ self.assertEqual(submission.status, "ESCALATED")
# Admin approves
submission.transition_to_approved(user=self.admin)
@@ -248,7 +207,7 @@ class SubmissionEscalationWorkflowTests(TestCase):
submission.save()
submission.refresh_from_db()
- self.assertEqual(submission.status, 'APPROVED')
+ self.assertEqual(submission.status, "APPROVED")
self.assertEqual(submission.handled_by, self.admin)
@@ -258,16 +217,10 @@ class ReportHandlingWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.reporter = User.objects.create_user(
- username='reporter',
- email='reporter@example.com',
- password='testpass123',
- role='USER'
+ username="reporter", email="reporter@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='mod_report',
- email='mod_report@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="mod_report", email="mod_report@example.com", password="testpass123", role="MODERATOR"
)
def test_report_resolution_workflow(self):
@@ -279,45 +232,42 @@ class ReportHandlingWorkflowTests(TestCase):
from apps.moderation.models import ModerationReport
from apps.parks.models import Company
- reported_company = Company.objects.create(
- name='Problematic Company',
- description='Some inappropriate content'
- )
+ reported_company = Company.objects.create(name="Problematic Company", description="Some inappropriate content")
content_type = ContentType.objects.get_for_model(reported_company)
# User reports content
report = ModerationReport.objects.create(
- report_type='CONTENT',
- status='PENDING',
- priority='HIGH',
- reported_entity_type='company',
+ report_type="CONTENT",
+ status="PENDING",
+ priority="HIGH",
+ reported_entity_type="company",
reported_entity_id=reported_company.id,
content_type=content_type,
- reason='INAPPROPRIATE',
- description='This content is inappropriate',
- reported_by=self.reporter
+ reason="INAPPROPRIATE",
+ description="This content is inappropriate",
+ reported_by=self.reporter,
)
- self.assertEqual(report.status, 'PENDING')
+ self.assertEqual(report.status, "PENDING")
# Moderator claims and starts review
report.transition_to_under_review(user=self.moderator)
report.assigned_moderator = self.moderator
report.save()
- self.assertEqual(report.status, 'UNDER_REVIEW')
+ self.assertEqual(report.status, "UNDER_REVIEW")
self.assertEqual(report.assigned_moderator, self.moderator)
# Moderator resolves
report.transition_to_resolved(user=self.moderator)
- report.resolution_action = 'CONTENT_REMOVED'
- report.resolution_notes = 'Content was removed'
+ report.resolution_action = "CONTENT_REMOVED"
+ report.resolution_notes = "Content was removed"
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
- self.assertEqual(report.status, 'RESOLVED')
+ self.assertEqual(report.status, "RESOLVED")
self.assertIsNotNone(report.resolved_at)
def test_report_dismissal_workflow(self):
@@ -329,23 +279,20 @@ class ReportHandlingWorkflowTests(TestCase):
from apps.moderation.models import ModerationReport
from apps.parks.models import Company
- company = Company.objects.create(
- name='Valid Company',
- description='Normal content'
- )
+ company = Company.objects.create(name="Valid Company", description="Normal content")
content_type = ContentType.objects.get_for_model(company)
report = ModerationReport.objects.create(
- report_type='CONTENT',
- status='PENDING',
- priority='LOW',
- reported_entity_type='company',
+ report_type="CONTENT",
+ status="PENDING",
+ priority="LOW",
+ reported_entity_type="company",
reported_entity_id=company.id,
content_type=content_type,
- reason='OTHER',
- description='I just do not like this',
- reported_by=self.reporter
+ reason="OTHER",
+ description="I just do not like this",
+ reported_by=self.reporter,
)
# Moderator claims
@@ -355,12 +302,12 @@ class ReportHandlingWorkflowTests(TestCase):
# Moderator dismisses as invalid
report.transition_to_dismissed(user=self.moderator)
- report.resolution_notes = 'Report does not violate any guidelines'
+ report.resolution_notes = "Report does not violate any guidelines"
report.resolved_at = timezone.now()
report.save()
report.refresh_from_db()
- self.assertEqual(report.status, 'DISMISSED')
+ self.assertEqual(report.status, "DISMISSED")
class BulkOperationWorkflowTests(TestCase):
@@ -369,10 +316,7 @@ class BulkOperationWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.admin = User.objects.create_user(
- username='admin_bulk',
- email='admin_bulk@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin_bulk", email="admin_bulk@example.com", password="testpass123", role="ADMIN"
)
def test_bulk_operation_success_workflow(self):
@@ -384,22 +328,22 @@ class BulkOperationWorkflowTests(TestCase):
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
- operation_type='APPROVE_SUBMISSIONS',
- status='PENDING',
+ operation_type="APPROVE_SUBMISSIONS",
+ status="PENDING",
total_items=10,
processed_items=0,
created_by=self.admin,
- parameters={'submission_ids': list(range(1, 11))}
+ parameters={"submission_ids": list(range(1, 11))},
)
- self.assertEqual(operation.status, 'PENDING')
+ self.assertEqual(operation.status, "PENDING")
# Start operation
operation.transition_to_running(user=self.admin)
operation.started_at = timezone.now()
operation.save()
- self.assertEqual(operation.status, 'RUNNING')
+ self.assertEqual(operation.status, "RUNNING")
# Simulate progress
for i in range(1, 11):
@@ -409,11 +353,11 @@ class BulkOperationWorkflowTests(TestCase):
# Complete operation
operation.transition_to_completed(user=self.admin)
operation.completed_at = timezone.now()
- operation.results = {'approved': 10, 'failed': 0}
+ operation.results = {"approved": 10, "failed": 0}
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'COMPLETED')
+ self.assertEqual(operation.status, "COMPLETED")
self.assertEqual(operation.processed_items, 10)
def test_bulk_operation_failure_workflow(self):
@@ -425,12 +369,12 @@ class BulkOperationWorkflowTests(TestCase):
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
- operation_type='DELETE_CONTENT',
- status='PENDING',
+ operation_type="DELETE_CONTENT",
+ status="PENDING",
total_items=5,
processed_items=0,
created_by=self.admin,
- parameters={'content_ids': list(range(1, 6))}
+ parameters={"content_ids": list(range(1, 6))},
)
operation.transition_to_running(user=self.admin)
@@ -442,11 +386,11 @@ class BulkOperationWorkflowTests(TestCase):
operation.failed_items = 3
operation.transition_to_failed(user=self.admin)
operation.completed_at = timezone.now()
- operation.results = {'error': 'Database connection lost', 'processed': 2}
+ operation.results = {"error": "Database connection lost", "processed": 2}
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'FAILED')
+ self.assertEqual(operation.status, "FAILED")
self.assertEqual(operation.failed_items, 3)
def test_bulk_operation_cancellation_workflow(self):
@@ -458,13 +402,13 @@ class BulkOperationWorkflowTests(TestCase):
from apps.moderation.models import BulkOperation
operation = BulkOperation.objects.create(
- operation_type='BATCH_UPDATE',
- status='PENDING',
+ operation_type="BATCH_UPDATE",
+ status="PENDING",
total_items=100,
processed_items=0,
created_by=self.admin,
- parameters={'update_field': 'status'},
- can_cancel=True
+ parameters={"update_field": "status"},
+ can_cancel=True,
)
operation.transition_to_running(user=self.admin)
@@ -477,11 +421,11 @@ class BulkOperationWorkflowTests(TestCase):
# Admin cancels
operation.transition_to_cancelled(user=self.admin)
operation.completed_at = timezone.now()
- operation.results = {'cancelled_at': 30, 'reason': 'User requested cancellation'}
+ operation.results = {"cancelled_at": 30, "reason": "User requested cancellation"}
operation.save()
operation.refresh_from_db()
- self.assertEqual(operation.status, 'CANCELLED')
+ self.assertEqual(operation.status, "CANCELLED")
self.assertEqual(operation.processed_items, 30)
@@ -491,10 +435,7 @@ class ModerationQueueWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='mod_queue',
- email='mod_queue@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="mod_queue", email="mod_queue@example.com", password="testpass123", role="MODERATOR"
)
def test_queue_completion_workflow(self):
@@ -506,14 +447,14 @@ class ModerationQueueWorkflowTests(TestCase):
from apps.moderation.models import ModerationQueue
queue_item = ModerationQueue.objects.create(
- queue_type='SUBMISSION_REVIEW',
- status='PENDING',
- priority='MEDIUM',
- item_type='edit_submission',
- item_id=123
+ queue_type="SUBMISSION_REVIEW",
+ status="PENDING",
+ priority="MEDIUM",
+ item_type="edit_submission",
+ item_id=123,
)
- self.assertEqual(queue_item.status, 'PENDING')
+ self.assertEqual(queue_item.status, "PENDING")
# Moderator claims
queue_item.transition_to_in_progress(user=self.moderator)
@@ -521,7 +462,7 @@ class ModerationQueueWorkflowTests(TestCase):
queue_item.assigned_at = timezone.now()
queue_item.save()
- self.assertEqual(queue_item.status, 'IN_PROGRESS')
+ self.assertEqual(queue_item.status, "IN_PROGRESS")
# Work completed
queue_item.transition_to_completed(user=self.moderator)
@@ -529,4 +470,4 @@ class ModerationQueueWorkflowTests(TestCase):
queue_item.save()
queue_item.refresh_from_db()
- self.assertEqual(queue_item.status, 'COMPLETED')
+ self.assertEqual(queue_item.status, "COMPLETED")
diff --git a/backend/apps/moderation/urls.py b/backend/apps/moderation/urls.py
index 548fa6b8..22038517 100644
--- a/backend/apps/moderation/urls.py
+++ b/backend/apps/moderation/urls.py
@@ -26,6 +26,7 @@ from .views import (
class ModerationDashboardView(TemplateView):
"""Moderation dashboard view with HTMX integration."""
+
template_name = "moderation/dashboard.html"
def get_context_data(self, **kwargs):
@@ -38,6 +39,7 @@ class ModerationDashboardView(TemplateView):
class SubmissionListView(TemplateView):
"""Submission list view with filtering."""
+
template_name = "moderation/partials/dashboard_content.html"
def get_context_data(self, **kwargs):
@@ -63,8 +65,10 @@ class SubmissionListView(TemplateView):
class HistoryPageView(TemplateView):
"""Main history page view."""
+
template_name = "moderation/history.html"
+
# Create router and register viewsets
router = DefaultRouter()
router.register(r"reports", ModerationReportViewSet, basename="moderation-reports")
diff --git a/backend/apps/moderation/views.py b/backend/apps/moderation/views.py
index 8a7b6c87..b15c5493 100644
--- a/backend/apps/moderation/views.py
+++ b/backend/apps/moderation/views.py
@@ -86,9 +86,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
filtering, search, and permission controls.
"""
- queryset = ModerationReport.objects.select_related(
- "reported_by", "assigned_moderator", "content_type"
- ).all()
+ queryset = ModerationReport.objects.select_related("reported_by", "assigned_moderator", "content_type").all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ModerationReportFilter
@@ -207,9 +205,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
except User.DoesNotExist:
- return Response(
- {"error": "Moderator not found"}, status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "Moderator not found"}, status=status.HTTP_404_NOT_FOUND)
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
def resolve(self, request, pk=None):
@@ -313,17 +309,11 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
overdue_reports += 1
# Reports by priority and type
- reports_by_priority = dict(
- queryset.values_list("priority").annotate(count=Count("id"))
- )
- reports_by_type = dict(
- queryset.values_list("report_type").annotate(count=Count("id"))
- )
+ reports_by_priority = dict(queryset.values_list("priority").annotate(count=Count("id")))
+ reports_by_type = dict(queryset.values_list("report_type").annotate(count=Count("id")))
# Average resolution time
- resolved_queryset = queryset.filter(
- status="RESOLVED", resolved_at__isnull=False
- )
+ resolved_queryset = queryset.filter(status="RESOLVED", resolved_at__isnull=False)
avg_resolution_time = 0
if resolved_queryset.exists():
@@ -430,9 +420,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
"log": None,
},
)
- return Response(
- {"error": "Log not found"}, status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "Log not found"}, status=status.HTTP_404_NOT_FOUND)
# Filter by model type with app_label support for correct ContentType resolution
model_type = request.query_params.get("model_type")
@@ -441,9 +429,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
try:
if app_label:
# Use both app_label and model for precise matching
- content_type = ContentType.objects.get_by_natural_key(
- app_label, model_type
- )
+ content_type = ContentType.objects.get_by_natural_key(app_label, model_type)
else:
# Map common model names to their app_labels for correct resolution
model_app_mapping = {
@@ -457,9 +443,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
}
mapped_app_label = model_app_mapping.get(model_type.lower())
if mapped_app_label:
- content_type = ContentType.objects.get_by_natural_key(
- mapped_app_label, model_type.lower()
- )
+ content_type = ContentType.objects.get_by_natural_key(mapped_app_label, model_type.lower())
else:
# Fallback to model-only lookup
content_type = ContentType.objects.get(model=model_type)
@@ -576,9 +560,7 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
completion, and progress tracking.
"""
- queryset = ModerationQueue.objects.select_related(
- "assigned_to", "related_report", "content_type"
- ).all()
+ queryset = ModerationQueue.objects.select_related("assigned_to", "related_report", "content_type").all()
serializer_class = ModerationQueueSerializer
permission_classes = [CanViewModerationData]
@@ -871,9 +853,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
and status management.
"""
- queryset = ModerationAction.objects.select_related(
- "moderator", "target_user", "related_report"
- ).all()
+ queryset = ModerationAction.objects.select_related("moderator", "target_user", "related_report").all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ModerationActionFilter
@@ -907,9 +887,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=["get"], permission_classes=[CanViewModerationData])
def active(self, request):
"""Get all active moderation actions."""
- queryset = self.get_queryset().filter(
- is_active=True, expires_at__gt=timezone.now()
- )
+ queryset = self.get_queryset().filter(is_active=True, expires_at__gt=timezone.now())
page = self.paginate_queryset(queryset)
if page is not None:
@@ -922,9 +900,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=["get"], permission_classes=[CanViewModerationData])
def expired(self, request):
"""Get all expired moderation actions."""
- queryset = self.get_queryset().filter(
- expires_at__lte=timezone.now(), is_active=True
- )
+ queryset = self.get_queryset().filter(expires_at__lte=timezone.now(), is_active=True)
page = self.paginate_queryset(queryset)
if page is not None:
@@ -1173,9 +1149,7 @@ class UserModerationViewSet(viewsets.ViewSet):
if not query:
return Response([])
- queryset = User.objects.filter(
- Q(username__icontains=query) | Q(email__icontains=query)
- )[:20]
+ queryset = User.objects.filter(Q(username__icontains=query) | Q(email__icontains=query))[:20]
users_data = [
{
@@ -1194,9 +1168,7 @@ class UserModerationViewSet(viewsets.ViewSet):
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
- return Response(
- {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
# Gather user moderation data
reports_made = ModerationReport.objects.filter(reported_by=user).count()
@@ -1206,12 +1178,8 @@ class UserModerationViewSet(viewsets.ViewSet):
actions_against = ModerationAction.objects.filter(target_user=user)
warnings_received = actions_against.filter(action_type="WARNING").count()
- suspensions_received = actions_against.filter(
- action_type="USER_SUSPENSION"
- ).count()
- active_restrictions = actions_against.filter(
- is_active=True, expires_at__gt=timezone.now()
- ).count()
+ suspensions_received = actions_against.filter(action_type="USER_SUSPENSION").count()
+ active_restrictions = actions_against.filter(is_active=True, expires_at__gt=timezone.now()).count()
# Risk assessment (simplified)
risk_factors = []
@@ -1230,9 +1198,7 @@ class UserModerationViewSet(viewsets.ViewSet):
risk_level = "HIGH"
# Recent activity
- recent_reports = ModerationReport.objects.filter(reported_by=user).order_by(
- "-created_at"
- )[:5]
+ recent_reports = ModerationReport.objects.filter(reported_by=user).order_by("-created_at")[:5]
recent_actions = actions_against.order_by("-created_at")[:5]
@@ -1244,9 +1210,7 @@ class UserModerationViewSet(viewsets.ViewSet):
account_status = "RESTRICTED"
last_violation = (
- actions_against.filter(
- action_type__in=["WARNING", "USER_SUSPENSION", "USER_BAN"]
- )
+ actions_against.filter(action_type__in=["WARNING", "USER_SUSPENSION", "USER_BAN"])
.order_by("-created_at")
.first()
)
@@ -1266,16 +1230,10 @@ class UserModerationViewSet(viewsets.ViewSet):
"active_restrictions": active_restrictions,
"risk_level": risk_level,
"risk_factors": risk_factors,
- "recent_reports": ModerationReportSerializer(
- recent_reports, many=True
- ).data,
- "recent_actions": ModerationActionSerializer(
- recent_actions, many=True
- ).data,
+ "recent_reports": ModerationReportSerializer(recent_reports, many=True).data,
+ "recent_actions": ModerationActionSerializer(recent_actions, many=True).data,
"account_status": account_status,
- "last_violation_date": (
- last_violation.created_at if last_violation else None
- ),
+ "last_violation_date": (last_violation.created_at if last_violation else None),
"next_review_date": None, # Would be calculated based on business rules
}
@@ -1287,13 +1245,9 @@ class UserModerationViewSet(viewsets.ViewSet):
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
- return Response(
- {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
- serializer = CreateModerationActionSerializer(
- data=request.data, context={"request": request}
- )
+ serializer = CreateModerationActionSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
# Override target_user_id with the user from URL
@@ -1331,9 +1285,7 @@ class UserModerationViewSet(viewsets.ViewSet):
queryset = User.objects.all()
if query:
- queryset = queryset.filter(
- Q(username__icontains=query) | Q(email__icontains=query)
- )
+ queryset = queryset.filter(Q(username__icontains=query) | Q(email__icontains=query))
if role:
queryset = queryset.filter(role=role)
@@ -1376,12 +1328,8 @@ class UserModerationViewSet(viewsets.ViewSet):
def stats(self, request):
"""Get overall user moderation statistics."""
total_actions = ModerationAction.objects.count()
- active_actions = ModerationAction.objects.filter(
- is_active=True, expires_at__gt=timezone.now()
- ).count()
- expired_actions = ModerationAction.objects.filter(
- expires_at__lte=timezone.now()
- ).count()
+ active_actions = ModerationAction.objects.filter(is_active=True, expires_at__gt=timezone.now()).count()
+ expired_actions = ModerationAction.objects.filter(expires_at__lte=timezone.now()).count()
stats_data = {
"total_actions": total_actions,
@@ -1404,6 +1352,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
Includes claim/unclaim endpoints with concurrency protection using
database row locking (select_for_update) to prevent race conditions.
"""
+
queryset = EditSubmission.objects.all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
search_fields = ["reason", "changes"]
@@ -1425,7 +1374,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# User filter
user_id = self.request.query_params.get("user")
if user_id:
- queryset = queryset.filter(user_id=user_id)
+ queryset = queryset.filter(user_id=user_id)
return queryset
@@ -1452,15 +1401,12 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# Lock the row for update - other transactions will fail immediately
submission = EditSubmission.objects.select_for_update(nowait=True).get(pk=pk)
except EditSubmission.DoesNotExist:
- return Response(
- {"error": "Submission not found"},
- status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "Submission not found"}, status=status.HTTP_404_NOT_FOUND)
except DatabaseError:
# Row is already locked by another transaction
return Response(
{"error": "Submission is being claimed by another moderator. Please try again."},
- status=status.HTTP_409_CONFLICT
+ status=status.HTTP_409_CONFLICT,
)
# Check if already claimed
@@ -1471,14 +1417,14 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
"claimed_by": submission.claimed_by.username if submission.claimed_by else None,
"claimed_at": submission.claimed_at.isoformat() if submission.claimed_at else None,
},
- status=status.HTTP_409_CONFLICT
+ status=status.HTTP_409_CONFLICT,
)
# Check if in valid state for claiming
if submission.status != "PENDING":
return Response(
{"error": f"Cannot claim submission in {submission.status} state"},
- status=status.HTTP_400_BAD_REQUEST
+ status=status.HTTP_400_BAD_REQUEST,
)
try:
@@ -1512,15 +1458,11 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# Only the claiming user or an admin can unclaim
if submission.claimed_by != request.user and not request.user.is_staff:
return Response(
- {"error": "Only the claiming moderator or an admin can unclaim"},
- status=status.HTTP_403_FORBIDDEN
+ {"error": "Only the claiming moderator or an admin can unclaim"}, status=status.HTTP_403_FORBIDDEN
)
if submission.status != "CLAIMED":
- return Response(
- {"error": "Submission is not claimed"},
- status=status.HTTP_400_BAD_REQUEST
- )
+ return Response({"error": "Submission is not claimed"}, status=status.HTTP_400_BAD_REQUEST)
try:
submission.unclaim(user=request.user)
@@ -1557,8 +1499,8 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
reason = request.data.get("reason", "")
try:
- submission.reject(moderator=user, reason=reason)
- return Response(self.get_serializer(submission).data)
+ submission.reject(moderator=user, reason=reason)
+ return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -1569,8 +1511,8 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
reason = request.data.get("reason", "")
try:
- submission.escalate(moderator=user, reason=reason)
- return Response(self.get_serializer(submission).data)
+ submission.escalate(moderator=user, reason=reason)
+ return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -1582,6 +1524,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
Includes claim/unclaim endpoints with concurrency protection using
database row locking (select_for_update) to prevent race conditions.
"""
+
queryset = PhotoSubmission.objects.all()
serializer_class = PhotoSubmissionSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@@ -1599,7 +1542,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
# User filter
user_id = self.request.query_params.get("user")
if user_id:
- queryset = queryset.filter(user_id=user_id)
+ queryset = queryset.filter(user_id=user_id)
return queryset
@@ -1617,14 +1560,11 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
try:
submission = PhotoSubmission.objects.select_for_update(nowait=True).get(pk=pk)
except PhotoSubmission.DoesNotExist:
- return Response(
- {"error": "Submission not found"},
- status=status.HTTP_404_NOT_FOUND
- )
+ return Response({"error": "Submission not found"}, status=status.HTTP_404_NOT_FOUND)
except DatabaseError:
return Response(
{"error": "Submission is being claimed by another moderator. Please try again."},
- status=status.HTTP_409_CONFLICT
+ status=status.HTTP_409_CONFLICT,
)
if submission.status == "CLAIMED":
@@ -1634,13 +1574,13 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
"claimed_by": submission.claimed_by.username if submission.claimed_by else None,
"claimed_at": submission.claimed_at.isoformat() if submission.claimed_at else None,
},
- status=status.HTTP_409_CONFLICT
+ status=status.HTTP_409_CONFLICT,
)
if submission.status != "PENDING":
return Response(
{"error": f"Cannot claim submission in {submission.status} state"},
- status=status.HTTP_400_BAD_REQUEST
+ status=status.HTTP_400_BAD_REQUEST,
)
try:
@@ -1669,15 +1609,11 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
if submission.claimed_by != request.user and not request.user.is_staff:
return Response(
- {"error": "Only the claiming moderator or an admin can unclaim"},
- status=status.HTTP_403_FORBIDDEN
+ {"error": "Only the claiming moderator or an admin can unclaim"}, status=status.HTTP_403_FORBIDDEN
)
if submission.status != "CLAIMED":
- return Response(
- {"error": "Submission is not claimed"},
- status=status.HTTP_400_BAD_REQUEST
- )
+ return Response({"error": "Submission is not claimed"}, status=status.HTTP_400_BAD_REQUEST)
try:
submission.unclaim(user=request.user)
@@ -1731,4 +1667,3 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
-
diff --git a/backend/apps/parks/admin.py b/backend/apps/parks/admin.py
index 9921c85a..021b1c95 100644
--- a/backend/apps/parks/admin.py
+++ b/backend/apps/parks/admin.py
@@ -156,7 +156,6 @@ class ParkLocationAdmin(QueryOptimizationMixin, GISModelAdmin):
"description": "OpenStreetMap identifiers for data synchronization.",
},
),
-
)
@admin.display(description="Park")
@@ -358,9 +357,7 @@ class ParkAdmin(
for park in queryset:
# Statistics are auto-calculated, so just touch the record
park.save(update_fields=["updated_at"])
- self.message_user(
- request, f"Successfully recalculated statistics for {queryset.count()} parks."
- )
+ self.message_user(request, f"Successfully recalculated statistics for {queryset.count()} parks.")
def get_actions(self, request):
"""Add custom actions to the admin."""
@@ -482,9 +479,7 @@ class CompanyHeadquartersInline(admin.StackedInline):
)
-class CompanyHeadquartersAdmin(
- QueryOptimizationMixin, TimestampFieldsMixin, BaseModelAdmin
-):
+class CompanyHeadquartersAdmin(QueryOptimizationMixin, TimestampFieldsMixin, BaseModelAdmin):
"""
Admin interface for standalone CompanyHeadquarters management.
@@ -661,7 +656,7 @@ class CompanyAdmin(
color = colors.get(role, "#6c757d")
badges.append(
f'{role}'
)
return format_html("".join(badges))
@@ -702,9 +697,7 @@ class CompanyAdmin(
"""Refresh park count statistics for selected companies."""
for company in queryset:
company.save(update_fields=["updated_at"])
- self.message_user(
- request, f"Successfully updated counts for {queryset.count()} companies."
- )
+ self.message_user(request, f"Successfully updated counts for {queryset.count()} companies.")
def get_actions(self, request):
"""Add custom actions to the admin."""
@@ -840,12 +833,8 @@ class ParkReviewAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin)
"""Display moderation status with color coding."""
if obj.moderated_by:
if obj.is_published:
- return format_html(
- 'Approved'
- )
- return format_html(
- 'Rejected'
- )
+ return format_html('Approved')
+ return format_html('Rejected')
return format_html('Pending')
def save_model(self, request, obj, form, change):
diff --git a/backend/apps/parks/apps.py b/backend/apps/parks/apps.py
index a88f63c7..7ccbe6a4 100644
--- a/backend/apps/parks/apps.py
+++ b/backend/apps/parks/apps.py
@@ -22,9 +22,7 @@ class ParksConfig(AppConfig):
from apps.parks.models import Park
# Register FSM transitions for Park
- apply_state_machine(
- Park, field_name="status", choice_group="statuses", domain="parks"
- )
+ apply_state_machine(Park, field_name="status", choice_group="statuses", domain="parks")
def _register_callbacks(self):
"""Register FSM transition callbacks for park models."""
@@ -42,31 +40,16 @@ class ParksConfig(AppConfig):
from apps.parks.models import Park
# Cache invalidation for all park status changes
- register_callback(
- Park, 'status', '*', '*',
- ParkCacheInvalidation()
- )
+ register_callback(Park, "status", "*", "*", ParkCacheInvalidation())
# API cache invalidation
- register_callback(
- Park, 'status', '*', '*',
- APICacheInvalidation(include_geo_cache=True)
- )
+ register_callback(Park, "status", "*", "*", APICacheInvalidation(include_geo_cache=True))
# Search text update
- register_callback(
- Park, 'status', '*', '*',
- SearchTextUpdateCallback()
- )
+ register_callback(Park, "status", "*", "*", SearchTextUpdateCallback())
# Notification for significant status changes
- register_callback(
- Park, 'status', '*', 'CLOSED_PERM',
- StatusChangeNotification(notify_admins=True)
- )
- register_callback(
- Park, 'status', '*', 'DEMOLISHED',
- StatusChangeNotification(notify_admins=True)
- )
+ register_callback(Park, "status", "*", "CLOSED_PERM", StatusChangeNotification(notify_admins=True))
+ register_callback(Park, "status", "*", "DEMOLISHED", StatusChangeNotification(notify_admins=True))
logger.debug("Registered park transition callbacks")
diff --git a/backend/apps/parks/choices.py b/backend/apps/parks/choices.py
index 37e83da8..78930077 100644
--- a/backend/apps/parks/choices.py
+++ b/backend/apps/parks/choices.py
@@ -15,101 +15,101 @@ PARK_STATUSES = [
label="Operating",
description="Park is currently open and operating normally",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 1,
- 'can_transition_to': [
- 'CLOSED_TEMP',
- 'CLOSED_PERM',
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800",
+ "sort_order": 1,
+ "can_transition_to": [
+ "CLOSED_TEMP",
+ "CLOSED_PERM",
],
- 'requires_moderator': False,
- 'is_final': False,
- 'is_initial': True,
+ "requires_moderator": False,
+ "is_final": False,
+ "is_initial": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSED_TEMP",
label="Temporarily Closed",
description="Park is temporarily closed for maintenance, weather, or seasonal reasons",
metadata={
- 'color': 'yellow',
- 'icon': 'pause-circle',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 2,
- 'can_transition_to': [
- 'CLOSED_PERM',
+ "color": "yellow",
+ "icon": "pause-circle",
+ "css_class": "bg-yellow-100 text-yellow-800",
+ "sort_order": 2,
+ "can_transition_to": [
+ "CLOSED_PERM",
],
- 'requires_moderator': False,
- 'is_final': False,
+ "requires_moderator": False,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSED_PERM",
label="Permanently Closed",
description="Park has been permanently closed and will not reopen",
metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 3,
- 'can_transition_to': [
- 'DEMOLISHED',
- 'RELOCATED',
+ "color": "red",
+ "icon": "x-circle",
+ "css_class": "bg-red-100 text-red-800",
+ "sort_order": 3,
+ "can_transition_to": [
+ "DEMOLISHED",
+ "RELOCATED",
],
- 'requires_moderator': True,
- 'is_final': False,
+ "requires_moderator": True,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="UNDER_CONSTRUCTION",
label="Under Construction",
description="Park is currently being built or undergoing major renovation",
metadata={
- 'color': 'blue',
- 'icon': 'tool',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 4,
- 'can_transition_to': [
- 'OPERATING',
+ "color": "blue",
+ "icon": "tool",
+ "css_class": "bg-blue-100 text-blue-800",
+ "sort_order": 4,
+ "can_transition_to": [
+ "OPERATING",
],
- 'requires_moderator': False,
- 'is_final': False,
+ "requires_moderator": False,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="DEMOLISHED",
label="Demolished",
description="Park has been completely demolished and removed",
metadata={
- 'color': 'gray',
- 'icon': 'trash',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 5,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_final': True,
+ "color": "gray",
+ "icon": "trash",
+ "css_class": "bg-gray-100 text-gray-800",
+ "sort_order": 5,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="RELOCATED",
label="Relocated",
description="Park has been moved to a different location",
metadata={
- 'color': 'purple',
- 'icon': 'arrow-right',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 6,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_final': True,
+ "color": "purple",
+ "icon": "arrow-right",
+ "css_class": "bg-purple-100 text-purple-800",
+ "sort_order": 6,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -119,133 +119,88 @@ PARK_TYPES = [
value="THEME_PARK",
label="Theme Park",
description="Large-scale amusement park with themed areas and attractions",
- metadata={
- 'color': 'red',
- 'icon': 'castle',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "red", "icon": "castle", "css_class": "bg-red-100 text-red-800", "sort_order": 1},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="AMUSEMENT_PARK",
label="Amusement Park",
description="Traditional amusement park with rides and games",
- metadata={
- 'color': 'blue',
- 'icon': 'ferris-wheel',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "blue", "icon": "ferris-wheel", "css_class": "bg-blue-100 text-blue-800", "sort_order": 2},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="WATER_PARK",
label="Water Park",
description="Park featuring water-based attractions and activities",
- metadata={
- 'color': 'cyan',
- 'icon': 'water',
- 'css_class': 'bg-cyan-100 text-cyan-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "cyan", "icon": "water", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 3},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FAMILY_ENTERTAINMENT_CENTER",
label="Family Entertainment Center",
description="Indoor entertainment facility with games and family attractions",
- metadata={
- 'color': 'green',
- 'icon': 'family',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 4},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CARNIVAL",
label="Carnival",
description="Traveling amusement show with rides, games, and entertainment",
- metadata={
- 'color': 'yellow',
- 'icon': 'carnival',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "yellow", "icon": "carnival", "css_class": "bg-yellow-100 text-yellow-800", "sort_order": 5},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FAIR",
label="Fair",
description="Temporary event featuring rides, games, and agricultural exhibits",
- metadata={
- 'color': 'orange',
- 'icon': 'fair',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 6
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "orange", "icon": "fair", "css_class": "bg-orange-100 text-orange-800", "sort_order": 6},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="PIER",
label="Pier",
description="Seaside entertainment pier with rides and attractions",
- metadata={
- 'color': 'teal',
- 'icon': 'pier',
- 'css_class': 'bg-teal-100 text-teal-800',
- 'sort_order': 7
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "teal", "icon": "pier", "css_class": "bg-teal-100 text-teal-800", "sort_order": 7},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="BOARDWALK",
label="Boardwalk",
description="Waterfront entertainment area with rides and attractions",
metadata={
- 'color': 'indigo',
- 'icon': 'boardwalk',
- 'css_class': 'bg-indigo-100 text-indigo-800',
- 'sort_order': 8
+ "color": "indigo",
+ "icon": "boardwalk",
+ "css_class": "bg-indigo-100 text-indigo-800",
+ "sort_order": 8,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="SAFARI_PARK",
label="Safari Park",
description="Wildlife park with drive-through animal experiences",
metadata={
- 'color': 'emerald',
- 'icon': 'safari',
- 'css_class': 'bg-emerald-100 text-emerald-800',
- 'sort_order': 9
+ "color": "emerald",
+ "icon": "safari",
+ "css_class": "bg-emerald-100 text-emerald-800",
+ "sort_order": 9,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="ZOO",
label="Zoo",
description="Zoological park with animal exhibits and educational programs",
- metadata={
- 'color': 'lime',
- 'icon': 'zoo',
- 'css_class': 'bg-lime-100 text-lime-800',
- 'sort_order': 10
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "lime", "icon": "zoo", "css_class": "bg-lime-100 text-lime-800", "sort_order": 10},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="OTHER",
label="Other",
description="Park type that doesn't fit into standard categories",
- metadata={
- 'color': 'gray',
- 'icon': 'other',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 11
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 11},
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -256,30 +211,30 @@ PARKS_COMPANY_ROLES = [
label="Park Operator",
description="Company that operates and manages theme parks and amusement facilities",
metadata={
- 'color': 'blue',
- 'icon': 'building-office',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 1,
- 'domain': 'parks',
- 'permissions': ['manage_parks', 'view_operations'],
- 'url_pattern': '/parks/operators/{slug}/'
+ "color": "blue",
+ "icon": "building-office",
+ "css_class": "bg-blue-100 text-blue-800",
+ "sort_order": 1,
+ "domain": "parks",
+ "permissions": ["manage_parks", "view_operations"],
+ "url_pattern": "/parks/operators/{slug}/",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="PROPERTY_OWNER",
label="Property Owner",
description="Company that owns the land and property where parks are located",
metadata={
- 'color': 'green',
- 'icon': 'home',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 2,
- 'domain': 'parks',
- 'permissions': ['manage_property', 'view_ownership'],
- 'url_pattern': '/parks/owners/{slug}/'
+ "color": "green",
+ "icon": "home",
+ "css_class": "bg-green-100 text-green-800",
+ "sort_order": 2,
+ "domain": "parks",
+ "permissions": ["manage_property", "view_ownership"],
+ "url_pattern": "/parks/owners/{slug}/",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -292,7 +247,7 @@ def register_parks_choices():
choices=PARK_STATUSES,
domain="parks",
description="Park operational status options",
- metadata={'domain': 'parks', 'type': 'status'}
+ metadata={"domain": "parks", "type": "status"},
)
register_choices(
@@ -300,7 +255,7 @@ def register_parks_choices():
choices=PARK_TYPES,
domain="parks",
description="Park type and category classifications",
- metadata={'domain': 'parks', 'type': 'park_type'}
+ metadata={"domain": "parks", "type": "park_type"},
)
register_choices(
@@ -308,7 +263,7 @@ def register_parks_choices():
choices=PARKS_COMPANY_ROLES,
domain="parks",
description="Company role classifications for parks domain (OPERATOR and PROPERTY_OWNER only)",
- metadata={'domain': 'parks', 'type': 'company_role'}
+ metadata={"domain": "parks", "type": "company_role"},
)
diff --git a/backend/apps/parks/filters.py b/backend/apps/parks/filters.py
index 25148dd9..80bcb450 100644
--- a/backend/apps/parks/filters.py
+++ b/backend/apps/parks/filters.py
@@ -29,7 +29,7 @@ def validate_positive_integer(value):
raise ValidationError(_("Value must be a positive integer"))
return int(value)
except (TypeError, ValueError):
- raise ValidationError(_("Invalid number format"))
+ raise ValidationError(_("Invalid number format")) from None
class ParkFilter(FilterSet):
@@ -341,9 +341,7 @@ class ParkFilter(FilterSet):
if value:
return queryset.filter(coaster_count__gt=0)
else:
- return queryset.filter(
- models.Q(coaster_count__isnull=True) | models.Q(coaster_count=0)
- )
+ return queryset.filter(models.Q(coaster_count__isnull=True) | models.Q(coaster_count=0))
def filter_min_rating(self, queryset, name, value):
"""Filter parks by minimum rating"""
diff --git a/backend/apps/parks/forms.py b/backend/apps/parks/forms.py
index 1be322eb..be2fcbb0 100644
--- a/backend/apps/parks/forms.py
+++ b/backend/apps/parks/forms.py
@@ -256,9 +256,7 @@ class ParkForm(forms.ModelForm):
# Validate range
if latitude < -90 or latitude > 90:
- raise forms.ValidationError(
- "Latitude must be between -90 and 90 degrees."
- )
+ raise forms.ValidationError("Latitude must be between -90 and 90 degrees.")
# Convert to string to preserve exact decimal places
return str(latitude)
@@ -277,9 +275,7 @@ class ParkForm(forms.ModelForm):
# Validate range
if longitude < -180 or longitude > 180:
- raise forms.ValidationError(
- "Longitude must be between -180 and 180 degrees."
- )
+ raise forms.ValidationError("Longitude must be between -180 and 180 degrees.")
# Convert to string to preserve exact decimal places
return str(longitude)
@@ -314,7 +310,7 @@ class ParkForm(forms.ModelForm):
setattr(park_location, key, value)
# Handle coordinates if provided
- if "latitude" in location_data and "longitude" in location_data:
+ if "latitude" in location_data and "longitude" in location_data: # noqa: SIM102
if location_data["latitude"] and location_data["longitude"]:
park_location.set_coordinates(
float(location_data["latitude"]),
@@ -324,7 +320,7 @@ class ParkForm(forms.ModelForm):
except ParkLocation.DoesNotExist:
# Create new ParkLocation
coordinates_data = {}
- if "latitude" in location_data and "longitude" in location_data:
+ if "latitude" in location_data and "longitude" in location_data: # noqa: SIM102
if location_data["latitude"] and location_data["longitude"]:
coordinates_data = {
"latitude": float(location_data["latitude"]),
@@ -332,19 +328,13 @@ class ParkForm(forms.ModelForm):
}
# Remove coordinate fields from location_data for creation
- creation_data = {
- k: v
- for k, v in location_data.items()
- if k not in ["latitude", "longitude"]
- }
+ creation_data = {k: v for k, v in location_data.items() if k not in ["latitude", "longitude"]}
creation_data.setdefault("country", "USA")
park_location = ParkLocation.objects.create(park=park, **creation_data)
if coordinates_data:
- park_location.set_coordinates(
- coordinates_data["latitude"], coordinates_data["longitude"]
- )
+ park_location.set_coordinates(coordinates_data["latitude"], coordinates_data["longitude"])
park_location.save()
if commit:
diff --git a/backend/apps/parks/management/commands/create_sample_data.py b/backend/apps/parks/management/commands/create_sample_data.py
index d89aa716..a0e6e21c 100644
--- a/backend/apps/parks/management/commands/create_sample_data.py
+++ b/backend/apps/parks/management/commands/create_sample_data.py
@@ -27,9 +27,7 @@ class Command(BaseCommand):
self.create_park_areas()
self.create_reviews()
- self.stdout.write(
- self.style.SUCCESS("Successfully created comprehensive sample data!")
- )
+ self.stdout.write(self.style.SUCCESS("Successfully created comprehensive sample data!"))
self.print_summary()
except Exception as e:
@@ -101,13 +99,9 @@ class Command(BaseCommand):
]
for company_data in park_operators_data:
- company, created = ParkCompany.objects.get_or_create(
- slug=company_data["slug"], defaults=company_data
- )
+ company, created = ParkCompany.objects.get_or_create(slug=company_data["slug"], defaults=company_data)
self.created_companies[company.slug] = company
- self.stdout.write(
- f" {'Created' if created else 'Found'} park company: {company.name}"
- )
+ self.stdout.write(f" {'Created' if created else 'Found'} park company: {company.name}")
# Ride manufacturers and designers (using rides.models.Company)
ride_companies_data = [
@@ -194,13 +188,9 @@ class Command(BaseCommand):
]
for company_data in ride_companies_data:
- company, created = RideCompany.objects.get_or_create(
- slug=company_data["slug"], defaults=company_data
- )
+ company, created = RideCompany.objects.get_or_create(slug=company_data["slug"], defaults=company_data)
self.created_companies[company.slug] = company
- self.stdout.write(
- f" {'Created' if created else 'Found'} ride company: {company.name}"
- )
+ self.stdout.write(f" {'Created' if created else 'Found'} ride company: {company.name}")
def create_parks(self):
"""Create parks with proper operator relationships."""
diff --git a/backend/apps/parks/management/commands/fix_migrations.py b/backend/apps/parks/management/commands/fix_migrations.py
index 6bec9c84..c8d45b91 100644
--- a/backend/apps/parks/management/commands/fix_migrations.py
+++ b/backend/apps/parks/management/commands/fix_migrations.py
@@ -31,6 +31,4 @@ class Command(BaseCommand):
"""
)
- self.stdout.write(
- self.style.SUCCESS("Successfully fixed migration history")
- )
+ self.stdout.write(self.style.SUCCESS("Successfully fixed migration history"))
diff --git a/backend/apps/parks/management/commands/seed_initial_data.py b/backend/apps/parks/management/commands/seed_initial_data.py
index 3f386436..0f164597 100644
--- a/backend/apps/parks/management/commands/seed_initial_data.py
+++ b/backend/apps/parks/management/commands/seed_initial_data.py
@@ -50,13 +50,9 @@ class Command(BaseCommand):
companies = {}
for company_data in companies_data:
- operator, created = Operator.objects.get_or_create(
- name=company_data["name"], defaults=company_data
- )
+ operator, created = Operator.objects.get_or_create(name=company_data["name"], defaults=company_data)
companies[operator.name] = operator
- self.stdout.write(
- f"{'Created' if created else 'Found'} company: {operator.name}"
- )
+ self.stdout.write(f"{'Created' if created else 'Found'} company: {operator.name}")
# Create parks with their locations
parks_data = [
@@ -317,9 +313,7 @@ class Command(BaseCommand):
postal_code=loc_data["postal_code"],
)
# Set coordinates using the helper method
- park_location.set_coordinates(
- loc_data["latitude"], loc_data["longitude"]
- )
+ park_location.set_coordinates(loc_data["latitude"], loc_data["longitude"])
park_location.save()
# Create areas for park
@@ -329,8 +323,6 @@ class Command(BaseCommand):
park=park,
defaults={"description": area_data["description"]},
)
- self.stdout.write(
- f"{'Created' if created else 'Found'} area: {area.name} in {park.name}"
- )
+ self.stdout.write(f"{'Created' if created else 'Found'} area: {area.name} in {park.name}")
self.stdout.write(self.style.SUCCESS("Successfully seeded initial park data"))
diff --git a/backend/apps/parks/management/commands/seed_sample_data.py b/backend/apps/parks/management/commands/seed_sample_data.py
index 623ab271..52454d59 100644
--- a/backend/apps/parks/management/commands/seed_sample_data.py
+++ b/backend/apps/parks/management/commands/seed_sample_data.py
@@ -43,19 +43,13 @@ class Command(BaseCommand):
# Log what will be deleted
self.stdout.write(f" Found {park_review_count} park reviews to delete")
self.stdout.write(f" Found {ride_review_count} ride reviews to delete")
- self.stdout.write(
- f" Found {rollercoaster_stats_count} roller coaster stats to delete"
- )
+ self.stdout.write(f" Found {rollercoaster_stats_count} roller coaster stats to delete")
self.stdout.write(f" Found {ride_count} rides to delete")
self.stdout.write(f" Found {ride_model_count} ride models to delete")
self.stdout.write(f" Found {park_area_count} park areas to delete")
- self.stdout.write(
- f" Found {park_location_count} park locations to delete"
- )
+ self.stdout.write(f" Found {park_location_count} park locations to delete")
self.stdout.write(f" Found {park_count} parks to delete")
- self.stdout.write(
- f" Found {ride_company_count} ride companies to delete"
- )
+ self.stdout.write(f" Found {ride_company_count} ride companies to delete")
self.stdout.write(f" Found {company_count} park companies to delete")
self.stdout.write(f" Found {test_user_count} test users to delete")
@@ -72,9 +66,7 @@ class Command(BaseCommand):
# Roller coaster stats (references Ride)
if rollercoaster_stats_count > 0:
RollerCoasterStats.objects.all().delete()
- self.stdout.write(
- f" Deleted {rollercoaster_stats_count} roller coaster stats"
- )
+ self.stdout.write(f" Deleted {rollercoaster_stats_count} roller coaster stats")
# Rides (references Park, RideCompany, RideModel)
if ride_count > 0:
@@ -116,18 +108,14 @@ class Command(BaseCommand):
User.objects.filter(username="testuser").delete()
self.stdout.write(f" Deleted {test_user_count} test users")
- self.stdout.write(
- self.style.SUCCESS("Successfully cleaned up existing sample data!")
- )
+ self.stdout.write(self.style.SUCCESS("Successfully cleaned up existing sample data!"))
except Exception as e:
self.logger.error(
f"Error during data cleanup: {str(e)}",
exc_info=True,
)
- self.stdout.write(
- self.style.ERROR(f"Failed to clean up existing data: {str(e)}")
- )
+ self.stdout.write(self.style.ERROR(f"Failed to clean up existing data: {str(e)}"))
raise
def handle(self, *args, **options):
@@ -137,9 +125,7 @@ class Command(BaseCommand):
# Check if required tables exist
if not self.check_required_tables():
self.stdout.write(
- self.style.ERROR(
- "Required database tables are missing. Please run migrations first."
- )
+ self.style.ERROR("Required database tables are missing. Please run migrations first.")
)
return
@@ -163,17 +149,11 @@ class Command(BaseCommand):
# Add sample reviews for testing
self.create_reviews()
- self.stdout.write(
- self.style.SUCCESS("Successfully created comprehensive sample data!")
- )
+ self.stdout.write(self.style.SUCCESS("Successfully created comprehensive sample data!"))
except Exception as e:
- self.logger.error(
- f"Error during sample data creation: {str(e)}", exc_info=True
- )
- self.stdout.write(
- self.style.ERROR(f"Failed to create sample data: {str(e)}")
- )
+ self.logger.error(f"Error during sample data creation: {str(e)}", exc_info=True)
+ self.stdout.write(self.style.ERROR(f"Failed to create sample data: {str(e)}"))
raise
def check_required_tables(self):
@@ -202,11 +182,7 @@ class Command(BaseCommand):
missing_tables.append(model._meta.label)
if missing_tables:
- self.stdout.write(
- self.style.WARNING(
- f"Missing tables for models: {', '.join(missing_tables)}"
- )
- )
+ self.stdout.write(self.style.WARNING(f"Missing tables for models: {', '.join(missing_tables)}"))
return False
self.stdout.write(self.style.SUCCESS("All required tables exist."))
@@ -357,9 +333,7 @@ class Command(BaseCommand):
}"
)
except Exception as e:
- self.logger.error(
- f"Error creating park company {data['name']}: {str(e)}"
- )
+ self.logger.error(f"Error creating park company {data['name']}: {str(e)}")
raise
# Create companies in rides app (for manufacturers and designers)
@@ -382,9 +356,7 @@ class Command(BaseCommand):
}"
)
except Exception as e:
- self.logger.error(
- f"Error creating ride company {data['name']}: {str(e)}"
- )
+ self.logger.error(f"Error creating ride company {data['name']}: {str(e)}")
raise
except Exception as e:
@@ -512,9 +484,7 @@ class Command(BaseCommand):
try:
operator = self.park_companies[park_data["operator"]]
property_owner = (
- self.park_companies.get(park_data["property_owner"])
- if park_data["property_owner"]
- else None
+ self.park_companies.get(park_data["property_owner"]) if park_data["property_owner"] else None
)
park, created = Park.objects.get_or_create(
@@ -530,9 +500,7 @@ class Command(BaseCommand):
},
)
self.parks[park_data["name"]] = park
- self.stdout.write(
- f" {'Created' if created else 'Found'} park: {park.name}"
- )
+ self.stdout.write(f" {'Created' if created else 'Found'} park: {park.name}")
# Create location for park
if created:
@@ -547,9 +515,7 @@ class Command(BaseCommand):
postal_code=loc_data["postal_code"],
)
# Set coordinates using the helper method
- park_location.set_coordinates(
- loc_data["latitude"], loc_data["longitude"]
- )
+ park_location.set_coordinates(loc_data["latitude"], loc_data["longitude"])
park_location.save()
except Exception as e:
self.logger.error(
@@ -560,9 +526,7 @@ class Command(BaseCommand):
raise
except Exception as e:
- self.logger.error(
- f"Error creating park {park_data['name']}: {str(e)}"
- )
+ self.logger.error(f"Error creating park {park_data['name']}: {str(e)}")
raise
except Exception as e:
@@ -633,9 +597,7 @@ class Command(BaseCommand):
}"
)
except Exception as e:
- self.logger.error(
- f"Error creating ride model {model_data['name']}: {str(e)}"
- )
+ self.logger.error(f"Error creating ride model {model_data['name']}: {str(e)}")
raise
# Create rides
@@ -834,9 +796,7 @@ class Command(BaseCommand):
for ride_data in rides_data:
try:
park = self.parks[ride_data["park"]]
- manufacturer = self.ride_companies.get(
- ride_data.get("manufacturer")
- )
+ manufacturer = self.ride_companies.get(ride_data.get("manufacturer"))
designer = self.ride_companies.get(ride_data.get("designer"))
ride_model = self.ride_models.get(ride_data.get("ride_model"))
@@ -854,9 +814,7 @@ class Command(BaseCommand):
},
)
self.rides[ride_data["name"]] = ride
- self.stdout.write(
- f" {'Created' if created else 'Found'} ride: {ride.name}"
- )
+ self.stdout.write(f" {'Created' if created else 'Found'} ride: {ride.name}")
# Create roller coaster stats if provided
if created and "coaster_stats" in ride_data:
@@ -872,9 +830,7 @@ class Command(BaseCommand):
raise
except Exception as e:
- self.logger.error(
- f"Error creating ride {ride_data['name']}: {str(e)}"
- )
+ self.logger.error(f"Error creating ride {ride_data['name']}: {str(e)}")
raise
except Exception as e:
@@ -1011,9 +967,7 @@ class Command(BaseCommand):
} in {park.name}"
)
except Exception as e:
- self.logger.error(
- f"Error creating areas for park {area_group['park']}: {str(e)}"
- )
+ self.logger.error(f"Error creating areas for park {area_group['park']}: {str(e)}")
raise
except Exception as e:
diff --git a/backend/apps/parks/management/commands/test_location.py b/backend/apps/parks/management/commands/test_location.py
index 9a945086..c840325c 100644
--- a/backend/apps/parks/management/commands/test_location.py
+++ b/backend/apps/parks/management/commands/test_location.py
@@ -85,9 +85,7 @@ class Command(BaseCommand):
"country": "USA",
},
)
- location2.set_coordinates(
- 34.4244, -118.5971
- ) # Six Flags Magic Mountain coordinates
+ location2.set_coordinates(34.4244, -118.5971) # Six Flags Magic Mountain coordinates
location2.save()
# Test distance calculation
@@ -107,9 +105,7 @@ class Command(BaseCommand):
# Find parks within 100km of a point
# Same as Disneyland
search_point = Point(-117.9190, 33.8121, srid=4326)
- nearby_locations = ParkLocation.objects.filter(
- point__distance_lte=(search_point, D(km=100))
- )
+ nearby_locations = ParkLocation.objects.filter(point__distance_lte=(search_point, D(km=100)))
self.stdout.write(f" Found {nearby_locations.count()} parks within 100km")
for loc in nearby_locations:
self.stdout.write(f" - {loc.park.name} in {loc.city}, {loc.state}")
diff --git a/backend/apps/parks/management/commands/update_park_counts.py b/backend/apps/parks/management/commands/update_park_counts.py
index 82bc925c..cca0d52f 100644
--- a/backend/apps/parks/management/commands/update_park_counts.py
+++ b/backend/apps/parks/management/commands/update_park_counts.py
@@ -20,11 +20,7 @@ class Command(BaseCommand):
total_coasters = park.rides.filter(operating_rides, category="RC").count()
# Update park counts
- Park.objects.filter(id=park.id).update(
- total_rides=total_rides, total_roller_coasters=total_coasters
- )
+ Park.objects.filter(id=park.id).update(total_rides=total_rides, total_roller_coasters=total_coasters)
updated += 1
- self.stdout.write(
- self.style.SUCCESS(f"Successfully updated counts for {updated} parks")
- )
+ self.stdout.write(self.style.SUCCESS(f"Successfully updated counts for {updated} parks"))
diff --git a/backend/apps/parks/managers.py b/backend/apps/parks/managers.py
index 68dbb084..0f0530bb 100644
--- a/backend/apps/parks/managers.py
+++ b/backend/apps/parks/managers.py
@@ -30,23 +30,15 @@ class ParkQuerySet(StatusQuerySet, ReviewableQuerySet, LocationQuerySet):
distinct=True,
),
area_count=Count("areas", distinct=True),
- review_count=Count(
- "reviews", filter=Q(reviews__is_published=True), distinct=True
- ),
- average_rating_calculated=Avg(
- "reviews__rating", filter=Q(reviews__is_published=True)
- ),
+ review_count=Count("reviews", filter=Q(reviews__is_published=True), distinct=True),
+ average_rating_calculated=Avg("reviews__rating", filter=Q(reviews__is_published=True)),
latest_ride_opening=Max("rides__opening_date"),
oldest_ride_opening=Min("rides__opening_date"),
)
def optimized_for_list(self):
"""Optimize for park list display."""
- return (
- self.select_related("operator", "property_owner")
- .prefetch_related("location")
- .with_complete_stats()
- )
+ return self.select_related("operator", "property_owner").prefetch_related("location").with_complete_stats()
def optimized_for_detail(self):
"""Optimize for park detail display."""
@@ -59,9 +51,9 @@ class ParkQuerySet(StatusQuerySet, ReviewableQuerySet, LocationQuerySet):
"areas",
Prefetch(
"rides",
- queryset=Ride.objects.select_related(
- "manufacturer", "designer", "ride_model", "park_area"
- ).order_by("name"),
+ queryset=Ride.objects.select_related("manufacturer", "designer", "ride_model", "park_area").order_by(
+ "name"
+ ),
),
Prefetch(
"reviews",
@@ -82,9 +74,7 @@ class ParkQuerySet(StatusQuerySet, ReviewableQuerySet, LocationQuerySet):
def with_minimum_coasters(self, *, min_coasters: int = 5):
"""Filter parks with minimum number of coasters."""
- return self.with_complete_stats().filter(
- coaster_count_calculated__gte=min_coasters
- )
+ return self.with_complete_stats().filter(coaster_count_calculated__gte=min_coasters)
def large_parks(self, *, min_acres: float = 100.0):
"""Filter for large parks."""
@@ -123,16 +113,10 @@ class ParkQuerySet(StatusQuerySet, ReviewableQuerySet, LocationQuerySet):
"""Optimized search for autocomplete."""
return (
self.filter(
- Q(name__icontains=query)
- | Q(location__city__icontains=query)
- | Q(location__state__icontains=query)
+ Q(name__icontains=query) | Q(location__city__icontains=query) | Q(location__state__icontains=query)
)
.select_related("operator", "location")
- .only(
- "id", "name", "slug",
- "location__city", "location__state",
- "operator__name"
- )[:limit]
+ .only("id", "name", "slug", "location__city", "location__state", "operator__name")[:limit]
)
def with_location(self):
@@ -247,9 +231,7 @@ class ParkReviewManager(BaseManager):
return self.get_queryset().for_park(park_id=park_id)
def by_rating_range(self, *, min_rating: int = 1, max_rating: int = 10):
- return self.get_queryset().by_rating_range(
- min_rating=min_rating, max_rating=max_rating
- )
+ return self.get_queryset().by_rating_range(min_rating=min_rating, max_rating=max_rating)
def moderation_required(self):
return self.get_queryset().moderation_required()
@@ -275,17 +257,12 @@ class CompanyQuerySet(BaseQuerySet):
return self.annotate(
operated_parks_count=Count("operated_parks", distinct=True),
owned_parks_count=Count("owned_parks", distinct=True),
- total_parks_involvement=Count("operated_parks", distinct=True)
- + Count("owned_parks", distinct=True),
+ total_parks_involvement=Count("operated_parks", distinct=True) + Count("owned_parks", distinct=True),
)
def major_operators(self, *, min_parks: int = 5):
"""Filter for major park operators."""
- return (
- self.operators()
- .with_park_counts()
- .filter(operated_parks_count__gte=min_parks)
- )
+ return self.operators().with_park_counts().filter(operated_parks_count__gte=min_parks)
def optimized_for_list(self):
"""Optimize for company list display."""
@@ -313,7 +290,7 @@ class CompanyManager(BaseManager):
self.get_queryset()
.manufacturers()
.annotate(ride_count=Count("manufactured_rides", distinct=True))
- .only('id', 'name', 'slug', 'roles', 'description')
+ .only("id", "name", "slug", "roles", "description")
.order_by("name")
)
@@ -323,7 +300,7 @@ class CompanyManager(BaseManager):
self.get_queryset()
.filter(roles__contains=["DESIGNER"])
.annotate(ride_count=Count("designed_rides", distinct=True))
- .only('id', 'name', 'slug', 'roles', 'description')
+ .only("id", "name", "slug", "roles", "description")
.order_by("name")
)
@@ -333,6 +310,6 @@ class CompanyManager(BaseManager):
self.get_queryset()
.operators()
.with_park_counts()
- .only('id', 'name', 'slug', 'roles', 'description')
+ .only("id", "name", "slug", "roles", "description")
.order_by("name")
)
diff --git a/backend/apps/parks/migrations/0001_initial.py b/backend/apps/parks/migrations/0001_initial.py
index 9b9e34c6..412369ba 100644
--- a/backend/apps/parks/migrations/0001_initial.py
+++ b/backend/apps/parks/migrations/0001_initial.py
@@ -102,16 +102,12 @@ class Migration(migrations.Migration):
),
(
"size_acres",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
("website", models.URLField(blank=True)),
(
"average_rating",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=3, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
),
("ride_count", models.IntegerField(blank=True, null=True)),
("coaster_count", models.IntegerField(blank=True, null=True)),
@@ -266,16 +262,12 @@ class Migration(migrations.Migration):
),
(
"size_acres",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
("website", models.URLField(blank=True)),
(
"average_rating",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=3, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
),
("ride_count", models.IntegerField(blank=True, null=True)),
("coaster_count", models.IntegerField(blank=True, null=True)),
@@ -678,9 +670,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="parklocation",
- index=models.Index(
- fields=["city", "state"], name="parks_parkl_city_7cc873_idx"
- ),
+ index=models.Index(fields=["city", "state"], name="parks_parkl_city_7cc873_idx"),
),
migrations.AlterUniqueTogether(
name="parkreview",
diff --git a/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py b/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py
index 839c05b6..82d30c5a 100644
--- a/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py
+++ b/backend/apps/parks/migrations/0007_companyheadquartersevent_parklocationevent_and_more.py
@@ -35,9 +35,7 @@ class Migration(migrations.Migration):
),
(
"state_province",
- models.CharField(
- blank=True, help_text="State/Province/Region", max_length=100
- ),
+ models.CharField(blank=True, help_text="State/Province/Region", max_length=100),
),
(
"country",
@@ -49,9 +47,7 @@ class Migration(migrations.Migration):
),
(
"postal_code",
- models.CharField(
- blank=True, help_text="ZIP or postal code", max_length=20
- ),
+ models.CharField(blank=True, help_text="ZIP or postal code", max_length=20),
),
(
"mailing_address",
diff --git a/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py b/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py
index 0b1d2fd0..1eef37f5 100644
--- a/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py
+++ b/backend/apps/parks/migrations/0008_parkphoto_parkphotoevent_and_more.py
@@ -133,21 +133,15 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="parkphoto",
- index=models.Index(
- fields=["park", "is_primary"], name="parks_parkp_park_id_eda26e_idx"
- ),
+ index=models.Index(fields=["park", "is_primary"], name="parks_parkp_park_id_eda26e_idx"),
),
migrations.AddIndex(
model_name="parkphoto",
- index=models.Index(
- fields=["park", "is_approved"], name="parks_parkp_park_id_5fe576_idx"
- ),
+ index=models.Index(fields=["park", "is_approved"], name="parks_parkp_park_id_5fe576_idx"),
),
migrations.AddIndex(
model_name="parkphoto",
- index=models.Index(
- fields=["created_at"], name="parks_parkp_created_033dc3_idx"
- ),
+ index=models.Index(fields=["created_at"], name="parks_parkp_created_033dc3_idx"),
),
migrations.AddConstraint(
model_name="parkphoto",
diff --git a/backend/apps/parks/migrations/0015_populate_hybrid_filtering_fields.py b/backend/apps/parks/migrations/0015_populate_hybrid_filtering_fields.py
index 8c6ea11b..2b686d3d 100644
--- a/backend/apps/parks/migrations/0015_populate_hybrid_filtering_fields.py
+++ b/backend/apps/parks/migrations/0015_populate_hybrid_filtering_fields.py
@@ -11,24 +11,29 @@ def populate_computed_fields(apps, schema_editor):
try:
# Use raw SQL to update opening_year from opening_date
- schema_editor.execute("""
+ schema_editor.execute(
+ """
UPDATE parks_park
SET opening_year = EXTRACT(YEAR FROM opening_date)
WHERE opening_date IS NOT NULL;
- """)
+ """
+ )
# Use raw SQL to populate search_text
# This is a simplified version - we'll populate it with just name and description
- schema_editor.execute("""
+ schema_editor.execute(
+ """
UPDATE parks_park
SET search_text = LOWER(
COALESCE(name, '') || ' ' ||
COALESCE(description, '')
);
- """)
+ """
+ )
# Update search_text to include operator names using a join
- schema_editor.execute("""
+ schema_editor.execute(
+ """
UPDATE parks_park
SET search_text = LOWER(
COALESCE(parks_park.name, '') || ' ' ||
@@ -37,7 +42,8 @@ def populate_computed_fields(apps, schema_editor):
)
FROM parks_company
WHERE parks_park.operator_id = parks_company.id;
- """)
+ """
+ )
finally:
# Re-enable pghistory triggers
@@ -46,8 +52,8 @@ def populate_computed_fields(apps, schema_editor):
def reverse_populate_computed_fields(apps, schema_editor):
"""Clear computed fields (reverse operation)"""
- Park = apps.get_model('parks', 'Park')
- Park.objects.update(opening_year=None, search_text='')
+ Park = apps.get_model("parks", "Park")
+ Park.objects.update(opening_year=None, search_text="")
class Migration(migrations.Migration):
diff --git a/backend/apps/parks/migrations/0016_add_hybrid_filtering_indexes.py b/backend/apps/parks/migrations/0016_add_hybrid_filtering_indexes.py
index 7b1678b2..4b3859bb 100644
--- a/backend/apps/parks/migrations/0016_add_hybrid_filtering_indexes.py
+++ b/backend/apps/parks/migrations/0016_add_hybrid_filtering_indexes.py
@@ -13,37 +13,34 @@ class Migration(migrations.Migration):
# Composite indexes for common filter combinations
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_status_park_type_idx ON parks_park (status, park_type);",
- reverse_sql="DROP INDEX IF EXISTS parks_park_status_park_type_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_status_park_type_idx;",
),
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_opening_year_status_idx ON parks_park (opening_year, status) WHERE opening_year IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS parks_park_opening_year_status_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_opening_year_status_idx;",
),
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_size_rating_idx ON parks_park (size_acres, average_rating) WHERE size_acres IS NOT NULL AND average_rating IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS parks_park_size_rating_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_size_rating_idx;",
),
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_ride_coaster_count_idx ON parks_park (ride_count, coaster_count) WHERE ride_count IS NOT NULL AND coaster_count IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS parks_park_ride_coaster_count_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_ride_coaster_count_idx;",
),
-
# Full-text search index for search_text field
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_search_text_gin_idx ON parks_park USING gin(to_tsvector('english', search_text));",
- reverse_sql="DROP INDEX IF EXISTS parks_park_search_text_gin_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_search_text_gin_idx;",
),
-
# Trigram index for fuzzy search on search_text
migrations.RunSQL(
"CREATE EXTENSION IF NOT EXISTS pg_trgm;",
- reverse_sql="-- Cannot drop extension as it might be used elsewhere"
+ reverse_sql="-- Cannot drop extension as it might be used elsewhere",
),
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_search_text_trgm_idx ON parks_park USING gin(search_text gin_trgm_ops);",
- reverse_sql="DROP INDEX IF EXISTS parks_park_search_text_trgm_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_search_text_trgm_idx;",
),
-
# Indexes for location-based filtering (assuming location relationship exists)
migrations.RunSQL(
"""
@@ -51,27 +48,23 @@ class Migration(migrations.Migration):
ON parks_parklocation (country, state)
WHERE country IS NOT NULL AND state IS NOT NULL;
""",
- reverse_sql="DROP INDEX IF EXISTS parks_parklocation_country_state_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_parklocation_country_state_idx;",
),
-
# Index for operator-based filtering
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_operator_status_idx ON parks_park (operator_id, status);",
- reverse_sql="DROP INDEX IF EXISTS parks_park_operator_status_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_operator_status_idx;",
),
-
# Partial indexes for common status filters
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_operating_parks_idx ON parks_park (name, opening_year) WHERE status IN ('OPERATING', 'CLOSED_TEMP');",
- reverse_sql="DROP INDEX IF EXISTS parks_park_operating_parks_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_operating_parks_idx;",
),
-
# Index for ordering by name (already exists but ensuring it's optimized)
migrations.RunSQL(
"CREATE INDEX IF NOT EXISTS parks_park_name_lower_idx ON parks_park (LOWER(name));",
- reverse_sql="DROP INDEX IF EXISTS parks_park_name_lower_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_name_lower_idx;",
),
-
# Covering index for common query patterns
migrations.RunSQL(
"""
@@ -80,6 +73,6 @@ class Migration(migrations.Migration):
INCLUDE (name, slug, size_acres, average_rating, ride_count, coaster_count, operator_id)
WHERE status IN ('OPERATING', 'CLOSED_TEMP');
""",
- reverse_sql="DROP INDEX IF EXISTS parks_park_hybrid_covering_idx;"
+ reverse_sql="DROP INDEX IF EXISTS parks_park_hybrid_covering_idx;",
),
]
diff --git a/backend/apps/parks/migrations/0019_fix_pghistory_timezone.py b/backend/apps/parks/migrations/0019_fix_pghistory_timezone.py
index d0eb0fa8..1e0e4615 100644
--- a/backend/apps/parks/migrations/0019_fix_pghistory_timezone.py
+++ b/backend/apps/parks/migrations/0019_fix_pghistory_timezone.py
@@ -47,6 +47,6 @@ class Migration(migrations.Migration):
reverse_sql="""
-- This is irreversible, but we can drop and recreate without timezone
DROP FUNCTION IF EXISTS pgtrigger_insert_insert_66883() CASCADE;
- """
+ """,
),
]
diff --git a/backend/apps/parks/migrations/0020_fix_pghistory_update_timezone.py b/backend/apps/parks/migrations/0020_fix_pghistory_update_timezone.py
index de8057bc..b042274f 100644
--- a/backend/apps/parks/migrations/0020_fix_pghistory_update_timezone.py
+++ b/backend/apps/parks/migrations/0020_fix_pghistory_update_timezone.py
@@ -47,6 +47,6 @@ class Migration(migrations.Migration):
reverse_sql="""
-- This is irreversible, but we can drop and recreate without timezone
DROP FUNCTION IF EXISTS pgtrigger_update_update_19f56() CASCADE;
- """
+ """,
),
]
diff --git a/backend/apps/parks/migrations/0023_add_company_roles_gin_index.py b/backend/apps/parks/migrations/0023_add_company_roles_gin_index.py
index 8f87a01b..08a9de82 100644
--- a/backend/apps/parks/migrations/0023_add_company_roles_gin_index.py
+++ b/backend/apps/parks/migrations/0023_add_company_roles_gin_index.py
@@ -14,7 +14,7 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('parks', '0022_alter_company_roles_alter_companyevent_roles'),
+ ("parks", "0022_alter_company_roles_alter_companyevent_roles"),
]
operations = [
diff --git a/backend/apps/parks/migrations/0024_add_timezone_default.py b/backend/apps/parks/migrations/0024_add_timezone_default.py
index 9c535dfe..a2372f0e 100644
--- a/backend/apps/parks/migrations/0024_add_timezone_default.py
+++ b/backend/apps/parks/migrations/0024_add_timezone_default.py
@@ -11,16 +11,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('parks', '0023_add_company_roles_gin_index'),
+ ("parks", "0023_add_company_roles_gin_index"),
]
operations = [
migrations.AlterField(
- model_name='park',
- name='timezone',
+ model_name="park",
+ name="timezone",
field=models.CharField(
blank=True,
- default='UTC',
+ default="UTC",
help_text="Timezone identifier for park operations (e.g., 'America/New_York')",
max_length=50,
),
diff --git a/backend/apps/parks/migrations/0025_alter_company_options_alter_park_options_and_more.py b/backend/apps/parks/migrations/0025_alter_company_options_alter_park_options_and_more.py
index 737c0988..657f7bf9 100644
--- a/backend/apps/parks/migrations/0025_alter_company_options_alter_park_options_and_more.py
+++ b/backend/apps/parks/migrations/0025_alter_company_options_alter_park_options_and_more.py
@@ -61,16 +61,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed company description"
- ),
+ field=models.TextField(blank=True, help_text="Detailed company description"),
),
migrations.AlterField(
model_name="company",
name="founded_year",
- field=models.PositiveIntegerField(
- blank=True, help_text="Year the company was founded", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Year the company was founded", null=True),
),
migrations.AlterField(
model_name="company",
@@ -80,16 +76,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="parks_count",
- field=models.IntegerField(
- default=0, help_text="Number of parks operated (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)"),
),
migrations.AlterField(
model_name="company",
name="rides_count",
- field=models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="company",
@@ -114,9 +106,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="slug",
- field=models.SlugField(
- help_text="URL-friendly identifier", max_length=255, unique=True
- ),
+ field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
),
migrations.AlterField(
model_name="company",
@@ -126,16 +116,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed company description"
- ),
+ field=models.TextField(blank=True, help_text="Detailed company description"),
),
migrations.AlterField(
model_name="companyevent",
name="founded_year",
- field=models.PositiveIntegerField(
- blank=True, help_text="Year the company was founded", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Year the company was founded", null=True),
),
migrations.AlterField(
model_name="companyevent",
@@ -145,16 +131,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="parks_count",
- field=models.IntegerField(
- default=0, help_text="Number of parks operated (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)"),
),
migrations.AlterField(
model_name="companyevent",
name="rides_count",
- field=models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="companyevent",
@@ -179,9 +161,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="slug",
- field=models.SlugField(
- db_index=False, help_text="URL-friendly identifier", max_length=255
- ),
+ field=models.SlugField(db_index=False, help_text="URL-friendly identifier", max_length=255),
),
migrations.AlterField(
model_name="companyevent",
@@ -229,9 +209,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="park",
name="coaster_count",
- field=models.IntegerField(
- blank=True, help_text="Total coaster count", null=True
- ),
+ field=models.IntegerField(blank=True, help_text="Total coaster count", null=True),
),
migrations.AlterField(
model_name="park",
@@ -251,16 +229,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="park",
name="operating_season",
- field=models.CharField(
- blank=True, help_text="Operating season", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Operating season", max_length=255),
),
migrations.AlterField(
model_name="park",
name="ride_count",
- field=models.IntegerField(
- blank=True, help_text="Total ride count", null=True
- ),
+ field=models.IntegerField(blank=True, help_text="Total ride count", null=True),
),
migrations.AlterField(
model_name="park",
@@ -276,9 +250,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="park",
name="slug",
- field=models.SlugField(
- help_text="URL-friendly identifier", max_length=255, unique=True
- ),
+ field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
),
migrations.AlterField(
model_name="park",
@@ -300,16 +272,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkarea",
name="closing_date",
- field=models.DateField(
- blank=True, help_text="Date this area closed (if applicable)", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date this area closed (if applicable)", null=True),
),
migrations.AlterField(
model_name="parkarea",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed description of the area"
- ),
+ field=models.TextField(blank=True, help_text="Detailed description of the area"),
),
migrations.AlterField(
model_name="parkarea",
@@ -319,9 +287,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkarea",
name="opening_date",
- field=models.DateField(
- blank=True, help_text="Date this area opened", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date this area opened", null=True),
),
migrations.AlterField(
model_name="parkarea",
@@ -336,23 +302,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkarea",
name="slug",
- field=models.SlugField(
- help_text="URL-friendly identifier (unique within park)", max_length=255
- ),
+ field=models.SlugField(help_text="URL-friendly identifier (unique within park)", max_length=255),
),
migrations.AlterField(
model_name="parkareaevent",
name="closing_date",
- field=models.DateField(
- blank=True, help_text="Date this area closed (if applicable)", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date this area closed (if applicable)", null=True),
),
migrations.AlterField(
model_name="parkareaevent",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed description of the area"
- ),
+ field=models.TextField(blank=True, help_text="Detailed description of the area"),
),
migrations.AlterField(
model_name="parkareaevent",
@@ -362,9 +322,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkareaevent",
name="opening_date",
- field=models.DateField(
- blank=True, help_text="Date this area opened", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date this area opened", null=True),
),
migrations.AlterField(
model_name="parkareaevent",
@@ -406,9 +364,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkevent",
name="coaster_count",
- field=models.IntegerField(
- blank=True, help_text="Total coaster count", null=True
- ),
+ field=models.IntegerField(blank=True, help_text="Total coaster count", null=True),
),
migrations.AlterField(
model_name="parkevent",
@@ -428,16 +384,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkevent",
name="operating_season",
- field=models.CharField(
- blank=True, help_text="Operating season", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Operating season", max_length=255),
),
migrations.AlterField(
model_name="parkevent",
name="ride_count",
- field=models.IntegerField(
- blank=True, help_text="Total ride count", null=True
- ),
+ field=models.IntegerField(blank=True, help_text="Total ride count", null=True),
),
migrations.AlterField(
model_name="parkevent",
@@ -453,9 +405,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkevent",
name="slug",
- field=models.SlugField(
- db_index=False, help_text="URL-friendly identifier", max_length=255
- ),
+ field=models.SlugField(db_index=False, help_text="URL-friendly identifier", max_length=255),
),
migrations.AlterField(
model_name="parkevent",
@@ -496,9 +446,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkphoto",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption or description", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Photo caption or description", max_length=255),
),
migrations.AlterField(
model_name="parkphoto",
@@ -549,9 +497,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkphotoevent",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption or description", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Photo caption or description", max_length=255),
),
migrations.AlterField(
model_name="parkphotoevent",
@@ -602,16 +548,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkreview",
name="is_published",
- field=models.BooleanField(
- default=True, help_text="Whether this review is publicly visible"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this review is publicly visible"),
),
migrations.AlterField(
model_name="parkreview",
name="moderated_at",
- field=models.DateTimeField(
- blank=True, help_text="When this review was moderated", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this review was moderated", null=True),
),
migrations.AlterField(
model_name="parkreview",
@@ -628,9 +570,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkreview",
name="moderation_notes",
- field=models.TextField(
- blank=True, help_text="Internal notes from moderators"
- ),
+ field=models.TextField(blank=True, help_text="Internal notes from moderators"),
),
migrations.AlterField(
model_name="parkreview",
@@ -681,16 +621,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkreviewevent",
name="is_published",
- field=models.BooleanField(
- default=True, help_text="Whether this review is publicly visible"
- ),
+ field=models.BooleanField(default=True, help_text="Whether this review is publicly visible"),
),
migrations.AlterField(
model_name="parkreviewevent",
name="moderated_at",
- field=models.DateTimeField(
- blank=True, help_text="When this review was moderated", null=True
- ),
+ field=models.DateTimeField(blank=True, help_text="When this review was moderated", null=True),
),
migrations.AlterField(
model_name="parkreviewevent",
@@ -709,9 +645,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="parkreviewevent",
name="moderation_notes",
- field=models.TextField(
- blank=True, help_text="Internal notes from moderators"
- ),
+ field=models.TextField(blank=True, help_text="Internal notes from moderators"),
),
migrations.AlterField(
model_name="parkreviewevent",
diff --git a/backend/apps/parks/models/__init__.py b/backend/apps/parks/models/__init__.py
index a9410647..4567d53c 100644
--- a/backend/apps/parks/models/__init__.py
+++ b/backend/apps/parks/models/__init__.py
@@ -9,7 +9,7 @@ while maintaining backward compatibility through the Company alias.
"""
# Import choices to trigger registration
-from ..choices import *
+from ..choices import * # noqa: F403
from .areas import ParkArea
from .companies import Company, CompanyHeadquarters
from .location import ParkLocation
diff --git a/backend/apps/parks/models/areas.py b/backend/apps/parks/models/areas.py
index 57df59ab..ab635071 100644
--- a/backend/apps/parks/models/areas.py
+++ b/backend/apps/parks/models/areas.py
@@ -21,16 +21,10 @@ class ParkArea(TrackedModel):
help_text="Park this area belongs to",
)
name = models.CharField(max_length=255, help_text="Name of the park area")
- slug = models.SlugField(
- max_length=255, help_text="URL-friendly identifier (unique within park)"
- )
+ slug = models.SlugField(max_length=255, help_text="URL-friendly identifier (unique within park)")
description = models.TextField(blank=True, help_text="Detailed description of the area")
- opening_date = models.DateField(
- null=True, blank=True, help_text="Date this area opened"
- )
- closing_date = models.DateField(
- null=True, blank=True, help_text="Date this area closed (if applicable)"
- )
+ opening_date = models.DateField(null=True, blank=True, help_text="Date this area opened")
+ closing_date = models.DateField(null=True, blank=True, help_text="Date this area closed (if applicable)")
def save(self, *args, **kwargs):
if not self.slug:
diff --git a/backend/apps/parks/models/companies.py b/backend/apps/parks/models/companies.py
index bc981a51..53616f42 100644
--- a/backend/apps/parks/models/companies.py
+++ b/backend/apps/parks/models/companies.py
@@ -26,15 +26,9 @@ class Company(TrackedModel):
website = models.URLField(blank=True, help_text="Company website URL")
# Operator-specific fields
- founded_year = models.PositiveIntegerField(
- blank=True, null=True, help_text="Year the company was founded"
- )
- parks_count = models.IntegerField(
- default=0, help_text="Number of parks operated (auto-calculated)"
- )
- rides_count = models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- )
+ founded_year = models.PositiveIntegerField(blank=True, null=True, help_text="Year the company was founded")
+ parks_count = models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)")
+ rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)")
def save(self, *args, **kwargs):
if not self.slug:
@@ -72,9 +66,7 @@ class CompanyHeadquarters(models.Model):
blank=True,
help_text="Mailing address if publicly available",
)
- city = models.CharField(
- max_length=100, db_index=True, help_text="Headquarters city"
- )
+ city = models.CharField(max_length=100, db_index=True, help_text="Headquarters city")
state_province = models.CharField(
max_length=100,
blank=True,
@@ -87,9 +79,7 @@ class CompanyHeadquarters(models.Model):
db_index=True,
help_text="Country where headquarters is located",
)
- postal_code = models.CharField(
- max_length=20, blank=True, help_text="ZIP or postal code"
- )
+ postal_code = models.CharField(max_length=20, blank=True, help_text="ZIP or postal code")
# Optional mailing address if different or more complete
mailing_address = models.TextField(
diff --git a/backend/apps/parks/models/location.py b/backend/apps/parks/models/location.py
index bae65b10..c684ca6c 100644
--- a/backend/apps/parks/models/location.py
+++ b/backend/apps/parks/models/location.py
@@ -9,9 +9,7 @@ class ParkLocation(models.Model):
Represents the geographic location and address of a park, with PostGIS support.
"""
- park = models.OneToOneField(
- "parks.Park", on_delete=models.CASCADE, related_name="location"
- )
+ park = models.OneToOneField("parks.Park", on_delete=models.CASCADE, related_name="location")
# Spatial Data
point = models.PointField(
@@ -27,10 +25,7 @@ class ParkLocation(models.Model):
state = models.CharField(max_length=100, db_index=True)
country = models.CharField(max_length=100, default="USA")
continent = models.CharField(
- max_length=50,
- blank=True,
- db_index=True,
- help_text="Continent where the park is located"
+ max_length=50, blank=True, db_index=True, help_text="Continent where the park is located"
)
postal_code = models.CharField(max_length=20, blank=True)
diff --git a/backend/apps/parks/models/media.py b/backend/apps/parks/models/media.py
index 48ae2bfd..562985fe 100644
--- a/backend/apps/parks/models/media.py
+++ b/backend/apps/parks/models/media.py
@@ -22,9 +22,7 @@ def park_photo_upload_path(instance: models.Model, filename: str) -> str:
if park is None:
raise ValueError("Park cannot be None")
- return MediaService.generate_upload_path(
- domain="park", identifier=park.slug, filename=filename
- )
+ return MediaService.generate_upload_path(domain="park", identifier=park.slug, filename=filename)
@pghistory.track()
@@ -39,23 +37,15 @@ class ParkPhoto(TrackedModel):
)
image = models.ForeignKey(
- 'django_cloudflareimages_toolkit.CloudflareImage',
+ "django_cloudflareimages_toolkit.CloudflareImage",
on_delete=models.CASCADE,
- help_text="Park photo stored on Cloudflare Images"
+ help_text="Park photo stored on Cloudflare Images",
)
- caption = models.CharField(
- max_length=255, blank=True, help_text="Photo caption or description"
- )
- alt_text = models.CharField(
- max_length=255, blank=True, help_text="Alternative text for accessibility"
- )
- is_primary = models.BooleanField(
- default=False, help_text="Whether this is the primary photo for the park"
- )
- is_approved = models.BooleanField(
- default=False, help_text="Whether this photo has been approved by moderators"
- )
+ caption = models.CharField(max_length=255, blank=True, help_text="Photo caption or description")
+ alt_text = models.CharField(max_length=255, blank=True, help_text="Alternative text for accessibility")
+ is_primary = models.BooleanField(default=False, help_text="Whether this is the primary photo for the park")
+ is_approved = models.BooleanField(default=False, help_text="Whether this photo has been approved by moderators")
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
@@ -100,9 +90,7 @@ class ParkPhoto(TrackedModel):
# Set default caption if not provided
if not self.caption and self.uploaded_by:
- self.caption = MediaService.generate_default_caption(
- self.uploaded_by.username
- )
+ self.caption = MediaService.generate_default_caption(self.uploaded_by.username)
# If this is marked as primary, unmark other primary photos for this park
if self.is_primary:
diff --git a/backend/apps/parks/models/parks.py b/backend/apps/parks/models/parks.py
index 1cc9b199..d154dba5 100644
--- a/backend/apps/parks/models/parks.py
+++ b/backend/apps/parks/models/parks.py
@@ -45,7 +45,7 @@ class Park(StateMachineMixin, TrackedModel):
max_length=30,
default="THEME_PARK",
db_index=True,
- help_text="Type/category of the park"
+ help_text="Type/category of the park",
)
# Location relationship - reverse relation from ParkLocation
@@ -118,23 +118,18 @@ class Park(StateMachineMixin, TrackedModel):
# Computed fields for hybrid filtering
opening_year = models.IntegerField(
- null=True,
- blank=True,
- db_index=True,
- help_text="Year the park opened (computed from opening_date)"
+ null=True, blank=True, db_index=True, help_text="Year the park opened (computed from opening_date)"
)
search_text = models.TextField(
- blank=True,
- db_index=True,
- help_text="Searchable text combining name, description, location, and operator"
+ blank=True, db_index=True, help_text="Searchable text combining name, description, location, and operator"
)
# Timezone for park operations
timezone = models.CharField(
max_length=50,
- default='UTC',
+ default="UTC",
blank=True,
- help_text="Timezone identifier for park operations (e.g., 'America/New_York')"
+ help_text="Timezone identifier for park operations (e.g., 'America/New_York')",
)
class Meta:
@@ -171,8 +166,7 @@ class Park(StateMachineMixin, TrackedModel):
),
models.CheckConstraint(
name="park_coaster_count_non_negative",
- check=models.Q(coaster_count__isnull=True)
- | models.Q(coaster_count__gte=0),
+ check=models.Q(coaster_count__isnull=True) | models.Q(coaster_count__gte=0),
violation_error_message="Coaster count must be non-negative",
),
# Business rule: Coaster count cannot exceed ride count
@@ -204,9 +198,7 @@ class Park(StateMachineMixin, TrackedModel):
self.transition_to_under_construction(user=user)
self.save()
- def close_permanently(
- self, *, closing_date=None, user: Optional["AbstractBaseUser"] = None
- ) -> None:
+ def close_permanently(self, *, closing_date=None, user: Optional["AbstractBaseUser"] = None) -> None:
"""Transition park to CLOSED_PERM status."""
self.transition_to_closed_perm(user=user)
if closing_date:
@@ -279,7 +271,7 @@ class Park(StateMachineMixin, TrackedModel):
# Add location information if available
try:
- if hasattr(self, 'location') and self.location:
+ if hasattr(self, "location") and self.location:
if self.location.city:
search_parts.append(self.location.city)
if self.location.state:
@@ -299,16 +291,14 @@ class Park(StateMachineMixin, TrackedModel):
search_parts.append(self.property_owner.name)
# Combine all parts into searchable text
- self.search_text = ' '.join(filter(None, search_parts)).lower()
+ self.search_text = " ".join(filter(None, search_parts)).lower()
def clean(self):
super().clean()
if self.operator and "OPERATOR" not in self.operator.roles:
raise ValidationError({"operator": "Company must have the OPERATOR role."})
if self.property_owner and "PROPERTY_OWNER" not in self.property_owner.roles:
- raise ValidationError(
- {"property_owner": "Company must have the PROPERTY_OWNER role."}
- )
+ raise ValidationError({"property_owner": "Company must have the PROPERTY_OWNER role."})
def get_absolute_url(self) -> str:
return reverse("parks:park_detail", kwargs={"slug": self.slug})
@@ -325,7 +315,7 @@ class Park(StateMachineMixin, TrackedModel):
"""Returns coordinates as a list [latitude, longitude]"""
if hasattr(self, "location") and self.location:
coords = self.location.coordinates
- if coords and isinstance(coords, (tuple, list)):
+ if coords and isinstance(coords, tuple | list):
return list(coords)
return None
@@ -349,9 +339,7 @@ class Park(StateMachineMixin, TrackedModel):
content_type = ContentType.objects.get_for_model(cls)
print(f"Searching HistoricalSlug with content_type: {content_type}")
historical = (
- HistoricalSlug.objects.filter(content_type=content_type, slug=slug)
- .order_by("-created_at")
- .first()
+ HistoricalSlug.objects.filter(content_type=content_type, slug=slug).order_by("-created_at").first()
)
if historical:
@@ -373,11 +361,7 @@ class Park(StateMachineMixin, TrackedModel):
print("Searching pghistory events")
event_model = getattr(cls, "event_model", None)
if event_model:
- historical_event = (
- event_model.objects.filter(slug=slug)
- .order_by("-pgh_created_at")
- .first()
- )
+ historical_event = event_model.objects.filter(slug=slug).order_by("-pgh_created_at").first()
if historical_event:
print(
@@ -394,4 +378,4 @@ class Park(StateMachineMixin, TrackedModel):
else:
print("No pghistory event found")
- raise cls.DoesNotExist("No park found with this slug")
+ raise cls.DoesNotExist("No park found with this slug") from None
diff --git a/backend/apps/parks/models/reviews.py b/backend/apps/parks/models/reviews.py
index 6450c7e7..50bd7dff 100644
--- a/backend/apps/parks/models/reviews.py
+++ b/backend/apps/parks/models/reviews.py
@@ -40,12 +40,8 @@ class ParkReview(TrackedModel):
updated_at = models.DateTimeField(auto_now=True)
# Moderation
- is_published = models.BooleanField(
- default=True, help_text="Whether this review is publicly visible"
- )
- moderation_notes = models.TextField(
- blank=True, help_text="Internal notes from moderators"
- )
+ is_published = models.BooleanField(default=True, help_text="Whether this review is publicly visible")
+ moderation_notes = models.TextField(blank=True, help_text="Internal notes from moderators")
moderated_by = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
@@ -54,9 +50,7 @@ class ParkReview(TrackedModel):
related_name="moderated_park_reviews",
help_text="Moderator who reviewed this",
)
- moderated_at = models.DateTimeField(
- null=True, blank=True, help_text="When this review was moderated"
- )
+ moderated_at = models.DateTimeField(null=True, blank=True, help_text="When this review was moderated")
class Meta(TrackedModel.Meta):
verbose_name = "Park Review"
@@ -82,10 +76,7 @@ class ParkReview(TrackedModel):
name="park_review_moderation_consistency",
check=models.Q(moderated_by__isnull=True, moderated_at__isnull=True)
| models.Q(moderated_by__isnull=False, moderated_at__isnull=False),
- violation_error_message=(
- "Moderated reviews must have both moderator and moderation "
- "timestamp"
- ),
+ violation_error_message=("Moderated reviews must have both moderator and moderation " "timestamp"),
),
]
diff --git a/backend/apps/parks/querysets.py b/backend/apps/parks/querysets.py
index d5347c31..2dde7e71 100644
--- a/backend/apps/parks/querysets.py
+++ b/backend/apps/parks/querysets.py
@@ -10,9 +10,7 @@ def get_base_park_queryset() -> QuerySet[Park]:
.prefetch_related("photos", "rides")
.annotate(
current_ride_count=Count("rides", distinct=True),
- current_coaster_count=Count(
- "rides", filter=Q(rides__category="RC"), distinct=True
- ),
+ current_coaster_count=Count("rides", filter=Q(rides__category="RC"), distinct=True),
)
.order_by("name")
)
diff --git a/backend/apps/parks/selectors.py b/backend/apps/parks/selectors.py
index 1f95a78a..cd2c0941 100644
--- a/backend/apps/parks/selectors.py
+++ b/backend/apps/parks/selectors.py
@@ -47,9 +47,7 @@ def park_list_with_stats(*, filters: dict[str, Any] | None = None) -> QuerySet[P
queryset = queryset.filter(location__country=filters["country"])
if "search" in filters:
search_term = filters["search"]
- queryset = queryset.filter(
- Q(name__icontains=search_term) | Q(description__icontains=search_term)
- )
+ queryset = queryset.filter(Q(name__icontains=search_term) | Q(description__icontains=search_term))
return queryset.order_by("name")
@@ -74,15 +72,11 @@ def park_detail_optimized(*, slug: str) -> Park:
"areas",
Prefetch(
"rides",
- queryset=Ride.objects.select_related(
- "manufacturer", "designer", "ride_model"
- ),
+ queryset=Ride.objects.select_related("manufacturer", "designer", "ride_model"),
),
Prefetch(
"reviews",
- queryset=ParkReview.objects.select_related("user").filter(
- is_published=True
- ),
+ queryset=ParkReview.objects.select_related("user").filter(is_published=True),
),
"photos",
)
@@ -90,9 +84,7 @@ def park_detail_optimized(*, slug: str) -> Park:
)
-def parks_near_location(
- *, point: Point, distance_km: float = 50, limit: int = 10
-) -> QuerySet[Park]:
+def parks_near_location(*, point: Point, distance_km: float = 50, limit: int = 10) -> QuerySet[Park]:
"""
Get parks near a specific geographic location.
@@ -176,16 +168,10 @@ def parks_with_recent_reviews(*, days: int = 30) -> QuerySet[Park]:
cutoff_date = timezone.now() - timedelta(days=days)
return (
- Park.objects.filter(
- reviews__created_at__gte=cutoff_date, reviews__is_published=True
- )
+ Park.objects.filter(reviews__created_at__gte=cutoff_date, reviews__is_published=True)
.select_related("operator")
.prefetch_related("location")
- .annotate(
- recent_review_count=Count(
- "reviews", filter=Q(reviews__created_at__gte=cutoff_date)
- )
- )
+ .annotate(recent_review_count=Count("reviews", filter=Q(reviews__created_at__gte=cutoff_date)))
.order_by("-recent_review_count")
.distinct()
)
@@ -204,9 +190,7 @@ def park_search_autocomplete(*, query: str, limit: int = 10) -> QuerySet[Park]:
"""
return (
Park.objects.filter(
- Q(name__icontains=query)
- | Q(location__city__icontains=query)
- | Q(location__region__icontains=query)
+ Q(name__icontains=query) | Q(location__city__icontains=query) | Q(location__region__icontains=query)
)
.select_related("operator")
.prefetch_related("location")
diff --git a/backend/apps/parks/services.py b/backend/apps/parks/services.py
index 26156824..47d67c51 100644
--- a/backend/apps/parks/services.py
+++ b/backend/apps/parks/services.py
@@ -212,9 +212,9 @@ class ParkService:
)
# Calculate average rating
- avg_rating = ParkReview.objects.filter(
- park=park, is_published=True
- ).aggregate(avg_rating=Avg("rating"))["avg_rating"]
+ avg_rating = ParkReview.objects.filter(park=park, is_published=True).aggregate(avg_rating=Avg("rating"))[
+ "avg_rating"
+ ]
# Update park fields
park.ride_count = ride_stats["total_rides"] or 0
diff --git a/backend/apps/parks/services/filter_service.py b/backend/apps/parks/services/filter_service.py
index 2e6f6dfa..7f9922a0 100644
--- a/backend/apps/parks/services/filter_service.py
+++ b/backend/apps/parks/services/filter_service.py
@@ -26,9 +26,7 @@ class ParkFilterService:
def __init__(self):
self.cache_prefix = "park_filter"
- def get_filter_counts(
- self, base_queryset: QuerySet | None = None
- ) -> dict[str, Any]:
+ def get_filter_counts(self, base_queryset: QuerySet | None = None) -> dict[str, Any]:
"""
Get counts for various filter options to show users what's available.
@@ -76,9 +74,7 @@ class ParkFilterService:
).count(),
}
- def _get_top_operators(
- self, queryset: QuerySet, limit: int = 10
- ) -> list[dict[str, Any]]:
+ def _get_top_operators(self, queryset: QuerySet, limit: int = 10) -> list[dict[str, Any]]:
"""Get the top operators by number of parks."""
return list(
queryset.values("operator__name", "operator__id")
@@ -87,9 +83,7 @@ class ParkFilterService:
.order_by("-park_count")[:limit]
)
- def _get_country_counts(
- self, queryset: QuerySet, limit: int = 10
- ) -> list[dict[str, Any]]:
+ def _get_country_counts(self, queryset: QuerySet, limit: int = 10) -> list[dict[str, Any]]:
"""Get countries with the most parks."""
return list(
queryset.filter(location__country__isnull=False)
@@ -123,21 +117,18 @@ class ParkFilterService:
if len(query) >= 2: # Only search for queries of 2+ characters
# Park name suggestions
- park_names = Park.objects.filter(name__icontains=query).values_list(
- "name", flat=True
- )[:5]
+ park_names = Park.objects.filter(name__icontains=query).values_list("name", flat=True)[:5]
suggestions["parks"] = list(park_names)
# Operator suggestions
- operator_names = Company.objects.filter(
- roles__contains=["OPERATOR"], name__icontains=query
- ).values_list("name", flat=True)[:5]
+ operator_names = Company.objects.filter(roles__contains=["OPERATOR"], name__icontains=query).values_list(
+ "name", flat=True
+ )[:5]
suggestions["operators"] = list(operator_names)
# Location suggestions (cities and countries)
locations = Park.objects.filter(
- Q(location__city__icontains=query)
- | Q(location__country__icontains=query)
+ Q(location__city__icontains=query) | Q(location__country__icontains=query)
).values_list("location__city", "location__country")[:5]
location_suggestions = []
@@ -264,14 +255,10 @@ class ParkFilterService:
# Apply location filters
if filters.get("country_filter"):
- queryset = queryset.filter(
- location__country__icontains=filters["country_filter"]
- )
+ queryset = queryset.filter(location__country__icontains=filters["country_filter"])
if filters.get("state_filter"):
- queryset = queryset.filter(
- location__state__icontains=filters["state_filter"]
- )
+ queryset = queryset.filter(location__state__icontains=filters["state_filter"])
# Apply ordering
if filters.get("ordering"):
diff --git a/backend/apps/parks/services/hybrid_loader.py b/backend/apps/parks/services/hybrid_loader.py
index 10dc5a37..32ebe406 100644
--- a/backend/apps/parks/services/hybrid_loader.py
+++ b/backend/apps/parks/services/hybrid_loader.py
@@ -21,8 +21,8 @@ class SmartParkLoader:
"""
# Cache configuration
- CACHE_TIMEOUT = getattr(settings, 'HYBRID_FILTER_CACHE_TIMEOUT', 300) # 5 minutes
- CACHE_KEY_PREFIX = 'hybrid_parks'
+ CACHE_TIMEOUT = getattr(settings, "HYBRID_FILTER_CACHE_TIMEOUT", 300) # 5 minutes
+ CACHE_KEY_PREFIX = "hybrid_parks"
# Progressive loading thresholds
INITIAL_LOAD_SIZE = 50
@@ -34,17 +34,22 @@ class SmartParkLoader:
def _get_optimized_queryset(self) -> models.QuerySet:
"""Get optimized base queryset with all necessary prefetches."""
- return Park.objects.select_related(
- 'operator',
- 'property_owner',
- 'banner_image',
- 'card_image',
- ).prefetch_related(
- 'location', # ParkLocation relationship
- ).filter(
- # Only include operating and temporarily closed parks by default
- status__in=['OPERATING', 'CLOSED_TEMP']
- ).order_by('name')
+ return (
+ Park.objects.select_related(
+ "operator",
+ "property_owner",
+ "banner_image",
+ "card_image",
+ )
+ .prefetch_related(
+ "location", # ParkLocation relationship
+ )
+ .filter(
+ # Only include operating and temporarily closed parks by default
+ status__in=["OPERATING", "CLOSED_TEMP"]
+ )
+ .order_by("name")
+ )
def get_initial_load(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
"""
@@ -56,7 +61,7 @@ class SmartParkLoader:
Returns:
Dictionary containing parks data and metadata
"""
- cache_key = self._generate_cache_key('initial', filters)
+ cache_key = self._generate_cache_key("initial", filters)
cached_result = cache.get(cache_key)
if cached_result:
@@ -74,21 +79,21 @@ class SmartParkLoader:
if total_count <= self.MAX_CLIENT_SIDE_RECORDS:
# Load all data for client-side filtering
parks = list(queryset.all())
- strategy = 'client_side'
+ strategy = "client_side"
has_more = False
else:
# Load initial batch for server-side pagination
- parks = list(queryset[:self.INITIAL_LOAD_SIZE])
- strategy = 'server_side'
+ parks = list(queryset[: self.INITIAL_LOAD_SIZE])
+ strategy = "server_side"
has_more = total_count > self.INITIAL_LOAD_SIZE
result = {
- 'parks': parks,
- 'total_count': total_count,
- 'strategy': strategy,
- 'has_more': has_more,
- 'next_offset': len(parks) if has_more else None,
- 'filter_metadata': self._get_filter_metadata(queryset),
+ "parks": parks,
+ "total_count": total_count,
+ "strategy": strategy,
+ "has_more": has_more,
+ "next_offset": len(parks) if has_more else None,
+ "filter_metadata": self._get_filter_metadata(queryset),
}
# Cache the result
@@ -96,11 +101,7 @@ class SmartParkLoader:
return result
- def get_progressive_load(
- self,
- offset: int,
- filters: dict[str, Any] | None = None
- ) -> dict[str, Any]:
+ def get_progressive_load(self, offset: int, filters: dict[str, Any] | None = None) -> dict[str, Any]:
"""
Get next batch of parks for progressive loading.
@@ -111,7 +112,7 @@ class SmartParkLoader:
Returns:
Dictionary containing parks data and metadata
"""
- cache_key = self._generate_cache_key(f'progressive_{offset}', filters)
+ cache_key = self._generate_cache_key(f"progressive_{offset}", filters)
cached_result = cache.get(cache_key)
if cached_result:
@@ -131,10 +132,10 @@ class SmartParkLoader:
has_more = end_offset < total_count
result = {
- 'parks': parks,
- 'total_count': total_count,
- 'has_more': has_more,
- 'next_offset': end_offset if has_more else None,
+ "parks": parks,
+ "total_count": total_count,
+ "has_more": has_more,
+ "next_offset": end_offset if has_more else None,
}
# Cache the result
@@ -152,7 +153,7 @@ class SmartParkLoader:
Returns:
Dictionary containing filter metadata
"""
- cache_key = self._generate_cache_key('metadata', filters)
+ cache_key = self._generate_cache_key("metadata", filters)
cached_result = cache.get(cache_key)
if cached_result:
@@ -174,72 +175,72 @@ class SmartParkLoader:
"""Apply filters to the queryset."""
# Status filter
- if 'status' in filters and filters['status']:
- if isinstance(filters['status'], list):
- queryset = queryset.filter(status__in=filters['status'])
+ if "status" in filters and filters["status"]:
+ if isinstance(filters["status"], list):
+ queryset = queryset.filter(status__in=filters["status"])
else:
- queryset = queryset.filter(status=filters['status'])
+ queryset = queryset.filter(status=filters["status"])
# Park type filter
- if 'park_type' in filters and filters['park_type']:
- if isinstance(filters['park_type'], list):
- queryset = queryset.filter(park_type__in=filters['park_type'])
+ if "park_type" in filters and filters["park_type"]:
+ if isinstance(filters["park_type"], list):
+ queryset = queryset.filter(park_type__in=filters["park_type"])
else:
- queryset = queryset.filter(park_type=filters['park_type'])
+ queryset = queryset.filter(park_type=filters["park_type"])
# Country filter
- if 'country' in filters and filters['country']:
- queryset = queryset.filter(location__country__in=filters['country'])
+ if "country" in filters and filters["country"]:
+ queryset = queryset.filter(location__country__in=filters["country"])
# State filter
- if 'state' in filters and filters['state']:
- queryset = queryset.filter(location__state__in=filters['state'])
+ if "state" in filters and filters["state"]:
+ queryset = queryset.filter(location__state__in=filters["state"])
# Opening year range
- if 'opening_year_min' in filters and filters['opening_year_min']:
- queryset = queryset.filter(opening_year__gte=filters['opening_year_min'])
+ if "opening_year_min" in filters and filters["opening_year_min"]:
+ queryset = queryset.filter(opening_year__gte=filters["opening_year_min"])
- if 'opening_year_max' in filters and filters['opening_year_max']:
- queryset = queryset.filter(opening_year__lte=filters['opening_year_max'])
+ if "opening_year_max" in filters and filters["opening_year_max"]:
+ queryset = queryset.filter(opening_year__lte=filters["opening_year_max"])
# Size range
- if 'size_min' in filters and filters['size_min']:
- queryset = queryset.filter(size_acres__gte=filters['size_min'])
+ if "size_min" in filters and filters["size_min"]:
+ queryset = queryset.filter(size_acres__gte=filters["size_min"])
- if 'size_max' in filters and filters['size_max']:
- queryset = queryset.filter(size_acres__lte=filters['size_max'])
+ if "size_max" in filters and filters["size_max"]:
+ queryset = queryset.filter(size_acres__lte=filters["size_max"])
# Rating range
- if 'rating_min' in filters and filters['rating_min']:
- queryset = queryset.filter(average_rating__gte=filters['rating_min'])
+ if "rating_min" in filters and filters["rating_min"]:
+ queryset = queryset.filter(average_rating__gte=filters["rating_min"])
- if 'rating_max' in filters and filters['rating_max']:
- queryset = queryset.filter(average_rating__lte=filters['rating_max'])
+ if "rating_max" in filters and filters["rating_max"]:
+ queryset = queryset.filter(average_rating__lte=filters["rating_max"])
# Ride count range
- if 'ride_count_min' in filters and filters['ride_count_min']:
- queryset = queryset.filter(ride_count__gte=filters['ride_count_min'])
+ if "ride_count_min" in filters and filters["ride_count_min"]:
+ queryset = queryset.filter(ride_count__gte=filters["ride_count_min"])
- if 'ride_count_max' in filters and filters['ride_count_max']:
- queryset = queryset.filter(ride_count__lte=filters['ride_count_max'])
+ if "ride_count_max" in filters and filters["ride_count_max"]:
+ queryset = queryset.filter(ride_count__lte=filters["ride_count_max"])
# Coaster count range
- if 'coaster_count_min' in filters and filters['coaster_count_min']:
- queryset = queryset.filter(coaster_count__gte=filters['coaster_count_min'])
+ if "coaster_count_min" in filters and filters["coaster_count_min"]:
+ queryset = queryset.filter(coaster_count__gte=filters["coaster_count_min"])
- if 'coaster_count_max' in filters and filters['coaster_count_max']:
- queryset = queryset.filter(coaster_count__lte=filters['coaster_count_max'])
+ if "coaster_count_max" in filters and filters["coaster_count_max"]:
+ queryset = queryset.filter(coaster_count__lte=filters["coaster_count_max"])
# Operator filter
- if 'operator' in filters and filters['operator']:
- if isinstance(filters['operator'], list):
- queryset = queryset.filter(operator__slug__in=filters['operator'])
+ if "operator" in filters and filters["operator"]:
+ if isinstance(filters["operator"], list):
+ queryset = queryset.filter(operator__slug__in=filters["operator"])
else:
- queryset = queryset.filter(operator__slug=filters['operator'])
+ queryset = queryset.filter(operator__slug=filters["operator"])
# Search query
- if 'search' in filters and filters['search']:
- search_term = filters['search'].lower()
+ if "search" in filters and filters["search"]:
+ search_term = filters["search"].lower()
queryset = queryset.filter(search_text__icontains=search_term)
return queryset
@@ -249,150 +250,125 @@ class SmartParkLoader:
# Get distinct values for categorical filters with counts
countries_data = list(
- queryset.values('location__country')
+ queryset.values("location__country")
.exclude(location__country__isnull=True)
- .annotate(count=models.Count('id'))
- .order_by('location__country')
+ .annotate(count=models.Count("id"))
+ .order_by("location__country")
)
states_data = list(
- queryset.values('location__state')
+ queryset.values("location__state")
.exclude(location__state__isnull=True)
- .annotate(count=models.Count('id'))
- .order_by('location__state')
+ .annotate(count=models.Count("id"))
+ .order_by("location__state")
)
park_types_data = list(
- queryset.values('park_type')
+ queryset.values("park_type")
.exclude(park_type__isnull=True)
- .annotate(count=models.Count('id'))
- .order_by('park_type')
+ .annotate(count=models.Count("id"))
+ .order_by("park_type")
)
- statuses_data = list(
- queryset.values('status')
- .annotate(count=models.Count('id'))
- .order_by('status')
- )
+ statuses_data = list(queryset.values("status").annotate(count=models.Count("id")).order_by("status"))
operators_data = list(
- queryset.select_related('operator')
- .values('operator__id', 'operator__name', 'operator__slug')
+ queryset.select_related("operator")
+ .values("operator__id", "operator__name", "operator__slug")
.exclude(operator__isnull=True)
- .annotate(count=models.Count('id'))
- .order_by('operator__name')
+ .annotate(count=models.Count("id"))
+ .order_by("operator__name")
)
# Convert to frontend-expected format with value/label/count
countries = [
- {
- 'value': item['location__country'],
- 'label': item['location__country'],
- 'count': item['count']
- }
+ {"value": item["location__country"], "label": item["location__country"], "count": item["count"]}
for item in countries_data
]
states = [
- {
- 'value': item['location__state'],
- 'label': item['location__state'],
- 'count': item['count']
- }
+ {"value": item["location__state"], "label": item["location__state"], "count": item["count"]}
for item in states_data
]
park_types = [
- {
- 'value': item['park_type'],
- 'label': item['park_type'],
- 'count': item['count']
- }
- for item in park_types_data
+ {"value": item["park_type"], "label": item["park_type"], "count": item["count"]} for item in park_types_data
]
statuses = [
- {
- 'value': item['status'],
- 'label': self._get_status_label(item['status']),
- 'count': item['count']
- }
+ {"value": item["status"], "label": self._get_status_label(item["status"]), "count": item["count"]}
for item in statuses_data
]
operators = [
- {
- 'value': item['operator__slug'],
- 'label': item['operator__name'],
- 'count': item['count']
- }
+ {"value": item["operator__slug"], "label": item["operator__name"], "count": item["count"]}
for item in operators_data
]
# Get ranges for numerical filters
aggregates = queryset.aggregate(
- opening_year_min=models.Min('opening_year'),
- opening_year_max=models.Max('opening_year'),
- size_min=models.Min('size_acres'),
- size_max=models.Max('size_acres'),
- rating_min=models.Min('average_rating'),
- rating_max=models.Max('average_rating'),
- ride_count_min=models.Min('ride_count'),
- ride_count_max=models.Max('ride_count'),
- coaster_count_min=models.Min('coaster_count'),
- coaster_count_max=models.Max('coaster_count'),
+ opening_year_min=models.Min("opening_year"),
+ opening_year_max=models.Max("opening_year"),
+ size_min=models.Min("size_acres"),
+ size_max=models.Max("size_acres"),
+ rating_min=models.Min("average_rating"),
+ rating_max=models.Max("average_rating"),
+ ride_count_min=models.Min("ride_count"),
+ ride_count_max=models.Max("ride_count"),
+ coaster_count_min=models.Min("coaster_count"),
+ coaster_count_max=models.Max("coaster_count"),
)
return {
- 'categorical': {
- 'countries': countries,
- 'states': states,
- 'park_types': park_types,
- 'statuses': statuses,
- 'operators': operators,
+ "categorical": {
+ "countries": countries,
+ "states": states,
+ "park_types": park_types,
+ "statuses": statuses,
+ "operators": operators,
},
- 'ranges': {
- 'opening_year': {
- 'min': aggregates['opening_year_min'],
- 'max': aggregates['opening_year_max'],
- 'step': 1,
- 'unit': 'year'
+ "ranges": {
+ "opening_year": {
+ "min": aggregates["opening_year_min"],
+ "max": aggregates["opening_year_max"],
+ "step": 1,
+ "unit": "year",
},
- 'size_acres': {
- 'min': float(aggregates['size_min']) if aggregates['size_min'] else None,
- 'max': float(aggregates['size_max']) if aggregates['size_max'] else None,
- 'step': 1.0,
- 'unit': 'acres'
+ "size_acres": {
+ "min": float(aggregates["size_min"]) if aggregates["size_min"] else None,
+ "max": float(aggregates["size_max"]) if aggregates["size_max"] else None,
+ "step": 1.0,
+ "unit": "acres",
},
- 'average_rating': {
- 'min': float(aggregates['rating_min']) if aggregates['rating_min'] else None,
- 'max': float(aggregates['rating_max']) if aggregates['rating_max'] else None,
- 'step': 0.1,
- 'unit': 'stars'
+ "average_rating": {
+ "min": float(aggregates["rating_min"]) if aggregates["rating_min"] else None,
+ "max": float(aggregates["rating_max"]) if aggregates["rating_max"] else None,
+ "step": 0.1,
+ "unit": "stars",
},
- 'ride_count': {
- 'min': aggregates['ride_count_min'],
- 'max': aggregates['ride_count_max'],
- 'step': 1,
- 'unit': 'rides'
+ "ride_count": {
+ "min": aggregates["ride_count_min"],
+ "max": aggregates["ride_count_max"],
+ "step": 1,
+ "unit": "rides",
},
- 'coaster_count': {
- 'min': aggregates['coaster_count_min'],
- 'max': aggregates['coaster_count_max'],
- 'step': 1,
- 'unit': 'coasters'
+ "coaster_count": {
+ "min": aggregates["coaster_count_min"],
+ "max": aggregates["coaster_count_max"],
+ "step": 1,
+ "unit": "coasters",
},
},
- 'total_count': queryset.count(),
+ "total_count": queryset.count(),
}
def _get_status_label(self, status: str) -> str:
"""Convert status code to human-readable label."""
status_labels = {
- 'OPERATING': 'Operating',
- 'CLOSED_TEMP': 'Temporarily Closed',
- 'CLOSED_PERM': 'Permanently Closed',
- 'UNDER_CONSTRUCTION': 'Under Construction',
+ "OPERATING": "Operating",
+ "CLOSED_TEMP": "Temporarily Closed",
+ "CLOSED_PERM": "Permanently Closed",
+ "UNDER_CONSTRUCTION": "Under Construction",
}
if status in status_labels:
return status_labels[status]
@@ -405,23 +381,23 @@ class SmartParkLoader:
if filters:
# Create a consistent string representation of filters
- filter_str = '_'.join(f"{k}:{v}" for k, v in sorted(filters.items()) if v)
+ filter_str = "_".join(f"{k}:{v}" for k, v in sorted(filters.items()) if v)
key_parts.append(filter_str)
- return '_'.join(key_parts)
+ return "_".join(key_parts)
def invalidate_cache(self, filters: dict[str, Any] | None = None) -> None:
"""Invalidate cached data for the given filters."""
# This is a simplified implementation
# In production, you might want to use cache versioning or tags
cache_keys = [
- self._generate_cache_key('initial', filters),
- self._generate_cache_key('metadata', filters),
+ self._generate_cache_key("initial", filters),
+ self._generate_cache_key("metadata", filters),
]
# Also invalidate progressive load caches
for offset in range(0, 1000, self.PROGRESSIVE_LOAD_SIZE):
- cache_keys.append(self._generate_cache_key(f'progressive_{offset}', filters))
+ cache_keys.append(self._generate_cache_key(f"progressive_{offset}", filters))
cache.delete_many(cache_keys)
diff --git a/backend/apps/parks/services/location_service.py b/backend/apps/parks/services/location_service.py
index d7eae540..1f944e50 100644
--- a/backend/apps/parks/services/location_service.py
+++ b/backend/apps/parks/services/location_service.py
@@ -245,9 +245,7 @@ class ParkLocationService:
return park_location
@classmethod
- def update_park_location(
- cls, park_location: ParkLocation, **updates
- ) -> ParkLocation:
+ def update_park_location(cls, park_location: ParkLocation, **updates) -> ParkLocation:
"""
Update park location with validation.
@@ -278,9 +276,7 @@ class ParkLocationService:
return park_location
@classmethod
- def find_nearby_parks(
- cls, latitude: float, longitude: float, radius_km: float = 50
- ) -> list[ParkLocation]:
+ def find_nearby_parks(cls, latitude: float, longitude: float, radius_km: float = 50) -> list[ParkLocation]:
"""
Find parks near given coordinates using PostGIS.
@@ -298,9 +294,7 @@ class ParkLocationService:
center_point = Point(longitude, latitude, srid=4326)
return list(
- ParkLocation.objects.filter(
- point__distance_lte=(center_point, Distance(km=radius_km))
- )
+ ParkLocation.objects.filter(point__distance_lte=(center_point, Distance(km=radius_km)))
.select_related("park", "park__operator")
.order_by("point__distance")
)
@@ -349,9 +343,7 @@ class ParkLocationService:
return park_location
@classmethod
- def _transform_osm_result(
- cls, osm_item: dict[str, Any]
- ) -> dict[str, Any] | None:
+ def _transform_osm_result(cls, osm_item: dict[str, Any]) -> dict[str, Any] | None:
"""Transform OSM search result to our standard format."""
try:
address = osm_item.get("address", {})
@@ -369,12 +361,7 @@ class ParkLocationService:
or ""
)
- state = (
- address.get("state")
- or address.get("province")
- or address.get("region")
- or ""
- )
+ state = address.get("state") or address.get("province") or address.get("region") or ""
country = address.get("country", "")
postal_code = address.get("postcode", "")
@@ -432,9 +419,7 @@ class ParkLocationService:
return None
@classmethod
- def _transform_osm_reverse_result(
- cls, osm_result: dict[str, Any]
- ) -> dict[str, Any]:
+ def _transform_osm_reverse_result(cls, osm_result: dict[str, Any]) -> dict[str, Any]:
"""Transform OSM reverse geocoding result to our standard format."""
address = osm_result.get("address", {})
@@ -443,20 +428,9 @@ class ParkLocationService:
street_name = address.get("road", "")
street_address = f"{street_number} {street_name}".strip()
- city = (
- address.get("city")
- or address.get("town")
- or address.get("village")
- or address.get("municipality")
- or ""
- )
+ city = address.get("city") or address.get("town") or address.get("village") or address.get("municipality") or ""
- state = (
- address.get("state")
- or address.get("province")
- or address.get("region")
- or ""
- )
+ state = address.get("state") or address.get("province") or address.get("region") or ""
country = address.get("country", "")
postal_code = address.get("postcode", "")
diff --git a/backend/apps/parks/services/media_service.py b/backend/apps/parks/services/media_service.py
index 3582b9fb..10493e86 100644
--- a/backend/apps/parks/services/media_service.py
+++ b/backend/apps/parks/services/media_service.py
@@ -79,9 +79,7 @@ class ParkMediaService:
return photo
@staticmethod
- def get_park_photos(
- park: Park, approved_only: bool = True, primary_first: bool = True
- ) -> list[ParkPhoto]:
+ def get_park_photos(park: Park, approved_only: bool = True, primary_first: bool = True) -> list[ParkPhoto]:
"""
Get photos for a park.
@@ -190,9 +188,7 @@ class ParkMediaService:
photo.image.delete(save=False)
photo.delete()
- logger.info(
- f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}"
- )
+ logger.info(f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}")
return True
except Exception as e:
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
@@ -238,7 +234,5 @@ class ParkMediaService:
if ParkMediaService.approve_photo(photo, approved_by):
approved_count += 1
- logger.info(
- f"Bulk approved {approved_count} photos by user {approved_by.username}"
- )
+ logger.info(f"Bulk approved {approved_count} photos by user {approved_by.username}")
return approved_count
diff --git a/backend/apps/parks/services/park_management.py b/backend/apps/parks/services/park_management.py
index e957296f..a09597b9 100644
--- a/backend/apps/parks/services/park_management.py
+++ b/backend/apps/parks/services/park_management.py
@@ -133,9 +133,7 @@ class ParkService:
return park
@staticmethod
- def delete_park(
- *, park_id: int, deleted_by: Optional["AbstractUser"] = None
- ) -> bool:
+ def delete_park(*, park_id: int, deleted_by: Optional["AbstractUser"] = None) -> bool:
"""
Soft delete a park by setting status to DEMOLISHED.
@@ -219,9 +217,9 @@ class ParkService:
)
# Calculate average rating
- avg_rating = ParkReview.objects.filter(
- park=park, is_published=True
- ).aggregate(avg_rating=Avg("rating"))["avg_rating"]
+ avg_rating = ParkReview.objects.filter(park=park, is_published=True).aggregate(avg_rating=Avg("rating"))[
+ "avg_rating"
+ ]
# Update park fields
park.ride_count = ride_stats["total_rides"] or 0
diff --git a/backend/apps/parks/services/roadtrip.py b/backend/apps/parks/services/roadtrip.py
index cefaa599..b6742c59 100644
--- a/backend/apps/parks/services/roadtrip.py
+++ b/backend/apps/parks/services/roadtrip.py
@@ -148,12 +148,8 @@ class RoadTripService:
# Configuration from Django settings
self.cache_timeout = getattr(settings, "ROADTRIP_CACHE_TIMEOUT", 3600 * 24)
- self.route_cache_timeout = getattr(
- settings, "ROADTRIP_ROUTE_CACHE_TIMEOUT", 3600 * 6
- )
- self.user_agent = getattr(
- settings, "ROADTRIP_USER_AGENT", "ThrillWiki Road Trip Planner"
- )
+ self.route_cache_timeout = getattr(settings, "ROADTRIP_ROUTE_CACHE_TIMEOUT", 3600 * 6)
+ self.user_agent = getattr(settings, "ROADTRIP_USER_AGENT", "ThrillWiki Road Trip Planner")
self.request_timeout = getattr(settings, "ROADTRIP_REQUEST_TIMEOUT", 10)
self.max_retries = getattr(settings, "ROADTRIP_MAX_RETRIES", 3)
self.backoff_factor = getattr(settings, "ROADTRIP_BACKOFF_FACTOR", 2)
@@ -179,9 +175,7 @@ class RoadTripService:
for attempt in range(self.max_retries):
try:
- response = self.session.get(
- url, params=params, timeout=self.request_timeout
- )
+ response = self.session.get(url, params=params, timeout=self.request_timeout)
response.raise_for_status()
return response.json()
@@ -192,9 +186,7 @@ class RoadTripService:
wait_time = self.backoff_factor**attempt
time.sleep(wait_time)
else:
- raise OSMAPIException(
- f"Failed to make request after {self.max_retries} attempts: {e}"
- )
+ raise OSMAPIException(f"Failed to make request after {self.max_retries} attempts: {e}") from e
def geocode_address(self, address: str) -> Coordinates | None:
"""
@@ -243,9 +235,7 @@ class RoadTripService:
self.cache_timeout,
)
- logger.info(
- f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}"
- )
+ logger.info(f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}")
return coords
else:
logger.warning(f"No geocoding results for address: {address}")
@@ -255,9 +245,7 @@ class RoadTripService:
logger.error(f"Geocoding failed for '{address}': {e}")
return None
- def calculate_route(
- self, start_coords: Coordinates, end_coords: Coordinates
- ) -> RouteInfo | None:
+ def calculate_route(self, start_coords: Coordinates, end_coords: Coordinates) -> RouteInfo | None:
"""
Calculate route between two coordinate points using OSRM.
@@ -327,9 +315,7 @@ class RoadTripService:
return route_info
else:
# Fallback to straight-line distance calculation
- logger.warning(
- "OSRM routing failed, falling back to straight-line distance"
- )
+ logger.warning("OSRM routing failed, falling back to straight-line distance")
return self._calculate_straight_line_route(start_coords, end_coords)
except Exception as e:
@@ -337,9 +323,7 @@ class RoadTripService:
# Fallback to straight-line distance
return self._calculate_straight_line_route(start_coords, end_coords)
- def _calculate_straight_line_route(
- self, start_coords: Coordinates, end_coords: Coordinates
- ) -> RouteInfo:
+ def _calculate_straight_line_route(self, start_coords: Coordinates, end_coords: Coordinates) -> RouteInfo:
"""
Calculate straight-line distance as fallback when routing fails.
"""
@@ -356,10 +340,7 @@ class RoadTripService:
dlat = lat2 - lat1
dlon = lon2 - lon1
- a = (
- math.sin(dlat / 2) ** 2
- + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
- )
+ a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
c = 2 * math.asin(math.sqrt(a))
# Earth's radius in kilometers
@@ -376,9 +357,7 @@ class RoadTripService:
geometry=None,
)
- def find_parks_along_route(
- self, start_park: "Park", end_park: "Park", max_detour_km: float = 50
- ) -> list["Park"]:
+ def find_parks_along_route(self, start_park: "Park", end_park: "Park", max_detour_km: float = 50) -> list["Park"]:
"""
Find parks along a route within specified detour distance.
@@ -443,9 +422,7 @@ class RoadTripService:
return parks_along_route
- def _calculate_detour_distance(
- self, start: Coordinates, end: Coordinates, waypoint: Coordinates
- ) -> float | None:
+ def _calculate_detour_distance(self, start: Coordinates, end: Coordinates, waypoint: Coordinates) -> float | None:
"""
Calculate the detour distance when visiting a waypoint.
"""
@@ -508,9 +485,7 @@ class RoadTripService:
return best_trip
- def _optimize_trip_nearest_neighbor(
- self, park_list: list["Park"]
- ) -> RoadTrip | None:
+ def _optimize_trip_nearest_neighbor(self, park_list: list["Park"]) -> RoadTrip | None:
"""
Optimize trip using nearest neighbor heuristic (for larger lists).
"""
@@ -536,9 +511,7 @@ class RoadTripService:
if not park_coords:
continue
- route = self.calculate_route(
- Coordinates(*current_coords), Coordinates(*park_coords)
- )
+ route = self.calculate_route(Coordinates(*current_coords), Coordinates(*park_coords))
if route and route.distance_km < min_distance:
min_distance = route.distance_km
@@ -553,9 +526,7 @@ class RoadTripService:
return self._create_trip_from_order(ordered_parks)
- def _create_trip_from_order(
- self, ordered_parks: list["Park"]
- ) -> RoadTrip | None:
+ def _create_trip_from_order(self, ordered_parks: list["Park"]) -> RoadTrip | None:
"""
Create a RoadTrip object from an ordered list of parks.
"""
@@ -576,9 +547,7 @@ class RoadTripService:
if not from_coords or not to_coords:
continue
- route = self.calculate_route(
- Coordinates(*from_coords), Coordinates(*to_coords)
- )
+ route = self.calculate_route(Coordinates(*from_coords), Coordinates(*to_coords))
if route:
legs.append(TripLeg(from_park=from_park, to_park=to_park, route=route))
@@ -595,9 +564,7 @@ class RoadTripService:
total_duration_minutes=total_duration,
)
- def get_park_distances(
- self, center_park: "Park", radius_km: float = 100
- ) -> list[dict[str, Any]]:
+ def get_park_distances(self, center_park: "Park", radius_km: float = 100) -> list[dict[str, Any]]:
"""
Get all parks within radius of a center park with distances.
@@ -621,9 +588,7 @@ class RoadTripService:
search_distance = Distance(km=radius_km)
nearby_parks = (
- Park.objects.filter(
- location__point__distance_lte=(center_point, search_distance)
- )
+ Park.objects.filter(location__point__distance_lte=(center_point, search_distance))
.exclude(id=center_park.id)
.select_related("location")
)
@@ -635,9 +600,7 @@ class RoadTripService:
if not park_coords:
continue
- route = self.calculate_route(
- Coordinates(*center_coords), Coordinates(*park_coords)
- )
+ route = self.calculate_route(Coordinates(*center_coords), Coordinates(*park_coords))
if route:
results.append(
@@ -691,9 +654,7 @@ class RoadTripService:
if coords:
location.set_coordinates(coords.latitude, coords.longitude)
location.save()
- logger.info(
- f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}"
- )
+ logger.info(f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}")
return True
return False
diff --git a/backend/apps/parks/signals.py b/backend/apps/parks/signals.py
index a67d6886..215d09c2 100644
--- a/backend/apps/parks/signals.py
+++ b/backend/apps/parks/signals.py
@@ -15,6 +15,7 @@ logger = logging.getLogger(__name__)
# Computed Field Maintenance Signals
# =============================================================================
+
def update_park_search_text(park):
"""
Update park's search_text computed field.
@@ -27,17 +28,17 @@ def update_park_search_text(park):
try:
park._populate_computed_fields()
- park.save(update_fields=['search_text'])
+ park.save(update_fields=["search_text"])
logger.debug(f"Updated search_text for park {park.pk}")
except Exception as e:
logger.exception(f"Failed to update search_text for park {park.pk}: {e}")
# Status values that count as "active" rides for counting purposes
-ACTIVE_STATUSES = {'OPERATING', 'SEASONAL', 'UNDER_CONSTRUCTION'}
+ACTIVE_STATUSES = {"OPERATING", "SEASONAL", "UNDER_CONSTRUCTION"}
# Status values that should decrement ride counts
-INACTIVE_STATUSES = {'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED', 'REMOVED'}
+INACTIVE_STATUSES = {"CLOSED_PERM", "DEMOLISHED", "RELOCATED", "REMOVED"}
def update_park_ride_counts(park, old_status=None, new_status=None):
@@ -54,11 +55,11 @@ def update_park_ride_counts(park, old_status=None, new_status=None):
return
# Get park ID
- park_id = park.pk if hasattr(park, 'pk') else park
+ park_id = park.pk if hasattr(park, "pk") else park
try:
# Fetch the park if we only have an ID
- if not hasattr(park, 'rides'):
+ if not hasattr(park, "rides"):
park = Park.objects.get(id=park_id)
# Build the query for active rides
@@ -72,14 +73,9 @@ def update_park_ride_counts(park, old_status=None, new_status=None):
coaster_count = park.rides.filter(operating_rides, category__in=["RC", "WC"]).count()
# Update park counts
- Park.objects.filter(id=park_id).update(
- ride_count=ride_count, coaster_count=coaster_count
- )
+ Park.objects.filter(id=park_id).update(ride_count=ride_count, coaster_count=coaster_count)
- logger.debug(
- f"Updated park {park_id} counts: "
- f"ride_count={ride_count}, coaster_count={coaster_count}"
- )
+ logger.debug(f"Updated park {park_id} counts: " f"ride_count={ride_count}, coaster_count={coaster_count}")
except Park.DoesNotExist:
logger.warning(f"Park {park_id} does not exist, cannot update counts")
@@ -124,14 +120,12 @@ def ride_saved(sender, instance, created, **kwargs):
return
# Check if status changed using model's tracker if available
- if hasattr(instance, 'tracker') and hasattr(instance.tracker, 'has_changed'):
- if instance.tracker.has_changed('status'):
- old_status = instance.tracker.previous('status')
+ if hasattr(instance, "tracker") and hasattr(instance.tracker, "has_changed"):
+ if instance.tracker.has_changed("status"):
+ old_status = instance.tracker.previous("status")
new_status = instance.status
if should_update_counts(old_status, new_status):
- logger.info(
- f"Ride {instance.pk} status changed: {old_status} → {new_status}"
- )
+ logger.info(f"Ride {instance.pk} status changed: {old_status} → {new_status}")
update_park_ride_counts(instance.park, old_status, new_status)
else:
# Fallback: always update counts on save
@@ -151,6 +145,7 @@ def ride_deleted(sender, instance, **kwargs):
# FSM transition signal handlers
+
def handle_ride_status_transition(instance, source, target, user, **kwargs):
"""
Handle ride status FSM transitions.
@@ -165,10 +160,7 @@ def handle_ride_status_transition(instance, source, target, user, **kwargs):
user: The user who initiated the transition.
"""
if should_update_counts(source, target):
- logger.info(
- f"FSM transition: Ride {instance.pk} {source} → {target} "
- f"by {user if user else 'system'}"
- )
+ logger.info(f"FSM transition: Ride {instance.pk} {source} → {target} " f"by {user if user else 'system'}")
update_park_ride_counts(instance.park, source, target)
@@ -176,7 +168,8 @@ def handle_ride_status_transition(instance, source, target, user, **kwargs):
# Computed Field Maintenance Signal Handlers
# =============================================================================
-@receiver(post_save, sender='parks.ParkLocation')
+
+@receiver(post_save, sender="parks.ParkLocation")
def update_park_search_text_on_location_change(sender, instance, **kwargs):
"""
Update park search_text when location changes.
@@ -186,13 +179,13 @@ def update_park_search_text_on_location_change(sender, instance, **kwargs):
location information.
"""
try:
- if hasattr(instance, 'park') and instance.park:
+ if hasattr(instance, "park") and instance.park:
update_park_search_text(instance.park)
except Exception as e:
logger.exception(f"Failed to update park search_text on location change: {e}")
-@receiver(post_save, sender='parks.Company')
+@receiver(post_save, sender="parks.Company")
def update_park_search_text_on_company_change(sender, instance, **kwargs):
"""
Update park search_text when operator/owner name changes.
diff --git a/backend/apps/parks/templatetags/park_tags.py b/backend/apps/parks/templatetags/park_tags.py
index 31370d15..fad5d7d1 100644
--- a/backend/apps/parks/templatetags/park_tags.py
+++ b/backend/apps/parks/templatetags/park_tags.py
@@ -5,48 +5,48 @@ register = template.Library()
# Status configuration mapping for parks and rides
STATUS_CONFIG = {
- 'OPERATING': {
- 'label': 'Operating',
- 'classes': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
- 'icon': True,
+ "OPERATING": {
+ "label": "Operating",
+ "classes": "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
+ "icon": True,
},
- 'CLOSED_TEMP': {
- 'label': 'Temporarily Closed',
- 'classes': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
- 'icon': True,
+ "CLOSED_TEMP": {
+ "label": "Temporarily Closed",
+ "classes": "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
+ "icon": True,
},
- 'CLOSED_PERM': {
- 'label': 'Permanently Closed',
- 'classes': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
- 'icon': True,
+ "CLOSED_PERM": {
+ "label": "Permanently Closed",
+ "classes": "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
+ "icon": True,
},
- 'CONSTRUCTION': {
- 'label': 'Under Construction',
- 'classes': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
- 'icon': True,
+ "CONSTRUCTION": {
+ "label": "Under Construction",
+ "classes": "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200",
+ "icon": True,
},
- 'DEMOLISHED': {
- 'label': 'Demolished',
- 'classes': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
- 'icon': True,
+ "DEMOLISHED": {
+ "label": "Demolished",
+ "classes": "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
+ "icon": True,
},
- 'RELOCATED': {
- 'label': 'Relocated',
- 'classes': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
- 'icon': True,
+ "RELOCATED": {
+ "label": "Relocated",
+ "classes": "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
+ "icon": True,
},
- 'SBNO': {
- 'label': 'Standing But Not Operating',
- 'classes': 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
- 'icon': True,
+ "SBNO": {
+ "label": "Standing But Not Operating",
+ "classes": "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200",
+ "icon": True,
},
}
# Default config for unknown statuses
DEFAULT_STATUS_CONFIG = {
- 'label': 'Unknown',
- 'classes': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
- 'icon': False,
+ "label": "Unknown",
+ "classes": "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
+ "icon": False,
}
diff --git a/backend/apps/parks/tests.py b/backend/apps/parks/tests.py
index 216b75b0..516f7f73 100644
--- a/backend/apps/parks/tests.py
+++ b/backend/apps/parks/tests.py
@@ -31,39 +31,28 @@ class ParkTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123',
- role='USER'
+ username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
# Create operator company
self.operator = Company.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
"""Helper to create a Park with specified status."""
defaults = {
- 'name': 'Test Park',
- 'slug': 'test-park',
- 'description': 'A test park',
- 'operator': self.operator,
- 'timezone': 'America/New_York'
+ "name": "Test Park",
+ "slug": "test-park",
+ "description": "A test park",
+ "operator": self.operator,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(status=status, **defaults)
@@ -74,25 +63,25 @@ class ParkTransitionTests(TestCase):
def test_operating_to_closed_temp_transition(self):
"""Test transition from OPERATING to CLOSED_TEMP."""
- park = self._create_park(status='OPERATING')
- self.assertEqual(park.status, 'OPERATING')
+ park = self._create_park(status="OPERATING")
+ self.assertEqual(park.status, "OPERATING")
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_TEMP')
+ self.assertEqual(park.status, "CLOSED_TEMP")
def test_operating_to_closed_perm_transition(self):
"""Test transition from OPERATING to CLOSED_PERM."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = date.today()
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
self.assertIsNotNone(park.closing_date)
# -------------------------------------------------------------------------
@@ -101,14 +90,14 @@ class ParkTransitionTests(TestCase):
def test_under_construction_to_operating_transition(self):
"""Test transition from UNDER_CONSTRUCTION to OPERATING."""
- park = self._create_park(status='UNDER_CONSTRUCTION')
- self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
+ park = self._create_park(status="UNDER_CONSTRUCTION")
+ self.assertEqual(park.status, "UNDER_CONSTRUCTION")
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
# -------------------------------------------------------------------------
# Closed temp transitions
@@ -116,24 +105,24 @@ class ParkTransitionTests(TestCase):
def test_closed_temp_to_operating_transition(self):
"""Test transition from CLOSED_TEMP to OPERATING (reopen)."""
- park = self._create_park(status='CLOSED_TEMP')
+ park = self._create_park(status="CLOSED_TEMP")
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
def test_closed_temp_to_closed_perm_transition(self):
"""Test transition from CLOSED_TEMP to CLOSED_PERM."""
- park = self._create_park(status='CLOSED_TEMP')
+ park = self._create_park(status="CLOSED_TEMP")
park.transition_to_closed_perm(user=self.moderator)
park.closing_date = date.today()
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
# -------------------------------------------------------------------------
# Closed perm transitions (to final states)
@@ -141,23 +130,23 @@ class ParkTransitionTests(TestCase):
def test_closed_perm_to_demolished_transition(self):
"""Test transition from CLOSED_PERM to DEMOLISHED."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'DEMOLISHED')
+ self.assertEqual(park.status, "DEMOLISHED")
def test_closed_perm_to_relocated_transition(self):
"""Test transition from CLOSED_PERM to RELOCATED."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'RELOCATED')
+ self.assertEqual(park.status, "RELOCATED")
# -------------------------------------------------------------------------
# Invalid transitions (final states)
@@ -165,28 +154,28 @@ class ParkTransitionTests(TestCase):
def test_demolished_cannot_transition(self):
"""Test that DEMOLISHED state cannot transition further."""
- park = self._create_park(status='DEMOLISHED')
+ park = self._create_park(status="DEMOLISHED")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
def test_relocated_cannot_transition(self):
"""Test that RELOCATED state cannot transition further."""
- park = self._create_park(status='RELOCATED')
+ park = self._create_park(status="RELOCATED")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_operating(user=self.moderator)
def test_operating_cannot_directly_demolish(self):
"""Test that OPERATING cannot directly transition to DEMOLISHED."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_demolished(user=self.moderator)
def test_operating_cannot_directly_relocate(self):
"""Test that OPERATING cannot directly transition to RELOCATED."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
park.transition_to_relocated(user=self.moderator)
@@ -197,69 +186,69 @@ class ParkTransitionTests(TestCase):
def test_reopen_wrapper_method(self):
"""Test the reopen() wrapper method."""
- park = self._create_park(status='CLOSED_TEMP')
+ park = self._create_park(status="CLOSED_TEMP")
park.reopen(user=self.user)
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
def test_close_temporarily_wrapper_method(self):
"""Test the close_temporarily() wrapper method."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.close_temporarily(user=self.user)
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_TEMP')
+ self.assertEqual(park.status, "CLOSED_TEMP")
def test_close_permanently_wrapper_method(self):
"""Test the close_permanently() wrapper method."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
closing = date(2025, 12, 31)
park.close_permanently(closing_date=closing, user=self.moderator)
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
self.assertEqual(park.closing_date, closing)
def test_close_permanently_without_date(self):
"""Test close_permanently() without closing_date."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.close_permanently(user=self.moderator)
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
self.assertIsNone(park.closing_date)
def test_demolish_wrapper_method(self):
"""Test the demolish() wrapper method."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
park.demolish(user=self.moderator)
park.refresh_from_db()
- self.assertEqual(park.status, 'DEMOLISHED')
+ self.assertEqual(park.status, "DEMOLISHED")
def test_relocate_wrapper_method(self):
"""Test the relocate() wrapper method."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
park.relocate(user=self.moderator)
park.refresh_from_db()
- self.assertEqual(park.status, 'RELOCATED')
+ self.assertEqual(park.status, "RELOCATED")
def test_start_construction_wrapper_method(self):
"""Test the start_construction() wrapper method if applicable."""
# This depends on allowed transitions - skip if not allowed
try:
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.start_construction(user=self.moderator)
park.refresh_from_db()
- self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
+ self.assertEqual(park.status, "UNDER_CONSTRUCTION")
except TransitionNotAllowed:
# If transition from OPERATING to UNDER_CONSTRUCTION is not allowed
pass
@@ -276,52 +265,44 @@ class ParkTransitionHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.operator = Company.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
- def _create_park(self, status='OPERATING'):
+ def _create_park(self, status="OPERATING"):
"""Helper to create a Park."""
return Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
status=status,
- timezone='America/New_York'
+ timezone="America/New_York",
)
def test_transition_creates_state_log(self):
"""Test that transitions create StateLog entries."""
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.transition_to_closed_temp(user=self.moderator)
park.save()
park_ct = ContentType.objects.get_for_model(park)
- log = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).first()
+ log = StateLog.objects.filter(content_type=park_ct, object_id=park.id).first()
self.assertIsNotNone(log)
- self.assertEqual(log.state, 'CLOSED_TEMP')
+ self.assertEqual(log.state, "CLOSED_TEMP")
self.assertEqual(log.by, self.moderator)
def test_multiple_transitions_create_multiple_logs(self):
"""Test that multiple transitions create multiple log entries."""
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park_ct = ContentType.objects.get_for_model(park)
# First transition
@@ -332,29 +313,23 @@ class ParkTransitionHistoryTests(TestCase):
park.transition_to_operating(user=self.moderator)
park.save()
- logs = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=park_ct, object_id=park.id).order_by("timestamp")
self.assertEqual(logs.count(), 2)
- self.assertEqual(logs[0].state, 'CLOSED_TEMP')
- self.assertEqual(logs[1].state, 'OPERATING')
+ self.assertEqual(logs[0].state, "CLOSED_TEMP")
+ self.assertEqual(logs[1].state, "OPERATING")
def test_transition_log_includes_user(self):
"""Test that transition logs include the user who made the change."""
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park.transition_to_closed_perm(user=self.moderator)
park.save()
park_ct = ContentType.objects.get_for_model(park)
- log = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).first()
+ log = StateLog.objects.filter(content_type=park_ct, object_id=park.id).first()
self.assertEqual(log.by, self.moderator)
@@ -370,24 +345,20 @@ class ParkBusinessLogicTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = Company.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.property_owner = Company.objects.create(
- name='Property Owner',
- description='Property owner company',
- roles=['PROPERTY_OWNER']
+ name="Property Owner", description="Property owner company", roles=["PROPERTY_OWNER"]
)
def test_park_creates_with_valid_operator(self):
"""Test park can be created with valid operator."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.assertEqual(park.operator, self.operator)
@@ -395,35 +366,32 @@ class ParkBusinessLogicTests(TestCase):
def test_park_slug_auto_generated(self):
"""Test that park slug is auto-generated from name."""
park = Park.objects.create(
- name='My Amazing Theme Park',
- description='A test park',
- operator=self.operator,
- timezone='America/New_York'
+ name="My Amazing Theme Park", description="A test park", operator=self.operator, timezone="America/New_York"
)
- self.assertEqual(park.slug, 'my-amazing-theme-park')
+ self.assertEqual(park.slug, "my-amazing-theme-park")
def test_park_url_generated(self):
"""Test that frontend URL is generated on save."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
- self.assertIn('test-park', park.url)
+ self.assertIn("test-park", park.url)
def test_opening_year_computed_from_opening_date(self):
"""Test that opening_year is computed from opening_date."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
opening_date=date(2020, 6, 15),
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.assertEqual(park.opening_year, 2020)
@@ -431,26 +399,26 @@ class ParkBusinessLogicTests(TestCase):
def test_search_text_populated(self):
"""Test that search_text is populated on save."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A wonderful theme park',
+ name="Test Park",
+ slug="test-park",
+ description="A wonderful theme park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
- self.assertIn('test park', park.search_text)
- self.assertIn('wonderful theme park', park.search_text)
- self.assertIn('test operator', park.search_text)
+ self.assertIn("test park", park.search_text)
+ self.assertIn("wonderful theme park", park.search_text)
+ self.assertIn("test operator", park.search_text)
def test_park_with_property_owner(self):
"""Test park with separate property owner."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
property_owner=self.property_owner,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.assertEqual(park.operator, self.operator)
@@ -468,9 +436,7 @@ class ParkSlugHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = Company.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
def test_historical_slug_created_on_name_change(self):
@@ -480,25 +446,18 @@ class ParkSlugHistoryTests(TestCase):
from apps.core.history import HistoricalSlug
park = Park.objects.create(
- name='Original Name',
- description='A test park',
- operator=self.operator,
- timezone='America/New_York'
+ name="Original Name", description="A test park", operator=self.operator, timezone="America/New_York"
)
original_slug = park.slug
# Change name
- park.name = 'New Name'
+ park.name = "New Name"
park.save()
# Check historical slug was created
park_ct = ContentType.objects.get_for_model(park)
- historical = HistoricalSlug.objects.filter(
- content_type=park_ct,
- object_id=park.id,
- slug=original_slug
- ).first()
+ historical = HistoricalSlug.objects.filter(content_type=park_ct, object_id=park.id, slug=original_slug).first()
self.assertIsNotNone(historical)
self.assertEqual(historical.slug, original_slug)
@@ -506,14 +465,14 @@ class ParkSlugHistoryTests(TestCase):
def test_get_by_slug_finds_current_slug(self):
"""Test get_by_slug finds park by current slug."""
park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
- found_park, is_historical = Park.get_by_slug('test-park')
+ found_park, is_historical = Park.get_by_slug("test-park")
self.assertEqual(found_park, park)
self.assertFalse(is_historical)
@@ -522,16 +481,13 @@ class ParkSlugHistoryTests(TestCase):
"""Test get_by_slug finds park by historical slug."""
park = Park.objects.create(
- name='Original Name',
- description='A test park',
- operator=self.operator,
- timezone='America/New_York'
+ name="Original Name", description="A test park", operator=self.operator, timezone="America/New_York"
)
original_slug = park.slug
# Change name to create historical slug
- park.name = 'New Name'
+ park.name = "New Name"
park.save()
# Find by historical slug
diff --git a/backend/apps/parks/tests/test_park_workflows.py b/backend/apps/parks/tests/test_park_workflows.py
index 6b953fad..06031e45 100644
--- a/backend/apps/parks/tests/test_park_workflows.py
+++ b/backend/apps/parks/tests/test_park_workflows.py
@@ -22,33 +22,24 @@ class ParkOpeningWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='park_user',
- email='park_user@example.com',
- password='testpass123',
- role='USER'
+ username="park_user", email="park_user@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='park_mod',
- email='park_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="park_mod", email="park_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
"""Helper to create a park."""
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator {status}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator {status}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park {status}',
- 'slug': f'test-park-{status.lower()}-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park {status}",
+ "slug": f"test-park-{status.lower()}-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -59,16 +50,16 @@ class ParkOpeningWorkflowTests(TestCase):
Flow: UNDER_CONSTRUCTION → OPERATING
"""
- park = self._create_park(status='UNDER_CONSTRUCTION')
+ park = self._create_park(status="UNDER_CONSTRUCTION")
- self.assertEqual(park.status, 'UNDER_CONSTRUCTION')
+ self.assertEqual(park.status, "UNDER_CONSTRUCTION")
# Park opens
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
class ParkTemporaryClosureWorkflowTests(TestCase):
@@ -77,26 +68,20 @@ class ParkTemporaryClosureWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='temp_closure_user',
- email='temp_closure@example.com',
- password='testpass123',
- role='USER'
+ username="temp_closure_user", email="temp_closure@example.com", password="testpass123", role="USER"
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Temp {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Temp {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Temp {timezone.now().timestamp()}',
- 'slug': f'test-park-temp-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Temp {timezone.now().timestamp()}",
+ "slug": f"test-park-temp-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -107,23 +92,23 @@ class ParkTemporaryClosureWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_TEMP → OPERATING
"""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
# Close temporarily (e.g., off-season)
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_TEMP')
+ self.assertEqual(park.status, "CLOSED_TEMP")
# Reopen
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
class ParkPermanentClosureWorkflowTests(TestCase):
@@ -132,26 +117,20 @@ class ParkPermanentClosureWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='perm_mod',
- email='perm_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="perm_mod", email="perm_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Perm {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Perm {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Perm {timezone.now().timestamp()}',
- 'slug': f'test-park-perm-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Perm {timezone.now().timestamp()}",
+ "slug": f"test-park-perm-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -162,7 +141,7 @@ class ParkPermanentClosureWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_PERM
"""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
# Close permanently
park.transition_to_closed_perm(user=self.moderator)
@@ -170,7 +149,7 @@ class ParkPermanentClosureWorkflowTests(TestCase):
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
self.assertIsNotNone(park.closing_date)
def test_park_permanent_closure_from_temp(self):
@@ -179,7 +158,7 @@ class ParkPermanentClosureWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_TEMP → CLOSED_PERM
"""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
# Temporary closure
park.transition_to_closed_temp(user=self.moderator)
@@ -191,7 +170,7 @@ class ParkPermanentClosureWorkflowTests(TestCase):
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
class ParkDemolitionWorkflowTests(TestCase):
@@ -200,26 +179,20 @@ class ParkDemolitionWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='demo_mod',
- email='demo_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="demo_mod", email="demo_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='CLOSED_PERM', **kwargs):
+ def _create_park(self, status="CLOSED_PERM", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Demo {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Demo {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Demo {timezone.now().timestamp()}',
- 'slug': f'test-park-demo-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Demo {timezone.now().timestamp()}",
+ "slug": f"test-park-demo-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -230,20 +203,20 @@ class ParkDemolitionWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_PERM → DEMOLISHED
"""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
# Demolish
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'DEMOLISHED')
+ self.assertEqual(park.status, "DEMOLISHED")
def test_demolished_is_final_state(self):
"""Test that demolished parks cannot transition further."""
from django_fsm import TransitionNotAllowed
- park = self._create_park(status='DEMOLISHED')
+ park = self._create_park(status="DEMOLISHED")
# Cannot transition from demolished
with self.assertRaises(TransitionNotAllowed):
@@ -256,26 +229,20 @@ class ParkRelocationWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='reloc_mod',
- email='reloc_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="reloc_mod", email="reloc_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='CLOSED_PERM', **kwargs):
+ def _create_park(self, status="CLOSED_PERM", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Reloc {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Reloc {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Reloc {timezone.now().timestamp()}',
- 'slug': f'test-park-reloc-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Reloc {timezone.now().timestamp()}",
+ "slug": f"test-park-reloc-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -286,20 +253,20 @@ class ParkRelocationWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_PERM → RELOCATED
"""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
# Relocate
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'RELOCATED')
+ self.assertEqual(park.status, "RELOCATED")
def test_relocated_is_final_state(self):
"""Test that relocated parks cannot transition further."""
from django_fsm import TransitionNotAllowed
- park = self._create_park(status='RELOCATED')
+ park = self._create_park(status="RELOCATED")
# Cannot transition from relocated
with self.assertRaises(TransitionNotAllowed):
@@ -312,71 +279,62 @@ class ParkWrapperMethodTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='wrapper_user',
- email='wrapper@example.com',
- password='testpass123',
- role='USER'
+ username="wrapper_user", email="wrapper@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='wrapper_mod',
- email='wrapper_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="wrapper_mod", email="wrapper_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Wrapper {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Wrapper {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Wrapper {timezone.now().timestamp()}',
- 'slug': f'test-park-wrapper-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Wrapper {timezone.now().timestamp()}",
+ "slug": f"test-park-wrapper-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
def test_close_temporarily_wrapper(self):
"""Test close_temporarily wrapper method."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
# Use wrapper method if it exists
- if hasattr(park, 'close_temporarily'):
+ if hasattr(park, "close_temporarily"):
park.close_temporarily(user=self.user)
else:
park.transition_to_closed_temp(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_TEMP')
+ self.assertEqual(park.status, "CLOSED_TEMP")
def test_reopen_wrapper(self):
"""Test reopen wrapper method."""
- park = self._create_park(status='CLOSED_TEMP')
+ park = self._create_park(status="CLOSED_TEMP")
# Use wrapper method if it exists
- if hasattr(park, 'reopen'):
+ if hasattr(park, "reopen"):
park.reopen(user=self.user)
else:
park.transition_to_operating(user=self.user)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'OPERATING')
+ self.assertEqual(park.status, "OPERATING")
def test_close_permanently_wrapper(self):
"""Test close_permanently wrapper method."""
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
closing_date = timezone.now().date()
# Use wrapper method if it exists
- if hasattr(park, 'close_permanently'):
+ if hasattr(park, "close_permanently"):
park.close_permanently(closing_date=closing_date, user=self.moderator)
else:
park.transition_to_closed_perm(user=self.moderator)
@@ -384,35 +342,35 @@ class ParkWrapperMethodTests(TestCase):
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'CLOSED_PERM')
+ self.assertEqual(park.status, "CLOSED_PERM")
def test_demolish_wrapper(self):
"""Test demolish wrapper method."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
# Use wrapper method if it exists
- if hasattr(park, 'demolish'):
+ if hasattr(park, "demolish"):
park.demolish(user=self.moderator)
else:
park.transition_to_demolished(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'DEMOLISHED')
+ self.assertEqual(park.status, "DEMOLISHED")
def test_relocate_wrapper(self):
"""Test relocate wrapper method."""
- park = self._create_park(status='CLOSED_PERM')
+ park = self._create_park(status="CLOSED_PERM")
# Use wrapper method if it exists
- if hasattr(park, 'relocate'):
+ if hasattr(park, "relocate"):
park.relocate(user=self.moderator)
else:
park.transition_to_relocated(user=self.moderator)
park.save()
park.refresh_from_db()
- self.assertEqual(park.status, 'RELOCATED')
+ self.assertEqual(park.status, "RELOCATED")
class ParkStateLogTests(TestCase):
@@ -421,32 +379,23 @@ class ParkStateLogTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='log_user',
- email='log_user@example.com',
- password='testpass123',
- role='USER'
+ username="log_user", email="log_user@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='log_mod',
- email='log_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="log_mod", email="log_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_park(self, status='OPERATING', **kwargs):
+ def _create_park(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
- operator = Company.objects.create(
- name=f'Operator Log {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator Log {timezone.now().timestamp()}", roles=["OPERATOR"])
defaults = {
- 'name': f'Test Park Log {timezone.now().timestamp()}',
- 'slug': f'test-park-log-{timezone.now().timestamp()}',
- 'operator': operator,
- 'status': status,
- 'timezone': 'America/New_York'
+ "name": f"Test Park Log {timezone.now().timestamp()}",
+ "slug": f"test-park-log-{timezone.now().timestamp()}",
+ "operator": operator,
+ "status": status,
+ "timezone": "America/New_York",
}
defaults.update(kwargs)
return Park.objects.create(**defaults)
@@ -456,7 +405,7 @@ class ParkStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park_ct = ContentType.objects.get_for_model(park)
# Perform transition
@@ -464,13 +413,10 @@ class ParkStateLogTests(TestCase):
park.save()
# Check log was created
- log = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).first()
+ log = StateLog.objects.filter(content_type=park_ct, object_id=park.id).first()
self.assertIsNotNone(log, "StateLog entry should be created")
- self.assertEqual(log.state, 'CLOSED_TEMP')
+ self.assertEqual(log.state, "CLOSED_TEMP")
self.assertEqual(log.by, self.user)
def test_multiple_transitions_logged(self):
@@ -478,7 +424,7 @@ class ParkStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park_ct = ContentType.objects.get_for_model(park)
# First transition: OPERATING -> CLOSED_TEMP
@@ -490,15 +436,12 @@ class ParkStateLogTests(TestCase):
park.save()
# Check multiple logs created
- logs = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=park_ct, object_id=park.id).order_by("timestamp")
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
- self.assertEqual(logs[0].state, 'CLOSED_TEMP')
+ self.assertEqual(logs[0].state, "CLOSED_TEMP")
self.assertEqual(logs[0].by, self.user)
- self.assertEqual(logs[1].state, 'CLOSED_PERM')
+ self.assertEqual(logs[1].state, "CLOSED_PERM")
self.assertEqual(logs[1].by, self.moderator)
def test_full_lifecycle_logged(self):
@@ -506,7 +449,7 @@ class ParkStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- park = self._create_park(status='OPERATING')
+ park = self._create_park(status="OPERATING")
park_ct = ContentType.objects.get_for_model(park)
# Full lifecycle: OPERATING -> CLOSED_TEMP -> OPERATING -> CLOSED_PERM -> DEMOLISHED
@@ -523,11 +466,8 @@ class ParkStateLogTests(TestCase):
park.save()
# Check all logs created
- logs = StateLog.objects.filter(
- content_type=park_ct,
- object_id=park.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=park_ct, object_id=park.id).order_by("timestamp")
self.assertEqual(logs.count(), 4, "Should have 4 log entries")
states = [log.state for log in logs]
- self.assertEqual(states, ['CLOSED_TEMP', 'OPERATING', 'CLOSED_PERM', 'DEMOLISHED'])
+ self.assertEqual(states, ["CLOSED_TEMP", "OPERATING", "CLOSED_PERM", "DEMOLISHED"])
diff --git a/backend/apps/parks/tests/test_query_optimization.py b/backend/apps/parks/tests/test_query_optimization.py
index 28c7e3ae..a895efee 100644
--- a/backend/apps/parks/tests/test_query_optimization.py
+++ b/backend/apps/parks/tests/test_query_optimization.py
@@ -55,9 +55,7 @@ class ParkQueryOptimizationTests(TestCase):
# Should be a small number of queries (main query + prefetch)
# The exact count depends on prefetch_related configuration
self.assertLessEqual(
- len(context.captured_queries),
- 5,
- f"Expected <= 5 queries, got {len(context.captured_queries)}"
+ len(context.captured_queries), 5, f"Expected <= 5 queries, got {len(context.captured_queries)}"
)
def test_optimized_for_detail_query_count(self):
@@ -72,9 +70,7 @@ class ParkQueryOptimizationTests(TestCase):
# Should be a reasonable number of queries
self.assertLessEqual(
- len(context.captured_queries),
- 10,
- f"Expected <= 10 queries, got {len(context.captured_queries)}"
+ len(context.captured_queries), 10, f"Expected <= 10 queries, got {len(context.captured_queries)}"
)
def test_with_location_includes_location(self):
@@ -94,10 +90,10 @@ class ParkQueryOptimizationTests(TestCase):
if result.exists():
first = result.first()
# Should include these fields
- self.assertIn('id', first)
- self.assertIn('name', first)
- self.assertIn('slug', first)
- self.assertIn('status', first)
+ self.assertIn("id", first)
+ self.assertIn("name", first)
+ self.assertIn("slug", first)
+ self.assertIn("status", first)
def test_search_autocomplete_limits_results(self):
"""Verify search_autocomplete respects limit parameter."""
@@ -148,7 +144,7 @@ class CompanyQueryOptimizationTests(TestCase):
if result.exists():
first = result.first()
# Should have ride_count attribute
- self.assertTrue(hasattr(first, 'ride_count'))
+ self.assertTrue(hasattr(first, "ride_count"))
def test_operators_with_park_count_includes_annotation(self):
"""Verify operators_with_park_count adds park count annotations."""
@@ -156,7 +152,7 @@ class CompanyQueryOptimizationTests(TestCase):
if result.exists():
first = result.first()
# Should have operated_parks_count attribute
- self.assertTrue(hasattr(first, 'operated_parks_count'))
+ self.assertTrue(hasattr(first, "operated_parks_count"))
class ComputedFieldMaintenanceTests(TestCase):
diff --git a/backend/apps/parks/tests_disabled/test_filters.py b/backend/apps/parks/tests_disabled/test_filters.py
index 02f53be3..672437b3 100644
--- a/backend/apps/parks/tests_disabled/test_filters.py
+++ b/backend/apps/parks/tests_disabled/test_filters.py
@@ -19,12 +19,8 @@ class ParkFilterTests(TestCase):
def setUpTestData(cls):
"""Set up test data for all filter tests"""
# Create operators
- cls.operator1 = Company.objects.create(
- name="Thrilling Adventures Inc", slug="thrilling-adventures"
- )
- cls.operator2 = Company.objects.create(
- name="Family Fun Corp", slug="family-fun"
- )
+ cls.operator1 = Company.objects.create(name="Thrilling Adventures Inc", slug="thrilling-adventures")
+ cls.operator2 = Company.objects.create(name="Family Fun Corp", slug="family-fun")
# Create parks with various attributes for testing all filters
cls.park1 = Park.objects.create(
diff --git a/backend/apps/parks/tests_disabled/test_models.py b/backend/apps/parks/tests_disabled/test_models.py
index cffc0740..fb16c4e3 100644
--- a/backend/apps/parks/tests_disabled/test_models.py
+++ b/backend/apps/parks/tests_disabled/test_models.py
@@ -89,9 +89,7 @@ class ParkModelTests(TestCase):
# Check pghistory records
event_model = getattr(Park, "event_model", None)
if event_model:
- historical_records = event_model.objects.filter(
- pgh_obj_id=park.id
- ).order_by("-pgh_created_at")
+ historical_records = event_model.objects.filter(pgh_obj_id=park.id).order_by("-pgh_created_at")
print("\nPG History records:")
for record in historical_records:
print(f"- Event ID: {record.pgh_id}")
@@ -104,17 +102,13 @@ class ParkModelTests(TestCase):
# Try to find by old slug
found_park, is_historical = Park.get_by_slug(original_slug)
self.assertEqual(found_park.id, park.id)
- print(
- f"Found park by old slug: {found_park.slug}, is_historical: {is_historical}"
- )
+ print(f"Found park by old slug: {found_park.slug}, is_historical: {is_historical}")
self.assertTrue(is_historical)
# Try current slug
found_park, is_historical = Park.get_by_slug(new_slug)
self.assertEqual(found_park.id, park.id)
- print(
- f"Found park by new slug: {found_park.slug}, is_historical: {is_historical}"
- )
+ print(f"Found park by new slug: {found_park.slug}, is_historical: {is_historical}")
self.assertFalse(is_historical)
def test_status_color_mapping(self):
@@ -141,15 +135,9 @@ class ParkModelTests(TestCase):
class ParkAreaModelTests(TestCase):
def setUp(self):
"""Set up test data"""
- self.operator = Company.objects.create(
- name="Test Company 2", slug="test-company-2"
- )
- self.park = Park.objects.create(
- name="Test Park", status="OPERATING", operator=self.operator
- )
- self.area = ParkArea.objects.create(
- park=self.park, name="Test Area", description="A test area"
- )
+ self.operator = Company.objects.create(name="Test Company 2", slug="test-company-2")
+ self.park = Park.objects.create(name="Test Park", status="OPERATING", operator=self.operator)
+ self.area = ParkArea.objects.create(park=self.park, name="Test Area", description="A test area")
def test_area_creation(self):
"""Test basic area creation and fields"""
diff --git a/backend/apps/parks/views.py b/backend/apps/parks/views.py
index b92cc612..a36820f0 100644
--- a/backend/apps/parks/views.py
+++ b/backend/apps/parks/views.py
@@ -42,9 +42,7 @@ logger = logging.getLogger(__name__)
# Constants
PARK_DETAIL_URL = "parks:park_detail"
PARK_LIST_ITEM_TEMPLATE = "parks/partials/park_list_item.html"
-REQUIRED_FIELDS_ERROR = (
- "Please correct the errors below. Required fields are marked with an asterisk (*)."
-)
+REQUIRED_FIELDS_ERROR = "Please correct the errors below. Required fields are marked with an asterisk (*)."
TRIP_PARKS_TEMPLATE = "parks/partials/trip_parks_list.html"
TRIP_SUMMARY_TEMPLATE = "parks/partials/trip_summary.html"
SAVED_TRIPS_TEMPLATE = "parks/partials/saved_trips.html"
@@ -87,18 +85,10 @@ def normalize_osm_result(result: dict) -> dict:
neighborhood = address.get("neighbourhood", "")
# Build city from available components
- city = (
- address.get("city")
- or address.get("town")
- or address.get("village")
- or address.get("municipality")
- or ""
- )
+ city = address.get("city") or address.get("town") or address.get("village") or address.get("municipality") or ""
# Get detailed state/region information
- state = (
- address.get("state") or address.get("province") or address.get("region") or ""
- )
+ state = address.get("state") or address.get("province") or address.get("region") or ""
# Get postal code with fallbacks
postal_code = address.get("postcode") or address.get("postal_code") or ""
@@ -170,9 +160,7 @@ def get_park_areas(request: HttpRequest) -> HttpResponse:
park = Park.objects.get(id=park_id)
areas = park.areas.all()
options = ['']
- options.extend(
- [f'' for area in areas]
- )
+ options.extend([f'' for area in areas])
return HttpResponse("\n".join(options))
except Park.DoesNotExist:
return HttpResponse('')
@@ -201,11 +189,7 @@ def location_search(request: HttpRequest) -> JsonResponse:
if response.status_code == 200:
results = response.json()
normalized_results = [normalize_osm_result(result) for result in results]
- valid_results = [
- r
- for r in normalized_results
- if r["lat"] is not None and r["lon"] is not None
- ]
+ valid_results = [r for r in normalized_results if r["lat"] is not None and r["lon"] is not None]
return JsonResponse({"results": valid_results})
return JsonResponse({"results": []})
@@ -226,13 +210,9 @@ def reverse_geocode(request: HttpRequest) -> JsonResponse:
lon = lon.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
if lat < -90 or lat > 90:
- return JsonResponse(
- {"error": "Latitude must be between -90 and 90"}, status=400
- )
+ return JsonResponse({"error": "Latitude must be between -90 and 90"}, status=400)
if lon < -180 or lon > 180:
- return JsonResponse(
- {"error": "Longitude must be between -180 and 180"}, status=400
- )
+ return JsonResponse({"error": "Longitude must be between -180 and 180"}, status=400)
response = requests.get(
"https://nominatim.openstreetmap.org/reverse",
@@ -306,9 +286,7 @@ class ParkListView(HTMXFilterableMixin, ListView):
try:
# Initialize filterset if not exists
if not hasattr(self, "filterset"):
- self.filterset = self.filter_class(
- self.request.GET, queryset=self.model.objects.none()
- )
+ self.filterset = self.filter_class(self.request.GET, queryset=self.model.objects.none())
context = super().get_context_data(**kwargs)
@@ -323,20 +301,14 @@ class ParkListView(HTMXFilterableMixin, ListView):
"search_query": self.request.GET.get("search", ""),
"filter_counts": filter_counts,
"popular_filters": popular_filters,
- "total_results": (
- context.get("paginator").count
- if context.get("paginator")
- else 0
- ),
+ "total_results": (context.get("paginator").count if context.get("paginator") else 0),
}
)
# Add filter suggestions for search queries
search_query = self.request.GET.get("search", "")
if search_query:
- context["filter_suggestions"] = (
- self.filter_service.get_filter_suggestions(search_query)
- )
+ context["filter_suggestions"] = self.filter_service.get_filter_suggestions(search_query)
return context
@@ -353,9 +325,7 @@ class ParkListView(HTMXFilterableMixin, ListView):
messages.error(self.request, f"Error applying filters: {str(e)}")
# Ensure filterset exists in error case
if not hasattr(self, "filterset"):
- self.filterset = self.filter_class(
- self.request.GET, queryset=self.model.objects.none()
- )
+ self.filterset = self.filter_class(self.request.GET, queryset=self.model.objects.none())
return {
"filter": self.filterset,
"error": "Unable to apply filters. Please try adjusting your criteria.",
@@ -427,9 +397,7 @@ class ParkListView(HTMXFilterableMixin, ListView):
return urlencode(url_params)
- def _get_pagination_urls(
- self, page_obj, filter_params: dict[str, Any]
- ) -> dict[str, str]:
+ def _get_pagination_urls(self, page_obj, filter_params: dict[str, Any]) -> dict[str, str]:
"""Generate pagination URLs that preserve filter state."""
base_query = self._build_filter_query_string(filter_params)
@@ -476,9 +444,7 @@ def search_parks(request: HttpRequest) -> HttpResponse:
# Get current view mode from request
current_view_mode = request.GET.get("view_mode", "grid")
- park_filter = ParkFilter(
- {"search": search_query}, queryset=get_base_park_queryset()
- )
+ park_filter = ParkFilter({"search": search_query}, queryset=get_base_park_queryset())
parks = park_filter.qs
if request.GET.get("quick_search"):
@@ -747,10 +713,7 @@ def htmx_optimize_route(request: HttpRequest) -> HttpResponse:
rlat1, rlon1, rlat2, rlon2 = map(math.radians, [lat1, lon1, lat2, lon2])
dlat = rlat2 - rlat1
dlon = rlon2 - rlon1
- a = (
- math.sin(dlat / 2) ** 2
- + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2
- )
+ a = math.sin(dlat / 2) ** 2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2
c = 2 * math.asin(min(1, math.sqrt(a)))
miles = 3958.8 * c
return miles
@@ -762,18 +725,14 @@ def htmx_optimize_route(request: HttpRequest) -> HttpResponse:
lat = getattr(loc, "latitude", None) if loc else None
lon = getattr(loc, "longitude", None) if loc else None
if lat is not None and lon is not None:
- waypoints.append(
- {"id": p.id, "name": p.name, "latitude": lat, "longitude": lon}
- )
+ waypoints.append({"id": p.id, "name": p.name, "latitude": lat, "longitude": lon})
# sum straight-line distances between consecutive waypoints
for i in range(len(waypoints) - 1):
a = waypoints[i]
b = waypoints[i + 1]
try:
- total_miles += haversine_miles(
- a["latitude"], a["longitude"], b["latitude"], b["longitude"]
- )
+ total_miles += haversine_miles(a["latitude"], a["longitude"], b["latitude"], b["longitude"])
except Exception as e:
log_exception(
logger,
@@ -807,9 +766,7 @@ def htmx_optimize_route(request: HttpRequest) -> HttpResponse:
"total_rides": sum(getattr(p, "ride_count", 0) or 0 for p in parks),
}
- html = render_to_string(
- TRIP_SUMMARY_TEMPLATE, {"summary": summary}, request=request
- )
+ html = render_to_string(TRIP_SUMMARY_TEMPLATE, {"summary": summary}, request=request)
resp = HttpResponse(html)
# Include waypoints payload in HX-Trigger so client can render route on the map
resp["HX-Trigger"] = json.dumps({"tripOptimized": {"parks": waypoints}})
@@ -843,9 +800,7 @@ def htmx_save_trip(request: HttpRequest) -> HttpResponse:
# attempt to associate parks if the Trip model supports it
with contextlib.suppress(Exception):
trip.parks.set([p.id for p in parks])
- trips = list(
- Trip.objects.filter(owner=request.user).order_by("-created_at")[:10]
- )
+ trips = list(Trip.objects.filter(owner=request.user).order_by("-created_at")[:10])
except Exception:
trips = []
@@ -892,14 +847,10 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
def normalize_coordinates(self, form: ParkForm) -> None:
if form.cleaned_data.get("latitude"):
lat = Decimal(str(form.cleaned_data["latitude"]))
- form.cleaned_data["latitude"] = lat.quantize(
- Decimal("0.000001"), rounding=ROUND_DOWN
- )
+ form.cleaned_data["latitude"] = lat.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
if form.cleaned_data.get("longitude"):
lon = Decimal(str(form.cleaned_data["longitude"]))
- form.cleaned_data["longitude"] = lon.quantize(
- Decimal("0.000001"), rounding=ROUND_DOWN
- )
+ form.cleaned_data["longitude"] = lon.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
def form_valid(self, form: ParkForm) -> HttpResponse:
self.normalize_coordinates(form)
@@ -942,8 +893,7 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
)
messages.success(
self.request,
- f"Successfully created {self.object.name}. "
- f"Added {service_result['uploaded_count']} photo(s).",
+ f"Successfully created {self.object.name}. " f"Added {service_result['uploaded_count']} photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
@@ -960,8 +910,7 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
)
messages.success(
self.request,
- "Your park submission has been sent for review. "
- "You will be notified when it is approved.",
+ "Your park submission has been sent for review. " "You will be notified when it is approved.",
)
return HttpResponseRedirect(reverse("parks:park_list"))
@@ -1016,14 +965,10 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
def normalize_coordinates(self, form: ParkForm) -> None:
if form.cleaned_data.get("latitude"):
lat = Decimal(str(form.cleaned_data["latitude"]))
- form.cleaned_data["latitude"] = lat.quantize(
- Decimal("0.000001"), rounding=ROUND_DOWN
- )
+ form.cleaned_data["latitude"] = lat.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
if form.cleaned_data.get("longitude"):
lon = Decimal(str(form.cleaned_data["longitude"]))
- form.cleaned_data["longitude"] = lon.quantize(
- Decimal("0.000001"), rounding=ROUND_DOWN
- )
+ form.cleaned_data["longitude"] = lon.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
def form_valid(self, form: ParkForm) -> HttpResponse:
self.normalize_coordinates(form)
@@ -1068,8 +1013,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
)
messages.success(
self.request,
- f"Successfully updated {self.object.name}. "
- f"Added {service_result['uploaded_count']} new photo(s).",
+ f"Successfully updated {self.object.name}. " f"Added {service_result['uploaded_count']} new photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
@@ -1090,9 +1034,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
f"Your changes to {self.object.name} have been sent for review. "
"You will be notified when they are approved.",
)
- return HttpResponseRedirect(
- reverse(PARK_DETAIL_URL, kwargs={"slug": self.object.slug})
- )
+ return HttpResponseRedirect(reverse(PARK_DETAIL_URL, kwargs={"slug": self.object.slug}))
elif service_result["status"] == "failed":
messages.error(
@@ -1143,11 +1085,7 @@ class ParkDetailView(
def get_queryset(self) -> QuerySet[Park]:
return cast(
QuerySet[Park],
- super()
- .get_queryset()
- .prefetch_related(
- "rides", "rides__manufacturer", "photos", "areas", "location"
- ),
+ super().get_queryset().prefetch_related("rides", "rides__manufacturer", "photos", "areas", "location"),
)
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
diff --git a/backend/apps/parks/views_roadtrip.py b/backend/apps/parks/views_roadtrip.py
index ff977ae1..b7696b51 100644
--- a/backend/apps/parks/views_roadtrip.py
+++ b/backend/apps/parks/views_roadtrip.py
@@ -119,9 +119,7 @@ class CreateTripView(RoadTripViewMixin, View):
# Get parks
parks = list(
- Park.objects.filter(
- id__in=park_ids, location__isnull=False
- ).select_related("location", "operator")
+ Park.objects.filter(id__in=park_ids, location__isnull=False).select_related("location", "operator")
)
if len(parks) != len(park_ids):
@@ -159,9 +157,7 @@ class CreateTripView(RoadTripViewMixin, View):
{
"status": "success",
"data": trip_data,
- "trip_url": reverse(
- "parks:roadtrip_detail", kwargs={"trip_id": "temp"}
- ),
+ "trip_url": reverse("parks:roadtrip_detail", kwargs={"trip_id": "temp"}),
}
)
@@ -258,12 +254,8 @@ class FindParksAlongRouteView(RoadTripViewMixin, View):
# Get start and end parks
try:
- start_park = Park.objects.select_related("location").get(
- id=start_park_id, location__isnull=False
- )
- end_park = Park.objects.select_related("location").get(
- id=end_park_id, location__isnull=False
- )
+ start_park = Park.objects.select_related("location").get(id=start_park_id, location__isnull=False)
+ end_park = Park.objects.select_related("location").get(id=end_park_id, location__isnull=False)
except Park.DoesNotExist:
return render(
request,
@@ -272,21 +264,21 @@ class FindParksAlongRouteView(RoadTripViewMixin, View):
)
# Find parks along route
- parks_along_route = self.roadtrip_service.find_parks_along_route(
- start_park, end_park, max_detour_km
- )
+ parks_along_route = self.roadtrip_service.find_parks_along_route(start_park, end_park, max_detour_km)
# Return JSON if requested
if request.headers.get("Accept") == "application/json" or request.content_type == "application/json":
- return JsonResponse({
- "status": "success",
- "data": {
- "parks": [self._park_to_dict(p) for p in parks_along_route],
- "start_park": self._park_to_dict(start_park),
- "end_park": self._park_to_dict(end_park),
- "count": len(parks_along_route)
+ return JsonResponse(
+ {
+ "status": "success",
+ "data": {
+ "parks": [self._park_to_dict(p) for p in parks_along_route],
+ "start_park": self._park_to_dict(start_park),
+ "end_park": self._park_to_dict(end_park),
+ "count": len(parks_along_route),
+ },
}
- })
+ )
return render(
request,
@@ -375,9 +367,7 @@ class GeocodeAddressView(RoadTripViewMixin, View):
"longitude": coordinates.longitude,
},
"address": address,
- "nearby_parks": [
- loc.to_dict() for loc in map_response.locations[:20]
- ],
+ "nearby_parks": [loc.to_dict() for loc in map_response.locations[:20]],
"radius_km": radius_km,
},
}
@@ -418,12 +408,8 @@ class ParkDistanceCalculatorView(RoadTripViewMixin, View):
# Get parks
try:
- park1 = Park.objects.select_related("location").get(
- id=park1_id, location__isnull=False
- )
- park2 = Park.objects.select_related("location").get(
- id=park2_id, location__isnull=False
- )
+ park1 = Park.objects.select_related("location").get(id=park1_id, location__isnull=False)
+ park2 = Park.objects.select_related("location").get(id=park2_id, location__isnull=False)
except Park.DoesNotExist:
return JsonResponse(
{
@@ -448,9 +434,7 @@ class ParkDistanceCalculatorView(RoadTripViewMixin, View):
from services.roadtrip import Coordinates
- route = self.roadtrip_service.calculate_route(
- Coordinates(*coords1), Coordinates(*coords2)
- )
+ route = self.roadtrip_service.calculate_route(Coordinates(*coords1), Coordinates(*coords2))
if not route:
return JsonResponse(
@@ -471,15 +455,11 @@ class ParkDistanceCalculatorView(RoadTripViewMixin, View):
"formatted_duration": route.formatted_duration,
"park1": {
"name": park1.name,
- "formatted_location": getattr(
- park1, "formatted_location", ""
- ),
+ "formatted_location": getattr(park1, "formatted_location", ""),
},
"park2": {
"name": park2.name,
- "formatted_location": getattr(
- park2, "formatted_location", ""
- ),
+ "formatted_location": getattr(park2, "formatted_location", ""),
},
},
}
diff --git a/backend/apps/reviews/models.py b/backend/apps/reviews/models.py
index 5ef49477..65fcdb79 100644
--- a/backend/apps/reviews/models.py
+++ b/backend/apps/reviews/models.py
@@ -34,14 +34,8 @@ class Review(TrackedModel):
text = models.TextField(blank=True, help_text="Review text (optional)")
# Metadata
- is_public = models.BooleanField(
- default=True,
- help_text="Whether this review is visible to others"
- )
- helpful_votes = models.PositiveIntegerField(
- default=0,
- help_text="Number of users who found this helpful"
- )
+ is_public = models.BooleanField(default=True, help_text="Whether this review is visible to others")
+ helpful_votes = models.PositiveIntegerField(default=0, help_text="Number of users who found this helpful")
class Meta(TrackedModel.Meta):
verbose_name = "Review"
diff --git a/backend/apps/reviews/signals.py b/backend/apps/reviews/signals.py
index dfa7f510..5ba630d8 100644
--- a/backend/apps/reviews/signals.py
+++ b/backend/apps/reviews/signals.py
@@ -17,16 +17,15 @@ def update_average_rating(sender, instance, **kwargs):
return
# Check if the content object has an 'average_rating' field
- if not hasattr(content_object, 'average_rating'):
+ if not hasattr(content_object, "average_rating"):
return
# Calculate new average
# We query the Review model filtering by content_type and object_id
- avg_rating = Review.objects.filter(
- content_type=instance.content_type,
- object_id=instance.object_id
- ).aggregate(Avg('rating'))['rating__avg']
+ avg_rating = Review.objects.filter(content_type=instance.content_type, object_id=instance.object_id).aggregate(
+ Avg("rating")
+ )["rating__avg"]
# Update field
content_object.average_rating = avg_rating or 0 # Default to 0 if no reviews
- content_object.save(update_fields=['average_rating'])
+ content_object.save(update_fields=["average_rating"])
diff --git a/backend/apps/rides/__init__.py b/backend/apps/rides/__init__.py
index 00ff67d8..de369588 100644
--- a/backend/apps/rides/__init__.py
+++ b/backend/apps/rides/__init__.py
@@ -9,4 +9,4 @@ companies, rankings, and search functionality.
from . import choices
# Ensure choices are registered on app startup
-__all__ = ['choices']
+__all__ = ["choices"]
diff --git a/backend/apps/rides/admin.py b/backend/apps/rides/admin.py
index 367c6db6..e7211344 100644
--- a/backend/apps/rides/admin.py
+++ b/backend/apps/rides/admin.py
@@ -875,12 +875,8 @@ class RideReviewAdmin(QueryOptimizationMixin, ExportActionMixin, BaseModelAdmin)
"""Display moderation status with color coding."""
if obj.moderated_by:
if obj.is_published:
- return format_html(
- 'Approved'
- )
- return format_html(
- 'Rejected'
- )
+ return format_html('Approved')
+ return format_html('Rejected')
return format_html('Pending')
def save_model(self, request, obj, form, change):
@@ -987,9 +983,7 @@ class CompanyAdmin(
(
"Company Details",
{
- "fields": (
- "founded_date",
- ),
+ "fields": ("founded_date",),
"classes": ("collapse",),
"description": "Historical information about the company.",
},
@@ -1024,7 +1018,7 @@ class CompanyAdmin(
color = colors.get(role, "#6c757d")
badges.append(
f'{role}'
)
return format_html("".join(badges))
diff --git a/backend/apps/rides/apps.py b/backend/apps/rides/apps.py
index 0888a8b8..53c81a87 100644
--- a/backend/apps/rides/apps.py
+++ b/backend/apps/rides/apps.py
@@ -23,9 +23,7 @@ class RidesConfig(AppConfig):
from apps.rides.models import Ride
# Register FSM transitions for Ride
- apply_state_machine(
- Ride, field_name="status", choice_group="statuses", domain="rides"
- )
+ apply_state_machine(Ride, field_name="status", choice_group="statuses", domain="rides")
def _register_callbacks(self):
"""Register FSM transition callbacks for ride models."""
@@ -41,43 +39,19 @@ class RidesConfig(AppConfig):
from apps.rides.models import Ride
# Cache invalidation for all ride status changes
- register_callback(
- Ride, 'status', '*', '*',
- RideCacheInvalidation()
- )
+ register_callback(Ride, "status", "*", "*", RideCacheInvalidation())
# API cache invalidation
- register_callback(
- Ride, 'status', '*', '*',
- APICacheInvalidation(include_geo_cache=True)
- )
+ register_callback(Ride, "status", "*", "*", APICacheInvalidation(include_geo_cache=True))
# Park count updates for status changes that affect active rides
- register_callback(
- Ride, 'status', '*', 'OPERATING',
- ParkCountUpdateCallback()
- )
- register_callback(
- Ride, 'status', 'OPERATING', '*',
- ParkCountUpdateCallback()
- )
- register_callback(
- Ride, 'status', '*', 'CLOSED_PERM',
- ParkCountUpdateCallback()
- )
- register_callback(
- Ride, 'status', '*', 'DEMOLISHED',
- ParkCountUpdateCallback()
- )
- register_callback(
- Ride, 'status', '*', 'RELOCATED',
- ParkCountUpdateCallback()
- )
+ register_callback(Ride, "status", "*", "OPERATING", ParkCountUpdateCallback())
+ register_callback(Ride, "status", "OPERATING", "*", ParkCountUpdateCallback())
+ register_callback(Ride, "status", "*", "CLOSED_PERM", ParkCountUpdateCallback())
+ register_callback(Ride, "status", "*", "DEMOLISHED", ParkCountUpdateCallback())
+ register_callback(Ride, "status", "*", "RELOCATED", ParkCountUpdateCallback())
# Search text update
- register_callback(
- Ride, 'status', '*', '*',
- SearchTextUpdateCallback()
- )
+ register_callback(Ride, "status", "*", "*", SearchTextUpdateCallback())
logger.debug("Registered ride transition callbacks")
diff --git a/backend/apps/rides/choices.py b/backend/apps/rides/choices.py
index 69a80308..62b4d15b 100644
--- a/backend/apps/rides/choices.py
+++ b/backend/apps/rides/choices.py
@@ -14,73 +14,48 @@ RIDE_CATEGORIES = [
value="RC",
label="Roller Coaster",
description="Thrill rides with tracks featuring hills, loops, and high speeds",
- metadata={
- 'color': 'red',
- 'icon': 'roller-coaster',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "red", "icon": "roller-coaster", "css_class": "bg-red-100 text-red-800", "sort_order": 1},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="DR",
label="Dark Ride",
description="Indoor rides with themed environments and storytelling",
metadata={
- 'color': 'purple',
- 'icon': 'dark-ride',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 2
+ "color": "purple",
+ "icon": "dark-ride",
+ "css_class": "bg-purple-100 text-purple-800",
+ "sort_order": 2,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FR",
label="Flat Ride",
description="Rides that move along a generally flat plane with spinning, swinging, or bouncing motions",
- metadata={
- 'color': 'blue',
- 'icon': 'flat-ride',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "blue", "icon": "flat-ride", "css_class": "bg-blue-100 text-blue-800", "sort_order": 3},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="WR",
label="Water Ride",
description="Rides that incorporate water elements like splashing, floating, or getting wet",
- metadata={
- 'color': 'cyan',
- 'icon': 'water-ride',
- 'css_class': 'bg-cyan-100 text-cyan-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "cyan", "icon": "water-ride", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 4},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="TR",
label="Transport Ride",
description="Rides primarily designed for transportation around the park",
- metadata={
- 'color': 'green',
- 'icon': 'transport',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "green", "icon": "transport", "css_class": "bg-green-100 text-green-800", "sort_order": 5},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="OT",
label="Other",
description="Rides that don't fit into standard categories",
- metadata={
- 'color': 'gray',
- 'icon': 'other',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 6
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6},
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -91,140 +66,140 @@ RIDE_STATUSES = [
label="Operating",
description="Ride is currently open and operating normally",
metadata={
- 'color': 'green',
- 'icon': 'check-circle',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 1,
- 'can_transition_to': [
- 'CLOSED_TEMP',
- 'SBNO',
- 'CLOSING',
+ "color": "green",
+ "icon": "check-circle",
+ "css_class": "bg-green-100 text-green-800",
+ "sort_order": 1,
+ "can_transition_to": [
+ "CLOSED_TEMP",
+ "SBNO",
+ "CLOSING",
],
- 'requires_moderator': False,
- 'is_final': False,
- 'is_initial': True,
+ "requires_moderator": False,
+ "is_final": False,
+ "is_initial": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSED_TEMP",
label="Temporarily Closed",
description="Ride is temporarily closed for maintenance, weather, or other short-term reasons",
metadata={
- 'color': 'yellow',
- 'icon': 'pause-circle',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 2,
- 'can_transition_to': [
- 'SBNO',
- 'CLOSING',
+ "color": "yellow",
+ "icon": "pause-circle",
+ "css_class": "bg-yellow-100 text-yellow-800",
+ "sort_order": 2,
+ "can_transition_to": [
+ "SBNO",
+ "CLOSING",
],
- 'requires_moderator': False,
- 'is_final': False,
+ "requires_moderator": False,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="SBNO",
label="Standing But Not Operating",
description="Ride structure remains but is not currently operating",
metadata={
- 'color': 'orange',
- 'icon': 'stop-circle',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 3,
- 'can_transition_to': [
- 'CLOSED_PERM',
- 'DEMOLISHED',
- 'RELOCATED',
+ "color": "orange",
+ "icon": "stop-circle",
+ "css_class": "bg-orange-100 text-orange-800",
+ "sort_order": 3,
+ "can_transition_to": [
+ "CLOSED_PERM",
+ "DEMOLISHED",
+ "RELOCATED",
],
- 'requires_moderator': True,
- 'is_final': False,
+ "requires_moderator": True,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSING",
label="Closing",
description="Ride is scheduled to close permanently",
metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 4,
- 'can_transition_to': [
- 'CLOSED_PERM',
- 'SBNO',
+ "color": "red",
+ "icon": "x-circle",
+ "css_class": "bg-red-100 text-red-800",
+ "sort_order": 4,
+ "can_transition_to": [
+ "CLOSED_PERM",
+ "SBNO",
],
- 'requires_moderator': True,
- 'is_final': False,
+ "requires_moderator": True,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSED_PERM",
label="Permanently Closed",
description="Ride has been permanently closed and will not reopen",
metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 5,
- 'can_transition_to': [
- 'DEMOLISHED',
- 'RELOCATED',
+ "color": "red",
+ "icon": "x-circle",
+ "css_class": "bg-red-100 text-red-800",
+ "sort_order": 5,
+ "can_transition_to": [
+ "DEMOLISHED",
+ "RELOCATED",
],
- 'requires_moderator': True,
- 'is_final': False,
+ "requires_moderator": True,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="UNDER_CONSTRUCTION",
label="Under Construction",
description="Ride is currently being built or undergoing major renovation",
metadata={
- 'color': 'blue',
- 'icon': 'tool',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 6,
- 'can_transition_to': [
- 'OPERATING',
+ "color": "blue",
+ "icon": "tool",
+ "css_class": "bg-blue-100 text-blue-800",
+ "sort_order": 6,
+ "can_transition_to": [
+ "OPERATING",
],
- 'requires_moderator': False,
- 'is_final': False,
+ "requires_moderator": False,
+ "is_final": False,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="DEMOLISHED",
label="Demolished",
description="Ride has been completely removed and demolished",
metadata={
- 'color': 'gray',
- 'icon': 'trash',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 7,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_final': True,
+ "color": "gray",
+ "icon": "trash",
+ "css_class": "bg-gray-100 text-gray-800",
+ "sort_order": 7,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="RELOCATED",
label="Relocated",
description="Ride has been moved to a different location",
metadata={
- 'color': 'purple',
- 'icon': 'arrow-right',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 8,
- 'can_transition_to': [],
- 'requires_moderator': True,
- 'is_final': True,
+ "color": "purple",
+ "icon": "arrow-right",
+ "css_class": "bg-purple-100 text-purple-800",
+ "sort_order": 8,
+ "can_transition_to": [],
+ "requires_moderator": True,
+ "is_final": True,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
]
@@ -235,24 +210,19 @@ POST_CLOSING_STATUSES = [
label="Standing But Not Operating",
description="Ride structure remains but is not operating after closure",
metadata={
- 'color': 'orange',
- 'icon': 'stop-circle',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 1
+ "color": "orange",
+ "icon": "stop-circle",
+ "css_class": "bg-orange-100 text-orange-800",
+ "sort_order": 1,
},
- category=ChoiceCategory.STATUS
+ category=ChoiceCategory.STATUS,
),
RichChoice(
value="CLOSED_PERM",
label="Permanently Closed",
description="Ride has been permanently closed after the closing date",
- metadata={
- 'color': 'red',
- 'icon': 'x-circle',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.STATUS
+ metadata={"color": "red", "icon": "x-circle", "css_class": "bg-red-100 text-red-800", "sort_order": 2},
+ category=ChoiceCategory.STATUS,
),
]
@@ -262,37 +232,22 @@ TRACK_MATERIALS = [
value="STEEL",
label="Steel",
description="Modern steel track construction providing smooth rides and complex layouts",
- metadata={
- 'color': 'gray',
- 'icon': 'steel',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "gray", "icon": "steel", "css_class": "bg-gray-100 text-gray-800", "sort_order": 1},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="WOOD",
label="Wood",
description="Traditional wooden track construction providing classic coaster experience",
- metadata={
- 'color': 'amber',
- 'icon': 'wood',
- 'css_class': 'bg-amber-100 text-amber-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "amber", "icon": "wood", "css_class": "bg-amber-100 text-amber-800", "sort_order": 2},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="HYBRID",
label="Hybrid",
description="Combination of steel and wooden construction elements",
- metadata={
- 'color': 'orange',
- 'icon': 'hybrid',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "orange", "icon": "hybrid", "css_class": "bg-orange-100 text-orange-800", "sort_order": 3},
+ category=ChoiceCategory.TECHNICAL,
),
]
@@ -302,133 +257,83 @@ COASTER_TYPES = [
value="SITDOWN",
label="Sit Down",
description="Traditional seated roller coaster with riders sitting upright",
- metadata={
- 'color': 'blue',
- 'icon': 'sitdown',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "blue", "icon": "sitdown", "css_class": "bg-blue-100 text-blue-800", "sort_order": 1},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="INVERTED",
label="Inverted",
description="Coaster where riders' feet dangle freely below the track",
- metadata={
- 'color': 'purple',
- 'icon': 'inverted',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "purple", "icon": "inverted", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FLYING",
label="Flying",
description="Riders lie face-down in a flying position",
- metadata={
- 'color': 'sky',
- 'icon': 'flying',
- 'css_class': 'bg-sky-100 text-sky-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "sky", "icon": "flying", "css_class": "bg-sky-100 text-sky-800", "sort_order": 3},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="STANDUP",
label="Stand Up",
description="Riders stand upright during the ride",
- metadata={
- 'color': 'green',
- 'icon': 'standup',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "green", "icon": "standup", "css_class": "bg-green-100 text-green-800", "sort_order": 4},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="WING",
label="Wing",
description="Riders sit on either side of the track with nothing above or below",
- metadata={
- 'color': 'indigo',
- 'icon': 'wing',
- 'css_class': 'bg-indigo-100 text-indigo-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "indigo", "icon": "wing", "css_class": "bg-indigo-100 text-indigo-800", "sort_order": 5},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="DIVE",
label="Dive",
description="Features a vertical or near-vertical drop as the main element",
- metadata={
- 'color': 'red',
- 'icon': 'dive',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 6
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "red", "icon": "dive", "css_class": "bg-red-100 text-red-800", "sort_order": 6},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FAMILY",
label="Family",
description="Designed for riders of all ages with moderate thrills",
metadata={
- 'color': 'emerald',
- 'icon': 'family',
- 'css_class': 'bg-emerald-100 text-emerald-800',
- 'sort_order': 7
+ "color": "emerald",
+ "icon": "family",
+ "css_class": "bg-emerald-100 text-emerald-800",
+ "sort_order": 7,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="WILD_MOUSE",
label="Wild Mouse",
description="Compact coaster with sharp turns and sudden drops",
- metadata={
- 'color': 'yellow',
- 'icon': 'mouse',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 8
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "yellow", "icon": "mouse", "css_class": "bg-yellow-100 text-yellow-800", "sort_order": 8},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="SPINNING",
label="Spinning",
description="Cars rotate freely during the ride",
- metadata={
- 'color': 'pink',
- 'icon': 'spinning',
- 'css_class': 'bg-pink-100 text-pink-800',
- 'sort_order': 9
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "pink", "icon": "spinning", "css_class": "bg-pink-100 text-pink-800", "sort_order": 9},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="FOURTH_DIMENSION",
label="4th Dimension",
description="Seats rotate independently of the track direction",
- metadata={
- 'color': 'violet',
- 'icon': '4d',
- 'css_class': 'bg-violet-100 text-violet-800',
- 'sort_order': 10
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "violet", "icon": "4d", "css_class": "bg-violet-100 text-violet-800", "sort_order": 10},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="OTHER",
label="Other",
description="Coaster type that doesn't fit standard classifications",
- metadata={
- 'color': 'gray',
- 'icon': 'other',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 11
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 11},
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -438,61 +343,36 @@ PROPULSION_SYSTEMS = [
value="CHAIN",
label="Chain Lift",
description="Traditional chain lift system to pull trains up the lift hill",
- metadata={
- 'color': 'gray',
- 'icon': 'chain',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "gray", "icon": "chain", "css_class": "bg-gray-100 text-gray-800", "sort_order": 1},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="LSM",
label="LSM Launch",
description="Linear Synchronous Motor launch system using magnetic propulsion",
- metadata={
- 'color': 'blue',
- 'icon': 'lightning',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "blue", "icon": "lightning", "css_class": "bg-blue-100 text-blue-800", "sort_order": 2},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="HYDRAULIC",
label="Hydraulic Launch",
description="High-pressure hydraulic launch system for rapid acceleration",
- metadata={
- 'color': 'red',
- 'icon': 'hydraulic',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "red", "icon": "hydraulic", "css_class": "bg-red-100 text-red-800", "sort_order": 3},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="GRAVITY",
label="Gravity",
description="Uses gravity and momentum without mechanical lift systems",
- metadata={
- 'color': 'green',
- 'icon': 'gravity',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "green", "icon": "gravity", "css_class": "bg-green-100 text-green-800", "sort_order": 4},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="OTHER",
label="Other",
description="Propulsion system that doesn't fit standard categories",
- metadata={
- 'color': 'gray',
- 'icon': 'other',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 5},
+ category=ChoiceCategory.TECHNICAL,
),
]
@@ -502,61 +382,36 @@ TARGET_MARKETS = [
value="FAMILY",
label="Family",
description="Designed for families with children, moderate thrills",
- metadata={
- 'color': 'green',
- 'icon': 'family',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 1},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="THRILL",
label="Thrill",
description="High-intensity rides for thrill seekers",
- metadata={
- 'color': 'red',
- 'icon': 'thrill',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "red", "icon": "thrill", "css_class": "bg-red-100 text-red-800", "sort_order": 2},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="EXTREME",
label="Extreme",
description="Maximum intensity rides for extreme thrill seekers",
- metadata={
- 'color': 'purple',
- 'icon': 'extreme',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "purple", "icon": "extreme", "css_class": "bg-purple-100 text-purple-800", "sort_order": 3},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="KIDDIE",
label="Kiddie",
description="Gentle rides designed specifically for young children",
- metadata={
- 'color': 'yellow',
- 'icon': 'kiddie',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "yellow", "icon": "kiddie", "css_class": "bg-yellow-100 text-yellow-800", "sort_order": 4},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="ALL_AGES",
label="All Ages",
description="Suitable for riders of all ages and thrill preferences",
- metadata={
- 'color': 'blue',
- 'icon': 'all-ages',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5},
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -566,61 +421,41 @@ PHOTO_TYPES = [
value="PROMOTIONAL",
label="Promotional",
description="Marketing and promotional photos of the ride model",
- metadata={
- 'color': 'blue',
- 'icon': 'camera',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "blue", "icon": "camera", "css_class": "bg-blue-100 text-blue-800", "sort_order": 1},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="TECHNICAL",
label="Technical Drawing",
description="Technical drawings and engineering diagrams",
- metadata={
- 'color': 'gray',
- 'icon': 'blueprint',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "gray", "icon": "blueprint", "css_class": "bg-gray-100 text-gray-800", "sort_order": 2},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="INSTALLATION",
label="Installation Example",
description="Photos of actual installations of this ride model",
metadata={
- 'color': 'green',
- 'icon': 'installation',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 3
+ "color": "green",
+ "icon": "installation",
+ "css_class": "bg-green-100 text-green-800",
+ "sort_order": 3,
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="RENDERING",
label="3D Rendering",
description="Computer-generated 3D renderings of the ride model",
- metadata={
- 'color': 'purple',
- 'icon': 'cube',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "purple", "icon": "cube", "css_class": "bg-purple-100 text-purple-800", "sort_order": 4},
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="CATALOG",
label="Catalog Image",
description="Official catalog and brochure images",
- metadata={
- 'color': 'orange',
- 'icon': 'catalog',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 5
- },
- category=ChoiceCategory.CLASSIFICATION
+ metadata={"color": "orange", "icon": "catalog", "css_class": "bg-orange-100 text-orange-800", "sort_order": 5},
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -630,97 +465,62 @@ SPEC_CATEGORIES = [
value="DIMENSIONS",
label="Dimensions",
description="Physical dimensions and measurements",
- metadata={
- 'color': 'blue',
- 'icon': 'ruler',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 1
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "blue", "icon": "ruler", "css_class": "bg-blue-100 text-blue-800", "sort_order": 1},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="PERFORMANCE",
label="Performance",
description="Performance specifications and capabilities",
- metadata={
- 'color': 'red',
- 'icon': 'speedometer',
- 'css_class': 'bg-red-100 text-red-800',
- 'sort_order': 2
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "red", "icon": "speedometer", "css_class": "bg-red-100 text-red-800", "sort_order": 2},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="CAPACITY",
label="Capacity",
description="Rider capacity and throughput specifications",
- metadata={
- 'color': 'green',
- 'icon': 'users',
- 'css_class': 'bg-green-100 text-green-800',
- 'sort_order': 3
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "green", "icon": "users", "css_class": "bg-green-100 text-green-800", "sort_order": 3},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="SAFETY",
label="Safety Features",
description="Safety systems and features",
- metadata={
- 'color': 'yellow',
- 'icon': 'shield',
- 'css_class': 'bg-yellow-100 text-yellow-800',
- 'sort_order': 4
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "yellow", "icon": "shield", "css_class": "bg-yellow-100 text-yellow-800", "sort_order": 4},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="ELECTRICAL",
label="Electrical Requirements",
description="Power and electrical system requirements",
metadata={
- 'color': 'purple',
- 'icon': 'lightning',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 5
+ "color": "purple",
+ "icon": "lightning",
+ "css_class": "bg-purple-100 text-purple-800",
+ "sort_order": 5,
},
- category=ChoiceCategory.TECHNICAL
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="FOUNDATION",
label="Foundation Requirements",
description="Foundation and structural requirements",
- metadata={
- 'color': 'gray',
- 'icon': 'foundation',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 6
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "gray", "icon": "foundation", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="MAINTENANCE",
label="Maintenance",
description="Maintenance requirements and procedures",
- metadata={
- 'color': 'orange',
- 'icon': 'wrench',
- 'css_class': 'bg-orange-100 text-orange-800',
- 'sort_order': 7
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "orange", "icon": "wrench", "css_class": "bg-orange-100 text-orange-800", "sort_order": 7},
+ category=ChoiceCategory.TECHNICAL,
),
RichChoice(
value="OTHER",
label="Other",
description="Other technical specifications",
- metadata={
- 'color': 'gray',
- 'icon': 'other',
- 'css_class': 'bg-gray-100 text-gray-800',
- 'sort_order': 8
- },
- category=ChoiceCategory.TECHNICAL
+ metadata={"color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 8},
+ category=ChoiceCategory.TECHNICAL,
),
]
@@ -731,30 +531,30 @@ RIDES_COMPANY_ROLES = [
label="Ride Manufacturer",
description="Company that designs and builds ride hardware and systems",
metadata={
- 'color': 'blue',
- 'icon': 'factory',
- 'css_class': 'bg-blue-100 text-blue-800',
- 'sort_order': 1,
- 'domain': 'rides',
- 'permissions': ['manage_ride_models', 'view_manufacturing'],
- 'url_pattern': '/rides/manufacturers/{slug}/'
+ "color": "blue",
+ "icon": "factory",
+ "css_class": "bg-blue-100 text-blue-800",
+ "sort_order": 1,
+ "domain": "rides",
+ "permissions": ["manage_ride_models", "view_manufacturing"],
+ "url_pattern": "/rides/manufacturers/{slug}/",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="DESIGNER",
label="Ride Designer",
description="Company that specializes in ride design, layout, and engineering",
metadata={
- 'color': 'purple',
- 'icon': 'design',
- 'css_class': 'bg-purple-100 text-purple-800',
- 'sort_order': 2,
- 'domain': 'rides',
- 'permissions': ['manage_ride_designs', 'view_design_specs'],
- 'url_pattern': '/rides/designers/{slug}/'
+ "color": "purple",
+ "icon": "design",
+ "css_class": "bg-purple-100 text-purple-800",
+ "sort_order": 2,
+ "domain": "rides",
+ "permissions": ["manage_ride_designs", "view_design_specs"],
+ "url_pattern": "/rides/designers/{slug}/",
},
- category=ChoiceCategory.CLASSIFICATION
+ category=ChoiceCategory.CLASSIFICATION,
),
]
@@ -767,7 +567,7 @@ def register_rides_choices():
choices=RIDE_CATEGORIES,
domain="rides",
description="Ride category classifications",
- metadata={'domain': 'rides', 'type': 'category'}
+ metadata={"domain": "rides", "type": "category"},
)
register_choices(
@@ -775,7 +575,7 @@ def register_rides_choices():
choices=RIDE_STATUSES,
domain="rides",
description="Ride operational status options",
- metadata={'domain': 'rides', 'type': 'status'}
+ metadata={"domain": "rides", "type": "status"},
)
register_choices(
@@ -783,7 +583,7 @@ def register_rides_choices():
choices=POST_CLOSING_STATUSES,
domain="rides",
description="Status options after ride closure",
- metadata={'domain': 'rides', 'type': 'post_closing_status'}
+ metadata={"domain": "rides", "type": "post_closing_status"},
)
register_choices(
@@ -791,7 +591,7 @@ def register_rides_choices():
choices=TRACK_MATERIALS,
domain="rides",
description="Roller coaster track material types",
- metadata={'domain': 'rides', 'type': 'track_material', 'applies_to': 'roller_coasters'}
+ metadata={"domain": "rides", "type": "track_material", "applies_to": "roller_coasters"},
)
register_choices(
@@ -799,7 +599,7 @@ def register_rides_choices():
choices=COASTER_TYPES,
domain="rides",
description="Roller coaster type classifications",
- metadata={'domain': 'rides', 'type': 'coaster_type', 'applies_to': 'roller_coasters'}
+ metadata={"domain": "rides", "type": "coaster_type", "applies_to": "roller_coasters"},
)
register_choices(
@@ -807,7 +607,7 @@ def register_rides_choices():
choices=PROPULSION_SYSTEMS,
domain="rides",
description="Roller coaster propulsion and lift systems",
- metadata={'domain': 'rides', 'type': 'propulsion_system', 'applies_to': 'roller_coasters'}
+ metadata={"domain": "rides", "type": "propulsion_system", "applies_to": "roller_coasters"},
)
register_choices(
@@ -815,7 +615,7 @@ def register_rides_choices():
choices=TARGET_MARKETS,
domain="rides",
description="Target market classifications for ride models",
- metadata={'domain': 'rides', 'type': 'target_market', 'applies_to': 'ride_models'}
+ metadata={"domain": "rides", "type": "target_market", "applies_to": "ride_models"},
)
register_choices(
@@ -823,7 +623,7 @@ def register_rides_choices():
choices=PHOTO_TYPES,
domain="rides",
description="Photo type classifications for ride model images",
- metadata={'domain': 'rides', 'type': 'photo_type', 'applies_to': 'ride_model_photos'}
+ metadata={"domain": "rides", "type": "photo_type", "applies_to": "ride_model_photos"},
)
register_choices(
@@ -831,7 +631,7 @@ def register_rides_choices():
choices=SPEC_CATEGORIES,
domain="rides",
description="Technical specification category classifications",
- metadata={'domain': 'rides', 'type': 'spec_category', 'applies_to': 'ride_model_specs'}
+ metadata={"domain": "rides", "type": "spec_category", "applies_to": "ride_model_specs"},
)
register_choices(
@@ -839,7 +639,7 @@ def register_rides_choices():
choices=RIDES_COMPANY_ROLES,
domain="rides",
description="Company role classifications for rides domain (MANUFACTURER and DESIGNER only)",
- metadata={'domain': 'rides', 'type': 'company_role'}
+ metadata={"domain": "rides", "type": "company_role"},
)
diff --git a/backend/apps/rides/events.py b/backend/apps/rides/events.py
index b0bcc805..db9120b5 100644
--- a/backend/apps/rides/events.py
+++ b/backend/apps/rides/events.py
@@ -1,5 +1,3 @@
-
-
def get_ride_display_changes(changes: dict) -> dict:
"""Returns a human-readable version of the ride changes"""
field_names = {
diff --git a/backend/apps/rides/forms.py b/backend/apps/rides/forms.py
index b225a3bb..febeb46a 100644
--- a/backend/apps/rides/forms.py
+++ b/backend/apps/rides/forms.py
@@ -348,9 +348,7 @@ class RideForm(forms.ModelForm):
# editing
if self.instance and self.instance.pk:
if self.instance.manufacturer:
- self.fields["manufacturer_search"].initial = (
- self.instance.manufacturer.name
- )
+ self.fields["manufacturer_search"].initial = self.instance.manufacturer.name
self.fields["manufacturer"].initial = self.instance.manufacturer
if self.instance.designer:
self.fields["designer_search"].initial = self.instance.designer.name
diff --git a/backend/apps/rides/forms/base.py b/backend/apps/rides/forms/base.py
index bb64a378..f09fbdf2 100644
--- a/backend/apps/rides/forms/base.py
+++ b/backend/apps/rides/forms/base.py
@@ -348,9 +348,7 @@ class RideForm(forms.ModelForm):
# editing
if self.instance and self.instance.pk:
if self.instance.manufacturer:
- self.fields["manufacturer_search"].initial = (
- self.instance.manufacturer.name
- )
+ self.fields["manufacturer_search"].initial = self.instance.manufacturer.name
self.fields["manufacturer"].initial = self.instance.manufacturer
if self.instance.designer:
self.fields["designer_search"].initial = self.instance.designer.name
diff --git a/backend/apps/rides/forms/search.py b/backend/apps/rides/forms/search.py
index 18ae69c2..37f10571 100644
--- a/backend/apps/rides/forms/search.py
+++ b/backend/apps/rides/forms/search.py
@@ -105,8 +105,8 @@ class BasicInfoForm(BaseFilterForm):
status_choices = [(choice.value, choice.label) for choice in get_choices("statuses", "rides")]
# Update field choices dynamically
- self.fields['category'].choices = category_choices
- self.fields['status'].choices = status_choices
+ self.fields["category"].choices = category_choices
+ self.fields["status"].choices = status_choices
category = forms.MultipleChoiceField(
choices=[], # Will be populated in __init__
@@ -123,17 +123,13 @@ class BasicInfoForm(BaseFilterForm):
park = forms.ModelMultipleChoiceField(
queryset=Park.objects.all(),
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "max-h-48 overflow-y-auto space-y-1"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "max-h-48 overflow-y-auto space-y-1"}),
)
park_area = forms.ModelMultipleChoiceField(
queryset=ParkArea.objects.all(),
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "max-h-48 overflow-y-auto space-y-1"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "max-h-48 overflow-y-auto space-y-1"}),
)
@@ -266,7 +262,7 @@ class NumberRangeField(forms.MultiValueField):
def validate(self, value):
super().validate(value)
- if value and value.get("min") is not None and value.get("max") is not None:
+ if value and value.get("min") is not None and value.get("max") is not None: # noqa: SIM102
if value["min"] > value["max"]:
raise ValidationError("Minimum value must be less than maximum value.")
@@ -282,17 +278,13 @@ class HeightSafetyForm(BaseFilterForm):
label="Minimum Height (inches)",
)
- max_height_range = NumberRangeField(
- min_val=0, max_val=84, step=1, required=False, label="Maximum Height (inches)"
- )
+ max_height_range = NumberRangeField(min_val=0, max_val=84, step=1, required=False, label="Maximum Height (inches)")
class PerformanceForm(BaseFilterForm):
"""Form for performance metric filters."""
- capacity_range = NumberRangeField(
- min_val=0, max_val=5000, step=50, required=False, label="Capacity per Hour"
- )
+ capacity_range = NumberRangeField(min_val=0, max_val=5000, step=50, required=False, label="Capacity per Hour")
duration_range = NumberRangeField(
min_val=0,
@@ -302,9 +294,7 @@ class PerformanceForm(BaseFilterForm):
label="Duration (seconds)",
)
- rating_range = NumberRangeField(
- min_val=0.0, max_val=10.0, step=0.1, required=False, label="Average Rating"
- )
+ rating_range = NumberRangeField(min_val=0.0, max_val=10.0, step=0.1, required=False, label="Average Rating")
class RelationshipsForm(BaseFilterForm):
@@ -313,25 +303,19 @@ class RelationshipsForm(BaseFilterForm):
manufacturer = forms.ModelMultipleChoiceField(
queryset=Company.objects.filter(roles__contains=["MANUFACTURER"]),
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "max-h-48 overflow-y-auto space-y-1"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "max-h-48 overflow-y-auto space-y-1"}),
)
designer = forms.ModelMultipleChoiceField(
queryset=Company.objects.filter(roles__contains=["DESIGNER"]),
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "max-h-48 overflow-y-auto space-y-1"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "max-h-48 overflow-y-auto space-y-1"}),
)
ride_model = forms.ModelMultipleChoiceField(
queryset=RideModel.objects.all(),
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "max-h-48 overflow-y-auto space-y-1"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "max-h-48 overflow-y-auto space-y-1"}),
)
@@ -347,28 +331,22 @@ class RollerCoasterForm(BaseFilterForm):
# Get choices - let exceptions propagate if registry fails
track_material_choices = [(choice.value, choice.label) for choice in get_choices("track_materials", "rides")]
coaster_type_choices = [(choice.value, choice.label) for choice in get_choices("coaster_types", "rides")]
- propulsion_system_choices = [(choice.value, choice.label) for choice in get_choices("propulsion_systems", "rides")]
+ propulsion_system_choices = [
+ (choice.value, choice.label) for choice in get_choices("propulsion_systems", "rides")
+ ]
# Update field choices dynamically
- self.fields['track_material'].choices = track_material_choices
- self.fields['coaster_type'].choices = coaster_type_choices
- self.fields['propulsion_system'].choices = propulsion_system_choices
+ self.fields["track_material"].choices = track_material_choices
+ self.fields["coaster_type"].choices = coaster_type_choices
+ self.fields["propulsion_system"].choices = propulsion_system_choices
- height_ft_range = NumberRangeField(
- min_val=0, max_val=500, step=1, required=False, label="Height (feet)"
- )
+ height_ft_range = NumberRangeField(min_val=0, max_val=500, step=1, required=False, label="Height (feet)")
- length_ft_range = NumberRangeField(
- min_val=0, max_val=10000, step=10, required=False, label="Length (feet)"
- )
+ length_ft_range = NumberRangeField(min_val=0, max_val=10000, step=10, required=False, label="Length (feet)")
- speed_mph_range = NumberRangeField(
- min_val=0, max_val=150, step=1, required=False, label="Speed (mph)"
- )
+ speed_mph_range = NumberRangeField(min_val=0, max_val=150, step=1, required=False, label="Speed (mph)")
- inversions_range = NumberRangeField(
- min_val=0, max_val=20, step=1, required=False, label="Number of Inversions"
- )
+ inversions_range = NumberRangeField(min_val=0, max_val=20, step=1, required=False, label="Number of Inversions")
track_material = forms.MultipleChoiceField(
choices=[], # Will be populated in __init__
@@ -379,17 +357,13 @@ class RollerCoasterForm(BaseFilterForm):
coaster_type = forms.MultipleChoiceField(
choices=[], # Will be populated in __init__
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto"}),
)
propulsion_system = forms.MultipleChoiceField(
choices=[], # Will be populated in __init__
required=False,
- widget=forms.CheckboxSelectMultiple(
- attrs={"class": "space-y-2 max-h-48 overflow-y-auto"}
- ),
+ widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2 max-h-48 overflow-y-auto"}),
)
@@ -408,8 +382,8 @@ class CompanyForm(BaseFilterForm):
role_choices = rides_roles + parks_roles
# Update field choices dynamically
- self.fields['manufacturer_roles'].choices = role_choices
- self.fields['designer_roles'].choices = role_choices
+ self.fields["manufacturer_roles"].choices = role_choices
+ self.fields["designer_roles"].choices = role_choices
manufacturer_roles = forms.MultipleChoiceField(
choices=[], # Will be populated in __init__
@@ -423,9 +397,7 @@ class CompanyForm(BaseFilterForm):
widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}),
)
- founded_date_range = DateRangeField(
- required=False, label="Company Founded Date Range"
- )
+ founded_date_range = DateRangeField(required=False, label="Company Founded Date Range")
class SortingForm(BaseFilterForm):
@@ -452,7 +424,7 @@ class SortingForm(BaseFilterForm):
("capacity_desc", "Capacity (Highest)"),
]
- self.fields['sort_by'].choices = sort_choices
+ self.fields["sort_by"].choices = sort_choices
sort_by = forms.ChoiceField(
choices=[], # Will be populated in __init__
@@ -578,8 +550,7 @@ class MasterFilterForm(BaseFilterForm):
{
"field": field_name,
"value": value,
- "label": self.fields[field_name].label
- or field_name.replace("_", " ").title(),
+ "label": self.fields[field_name].label or field_name.replace("_", " ").title(),
}
)
diff --git a/backend/apps/rides/management/commands/update_ride_rankings.py b/backend/apps/rides/management/commands/update_ride_rankings.py
index fd1f587b..b25f472a 100644
--- a/backend/apps/rides/management/commands/update_ride_rankings.py
+++ b/backend/apps/rides/management/commands/update_ride_rankings.py
@@ -19,11 +19,7 @@ class Command(BaseCommand):
category = options.get("category")
service = RideRankingService()
- self.stdout.write(
- self.style.SUCCESS(
- f"Starting ride ranking calculation at {timezone.now().isoformat()}"
- )
- )
+ self.stdout.write(self.style.SUCCESS(f"Starting ride ranking calculation at {timezone.now().isoformat()}"))
result = service.update_all_rankings(category=category)
diff --git a/backend/apps/rides/managers.py b/backend/apps/rides/managers.py
index 0b42dee7..9f12d2ab 100644
--- a/backend/apps/rides/managers.py
+++ b/backend/apps/rides/managers.py
@@ -3,7 +3,6 @@ Custom managers and QuerySets for Rides models.
Optimized queries following Django styleguide patterns.
"""
-
from django.db.models import Count, F, Prefetch, Q
from apps.core.managers import (
@@ -35,9 +34,7 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
def family_friendly(self, *, max_height_requirement: int = 42):
"""Filter for family-friendly rides."""
- return self.filter(
- Q(min_height_in__lte=max_height_requirement) | Q(min_height_in__isnull=True)
- )
+ return self.filter(Q(min_height_in__lte=max_height_requirement) | Q(min_height_in__isnull=True))
def by_park(self, *, park_id: int):
"""Filter rides by park."""
@@ -54,8 +51,7 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
def with_capacity_info(self):
"""Add capacity-related annotations."""
return self.annotate(
- estimated_daily_capacity=F("capacity_per_hour")
- * 10, # Assuming 10 operating hours
+ estimated_daily_capacity=F("capacity_per_hour") * 10, # Assuming 10 operating hours
duration_minutes=F("ride_duration_seconds") / 60.0,
)
@@ -65,9 +61,7 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
def optimized_for_list(self):
"""Optimize for ride list display."""
- return self.select_related(
- "park", "park_area", "manufacturer", "designer", "ride_model"
- ).with_review_stats()
+ return self.select_related("park", "park_area", "manufacturer", "designer", "ride_model").with_review_stats()
def optimized_for_detail(self):
"""Optimize for ride detail display."""
@@ -94,9 +88,7 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
def with_coaster_stats(self):
"""Always prefetch coaster_stats for roller coaster queries."""
- return self.select_related(
- "park", "manufacturer", "ride_model"
- ).prefetch_related("coaster_stats")
+ return self.select_related("park", "manufacturer", "ride_model").prefetch_related("coaster_stats")
def for_map_display(self):
"""Optimize for map display."""
@@ -129,14 +121,12 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
if min_height:
queryset = queryset.filter(
- Q(rollercoaster_stats__height_ft__gte=min_height)
- | Q(min_height_in__gte=min_height)
+ Q(rollercoaster_stats__height_ft__gte=min_height) | Q(min_height_in__gte=min_height)
)
if max_height:
queryset = queryset.filter(
- Q(rollercoaster_stats__height_ft__lte=max_height)
- | Q(max_height_in__lte=max_height)
+ Q(rollercoaster_stats__height_ft__lte=max_height) | Q(max_height_in__lte=max_height)
)
if min_speed:
@@ -146,10 +136,7 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
if inversions:
queryset = queryset.filter(rollercoaster_stats__inversions__gt=0)
else:
- queryset = queryset.filter(
- Q(rollercoaster_stats__inversions=0)
- | Q(rollercoaster_stats__isnull=True)
- )
+ queryset = queryset.filter(Q(rollercoaster_stats__inversions=0) | Q(rollercoaster_stats__isnull=True))
return queryset
@@ -167,9 +154,7 @@ class RideManager(StatusManager, ReviewableManager):
return self.get_queryset().thrill_rides()
def family_friendly(self, *, max_height_requirement: int = 42):
- return self.get_queryset().family_friendly(
- max_height_requirement=max_height_requirement
- )
+ return self.get_queryset().family_friendly(max_height_requirement=max_height_requirement)
def by_park(self, *, park_id: int):
return self.get_queryset().by_park(park_id=park_id)
@@ -203,9 +188,7 @@ class RideModelQuerySet(BaseQuerySet):
"""Add count of rides using this model."""
return self.annotate(
ride_count=Count("rides", distinct=True),
- operating_rides_count=Count(
- "rides", filter=Q(rides__status="OPERATING"), distinct=True
- ),
+ operating_rides_count=Count("rides", filter=Q(rides__status="OPERATING"), distinct=True),
)
def popular_models(self, *, min_installations: int = 5):
@@ -260,9 +243,7 @@ class RideReviewManager(BaseManager):
return self.get_queryset().for_ride(ride_id=ride_id)
def by_rating_range(self, *, min_rating: int = 1, max_rating: int = 10):
- return self.get_queryset().by_rating_range(
- min_rating=min_rating, max_rating=max_rating
- )
+ return self.get_queryset().by_rating_range(min_rating=min_rating, max_rating=max_rating)
class RollerCoasterStatsQuerySet(BaseQuerySet):
diff --git a/backend/apps/rides/migrations/0001_initial.py b/backend/apps/rides/migrations/0001_initial.py
index 95eb46ed..17c09df0 100644
--- a/backend/apps/rides/migrations/0001_initial.py
+++ b/backend/apps/rides/migrations/0001_initial.py
@@ -190,9 +190,7 @@ class Migration(migrations.Migration):
),
(
"average_rating",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=3, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
),
],
options={
@@ -374,21 +372,15 @@ class Migration(migrations.Migration):
),
(
"height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"length_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=7, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
),
(
"speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
("inversions", models.PositiveIntegerField(default=0)),
(
@@ -432,9 +424,7 @@ class Migration(migrations.Migration):
),
(
"max_drop_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"launch_type",
@@ -692,9 +682,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="ridelocation",
- index=models.Index(
- fields=["park_area"], name="rides_ridel_park_ar_26c90c_idx"
- ),
+ index=models.Index(fields=["park_area"], name="rides_ridel_park_ar_26c90c_idx"),
),
migrations.AlterUniqueTogether(
name="ridemodel",
diff --git a/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py b/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py
index 2a519cbe..ea955b33 100644
--- a/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py
+++ b/backend/apps/rides/migrations/0004_rideevent_ridemodelevent_rollercoasterstatsevent_and_more.py
@@ -89,9 +89,7 @@ class Migration(migrations.Migration):
),
(
"average_rating",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=3, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True),
),
],
options={
@@ -140,21 +138,15 @@ class Migration(migrations.Migration):
("id", models.BigIntegerField()),
(
"height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"length_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=7, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True),
),
(
"speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
("inversions", models.PositiveIntegerField(default=0)),
(
@@ -198,9 +190,7 @@ class Migration(migrations.Migration):
),
(
"max_drop_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"launch_type",
diff --git a/backend/apps/rides/migrations/0006_add_ride_rankings.py b/backend/apps/rides/migrations/0006_add_ride_rankings.py
index 37cdacbc..c724f495 100644
--- a/backend/apps/rides/migrations/0006_add_ride_rankings.py
+++ b/backend/apps/rides/migrations/0006_add_ride_rankings.py
@@ -220,9 +220,7 @@ class Migration(migrations.Migration):
),
(
"rank",
- models.PositiveIntegerField(
- db_index=True, help_text="Overall rank position (1 = best)"
- ),
+ models.PositiveIntegerField(db_index=True, help_text="Overall rank position (1 = best)"),
),
(
"wins",
@@ -323,9 +321,7 @@ class Migration(migrations.Migration):
("id", models.BigIntegerField()),
(
"rank",
- models.PositiveIntegerField(
- help_text="Overall rank position (1 = best)"
- ),
+ models.PositiveIntegerField(help_text="Overall rank position (1 = best)"),
),
(
"wins",
@@ -487,15 +483,11 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="ridepaircomparison",
- index=models.Index(
- fields=["ride_a", "ride_b"], name="rides_ridep_ride_a__eb0674_idx"
- ),
+ index=models.Index(fields=["ride_a", "ride_b"], name="rides_ridep_ride_a__eb0674_idx"),
),
migrations.AddIndex(
model_name="ridepaircomparison",
- index=models.Index(
- fields=["last_calculated"], name="rides_ridep_last_ca_bd9f6c_idx"
- ),
+ index=models.Index(fields=["last_calculated"], name="rides_ridep_last_ca_bd9f6c_idx"),
),
migrations.AlterUniqueTogether(
name="ridepaircomparison",
@@ -551,9 +543,7 @@ class Migration(migrations.Migration):
migrations.AddConstraint(
model_name="rideranking",
constraint=models.CheckConstraint(
- condition=models.Q(
- ("winning_percentage__gte", 0), ("winning_percentage__lte", 1)
- ),
+ condition=models.Q(("winning_percentage__gte", 0), ("winning_percentage__lte", 1)),
name="rideranking_winning_percentage_range",
violation_error_message="Winning percentage must be between 0 and 1",
),
diff --git a/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py b/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py
index ba68d080..37f4c595 100644
--- a/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py
+++ b/backend/apps/rides/migrations/0007_ridephoto_ridephotoevent_and_more.py
@@ -163,27 +163,19 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="ridephoto",
- index=models.Index(
- fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx"
- ),
+ index=models.Index(fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx"),
),
migrations.AddIndex(
model_name="ridephoto",
- index=models.Index(
- fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx"
- ),
+ index=models.Index(fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx"),
),
migrations.AddIndex(
model_name="ridephoto",
- index=models.Index(
- fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx"
- ),
+ index=models.Index(fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx"),
),
migrations.AddIndex(
model_name="ridephoto",
- index=models.Index(
- fields=["created_at"], name="rides_ridep_created_106e02_idx"
- ),
+ index=models.Index(fields=["created_at"], name="rides_ridep_created_106e02_idx"),
),
migrations.AddConstraint(
model_name="ridephoto",
diff --git a/backend/apps/rides/migrations/0010_add_comprehensive_ride_model_system.py b/backend/apps/rides/migrations/0010_add_comprehensive_ride_model_system.py
index 49b7fb7c..08e09c38 100644
--- a/backend/apps/rides/migrations/0010_add_comprehensive_ride_model_system.py
+++ b/backend/apps/rides/migrations/0010_add_comprehensive_ride_model_system.py
@@ -147,21 +147,15 @@ class Migration(migrations.Migration):
),
(
"spec_name",
- models.CharField(
- help_text="Name of the specification", max_length=100
- ),
+ models.CharField(help_text="Name of the specification", max_length=100),
),
(
"spec_value",
- models.CharField(
- help_text="Value of the specification", max_length=255
- ),
+ models.CharField(help_text="Value of the specification", max_length=255),
),
(
"spec_unit",
- models.CharField(
- blank=True, help_text="Unit of measurement", max_length=20
- ),
+ models.CharField(blank=True, help_text="Unit of measurement", max_length=20),
),
(
"notes",
@@ -203,21 +197,15 @@ class Migration(migrations.Migration):
),
(
"spec_name",
- models.CharField(
- help_text="Name of the specification", max_length=100
- ),
+ models.CharField(help_text="Name of the specification", max_length=100),
),
(
"spec_value",
- models.CharField(
- help_text="Value of the specification", max_length=255
- ),
+ models.CharField(help_text="Value of the specification", max_length=255),
),
(
"spec_unit",
- models.CharField(
- blank=True, help_text="Unit of measurement", max_length=20
- ),
+ models.CharField(blank=True, help_text="Unit of measurement", max_length=20),
),
(
"notes",
@@ -251,33 +239,23 @@ class Migration(migrations.Migration):
),
(
"description",
- models.TextField(
- blank=True, help_text="Description of variant differences"
- ),
+ models.TextField(blank=True, help_text="Description of variant differences"),
),
(
"min_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"max_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"min_speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
(
"max_speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
(
"distinguishing_features",
@@ -307,33 +285,23 @@ class Migration(migrations.Migration):
),
(
"description",
- models.TextField(
- blank=True, help_text="Description of variant differences"
- ),
+ models.TextField(blank=True, help_text="Description of variant differences"),
),
(
"min_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"max_height_ft",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=6, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True),
),
(
"min_speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
(
"max_speed_mph",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
+ models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True),
),
(
"distinguishing_features",
@@ -750,9 +718,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodel",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed description of the ride model"
- ),
+ field=models.TextField(blank=True, help_text="Detailed description of the ride model"),
),
migrations.AlterField(
model_name="ridemodel",
@@ -794,9 +760,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelevent",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed description of the ride model"
- ),
+ field=models.TextField(blank=True, help_text="Detailed description of the ride model"),
),
migrations.AlterField(
model_name="ridemodelevent",
diff --git a/backend/apps/rides/migrations/0012_make_ride_model_slug_unique.py b/backend/apps/rides/migrations/0012_make_ride_model_slug_unique.py
index ec916c59..123dc3d1 100644
--- a/backend/apps/rides/migrations/0012_make_ride_model_slug_unique.py
+++ b/backend/apps/rides/migrations/0012_make_ride_model_slug_unique.py
@@ -13,8 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodel",
name="slug",
- field=models.SlugField(
- help_text="URL-friendly identifier", max_length=255, unique=True
- ),
+ field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
),
]
diff --git a/backend/apps/rides/migrations/0014_update_ride_model_slugs_data.py b/backend/apps/rides/migrations/0014_update_ride_model_slugs_data.py
index 12d1d39e..49eef36d 100644
--- a/backend/apps/rides/migrations/0014_update_ride_model_slugs_data.py
+++ b/backend/apps/rides/migrations/0014_update_ride_model_slugs_data.py
@@ -16,9 +16,7 @@ def update_ride_model_slugs(apps, schema_editor):
counter = 1
base_slug = new_slug
while (
- RideModel.objects.filter(
- manufacturer=ride_model.manufacturer, slug=new_slug
- )
+ RideModel.objects.filter(manufacturer=ride_model.manufacturer, slug=new_slug)
.exclude(pk=ride_model.pk)
.exists()
):
@@ -37,16 +35,12 @@ def reverse_ride_model_slugs(apps, schema_editor):
for ride_model in RideModel.objects.all():
# Generate old-style slug with manufacturer + name
- old_slug = slugify(
- f"{ride_model.manufacturer.name if ride_model.manufacturer else ''} {ride_model.name}"
- )
+ old_slug = slugify(f"{ride_model.manufacturer.name if ride_model.manufacturer else ''} {ride_model.name}")
# Ensure uniqueness globally (old way)
counter = 1
base_slug = old_slug
- while (
- RideModel.objects.filter(slug=old_slug).exclude(pk=ride_model.pk).exists()
- ):
+ while RideModel.objects.filter(slug=old_slug).exclude(pk=ride_model.pk).exists():
old_slug = f"{base_slug}-{counter}"
counter += 1
diff --git a/backend/apps/rides/migrations/0015_remove_company_insert_insert_and_more.py b/backend/apps/rides/migrations/0015_remove_company_insert_insert_and_more.py
index b947f8ed..3edea3d7 100644
--- a/backend/apps/rides/migrations/0015_remove_company_insert_insert_and_more.py
+++ b/backend/apps/rides/migrations/0015_remove_company_insert_insert_and_more.py
@@ -39,16 +39,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="company",
name="url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this company"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this company"),
),
migrations.AddField(
model_name="companyevent",
name="url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this company"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this company"),
),
migrations.AddField(
model_name="ride",
@@ -63,16 +59,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="ridemodel",
name="url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this ride model"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this ride model"),
),
migrations.AddField(
model_name="ridemodelevent",
name="url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this ride model"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this ride model"),
),
pgtrigger.migrations.AddTrigger(
model_name="company",
diff --git a/backend/apps/rides/migrations/0016_remove_ride_insert_insert_remove_ride_update_update_and_more.py b/backend/apps/rides/migrations/0016_remove_ride_insert_insert_remove_ride_update_update_and_more.py
index 54aa5f2d..34a9a4cf 100644
--- a/backend/apps/rides/migrations/0016_remove_ride_insert_insert_remove_ride_update_update_and_more.py
+++ b/backend/apps/rides/migrations/0016_remove_ride_insert_insert_remove_ride_update_update_and_more.py
@@ -23,16 +23,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="ride",
name="park_url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this ride's park"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this ride's park"),
),
migrations.AddField(
model_name="rideevent",
name="park_url",
- field=models.URLField(
- blank=True, help_text="Frontend URL for this ride's park"
- ),
+ field=models.URLField(blank=True, help_text="Frontend URL for this ride's park"),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
diff --git a/backend/apps/rides/migrations/0019_populate_hybrid_filtering_fields.py b/backend/apps/rides/migrations/0019_populate_hybrid_filtering_fields.py
index afb3cf09..dfb2d311 100644
--- a/backend/apps/rides/migrations/0019_populate_hybrid_filtering_fields.py
+++ b/backend/apps/rides/migrations/0019_populate_hybrid_filtering_fields.py
@@ -12,13 +12,15 @@ from django.db import migrations
def populate_computed_fields(apps, schema_editor):
"""Populate computed fields for all existing rides."""
- Ride = apps.get_model('rides', 'Ride')
+ Ride = apps.get_model("rides", "Ride")
# Disable pghistory triggers during bulk operations to avoid performance issues
with pghistory.context(disable=True):
- rides = list(Ride.objects.all().select_related(
- 'park', 'park__location', 'park_area', 'manufacturer', 'designer', 'ride_model'
- ))
+ rides = list(
+ Ride.objects.all().select_related(
+ "park", "park__location", "park_area", "manufacturer", "designer", "ride_model"
+ )
+ )
for ride in rides:
# Extract opening year from opening_date
@@ -39,7 +41,7 @@ def populate_computed_fields(apps, schema_editor):
# Park info
if ride.park:
search_parts.append(ride.park.name)
- if hasattr(ride.park, 'location') and ride.park.location:
+ if hasattr(ride.park, "location") and ride.park.location:
if ride.park.location.city:
search_parts.append(ride.park.location.city)
if ride.park.location.state:
@@ -62,7 +64,7 @@ def populate_computed_fields(apps, schema_editor):
("TR", "Transport"),
("OT", "Other"),
]
- category_display = dict(category_choices).get(ride.category, '')
+ category_display = dict(category_choices).get(ride.category, "")
if category_display:
search_parts.append(category_display)
@@ -79,7 +81,7 @@ def populate_computed_fields(apps, schema_editor):
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
]
- status_display = dict(status_choices).get(ride.status, '')
+ status_display = dict(status_choices).get(ride.status, "")
if status_display:
search_parts.append(status_display)
@@ -95,24 +97,24 @@ def populate_computed_fields(apps, schema_editor):
if ride.ride_model.manufacturer:
search_parts.append(ride.ride_model.manufacturer.name)
- ride.search_text = ' '.join(filter(None, search_parts)).lower()
+ ride.search_text = " ".join(filter(None, search_parts)).lower()
# Bulk update all rides
- Ride.objects.bulk_update(rides, ['opening_year', 'search_text'], batch_size=1000)
+ Ride.objects.bulk_update(rides, ["opening_year", "search_text"], batch_size=1000)
def reverse_populate_computed_fields(apps, schema_editor):
"""Clear computed fields (reverse operation)."""
- Ride = apps.get_model('rides', 'Ride')
+ Ride = apps.get_model("rides", "Ride")
# Disable pghistory triggers during bulk operations
with pghistory.context(disable=True):
- Ride.objects.all().update(opening_year=None, search_text='')
+ Ride.objects.all().update(opening_year=None, search_text="")
class Migration(migrations.Migration):
dependencies = [
- ('rides', '0018_add_hybrid_filtering_fields'),
+ ("rides", "0018_add_hybrid_filtering_fields"),
]
operations = [
diff --git a/backend/apps/rides/migrations/0020_add_hybrid_filtering_indexes.py b/backend/apps/rides/migrations/0020_add_hybrid_filtering_indexes.py
index d64ef750..d60d397b 100644
--- a/backend/apps/rides/migrations/0020_add_hybrid_filtering_indexes.py
+++ b/backend/apps/rides/migrations/0020_add_hybrid_filtering_indexes.py
@@ -19,163 +19,136 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('rides', '0019_populate_hybrid_filtering_fields'),
+ ("rides", "0019_populate_hybrid_filtering_fields"),
]
operations = [
# Composite index for park + category filtering (very common)
migrations.RunSQL(
"CREATE INDEX rides_ride_park_category_idx ON rides_ride (park_id, category) WHERE category != '';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_park_category_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_park_category_idx;",
),
-
# Composite index for park + status filtering (common)
migrations.RunSQL(
"CREATE INDEX rides_ride_park_status_idx ON rides_ride (park_id, status);",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_park_status_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_park_status_idx;",
),
-
# Composite index for category + status filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_category_status_idx ON rides_ride (category, status) WHERE category != '';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_category_status_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_category_status_idx;",
),
-
# Composite index for manufacturer + category
migrations.RunSQL(
"CREATE INDEX rides_ride_manufacturer_category_idx ON rides_ride (manufacturer_id, category) WHERE manufacturer_id IS NOT NULL AND category != '';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_manufacturer_category_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_manufacturer_category_idx;",
),
-
# Composite index for opening year + category (for timeline filtering)
migrations.RunSQL(
"CREATE INDEX rides_ride_opening_year_category_idx ON rides_ride (opening_year, category) WHERE opening_year IS NOT NULL AND category != '';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_opening_year_category_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_opening_year_category_idx;",
),
-
# Partial index for operating rides only (most common filter)
migrations.RunSQL(
"CREATE INDEX rides_ride_operating_only_idx ON rides_ride (park_id, category, opening_year) WHERE status = 'OPERATING';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_operating_only_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_operating_only_idx;",
),
-
# Partial index for roller coasters only (popular category)
migrations.RunSQL(
"CREATE INDEX rides_ride_roller_coasters_idx ON rides_ride (park_id, status, opening_year) WHERE category = 'RC';",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_roller_coasters_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_roller_coasters_idx;",
),
-
# Covering index for list views (includes commonly displayed fields)
migrations.RunSQL(
"CREATE INDEX rides_ride_list_covering_idx ON rides_ride (park_id, category, status) INCLUDE (name, opening_date, average_rating);",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_list_covering_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_list_covering_idx;",
),
-
# GIN index for full-text search on computed search_text field
migrations.RunSQL(
"CREATE INDEX rides_ride_search_text_gin_idx ON rides_ride USING gin(to_tsvector('english', search_text));",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_gin_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_gin_idx;",
),
-
# Trigram index for fuzzy text search
migrations.RunSQL(
"CREATE INDEX rides_ride_search_text_trgm_idx ON rides_ride USING gin(search_text gin_trgm_ops);",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_trgm_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_search_text_trgm_idx;",
),
-
# Index for rating-based filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_rating_idx ON rides_ride (average_rating) WHERE average_rating IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_rating_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_rating_idx;",
),
-
# Index for capacity-based filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_capacity_idx ON rides_ride (capacity_per_hour) WHERE capacity_per_hour IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_capacity_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_capacity_idx;",
),
-
# Index for height requirement filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_height_req_idx ON rides_ride (min_height_in, max_height_in) WHERE min_height_in IS NOT NULL OR max_height_in IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_height_req_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_height_req_idx;",
),
-
# Composite index for ride model filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_model_manufacturer_idx ON rides_ride (ride_model_id, manufacturer_id) WHERE ride_model_id IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_model_manufacturer_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_model_manufacturer_idx;",
),
-
# Index for designer filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_designer_idx ON rides_ride (designer_id, category) WHERE designer_id IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_designer_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_designer_idx;",
),
-
# Index for park area filtering
migrations.RunSQL(
"CREATE INDEX rides_ride_park_area_idx ON rides_ride (park_area_id, status) WHERE park_area_id IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ride_park_area_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ride_park_area_idx;",
),
-
# Roller coaster stats indexes for performance
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_height_idx ON rides_rollercoasterstats (height_ft) WHERE height_ft IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_height_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_height_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_speed_idx ON rides_rollercoasterstats (speed_mph) WHERE speed_mph IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_speed_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_speed_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_inversions_idx ON rides_rollercoasterstats (inversions);",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_inversions_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_inversions_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_type_material_idx ON rides_rollercoasterstats (roller_coaster_type, track_material);",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_type_material_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_type_material_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_launch_type_idx ON rides_rollercoasterstats (launch_type);",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_launch_type_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_launch_type_idx;",
),
-
# Composite index for complex roller coaster filtering
migrations.RunSQL(
"CREATE INDEX rides_rollercoasterstats_complex_idx ON rides_rollercoasterstats (roller_coaster_type, track_material, launch_type) INCLUDE (height_ft, speed_mph, inversions);",
- reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_complex_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_rollercoasterstats_complex_idx;",
),
-
# Index for ride model filtering and search
migrations.RunSQL(
"CREATE INDEX rides_ridemodel_manufacturer_category_idx ON rides_ridemodel (manufacturer_id, category) WHERE manufacturer_id IS NOT NULL;",
- reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_manufacturer_category_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_manufacturer_category_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_ridemodel_name_trgm_idx ON rides_ridemodel USING gin(name gin_trgm_ops);",
- reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_name_trgm_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_ridemodel_name_trgm_idx;",
),
-
# Index for company role-based filtering
migrations.RunSQL(
"CREATE INDEX rides_company_manufacturer_role_idx ON rides_company USING gin(roles) WHERE 'MANUFACTURER' = ANY(roles);",
- reverse_sql="DROP INDEX IF EXISTS rides_company_manufacturer_role_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_company_manufacturer_role_idx;",
),
-
migrations.RunSQL(
"CREATE INDEX rides_company_designer_role_idx ON rides_company USING gin(roles) WHERE 'DESIGNER' = ANY(roles);",
- reverse_sql="DROP INDEX IF EXISTS rides_company_designer_role_idx;"
+ reverse_sql="DROP INDEX IF EXISTS rides_company_designer_role_idx;",
),
-
# Ensure trigram extension is available for fuzzy search
migrations.RunSQL(
- "CREATE EXTENSION IF NOT EXISTS pg_trgm;",
- reverse_sql="-- Cannot safely drop pg_trgm extension"
+ "CREATE EXTENSION IF NOT EXISTS pg_trgm;", reverse_sql="-- Cannot safely drop pg_trgm extension"
),
]
diff --git a/backend/apps/rides/migrations/0026_convert_unique_together_to_constraints.py b/backend/apps/rides/migrations/0026_convert_unique_together_to_constraints.py
index 2e6d094e..a3a1eeef 100644
--- a/backend/apps/rides/migrations/0026_convert_unique_together_to_constraints.py
+++ b/backend/apps/rides/migrations/0026_convert_unique_together_to_constraints.py
@@ -11,30 +11,30 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('rides', '0025_convert_ride_status_to_fsm'),
+ ("rides", "0025_convert_ride_status_to_fsm"),
]
operations = [
# Remove the old unique_together constraint
migrations.AlterUniqueTogether(
- name='ridemodel',
+ name="ridemodel",
unique_together=set(),
),
# Add new UniqueConstraints with better error messages
migrations.AddConstraint(
- model_name='ridemodel',
+ model_name="ridemodel",
constraint=models.UniqueConstraint(
- fields=['manufacturer', 'name'],
- name='ridemodel_manufacturer_name_unique',
- violation_error_message='A ride model with this name already exists for this manufacturer'
+ fields=["manufacturer", "name"],
+ name="ridemodel_manufacturer_name_unique",
+ violation_error_message="A ride model with this name already exists for this manufacturer",
),
),
migrations.AddConstraint(
- model_name='ridemodel',
+ model_name="ridemodel",
constraint=models.UniqueConstraint(
- fields=['manufacturer', 'slug'],
- name='ridemodel_manufacturer_slug_unique',
- violation_error_message='A ride model with this slug already exists for this manufacturer'
+ fields=["manufacturer", "slug"],
+ name="ridemodel_manufacturer_slug_unique",
+ violation_error_message="A ride model with this slug already exists for this manufacturer",
),
),
]
diff --git a/backend/apps/rides/migrations/0027_alter_company_options_alter_rankingsnapshot_options_and_more.py b/backend/apps/rides/migrations/0027_alter_company_options_alter_rankingsnapshot_options_and_more.py
index 759a2659..606564e0 100644
--- a/backend/apps/rides/migrations/0027_alter_company_options_alter_rankingsnapshot_options_and_more.py
+++ b/backend/apps/rides/migrations/0027_alter_company_options_alter_rankingsnapshot_options_and_more.py
@@ -98,23 +98,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="coasters_count",
- field=models.IntegerField(
- default=0, help_text="Number of coasters manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of coasters manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="company",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed company description"
- ),
+ field=models.TextField(blank=True, help_text="Detailed company description"),
),
migrations.AlterField(
model_name="company",
name="founded_date",
- field=models.DateField(
- blank=True, help_text="Date the company was founded", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date the company was founded", null=True),
),
migrations.AlterField(
model_name="company",
@@ -124,9 +118,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="rides_count",
- field=models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="company",
@@ -151,9 +143,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="company",
name="slug",
- field=models.SlugField(
- help_text="URL-friendly identifier", max_length=255, unique=True
- ),
+ field=models.SlugField(help_text="URL-friendly identifier", max_length=255, unique=True),
),
migrations.AlterField(
model_name="company",
@@ -163,23 +153,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="coasters_count",
- field=models.IntegerField(
- default=0, help_text="Number of coasters manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of coasters manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="companyevent",
name="description",
- field=models.TextField(
- blank=True, help_text="Detailed company description"
- ),
+ field=models.TextField(blank=True, help_text="Detailed company description"),
),
migrations.AlterField(
model_name="companyevent",
name="founded_date",
- field=models.DateField(
- blank=True, help_text="Date the company was founded", null=True
- ),
+ field=models.DateField(blank=True, help_text="Date the company was founded", null=True),
),
migrations.AlterField(
model_name="companyevent",
@@ -210,9 +194,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="rides_count",
- field=models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- ),
+ field=models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)"),
),
migrations.AlterField(
model_name="companyevent",
@@ -237,9 +219,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="companyevent",
name="slug",
- field=models.SlugField(
- db_index=False, help_text="URL-friendly identifier", max_length=255
- ),
+ field=models.SlugField(db_index=False, help_text="URL-friendly identifier", max_length=255),
),
migrations.AlterField(
model_name="companyevent",
@@ -321,23 +301,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelphoto",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption or description", max_length=500
- ),
+ field=models.CharField(blank=True, help_text="Photo caption or description", max_length=500),
),
migrations.AlterField(
model_name="ridemodelphoto",
name="copyright_info",
- field=models.CharField(
- blank=True, help_text="Copyright information", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Copyright information", max_length=255),
),
migrations.AlterField(
model_name="ridemodelphoto",
name="photographer",
- field=models.CharField(
- blank=True, help_text="Name of the photographer", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Name of the photographer", max_length=255),
),
migrations.AlterField(
model_name="ridemodelphoto",
@@ -352,9 +326,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelphoto",
name="source",
- field=models.CharField(
- blank=True, help_text="Source of the photo", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Source of the photo", max_length=255),
),
migrations.AlterField(
model_name="ridemodelphotoevent",
@@ -368,16 +340,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelphotoevent",
name="caption",
- field=models.CharField(
- blank=True, help_text="Photo caption or description", max_length=500
- ),
+ field=models.CharField(blank=True, help_text="Photo caption or description", max_length=500),
),
migrations.AlterField(
model_name="ridemodelphotoevent",
name="copyright_info",
- field=models.CharField(
- blank=True, help_text="Copyright information", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Copyright information", max_length=255),
),
migrations.AlterField(
model_name="ridemodelphotoevent",
@@ -403,9 +371,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelphotoevent",
name="photographer",
- field=models.CharField(
- blank=True, help_text="Name of the photographer", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Name of the photographer", max_length=255),
),
migrations.AlterField(
model_name="ridemodelphotoevent",
@@ -422,9 +388,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="ridemodelphotoevent",
name="source",
- field=models.CharField(
- blank=True, help_text="Source of the photo", max_length=255
- ),
+ field=models.CharField(blank=True, help_text="Source of the photo", max_length=255),
),
migrations.AlterField(
model_name="ridemodeltechnicalspec",
@@ -709,9 +673,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstats",
name="cars_per_train",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of cars per train", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of cars per train", null=True),
),
migrations.AlterField(
model_name="rollercoasterstats",
@@ -727,9 +689,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstats",
name="inversions",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of inversions"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of inversions"),
),
migrations.AlterField(
model_name="rollercoasterstats",
@@ -766,16 +726,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstats",
name="ride_time_seconds",
- field=models.PositiveIntegerField(
- blank=True, help_text="Duration of the ride in seconds", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Duration of the ride in seconds", null=True),
),
migrations.AlterField(
model_name="rollercoasterstats",
name="seats_per_car",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of seats per car", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of seats per car", null=True),
),
migrations.AlterField(
model_name="rollercoasterstats",
@@ -809,16 +765,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstats",
name="trains_count",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of trains", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of trains", null=True),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="cars_per_train",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of cars per train", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of cars per train", null=True),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
@@ -834,9 +786,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="inversions",
- field=models.PositiveIntegerField(
- default=0, help_text="Number of inversions"
- ),
+ field=models.PositiveIntegerField(default=0, help_text="Number of inversions"),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
@@ -896,16 +846,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="ride_time_seconds",
- field=models.PositiveIntegerField(
- blank=True, help_text="Duration of the ride in seconds", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Duration of the ride in seconds", null=True),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="seats_per_car",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of seats per car", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of seats per car", null=True),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
@@ -939,8 +885,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="trains_count",
- field=models.PositiveIntegerField(
- blank=True, help_text="Number of trains", null=True
- ),
+ field=models.PositiveIntegerField(blank=True, help_text="Number of trains", null=True),
),
]
diff --git a/backend/apps/rides/mixins.py b/backend/apps/rides/mixins.py
index cf0901b5..a13b08cf 100644
--- a/backend/apps/rides/mixins.py
+++ b/backend/apps/rides/mixins.py
@@ -34,16 +34,11 @@ class RideFormMixin:
Returns:
Dictionary with submission results from RideService
"""
- result = RideService.handle_new_entity_suggestions(
- form_data=form.cleaned_data,
- submitter=self.request.user
- )
+ result = RideService.handle_new_entity_suggestions(form_data=form.cleaned_data, submitter=self.request.user)
- if result['total_submissions'] > 0:
+ if result["total_submissions"] > 0:
messages.info(
- self.request,
- f"Created {result['total_submissions']} moderation submission(s) "
- "for new entities"
+ self.request, f"Created {result['total_submissions']} moderation submission(s) " "for new entities"
)
return result
diff --git a/backend/apps/rides/models/company.py b/backend/apps/rides/models/company.py
index 26f6b153..5d29094c 100644
--- a/backend/apps/rides/models/company.py
+++ b/backend/apps/rides/models/company.py
@@ -24,17 +24,11 @@ class Company(TrackedModel):
website = models.URLField(blank=True, help_text="Company website URL")
# General company info
- founded_date = models.DateField(
- null=True, blank=True, help_text="Date the company was founded"
- )
+ founded_date = models.DateField(null=True, blank=True, help_text="Date the company was founded")
# Manufacturer-specific fields
- rides_count = models.IntegerField(
- default=0, help_text="Number of rides manufactured (auto-calculated)"
- )
- coasters_count = models.IntegerField(
- default=0, help_text="Number of coasters manufactured (auto-calculated)"
- )
+ rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)")
+ coasters_count = models.IntegerField(default=0, help_text="Number of coasters manufactured (auto-calculated)")
# Frontend URL
url = models.URLField(blank=True, help_text="Frontend URL for this company")
@@ -50,9 +44,7 @@ class Company(TrackedModel):
# CRITICAL: Only MANUFACTURER and DESIGNER are for rides domain
# OPERATOR and PROPERTY_OWNER are for parks domain and handled separately
if self.roles:
- frontend_domain = getattr(
- settings, "FRONTEND_DOMAIN", "https://thrillwiki.com"
- )
+ frontend_domain = getattr(settings, "FRONTEND_DOMAIN", "https://thrillwiki.com")
primary_role = self.roles[0] # Use first role as primary
if primary_role == "MANUFACTURER":
@@ -76,12 +68,9 @@ class Company(TrackedModel):
# Check pghistory first
try:
from django.apps import apps
- history_model = apps.get_model('rides', f'{cls.__name__}Event')
- history_entry = (
- history_model.objects.filter(slug=slug)
- .order_by("-pgh_created_at")
- .first()
- )
+
+ history_model = apps.get_model("rides", f"{cls.__name__}Event")
+ history_entry = history_model.objects.filter(slug=slug).order_by("-pgh_created_at").first()
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
except LookupError:
@@ -90,12 +79,10 @@ class Company(TrackedModel):
# Check manual slug history as fallback
try:
- historical = HistoricalSlug.objects.get(
- content_type__model="company", slug=slug
- )
+ historical = HistoricalSlug.objects.get(content_type__model="company", slug=slug)
return cls.objects.get(pk=historical.object_id), True
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
- raise cls.DoesNotExist("No company found with this slug")
+ raise cls.DoesNotExist("No company found with this slug") from None
class Meta(TrackedModel.Meta):
app_label = "rides"
diff --git a/backend/apps/rides/models/credits.py b/backend/apps/rides/models/credits.py
index b3feb634..74efb723 100644
--- a/backend/apps/rides/models/credits.py
+++ b/backend/apps/rides/models/credits.py
@@ -27,27 +27,17 @@ class RideCredit(TrackedModel):
)
# Credit Details
- count = models.PositiveIntegerField(
- default=1, help_text="Number of times ridden"
- )
+ count = models.PositiveIntegerField(default=1, help_text="Number of times ridden")
rating = models.IntegerField(
null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)],
help_text="Personal rating (1-5)",
)
- first_ridden_at = models.DateField(
- null=True, blank=True, help_text="Date of first ride"
- )
- last_ridden_at = models.DateField(
- null=True, blank=True, help_text="Date of most recent ride"
- )
- notes = models.TextField(
- blank=True, help_text="Personal notes about the experience"
- )
- display_order = models.PositiveIntegerField(
- default=0, help_text="User-defined display order for drag-drop sorting"
- )
+ first_ridden_at = models.DateField(null=True, blank=True, help_text="Date of first ride")
+ last_ridden_at = models.DateField(null=True, blank=True, help_text="Date of most recent ride")
+ notes = models.TextField(blank=True, help_text="Personal notes about the experience")
+ display_order = models.PositiveIntegerField(default=0, help_text="User-defined display order for drag-drop sorting")
class Meta(TrackedModel.Meta):
verbose_name = "Ride Credit"
diff --git a/backend/apps/rides/models/location.py b/backend/apps/rides/models/location.py
index 19b60d0a..72d0bbab 100644
--- a/backend/apps/rides/models/location.py
+++ b/backend/apps/rides/models/location.py
@@ -12,9 +12,7 @@ class RideLocation(models.Model):
"""
# Relationships
- ride = models.OneToOneField(
- "rides.Ride", on_delete=models.CASCADE, related_name="ride_location"
- )
+ ride = models.OneToOneField("rides.Ride", on_delete=models.CASCADE, related_name="ride_location")
# Optional Spatial Data - keep it simple with single point
point = gis_models.PointField(
@@ -29,9 +27,7 @@ class RideLocation(models.Model):
max_length=100,
blank=True,
db_index=True,
- help_text=(
- "Themed area or land within the park (e.g., 'Frontierland', 'Tomorrowland')"
- ),
+ help_text=("Themed area or land within the park (e.g., 'Frontierland', 'Tomorrowland')"),
)
# General notes field to match database schema
diff --git a/backend/apps/rides/models/media.py b/backend/apps/rides/models/media.py
index 5a603c01..6eb6fc0a 100644
--- a/backend/apps/rides/models/media.py
+++ b/backend/apps/rides/models/media.py
@@ -35,14 +35,12 @@ def ride_photo_upload_path(instance: models.Model, filename: str) -> str:
class RidePhoto(TrackedModel):
"""Photo model specific to rides."""
- ride = models.ForeignKey(
- "rides.Ride", on_delete=models.CASCADE, related_name="photos"
- )
+ ride = models.ForeignKey("rides.Ride", on_delete=models.CASCADE, related_name="photos")
image = models.ForeignKey(
- 'django_cloudflareimages_toolkit.CloudflareImage',
+ "django_cloudflareimages_toolkit.CloudflareImage",
on_delete=models.CASCADE,
- help_text="Ride photo stored on Cloudflare Images"
+ help_text="Ride photo stored on Cloudflare Images",
)
caption = models.CharField(max_length=255, blank=True)
@@ -56,7 +54,7 @@ class RidePhoto(TrackedModel):
domain="rides",
max_length=50,
default="exterior",
- help_text="Type of photo for categorization and display purposes"
+ help_text="Type of photo for categorization and display purposes",
)
# Metadata
@@ -100,9 +98,7 @@ class RidePhoto(TrackedModel):
# Set default caption if not provided
if not self.caption and self.uploaded_by:
- self.caption = MediaService.generate_default_caption(
- self.uploaded_by.username
- )
+ self.caption = MediaService.generate_default_caption(self.uploaded_by.username)
# If this is marked as primary, unmark other primary photos for this ride
if self.is_primary:
diff --git a/backend/apps/rides/models/rankings.py b/backend/apps/rides/models/rankings.py
index 0c82a99d..dc357c03 100644
--- a/backend/apps/rides/models/rankings.py
+++ b/backend/apps/rides/models/rankings.py
@@ -22,17 +22,12 @@ class RideRanking(models.Model):
"""
ride = models.OneToOneField(
- "rides.Ride", on_delete=models.CASCADE, related_name="ranking",
- help_text="Ride this ranking entry describes"
+ "rides.Ride", on_delete=models.CASCADE, related_name="ranking", help_text="Ride this ranking entry describes"
)
# Core ranking metrics
- rank = models.PositiveIntegerField(
- db_index=True, help_text="Overall rank position (1 = best)"
- )
- wins = models.PositiveIntegerField(
- default=0, help_text="Number of rides this ride beats in pairwise comparisons"
- )
+ rank = models.PositiveIntegerField(db_index=True, help_text="Overall rank position (1 = best)")
+ wins = models.PositiveIntegerField(default=0, help_text="Number of rides this ride beats in pairwise comparisons")
losses = models.PositiveIntegerField(
default=0,
help_text="Number of rides that beat this ride in pairwise comparisons",
@@ -66,9 +61,7 @@ class RideRanking(models.Model):
)
# Metadata
- last_calculated = models.DateTimeField(
- default=timezone.now, help_text="When this ranking was last calculated"
- )
+ last_calculated = models.DateTimeField(default=timezone.now, help_text="When this ranking was last calculated")
calculation_version = models.CharField(
max_length=10, default="1.0", help_text="Algorithm version used for calculation"
)
@@ -85,8 +78,7 @@ class RideRanking(models.Model):
constraints = [
models.CheckConstraint(
name="rideranking_winning_percentage_range",
- check=models.Q(winning_percentage__gte=0)
- & models.Q(winning_percentage__lte=1),
+ check=models.Q(winning_percentage__gte=0) & models.Q(winning_percentage__lte=1),
violation_error_message="Winning percentage must be between 0 and 1",
),
models.CheckConstraint(
@@ -115,23 +107,13 @@ class RidePairComparison(models.Model):
(users who have rated both rides). It's used to speed up ranking calculations.
"""
- ride_a = models.ForeignKey(
- "rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_a"
- )
- ride_b = models.ForeignKey(
- "rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_b"
- )
+ ride_a = models.ForeignKey("rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_a")
+ ride_b = models.ForeignKey("rides.Ride", on_delete=models.CASCADE, related_name="comparisons_as_b")
# Comparison results
- ride_a_wins = models.PositiveIntegerField(
- default=0, help_text="Number of mutual riders who rated ride_a higher"
- )
- ride_b_wins = models.PositiveIntegerField(
- default=0, help_text="Number of mutual riders who rated ride_b higher"
- )
- ties = models.PositiveIntegerField(
- default=0, help_text="Number of mutual riders who rated both rides equally"
- )
+ ride_a_wins = models.PositiveIntegerField(default=0, help_text="Number of mutual riders who rated ride_a higher")
+ ride_b_wins = models.PositiveIntegerField(default=0, help_text="Number of mutual riders who rated ride_b higher")
+ ties = models.PositiveIntegerField(default=0, help_text="Number of mutual riders who rated both rides equally")
# Metrics
mutual_riders_count = models.PositiveIntegerField(
@@ -153,9 +135,7 @@ class RidePairComparison(models.Model):
)
# Metadata
- last_calculated = models.DateTimeField(
- auto_now=True, help_text="When this comparison was last calculated"
- )
+ last_calculated = models.DateTimeField(auto_now=True, help_text="When this comparison was last calculated")
class Meta:
verbose_name = "Ride Pair Comparison"
@@ -197,14 +177,10 @@ class RankingSnapshot(models.Model):
This allows us to show ranking trends and movements.
"""
- ride = models.ForeignKey(
- "rides.Ride", on_delete=models.CASCADE, related_name="ranking_history"
- )
+ ride = models.ForeignKey("rides.Ride", on_delete=models.CASCADE, related_name="ranking_history")
rank = models.PositiveIntegerField()
winning_percentage = models.DecimalField(max_digits=5, decimal_places=4)
- snapshot_date = models.DateField(
- db_index=True, help_text="Date when this ranking snapshot was taken"
- )
+ snapshot_date = models.DateField(db_index=True, help_text="Date when this ranking snapshot was taken")
class Meta:
verbose_name = "Ranking Snapshot"
diff --git a/backend/apps/rides/models/reviews.py b/backend/apps/rides/models/reviews.py
index 2ba98d8b..cc886fba 100644
--- a/backend/apps/rides/models/reviews.py
+++ b/backend/apps/rides/models/reviews.py
@@ -12,15 +12,9 @@ class RideReview(TrackedModel):
A review of a ride.
"""
- ride = models.ForeignKey(
- "rides.Ride", on_delete=models.CASCADE, related_name="reviews"
- )
- user = models.ForeignKey(
- "accounts.User", on_delete=models.CASCADE, related_name="ride_reviews"
- )
- rating = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1), MaxValueValidator(10)]
- )
+ ride = models.ForeignKey("rides.Ride", on_delete=models.CASCADE, related_name="reviews")
+ user = models.ForeignKey("accounts.User", on_delete=models.CASCADE, related_name="ride_reviews")
+ rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
title = models.CharField(max_length=200)
content = models.TextField()
visit_date = models.DateField()
@@ -63,10 +57,7 @@ class RideReview(TrackedModel):
name="ride_review_moderation_consistency",
check=models.Q(moderated_by__isnull=True, moderated_at__isnull=True)
| models.Q(moderated_by__isnull=False, moderated_at__isnull=False),
- violation_error_message=(
- "Moderated reviews must have both moderator and moderation "
- "timestamp"
- ),
+ violation_error_message=("Moderated reviews must have both moderator and moderation " "timestamp"),
),
]
diff --git a/backend/apps/rides/models/rides.py b/backend/apps/rides/models/rides.py
index 3e90d75f..687071f5 100644
--- a/backend/apps/rides/models/rides.py
+++ b/backend/apps/rides/models/rides.py
@@ -1,5 +1,4 @@
import contextlib
-from typing import TYPE_CHECKING
import pghistory
from django.contrib.auth.models import AbstractBaseUser
@@ -14,10 +13,6 @@ from config.django import base as settings
from .company import Company
-if TYPE_CHECKING:
- from .rides import RollerCoasterStats
-
-
@pghistory.track()
class RideModel(TrackedModel):
@@ -30,9 +25,7 @@ class RideModel(TrackedModel):
"""
name = models.CharField(max_length=255, help_text="Name of the ride model")
- slug = models.SlugField(
- max_length=255, help_text="URL-friendly identifier (unique within manufacturer)"
- )
+ slug = models.SlugField(max_length=255, help_text="URL-friendly identifier (unique within manufacturer)")
manufacturer = models.ForeignKey(
Company,
on_delete=models.SET_NULL,
@@ -42,9 +35,7 @@ class RideModel(TrackedModel):
limit_choices_to={"roles__contains": ["MANUFACTURER"]},
help_text="Primary manufacturer of this ride model",
)
- description = models.TextField(
- blank=True, help_text="Detailed description of the ride model"
- )
+ description = models.TextField(blank=True, help_text="Detailed description of the ride model")
category = RichChoiceField(
choice_group="categories",
domain="rides",
@@ -125,9 +116,7 @@ class RideModel(TrackedModel):
blank=True,
help_text="Year of last installation of this model (if discontinued)",
)
- is_discontinued = models.BooleanField(
- default=False, help_text="Whether this model is no longer being manufactured"
- )
+ is_discontinued = models.BooleanField(default=False, help_text="Whether this model is no longer being manufactured")
total_installations = models.PositiveIntegerField(
default=0, help_text="Total number of installations worldwide (auto-calculated)"
)
@@ -156,9 +145,7 @@ class RideModel(TrackedModel):
)
# SEO and metadata
- meta_title = models.CharField(
- max_length=60, blank=True, help_text="SEO meta title (auto-generated if blank)"
- )
+ meta_title = models.CharField(max_length=60, blank=True, help_text="SEO meta title (auto-generated if blank)")
meta_description = models.CharField(
max_length=160,
blank=True,
@@ -175,25 +162,21 @@ class RideModel(TrackedModel):
constraints = [
# Unique constraints (replacing unique_together for better error messages)
models.UniqueConstraint(
- fields=['manufacturer', 'name'],
- name='ridemodel_manufacturer_name_unique',
- violation_error_message='A ride model with this name already exists for this manufacturer'
+ fields=["manufacturer", "name"],
+ name="ridemodel_manufacturer_name_unique",
+ violation_error_message="A ride model with this name already exists for this manufacturer",
),
models.UniqueConstraint(
- fields=['manufacturer', 'slug'],
- name='ridemodel_manufacturer_slug_unique',
- violation_error_message='A ride model with this slug already exists for this manufacturer'
+ fields=["manufacturer", "slug"],
+ name="ridemodel_manufacturer_slug_unique",
+ violation_error_message="A ride model with this slug already exists for this manufacturer",
),
# Height range validation
models.CheckConstraint(
name="ride_model_height_range_logical",
condition=models.Q(typical_height_range_min_ft__isnull=True)
| models.Q(typical_height_range_max_ft__isnull=True)
- | models.Q(
- typical_height_range_min_ft__lte=models.F(
- "typical_height_range_max_ft"
- )
- ),
+ | models.Q(typical_height_range_min_ft__lte=models.F("typical_height_range_max_ft")),
violation_error_message="Minimum height cannot exceed maximum height",
),
# Speed range validation
@@ -201,11 +184,7 @@ class RideModel(TrackedModel):
name="ride_model_speed_range_logical",
condition=models.Q(typical_speed_range_min_mph__isnull=True)
| models.Q(typical_speed_range_max_mph__isnull=True)
- | models.Q(
- typical_speed_range_min_mph__lte=models.F(
- "typical_speed_range_max_mph"
- )
- ),
+ | models.Q(typical_speed_range_min_mph__lte=models.F("typical_speed_range_max_mph")),
violation_error_message="Minimum speed cannot exceed maximum speed",
),
# Capacity range validation
@@ -213,11 +192,7 @@ class RideModel(TrackedModel):
name="ride_model_capacity_range_logical",
condition=models.Q(typical_capacity_range_min__isnull=True)
| models.Q(typical_capacity_range_max__isnull=True)
- | models.Q(
- typical_capacity_range_min__lte=models.F(
- "typical_capacity_range_max"
- )
- ),
+ | models.Q(typical_capacity_range_min__lte=models.F("typical_capacity_range_max")),
violation_error_message="Minimum capacity cannot exceed maximum capacity",
),
# Installation years validation
@@ -225,27 +200,19 @@ class RideModel(TrackedModel):
name="ride_model_installation_years_logical",
condition=models.Q(first_installation_year__isnull=True)
| models.Q(last_installation_year__isnull=True)
- | models.Q(
- first_installation_year__lte=models.F("last_installation_year")
- ),
+ | models.Q(first_installation_year__lte=models.F("last_installation_year")),
violation_error_message="First installation year cannot be after last installation year",
),
]
def __str__(self) -> str:
- return (
- self.name
- if not self.manufacturer
- else f"{self.manufacturer.name} {self.name}"
- )
+ return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}"
def clean(self) -> None:
"""Validate RideModel business rules."""
super().clean()
if self.is_discontinued and not self.last_installation_year:
- raise ValidationError({
- 'last_installation_year': 'Discontinued models must have a last installation year'
- })
+ raise ValidationError({"last_installation_year": "Discontinued models must have a last installation year"})
def save(self, *args, **kwargs) -> None:
if not self.slug:
@@ -257,11 +224,7 @@ class RideModel(TrackedModel):
# Ensure uniqueness within the same manufacturer
counter = 1
- while (
- RideModel.objects.filter(manufacturer=self.manufacturer, slug=self.slug)
- .exclude(pk=self.pk)
- .exists()
- ):
+ while RideModel.objects.filter(manufacturer=self.manufacturer, slug=self.slug).exclude(pk=self.pk).exists():
self.slug = f"{base_slug}-{counter}"
counter += 1
@@ -269,16 +232,12 @@ class RideModel(TrackedModel):
if not self.meta_title:
self.meta_title = str(self)[:60]
if not self.meta_description:
- desc = (
- f"{self} - {self.description[:100]}" if self.description else str(self)
- )
+ desc = f"{self} - {self.description[:100]}" if self.description else str(self)
self.meta_description = desc[:160]
# Generate frontend URL
if self.manufacturer:
- frontend_domain = getattr(
- settings, "FRONTEND_DOMAIN", "https://thrillwiki.com"
- )
+ frontend_domain = getattr(settings, "FRONTEND_DOMAIN", "https://thrillwiki.com")
self.url = f"{frontend_domain}/rides/manufacturers/{self.manufacturer.slug}/{self.slug}/"
super().save(*args, **kwargs)
@@ -342,9 +301,7 @@ class RideModelVariant(TrackedModel):
help_text="Base ride model this variant belongs to",
)
name = models.CharField(max_length=255, help_text="Name of this variant")
- description = models.TextField(
- blank=True, help_text="Description of variant differences"
- )
+ description = models.TextField(blank=True, help_text="Description of variant differences")
# Variant-specific specifications
min_height_ft = models.DecimalField(
@@ -402,16 +359,12 @@ class RideModelPhoto(TrackedModel):
help_text="Ride model this photo belongs to",
)
image = models.ForeignKey(
- 'django_cloudflareimages_toolkit.CloudflareImage',
+ "django_cloudflareimages_toolkit.CloudflareImage",
on_delete=models.CASCADE,
- help_text="Photo of the ride model stored on Cloudflare Images"
- )
- caption = models.CharField(
- max_length=500, blank=True, help_text="Photo caption or description"
- )
- alt_text = models.CharField(
- max_length=255, blank=True, help_text="Alternative text for accessibility"
+ help_text="Photo of the ride model stored on Cloudflare Images",
)
+ caption = models.CharField(max_length=500, blank=True, help_text="Photo caption or description")
+ alt_text = models.CharField(max_length=255, blank=True, help_text="Alternative text for accessibility")
# Photo metadata
photo_type = RichChoiceField(
@@ -422,18 +375,12 @@ class RideModelPhoto(TrackedModel):
help_text="Type of photo for categorization and display purposes",
)
- is_primary = models.BooleanField(
- default=False, help_text="Whether this is the primary photo for the ride model"
- )
+ is_primary = models.BooleanField(default=False, help_text="Whether this is the primary photo for the ride model")
# Attribution
- photographer = models.CharField(
- max_length=255, blank=True, help_text="Name of the photographer"
- )
+ photographer = models.CharField(max_length=255, blank=True, help_text="Name of the photographer")
source = models.CharField(max_length=255, blank=True, help_text="Source of the photo")
- copyright_info = models.CharField(
- max_length=255, blank=True, help_text="Copyright information"
- )
+ copyright_info = models.CharField(max_length=255, blank=True, help_text="Copyright information")
class Meta(TrackedModel.Meta):
verbose_name = "Ride Model Photo"
@@ -446,9 +393,9 @@ class RideModelPhoto(TrackedModel):
def save(self, *args, **kwargs) -> None:
# Ensure only one primary photo per ride model
if self.is_primary:
- RideModelPhoto.objects.filter(
- ride_model=self.ride_model, is_primary=True
- ).exclude(pk=self.pk).update(is_primary=False)
+ RideModelPhoto.objects.filter(ride_model=self.ride_model, is_primary=True).exclude(pk=self.pk).update(
+ is_primary=False
+ )
super().save(*args, **kwargs)
@@ -474,15 +421,9 @@ class RideModelTechnicalSpec(TrackedModel):
)
spec_name = models.CharField(max_length=100, help_text="Name of the specification")
- spec_value = models.CharField(
- max_length=255, help_text="Value of the specification"
- )
- spec_unit = models.CharField(
- max_length=20, blank=True, help_text="Unit of measurement"
- )
- notes = models.TextField(
- blank=True, help_text="Additional notes about this specification"
- )
+ spec_value = models.CharField(max_length=255, help_text="Value of the specification")
+ spec_unit = models.CharField(max_length=20, blank=True, help_text="Unit of measurement")
+ notes = models.TextField(blank=True, help_text="Additional notes about this specification")
class Meta(TrackedModel.Meta):
verbose_name = "Ride Model Technical Specification"
@@ -503,17 +444,15 @@ class Ride(StateMachineMixin, TrackedModel):
jobs. Use selectors or annotations for real-time calculations if needed.
"""
- if TYPE_CHECKING:
- coaster_stats: 'RollerCoasterStats'
+ # Type hint for the reverse relation from RollerCoasterStats
+ coaster_stats: "RollerCoasterStats"
state_field_name = "status"
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
description = models.TextField(blank=True)
- park = models.ForeignKey(
- "parks.Park", on_delete=models.CASCADE, related_name="rides"
- )
+ park = models.ForeignKey("parks.Park", on_delete=models.CASCADE, related_name="rides")
park_area = models.ForeignKey(
"parks.ParkArea",
on_delete=models.SET_NULL,
@@ -527,7 +466,7 @@ class Ride(StateMachineMixin, TrackedModel):
max_length=2,
default="",
blank=True,
- help_text="Ride category classification"
+ help_text="Ride category classification",
)
manufacturer = models.ForeignKey(
Company,
@@ -558,7 +497,7 @@ class Ride(StateMachineMixin, TrackedModel):
domain="rides",
max_length=20,
default="OPERATING",
- help_text="Current operational status of the ride"
+ help_text="Current operational status of the ride",
)
post_closing_status = RichChoiceField(
choice_group="post_closing_statuses",
@@ -575,9 +514,7 @@ class Ride(StateMachineMixin, TrackedModel):
max_height_in = models.PositiveIntegerField(null=True, blank=True)
capacity_per_hour = models.PositiveIntegerField(null=True, blank=True)
ride_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
- average_rating = models.DecimalField(
- max_digits=3, decimal_places=2, null=True, blank=True
- )
+ average_rating = models.DecimalField(max_digits=3, decimal_places=2, null=True, blank=True)
# Computed fields for hybrid filtering
opening_year = models.IntegerField(null=True, blank=True, db_index=True)
@@ -603,9 +540,7 @@ class Ride(StateMachineMixin, TrackedModel):
# Frontend URL
url = models.URLField(blank=True, help_text="Frontend URL for this ride")
- park_url = models.URLField(
- blank=True, help_text="Frontend URL for this ride's park"
- )
+ park_url = models.URLField(blank=True, help_text="Frontend URL for this ride's park")
class Meta(TrackedModel.Meta):
verbose_name = "Ride"
@@ -635,17 +570,13 @@ class Ride(StateMachineMixin, TrackedModel):
name="ride_min_height_reasonable",
condition=models.Q(min_height_in__isnull=True)
| (models.Q(min_height_in__gte=30) & models.Q(min_height_in__lte=90)),
- violation_error_message=(
- "Minimum height must be between 30 and 90 inches"
- ),
+ violation_error_message=("Minimum height must be between 30 and 90 inches"),
),
models.CheckConstraint(
name="ride_max_height_reasonable",
condition=models.Q(max_height_in__isnull=True)
| (models.Q(max_height_in__gte=30) & models.Q(max_height_in__lte=90)),
- violation_error_message=(
- "Maximum height must be between 30 and 90 inches"
- ),
+ violation_error_message=("Maximum height must be between 30 and 90 inches"),
),
# Business rule: Rating must be between 1 and 10
models.CheckConstraint(
@@ -657,14 +588,12 @@ class Ride(StateMachineMixin, TrackedModel):
# Business rule: Capacity and duration must be positive
models.CheckConstraint(
name="ride_capacity_positive",
- condition=models.Q(capacity_per_hour__isnull=True)
- | models.Q(capacity_per_hour__gt=0),
+ condition=models.Q(capacity_per_hour__isnull=True) | models.Q(capacity_per_hour__gt=0),
violation_error_message="Hourly capacity must be positive",
),
models.CheckConstraint(
name="ride_duration_positive",
- condition=models.Q(ride_duration_seconds__isnull=True)
- | models.Q(ride_duration_seconds__gt=0),
+ condition=models.Q(ride_duration_seconds__isnull=True) | models.Q(ride_duration_seconds__gt=0),
violation_error_message="Ride duration must be positive",
),
]
@@ -699,9 +628,7 @@ class Ride(StateMachineMixin, TrackedModel):
from django.core.exceptions import ValidationError
if not post_closing_status:
- raise ValidationError(
- "post_closing_status must be set when entering CLOSING status"
- )
+ raise ValidationError("post_closing_status must be set when entering CLOSING status")
self.transition_to_closing(user=user)
self.closing_date = closing_date
self.post_closing_status = post_closing_status
@@ -770,7 +697,7 @@ class Ride(StateMachineMixin, TrackedModel):
self._ensure_unique_slug_in_park()
# Handle park area validation when park changes
- if park_changed and self.park_area:
+ if park_changed and self.park_area: # noqa: SIM102
# Check if park_area belongs to the new park
if self.park_area.park.id != self.park.id:
# Clear park_area if it doesn't belong to the new park
@@ -786,9 +713,7 @@ class Ride(StateMachineMixin, TrackedModel):
# Generate frontend URLs
if self.park:
- frontend_domain = getattr(
- settings, "FRONTEND_DOMAIN", "https://thrillwiki.com"
- )
+ frontend_domain = getattr(settings, "FRONTEND_DOMAIN", "https://thrillwiki.com")
self.url = f"{frontend_domain}/parks/{self.park.slug}/rides/{self.slug}/"
self.park_url = f"{frontend_domain}/parks/{self.park.slug}/"
@@ -817,7 +742,7 @@ class Ride(StateMachineMixin, TrackedModel):
# Park info
if self.park:
search_parts.append(self.park.name)
- if hasattr(self.park, 'location') and self.park.location:
+ if hasattr(self.park, "location") and self.park.location:
if self.park.location.city:
search_parts.append(self.park.location.city)
if self.park.location.state:
@@ -855,7 +780,7 @@ class Ride(StateMachineMixin, TrackedModel):
# Roller coaster stats if available
try:
- if hasattr(self, 'coaster_stats') and self.coaster_stats:
+ if hasattr(self, "coaster_stats") and self.coaster_stats:
stats = self.coaster_stats
if stats.track_type:
search_parts.append(stats.track_type)
@@ -877,7 +802,7 @@ class Ride(StateMachineMixin, TrackedModel):
# Ignore if coaster_stats doesn't exist or has issues
pass
- self.search_text = ' '.join(filter(None, search_parts)).lower()
+ self.search_text = " ".join(filter(None, search_parts)).lower()
def _ensure_unique_slug_in_park(self) -> None:
"""Ensure the ride's slug is unique within its park."""
@@ -885,11 +810,7 @@ class Ride(StateMachineMixin, TrackedModel):
self.slug = base_slug
counter = 1
- while (
- Ride.objects.filter(park=self.park, slug=self.slug)
- .exclude(pk=self.pk)
- .exists()
- ):
+ while Ride.objects.filter(park=self.park, slug=self.slug).exclude(pk=self.pk).exists():
self.slug = f"{base_slug}-{counter}"
counter += 1
@@ -921,26 +842,15 @@ class Ride(StateMachineMixin, TrackedModel):
# Return summary of changes
changes = {
- 'old_park': {
- 'id': old_park.id,
- 'name': old_park.name,
- 'slug': old_park.slug
- },
- 'new_park': {
- 'id': new_park.id,
- 'name': new_park.name,
- 'slug': new_park.slug
- },
- 'url_changed': old_url != self.url,
- 'old_url': old_url,
- 'new_url': self.url,
- 'park_area_cleared': clear_park_area and old_park_area is not None,
- 'old_park_area': {
- 'id': old_park_area.id,
- 'name': old_park_area.name
- } if old_park_area else None,
- 'slug_changed': self.slug != slugify(self.name),
- 'final_slug': self.slug
+ "old_park": {"id": old_park.id, "name": old_park.name, "slug": old_park.slug},
+ "new_park": {"id": new_park.id, "name": new_park.name, "slug": new_park.slug},
+ "url_changed": old_url != self.url,
+ "old_url": old_url,
+ "new_url": self.url,
+ "park_area_cleared": clear_park_area and old_park_area is not None,
+ "old_park_area": {"id": old_park_area.id, "name": old_park_area.name} if old_park_area else None,
+ "slug_changed": self.slug != slugify(self.name),
+ "final_slug": self.slug,
}
return changes
@@ -963,9 +873,9 @@ class Ride(StateMachineMixin, TrackedModel):
except cls.DoesNotExist:
# Try historical slugs in HistoricalSlug model
content_type = ContentType.objects.get_for_model(cls)
- historical_query = HistoricalSlug.objects.filter(
- content_type=content_type, slug=slug
- ).order_by("-created_at")
+ historical_query = HistoricalSlug.objects.filter(content_type=content_type, slug=slug).order_by(
+ "-created_at"
+ )
for historical in historical_query:
try:
@@ -986,14 +896,13 @@ class Ride(StateMachineMixin, TrackedModel):
except cls.DoesNotExist:
continue
- raise cls.DoesNotExist("No ride found with this slug")
+ raise cls.DoesNotExist("No ride found with this slug") from None
@pghistory.track()
class RollerCoasterStats(models.Model):
"""Model for tracking roller coaster specific statistics"""
-
ride = models.OneToOneField(
Ride,
on_delete=models.CASCADE,
@@ -1021,22 +930,16 @@ class RollerCoasterStats(models.Model):
blank=True,
help_text="Maximum speed in mph",
)
- inversions = models.PositiveIntegerField(
- default=0, help_text="Number of inversions"
- )
- ride_time_seconds = models.PositiveIntegerField(
- null=True, blank=True, help_text="Duration of the ride in seconds"
- )
- track_type = models.CharField(
- max_length=255, blank=True, help_text="Type of track (e.g., tubular steel, wooden)"
- )
+ inversions = models.PositiveIntegerField(default=0, help_text="Number of inversions")
+ ride_time_seconds = models.PositiveIntegerField(null=True, blank=True, help_text="Duration of the ride in seconds")
+ track_type = models.CharField(max_length=255, blank=True, help_text="Type of track (e.g., tubular steel, wooden)")
track_material = RichChoiceField(
choice_group="track_materials",
domain="rides",
max_length=20,
default="STEEL",
blank=True,
- help_text="Track construction material type"
+ help_text="Track construction material type",
)
roller_coaster_type = RichChoiceField(
choice_group="coaster_types",
@@ -1044,7 +947,7 @@ class RollerCoasterStats(models.Model):
max_length=20,
default="SITDOWN",
blank=True,
- help_text="Roller coaster type classification"
+ help_text="Roller coaster type classification",
)
max_drop_height_ft = models.DecimalField(
max_digits=6,
@@ -1058,20 +961,12 @@ class RollerCoasterStats(models.Model):
domain="rides",
max_length=20,
default="CHAIN",
- help_text="Propulsion or lift system type"
- )
- train_style = models.CharField(
- max_length=255, blank=True, help_text="Style of train (e.g., floorless, inverted)"
- )
- trains_count = models.PositiveIntegerField(
- null=True, blank=True, help_text="Number of trains"
- )
- cars_per_train = models.PositiveIntegerField(
- null=True, blank=True, help_text="Number of cars per train"
- )
- seats_per_car = models.PositiveIntegerField(
- null=True, blank=True, help_text="Number of seats per car"
+ help_text="Propulsion or lift system type",
)
+ train_style = models.CharField(max_length=255, blank=True, help_text="Style of train (e.g., floorless, inverted)")
+ trains_count = models.PositiveIntegerField(null=True, blank=True, help_text="Number of trains")
+ cars_per_train = models.PositiveIntegerField(null=True, blank=True, help_text="Number of cars per train")
+ seats_per_car = models.PositiveIntegerField(null=True, blank=True, help_text="Number of seats per car")
class Meta:
verbose_name = "Roller Coaster Statistics"
diff --git a/backend/apps/rides/selectors.py b/backend/apps/rides/selectors.py
index 1e778a31..e40a9164 100644
--- a/backend/apps/rides/selectors.py
+++ b/backend/apps/rides/selectors.py
@@ -13,9 +13,7 @@ from .choices import RIDE_CATEGORIES
from .models import Ride, RideModel, RideReview
-def ride_list_for_display(
- *, filters: dict[str, Any] | None = None
-) -> QuerySet[Ride]:
+def ride_list_for_display(*, filters: dict[str, Any] | None = None) -> QuerySet[Ride]:
"""
Get rides optimized for list display with related data.
@@ -85,9 +83,7 @@ def ride_detail_optimized(*, slug: str, park_slug: str) -> Ride:
"park__location",
Prefetch(
"reviews",
- queryset=RideReview.objects.select_related("user").filter(
- is_published=True
- ),
+ queryset=RideReview.objects.select_related("user").filter(is_published=True),
),
"photos",
)
@@ -171,9 +167,7 @@ def rides_in_park(*, park_slug: str) -> QuerySet[Ride]:
)
-def rides_near_location(
- *, point: Point, distance_km: float = 50, limit: int = 10
-) -> QuerySet[Ride]:
+def rides_near_location(*, point: Point, distance_km: float = 50, limit: int = 10) -> QuerySet[Ride]:
"""
Get rides near a specific geographic location.
@@ -227,9 +221,7 @@ def ride_search_autocomplete(*, query: str, limit: int = 10) -> QuerySet[Ride]:
"""
return (
Ride.objects.filter(
- Q(name__icontains=query)
- | Q(park__name__icontains=query)
- | Q(manufacturer__name__icontains=query)
+ Q(name__icontains=query) | Q(park__name__icontains=query) | Q(manufacturer__name__icontains=query)
)
.select_related("park", "manufacturer")
.prefetch_related("park__location")
@@ -254,16 +246,10 @@ def rides_with_recent_reviews(*, days: int = 30) -> QuerySet[Ride]:
cutoff_date = timezone.now() - timedelta(days=days)
return (
- Ride.objects.filter(
- reviews__created_at__gte=cutoff_date, reviews__is_published=True
- )
+ Ride.objects.filter(reviews__created_at__gte=cutoff_date, reviews__is_published=True)
.select_related("park", "manufacturer")
.prefetch_related("park__location")
- .annotate(
- recent_review_count=Count(
- "reviews", filter=Q(reviews__created_at__gte=cutoff_date)
- )
- )
+ .annotate(recent_review_count=Count("reviews", filter=Q(reviews__created_at__gte=cutoff_date)))
.order_by("-recent_review_count")
.distinct()
)
diff --git a/backend/apps/rides/services/__init__.py b/backend/apps/rides/services/__init__.py
index fcae25c3..2bc4fe19 100644
--- a/backend/apps/rides/services/__init__.py
+++ b/backend/apps/rides/services/__init__.py
@@ -4,4 +4,3 @@ from .location_service import RideLocationService
from .media_service import RideMediaService
__all__ = ["RideLocationService", "RideMediaService", "RideService"]
-
diff --git a/backend/apps/rides/services/hybrid_loader.py b/backend/apps/rides/services/hybrid_loader.py
index 86147ba5..4f1f6de7 100644
--- a/backend/apps/rides/services/hybrid_loader.py
+++ b/backend/apps/rides/services/hybrid_loader.py
@@ -95,13 +95,13 @@ class SmartRideLoader:
total_count = queryset.count()
# Get progressive batch
- rides = list(queryset[offset:offset + self.PROGRESSIVE_LOAD_SIZE])
+ rides = list(queryset[offset : offset + self.PROGRESSIVE_LOAD_SIZE])
return {
- 'rides': self._serialize_rides(rides),
- 'total_count': total_count,
- 'has_more': len(rides) == self.PROGRESSIVE_LOAD_SIZE,
- 'next_offset': offset + len(rides) if len(rides) == self.PROGRESSIVE_LOAD_SIZE else None
+ "rides": self._serialize_rides(rides),
+ "total_count": total_count,
+ "has_more": len(rides) == self.PROGRESSIVE_LOAD_SIZE,
+ "next_offset": offset + len(rides) if len(rides) == self.PROGRESSIVE_LOAD_SIZE else None,
}
def get_filter_metadata(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -148,8 +148,7 @@ class SmartRideLoader:
return count
- def _get_client_side_data(self, filters: dict[str, Any] | None,
- total_count: int) -> dict[str, Any]:
+ def _get_client_side_data(self, filters: dict[str, Any] | None, total_count: int) -> dict[str, Any]:
"""Get all data for client-side filtering."""
cache_key = f"{self.cache_prefix}client_side_all"
cached_data = cache.get(cache_key)
@@ -158,45 +157,46 @@ class SmartRideLoader:
from apps.rides.models import Ride
# Load all rides with optimized query
- queryset = Ride.objects.select_related(
- 'park',
- 'park__location',
- 'park_area',
- 'manufacturer',
- 'designer',
- 'ride_model',
- 'ride_model__manufacturer'
- ).prefetch_related(
- 'coaster_stats'
- ).order_by('name')
+ queryset = (
+ Ride.objects.select_related(
+ "park",
+ "park__location",
+ "park_area",
+ "manufacturer",
+ "designer",
+ "ride_model",
+ "ride_model__manufacturer",
+ )
+ .prefetch_related("coaster_stats")
+ .order_by("name")
+ )
rides = list(queryset)
cached_data = self._serialize_rides(rides)
cache.set(cache_key, cached_data, self.CACHE_TIMEOUT)
return {
- 'strategy': 'client_side',
- 'rides': cached_data,
- 'total_count': total_count,
- 'has_more': False,
- 'filter_metadata': self.get_filter_metadata(filters)
+ "strategy": "client_side",
+ "rides": cached_data,
+ "total_count": total_count,
+ "has_more": False,
+ "filter_metadata": self.get_filter_metadata(filters),
}
- def _get_server_side_data(self, filters: dict[str, Any] | None,
- total_count: int) -> dict[str, Any]:
+ def _get_server_side_data(self, filters: dict[str, Any] | None, total_count: int) -> dict[str, Any]:
"""Get initial batch for server-side filtering."""
# Build filtered queryset
queryset = self._build_filtered_queryset(filters)
# Get initial batch
- rides = list(queryset[:self.INITIAL_LOAD_SIZE])
+ rides = list(queryset[: self.INITIAL_LOAD_SIZE])
return {
- 'strategy': 'server_side',
- 'rides': self._serialize_rides(rides),
- 'total_count': total_count,
- 'has_more': len(rides) == self.INITIAL_LOAD_SIZE,
- 'next_offset': len(rides) if len(rides) == self.INITIAL_LOAD_SIZE else None
+ "strategy": "server_side",
+ "rides": self._serialize_rides(rides),
+ "total_count": total_count,
+ "has_more": len(rides) == self.INITIAL_LOAD_SIZE,
+ "next_offset": len(rides) if len(rides) == self.INITIAL_LOAD_SIZE else None,
}
def _build_filtered_queryset(self, filters: dict[str, Any] | None):
@@ -205,118 +205,110 @@ class SmartRideLoader:
# Start with optimized base queryset
queryset = Ride.objects.select_related(
- 'park',
- 'park__location',
- 'park_area',
- 'manufacturer',
- 'designer',
- 'ride_model',
- 'ride_model__manufacturer'
- ).prefetch_related(
- 'coaster_stats'
- )
+ "park", "park__location", "park_area", "manufacturer", "designer", "ride_model", "ride_model__manufacturer"
+ ).prefetch_related("coaster_stats")
if not filters:
- return queryset.order_by('name')
+ return queryset.order_by("name")
# Apply filters
q_objects = Q()
# Text search using computed search_text field
- if 'search' in filters and filters['search']:
- search_term = filters['search'].lower()
+ if "search" in filters and filters["search"]:
+ search_term = filters["search"].lower()
q_objects &= Q(search_text__icontains=search_term)
# Park filters
- if 'park_slug' in filters and filters['park_slug']:
- q_objects &= Q(park__slug=filters['park_slug'])
+ if "park_slug" in filters and filters["park_slug"]:
+ q_objects &= Q(park__slug=filters["park_slug"])
- if 'park_id' in filters and filters['park_id']:
- q_objects &= Q(park_id=filters['park_id'])
+ if "park_id" in filters and filters["park_id"]:
+ q_objects &= Q(park_id=filters["park_id"])
# Category filters
- if 'category' in filters and filters['category']:
- q_objects &= Q(category__in=filters['category'])
+ if "category" in filters and filters["category"]:
+ q_objects &= Q(category__in=filters["category"])
# Status filters
- if 'status' in filters and filters['status']:
- q_objects &= Q(status__in=filters['status'])
+ if "status" in filters and filters["status"]:
+ q_objects &= Q(status__in=filters["status"])
# Company filters
- if 'manufacturer_ids' in filters and filters['manufacturer_ids']:
- q_objects &= Q(manufacturer_id__in=filters['manufacturer_ids'])
+ if "manufacturer_ids" in filters and filters["manufacturer_ids"]:
+ q_objects &= Q(manufacturer_id__in=filters["manufacturer_ids"])
- if 'designer_ids' in filters and filters['designer_ids']:
- q_objects &= Q(designer_id__in=filters['designer_ids'])
+ if "designer_ids" in filters and filters["designer_ids"]:
+ q_objects &= Q(designer_id__in=filters["designer_ids"])
# Ride model filters
- if 'ride_model_ids' in filters and filters['ride_model_ids']:
- q_objects &= Q(ride_model_id__in=filters['ride_model_ids'])
+ if "ride_model_ids" in filters and filters["ride_model_ids"]:
+ q_objects &= Q(ride_model_id__in=filters["ride_model_ids"])
# Opening year filters using computed opening_year field
- if 'opening_year' in filters and filters['opening_year']:
- q_objects &= Q(opening_year=filters['opening_year'])
+ if "opening_year" in filters and filters["opening_year"]:
+ q_objects &= Q(opening_year=filters["opening_year"])
- if 'min_opening_year' in filters and filters['min_opening_year']:
- q_objects &= Q(opening_year__gte=filters['min_opening_year'])
+ if "min_opening_year" in filters and filters["min_opening_year"]:
+ q_objects &= Q(opening_year__gte=filters["min_opening_year"])
- if 'max_opening_year' in filters and filters['max_opening_year']:
- q_objects &= Q(opening_year__lte=filters['max_opening_year'])
+ if "max_opening_year" in filters and filters["max_opening_year"]:
+ q_objects &= Q(opening_year__lte=filters["max_opening_year"])
# Rating filters
- if 'min_rating' in filters and filters['min_rating']:
- q_objects &= Q(average_rating__gte=filters['min_rating'])
+ if "min_rating" in filters and filters["min_rating"]:
+ q_objects &= Q(average_rating__gte=filters["min_rating"])
- if 'max_rating' in filters and filters['max_rating']:
- q_objects &= Q(average_rating__lte=filters['max_rating'])
+ if "max_rating" in filters and filters["max_rating"]:
+ q_objects &= Q(average_rating__lte=filters["max_rating"])
# Height requirement filters
- if 'min_height_requirement' in filters and filters['min_height_requirement']:
- q_objects &= Q(min_height_in__gte=filters['min_height_requirement'])
+ if "min_height_requirement" in filters and filters["min_height_requirement"]:
+ q_objects &= Q(min_height_in__gte=filters["min_height_requirement"])
- if 'max_height_requirement' in filters and filters['max_height_requirement']:
- q_objects &= Q(max_height_in__lte=filters['max_height_requirement'])
+ if "max_height_requirement" in filters and filters["max_height_requirement"]:
+ q_objects &= Q(max_height_in__lte=filters["max_height_requirement"])
# Capacity filters
- if 'min_capacity' in filters and filters['min_capacity']:
- q_objects &= Q(capacity_per_hour__gte=filters['min_capacity'])
+ if "min_capacity" in filters and filters["min_capacity"]:
+ q_objects &= Q(capacity_per_hour__gte=filters["min_capacity"])
- if 'max_capacity' in filters and filters['max_capacity']:
- q_objects &= Q(capacity_per_hour__lte=filters['max_capacity'])
+ if "max_capacity" in filters and filters["max_capacity"]:
+ q_objects &= Q(capacity_per_hour__lte=filters["max_capacity"])
# Roller coaster specific filters
- if 'roller_coaster_type' in filters and filters['roller_coaster_type']:
- q_objects &= Q(coaster_stats__roller_coaster_type__in=filters['roller_coaster_type'])
+ if "roller_coaster_type" in filters and filters["roller_coaster_type"]:
+ q_objects &= Q(coaster_stats__roller_coaster_type__in=filters["roller_coaster_type"])
- if 'track_material' in filters and filters['track_material']:
- q_objects &= Q(coaster_stats__track_material__in=filters['track_material'])
+ if "track_material" in filters and filters["track_material"]:
+ q_objects &= Q(coaster_stats__track_material__in=filters["track_material"])
- if 'propulsion_system' in filters and filters['propulsion_system']:
- q_objects &= Q(coaster_stats__propulsion_system__in=filters['propulsion_system'])
+ if "propulsion_system" in filters and filters["propulsion_system"]:
+ q_objects &= Q(coaster_stats__propulsion_system__in=filters["propulsion_system"])
# Roller coaster height filters
- if 'min_height_ft' in filters and filters['min_height_ft']:
- q_objects &= Q(coaster_stats__height_ft__gte=filters['min_height_ft'])
+ if "min_height_ft" in filters and filters["min_height_ft"]:
+ q_objects &= Q(coaster_stats__height_ft__gte=filters["min_height_ft"])
- if 'max_height_ft' in filters and filters['max_height_ft']:
- q_objects &= Q(coaster_stats__height_ft__lte=filters['max_height_ft'])
+ if "max_height_ft" in filters and filters["max_height_ft"]:
+ q_objects &= Q(coaster_stats__height_ft__lte=filters["max_height_ft"])
# Roller coaster speed filters
- if 'min_speed_mph' in filters and filters['min_speed_mph']:
- q_objects &= Q(coaster_stats__speed_mph__gte=filters['min_speed_mph'])
+ if "min_speed_mph" in filters and filters["min_speed_mph"]:
+ q_objects &= Q(coaster_stats__speed_mph__gte=filters["min_speed_mph"])
- if 'max_speed_mph' in filters and filters['max_speed_mph']:
- q_objects &= Q(coaster_stats__speed_mph__lte=filters['max_speed_mph'])
+ if "max_speed_mph" in filters and filters["max_speed_mph"]:
+ q_objects &= Q(coaster_stats__speed_mph__lte=filters["max_speed_mph"])
# Inversion filters
- if 'min_inversions' in filters and filters['min_inversions']:
- q_objects &= Q(coaster_stats__inversions__gte=filters['min_inversions'])
+ if "min_inversions" in filters and filters["min_inversions"]:
+ q_objects &= Q(coaster_stats__inversions__gte=filters["min_inversions"])
- if 'max_inversions' in filters and filters['max_inversions']:
- q_objects &= Q(coaster_stats__inversions__lte=filters['max_inversions'])
+ if "max_inversions" in filters and filters["max_inversions"]:
+ q_objects &= Q(coaster_stats__inversions__lte=filters["max_inversions"])
- if 'has_inversions' in filters and filters['has_inversions'] is not None:
- if filters['has_inversions']:
+ if "has_inversions" in filters and filters["has_inversions"] is not None:
+ if filters["has_inversions"]:
q_objects &= Q(coaster_stats__inversions__gt=0)
else:
q_objects &= Q(coaster_stats__inversions=0)
@@ -325,10 +317,12 @@ class SmartRideLoader:
queryset = queryset.filter(q_objects)
# Apply ordering
- ordering = filters.get('ordering', 'name')
- if ordering in ['height_ft', '-height_ft', 'speed_mph', '-speed_mph']:
+ ordering = filters.get("ordering", "name")
+ if ordering in ["height_ft", "-height_ft", "speed_mph", "-speed_mph"]:
# For coaster stats ordering, we need to join and order by the stats
- ordering_field = ordering.replace('height_ft', 'coaster_stats__height_ft').replace('speed_mph', 'coaster_stats__speed_mph')
+ ordering_field = ordering.replace("height_ft", "coaster_stats__height_ft").replace(
+ "speed_mph", "coaster_stats__speed_mph"
+ )
queryset = queryset.order_by(ordering_field)
else:
queryset = queryset.order_by(ordering)
@@ -342,99 +336,99 @@ class SmartRideLoader:
for ride in rides:
# Basic ride data
ride_data = {
- 'id': ride.id,
- 'name': ride.name,
- 'slug': ride.slug,
- 'description': ride.description,
- 'category': ride.category,
- 'status': ride.status,
- 'opening_date': ride.opening_date.isoformat() if ride.opening_date else None,
- 'closing_date': ride.closing_date.isoformat() if ride.closing_date else None,
- 'opening_year': ride.opening_year,
- 'min_height_in': ride.min_height_in,
- 'max_height_in': ride.max_height_in,
- 'capacity_per_hour': ride.capacity_per_hour,
- 'ride_duration_seconds': ride.ride_duration_seconds,
- 'average_rating': float(ride.average_rating) if ride.average_rating else None,
- 'url': ride.url,
- 'park_url': ride.park_url,
- 'created_at': ride.created_at.isoformat(),
- 'updated_at': ride.updated_at.isoformat(),
+ "id": ride.id,
+ "name": ride.name,
+ "slug": ride.slug,
+ "description": ride.description,
+ "category": ride.category,
+ "status": ride.status,
+ "opening_date": ride.opening_date.isoformat() if ride.opening_date else None,
+ "closing_date": ride.closing_date.isoformat() if ride.closing_date else None,
+ "opening_year": ride.opening_year,
+ "min_height_in": ride.min_height_in,
+ "max_height_in": ride.max_height_in,
+ "capacity_per_hour": ride.capacity_per_hour,
+ "ride_duration_seconds": ride.ride_duration_seconds,
+ "average_rating": float(ride.average_rating) if ride.average_rating else None,
+ "url": ride.url,
+ "park_url": ride.park_url,
+ "created_at": ride.created_at.isoformat(),
+ "updated_at": ride.updated_at.isoformat(),
}
# Park data
if ride.park:
- ride_data['park'] = {
- 'id': ride.park.id,
- 'name': ride.park.name,
- 'slug': ride.park.slug,
+ ride_data["park"] = {
+ "id": ride.park.id,
+ "name": ride.park.name,
+ "slug": ride.park.slug,
}
# Park location data
- if hasattr(ride.park, 'location') and ride.park.location:
- ride_data['park']['location'] = {
- 'city': ride.park.location.city,
- 'state': ride.park.location.state,
- 'country': ride.park.location.country,
+ if hasattr(ride.park, "location") and ride.park.location:
+ ride_data["park"]["location"] = {
+ "city": ride.park.location.city,
+ "state": ride.park.location.state,
+ "country": ride.park.location.country,
}
# Park area data
if ride.park_area:
- ride_data['park_area'] = {
- 'id': ride.park_area.id,
- 'name': ride.park_area.name,
- 'slug': ride.park_area.slug,
+ ride_data["park_area"] = {
+ "id": ride.park_area.id,
+ "name": ride.park_area.name,
+ "slug": ride.park_area.slug,
}
# Company data
if ride.manufacturer:
- ride_data['manufacturer'] = {
- 'id': ride.manufacturer.id,
- 'name': ride.manufacturer.name,
- 'slug': ride.manufacturer.slug,
+ ride_data["manufacturer"] = {
+ "id": ride.manufacturer.id,
+ "name": ride.manufacturer.name,
+ "slug": ride.manufacturer.slug,
}
if ride.designer:
- ride_data['designer'] = {
- 'id': ride.designer.id,
- 'name': ride.designer.name,
- 'slug': ride.designer.slug,
+ ride_data["designer"] = {
+ "id": ride.designer.id,
+ "name": ride.designer.name,
+ "slug": ride.designer.slug,
}
# Ride model data
if ride.ride_model:
- ride_data['ride_model'] = {
- 'id': ride.ride_model.id,
- 'name': ride.ride_model.name,
- 'slug': ride.ride_model.slug,
- 'category': ride.ride_model.category,
+ ride_data["ride_model"] = {
+ "id": ride.ride_model.id,
+ "name": ride.ride_model.name,
+ "slug": ride.ride_model.slug,
+ "category": ride.ride_model.category,
}
if ride.ride_model.manufacturer:
- ride_data['ride_model']['manufacturer'] = {
- 'id': ride.ride_model.manufacturer.id,
- 'name': ride.ride_model.manufacturer.name,
- 'slug': ride.ride_model.manufacturer.slug,
+ ride_data["ride_model"]["manufacturer"] = {
+ "id": ride.ride_model.manufacturer.id,
+ "name": ride.ride_model.manufacturer.name,
+ "slug": ride.ride_model.manufacturer.slug,
}
# Roller coaster stats
- if hasattr(ride, 'coaster_stats') and ride.coaster_stats:
+ if hasattr(ride, "coaster_stats") and ride.coaster_stats:
stats = ride.coaster_stats
- ride_data['coaster_stats'] = {
- 'height_ft': float(stats.height_ft) if stats.height_ft else None,
- 'length_ft': float(stats.length_ft) if stats.length_ft else None,
- 'speed_mph': float(stats.speed_mph) if stats.speed_mph else None,
- 'inversions': stats.inversions,
- 'ride_time_seconds': stats.ride_time_seconds,
- 'track_type': stats.track_type,
- 'track_material': stats.track_material,
- 'roller_coaster_type': stats.roller_coaster_type,
- 'max_drop_height_ft': float(stats.max_drop_height_ft) if stats.max_drop_height_ft else None,
- 'propulsion_system': stats.propulsion_system,
- 'train_style': stats.train_style,
- 'trains_count': stats.trains_count,
- 'cars_per_train': stats.cars_per_train,
- 'seats_per_car': stats.seats_per_car,
+ ride_data["coaster_stats"] = {
+ "height_ft": float(stats.height_ft) if stats.height_ft else None,
+ "length_ft": float(stats.length_ft) if stats.length_ft else None,
+ "speed_mph": float(stats.speed_mph) if stats.speed_mph else None,
+ "inversions": stats.inversions,
+ "ride_time_seconds": stats.ride_time_seconds,
+ "track_type": stats.track_type,
+ "track_material": stats.track_material,
+ "roller_coaster_type": stats.roller_coaster_type,
+ "max_drop_height_ft": float(stats.max_drop_height_ft) if stats.max_drop_height_ft else None,
+ "propulsion_system": stats.propulsion_system,
+ "train_style": stats.train_style,
+ "trains_count": stats.trains_count,
+ "cars_per_train": stats.cars_per_train,
+ "seats_per_car": stats.seats_per_car,
}
serialized.append(ride_data)
@@ -448,267 +442,250 @@ class SmartRideLoader:
from apps.rides.models.rides import RollerCoasterStats
# Get unique values from database with counts
- parks_data = list(Ride.objects.exclude(
- park__isnull=True
- ).select_related('park').values(
- 'park__id', 'park__name', 'park__slug'
- ).annotate(count=models.Count('id')).distinct().order_by('park__name'))
+ parks_data = list(
+ Ride.objects.exclude(park__isnull=True)
+ .select_related("park")
+ .values("park__id", "park__name", "park__slug")
+ .annotate(count=models.Count("id"))
+ .distinct()
+ .order_by("park__name")
+ )
- park_areas_data = list(Ride.objects.exclude(
- park_area__isnull=True
- ).select_related('park_area').values(
- 'park_area__id', 'park_area__name', 'park_area__slug'
- ).annotate(count=models.Count('id')).distinct().order_by('park_area__name'))
+ park_areas_data = list(
+ Ride.objects.exclude(park_area__isnull=True)
+ .select_related("park_area")
+ .values("park_area__id", "park_area__name", "park_area__slug")
+ .annotate(count=models.Count("id"))
+ .distinct()
+ .order_by("park_area__name")
+ )
- manufacturers_data = list(Company.objects.filter(
- roles__contains=['MANUFACTURER']
- ).values('id', 'name', 'slug').annotate(
- count=models.Count('manufactured_rides')
- ).order_by('name'))
+ manufacturers_data = list(
+ Company.objects.filter(roles__contains=["MANUFACTURER"])
+ .values("id", "name", "slug")
+ .annotate(count=models.Count("manufactured_rides"))
+ .order_by("name")
+ )
- designers_data = list(Company.objects.filter(
- roles__contains=['DESIGNER']
- ).values('id', 'name', 'slug').annotate(
- count=models.Count('designed_rides')
- ).order_by('name'))
+ designers_data = list(
+ Company.objects.filter(roles__contains=["DESIGNER"])
+ .values("id", "name", "slug")
+ .annotate(count=models.Count("designed_rides"))
+ .order_by("name")
+ )
- ride_models_data = list(RideModel.objects.select_related(
- 'manufacturer'
- ).values(
- 'id', 'name', 'slug', 'manufacturer__name', 'manufacturer__slug', 'category'
- ).annotate(count=models.Count('rides')).order_by('manufacturer__name', 'name'))
+ ride_models_data = list(
+ RideModel.objects.select_related("manufacturer")
+ .values("id", "name", "slug", "manufacturer__name", "manufacturer__slug", "category")
+ .annotate(count=models.Count("rides"))
+ .order_by("manufacturer__name", "name")
+ )
# Get categories and statuses with counts
- categories_data = list(Ride.objects.values('category').annotate(
- count=models.Count('id')
- ).order_by('category'))
+ categories_data = list(Ride.objects.values("category").annotate(count=models.Count("id")).order_by("category"))
- statuses_data = list(Ride.objects.values('status').annotate(
- count=models.Count('id')
- ).order_by('status'))
+ statuses_data = list(Ride.objects.values("status").annotate(count=models.Count("id")).order_by("status"))
# Get roller coaster specific data with counts
- rc_types_data = list(RollerCoasterStats.objects.values('roller_coaster_type').annotate(
- count=models.Count('ride')
- ).exclude(roller_coaster_type__isnull=True).order_by('roller_coaster_type'))
+ rc_types_data = list(
+ RollerCoasterStats.objects.values("roller_coaster_type")
+ .annotate(count=models.Count("ride"))
+ .exclude(roller_coaster_type__isnull=True)
+ .order_by("roller_coaster_type")
+ )
- track_materials_data = list(RollerCoasterStats.objects.values('track_material').annotate(
- count=models.Count('ride')
- ).exclude(track_material__isnull=True).order_by('track_material'))
+ track_materials_data = list(
+ RollerCoasterStats.objects.values("track_material")
+ .annotate(count=models.Count("ride"))
+ .exclude(track_material__isnull=True)
+ .order_by("track_material")
+ )
- propulsion_systems_data = list(RollerCoasterStats.objects.values('propulsion_system').annotate(
- count=models.Count('ride')
- ).exclude(propulsion_system__isnull=True).order_by('propulsion_system'))
+ propulsion_systems_data = list(
+ RollerCoasterStats.objects.values("propulsion_system")
+ .annotate(count=models.Count("ride"))
+ .exclude(propulsion_system__isnull=True)
+ .order_by("propulsion_system")
+ )
# Convert to frontend-expected format with value/label/count
categories = [
- {
- 'value': item['category'],
- 'label': self._get_category_label(item['category']),
- 'count': item['count']
- }
+ {"value": item["category"], "label": self._get_category_label(item["category"]), "count": item["count"]}
for item in categories_data
]
statuses = [
- {
- 'value': item['status'],
- 'label': self._get_status_label(item['status']),
- 'count': item['count']
- }
+ {"value": item["status"], "label": self._get_status_label(item["status"]), "count": item["count"]}
for item in statuses_data
]
roller_coaster_types = [
{
- 'value': item['roller_coaster_type'],
- 'label': self._get_rc_type_label(item['roller_coaster_type']),
- 'count': item['count']
+ "value": item["roller_coaster_type"],
+ "label": self._get_rc_type_label(item["roller_coaster_type"]),
+ "count": item["count"],
}
for item in rc_types_data
]
track_materials = [
{
- 'value': item['track_material'],
- 'label': self._get_track_material_label(item['track_material']),
- 'count': item['count']
+ "value": item["track_material"],
+ "label": self._get_track_material_label(item["track_material"]),
+ "count": item["count"],
}
for item in track_materials_data
]
propulsion_systems = [
{
- 'value': item['propulsion_system'],
- 'label': self._get_propulsion_system_label(item['propulsion_system']),
- 'count': item['count']
+ "value": item["propulsion_system"],
+ "label": self._get_propulsion_system_label(item["propulsion_system"]),
+ "count": item["count"],
}
for item in propulsion_systems_data
]
# Convert other data to expected format
parks = [
- {
- 'value': str(item['park__id']),
- 'label': item['park__name'],
- 'count': item['count']
- }
- for item in parks_data
+ {"value": str(item["park__id"]), "label": item["park__name"], "count": item["count"]} for item in parks_data
]
park_areas = [
- {
- 'value': str(item['park_area__id']),
- 'label': item['park_area__name'],
- 'count': item['count']
- }
+ {"value": str(item["park_area__id"]), "label": item["park_area__name"], "count": item["count"]}
for item in park_areas_data
]
manufacturers = [
- {
- 'value': str(item['id']),
- 'label': item['name'],
- 'count': item['count']
- }
- for item in manufacturers_data
+ {"value": str(item["id"]), "label": item["name"], "count": item["count"]} for item in manufacturers_data
]
designers = [
- {
- 'value': str(item['id']),
- 'label': item['name'],
- 'count': item['count']
- }
- for item in designers_data
+ {"value": str(item["id"]), "label": item["name"], "count": item["count"]} for item in designers_data
]
ride_models = [
- {
- 'value': str(item['id']),
- 'label': f"{item['manufacturer__name']} {item['name']}",
- 'count': item['count']
- }
+ {"value": str(item["id"]), "label": f"{item['manufacturer__name']} {item['name']}", "count": item["count"]}
for item in ride_models_data
]
# Calculate ranges from actual data
ride_stats = Ride.objects.aggregate(
- min_rating=Min('average_rating'),
- max_rating=Max('average_rating'),
- min_height_req=Min('min_height_in'),
- max_height_req=Max('max_height_in'),
- min_capacity=Min('capacity_per_hour'),
- max_capacity=Max('capacity_per_hour'),
- min_duration=Min('ride_duration_seconds'),
- max_duration=Max('ride_duration_seconds'),
- min_year=Min('opening_year'),
- max_year=Max('opening_year'),
+ min_rating=Min("average_rating"),
+ max_rating=Max("average_rating"),
+ min_height_req=Min("min_height_in"),
+ max_height_req=Max("max_height_in"),
+ min_capacity=Min("capacity_per_hour"),
+ max_capacity=Max("capacity_per_hour"),
+ min_duration=Min("ride_duration_seconds"),
+ max_duration=Max("ride_duration_seconds"),
+ min_year=Min("opening_year"),
+ max_year=Max("opening_year"),
)
# Calculate roller coaster specific ranges
coaster_stats = RollerCoasterStats.objects.aggregate(
- min_height_ft=Min('height_ft'),
- max_height_ft=Max('height_ft'),
- min_length_ft=Min('length_ft'),
- max_length_ft=Max('length_ft'),
- min_speed_mph=Min('speed_mph'),
- max_speed_mph=Max('speed_mph'),
- min_inversions=Min('inversions'),
- max_inversions=Max('inversions'),
- min_ride_time=Min('ride_time_seconds'),
- max_ride_time=Max('ride_time_seconds'),
- min_drop_height=Min('max_drop_height_ft'),
- max_drop_height=Max('max_drop_height_ft'),
- min_trains=Min('trains_count'),
- max_trains=Max('trains_count'),
- min_cars=Min('cars_per_train'),
- max_cars=Max('cars_per_train'),
- min_seats=Min('seats_per_car'),
- max_seats=Max('seats_per_car'),
+ min_height_ft=Min("height_ft"),
+ max_height_ft=Max("height_ft"),
+ min_length_ft=Min("length_ft"),
+ max_length_ft=Max("length_ft"),
+ min_speed_mph=Min("speed_mph"),
+ max_speed_mph=Max("speed_mph"),
+ min_inversions=Min("inversions"),
+ max_inversions=Max("inversions"),
+ min_ride_time=Min("ride_time_seconds"),
+ max_ride_time=Max("ride_time_seconds"),
+ min_drop_height=Min("max_drop_height_ft"),
+ max_drop_height=Max("max_drop_height_ft"),
+ min_trains=Min("trains_count"),
+ max_trains=Max("trains_count"),
+ min_cars=Min("cars_per_train"),
+ max_cars=Max("cars_per_train"),
+ min_seats=Min("seats_per_car"),
+ max_seats=Max("seats_per_car"),
)
return {
- 'categorical': {
- 'categories': categories,
- 'statuses': statuses,
- 'roller_coaster_types': roller_coaster_types,
- 'track_materials': track_materials,
- 'propulsion_systems': propulsion_systems,
- 'parks': parks,
- 'park_areas': park_areas,
- 'manufacturers': manufacturers,
- 'designers': designers,
- 'ride_models': ride_models,
+ "categorical": {
+ "categories": categories,
+ "statuses": statuses,
+ "roller_coaster_types": roller_coaster_types,
+ "track_materials": track_materials,
+ "propulsion_systems": propulsion_systems,
+ "parks": parks,
+ "park_areas": park_areas,
+ "manufacturers": manufacturers,
+ "designers": designers,
+ "ride_models": ride_models,
},
- 'ranges': {
- 'rating': {
- 'min': float(ride_stats['min_rating'] or 1),
- 'max': float(ride_stats['max_rating'] or 10),
- 'step': 0.1,
- 'unit': 'stars'
+ "ranges": {
+ "rating": {
+ "min": float(ride_stats["min_rating"] or 1),
+ "max": float(ride_stats["max_rating"] or 10),
+ "step": 0.1,
+ "unit": "stars",
},
- 'height_requirement': {
- 'min': ride_stats['min_height_req'] or 30,
- 'max': ride_stats['max_height_req'] or 90,
- 'step': 1,
- 'unit': 'inches'
+ "height_requirement": {
+ "min": ride_stats["min_height_req"] or 30,
+ "max": ride_stats["max_height_req"] or 90,
+ "step": 1,
+ "unit": "inches",
},
- 'capacity': {
- 'min': ride_stats['min_capacity'] or 0,
- 'max': ride_stats['max_capacity'] or 5000,
- 'step': 50,
- 'unit': 'riders/hour'
+ "capacity": {
+ "min": ride_stats["min_capacity"] or 0,
+ "max": ride_stats["max_capacity"] or 5000,
+ "step": 50,
+ "unit": "riders/hour",
},
- 'ride_duration': {
- 'min': ride_stats['min_duration'] or 0,
- 'max': ride_stats['max_duration'] or 600,
- 'step': 10,
- 'unit': 'seconds'
+ "ride_duration": {
+ "min": ride_stats["min_duration"] or 0,
+ "max": ride_stats["max_duration"] or 600,
+ "step": 10,
+ "unit": "seconds",
},
- 'opening_year': {
- 'min': ride_stats['min_year'] or 1800,
- 'max': ride_stats['max_year'] or 2030,
- 'step': 1,
- 'unit': 'year'
+ "opening_year": {
+ "min": ride_stats["min_year"] or 1800,
+ "max": ride_stats["max_year"] or 2030,
+ "step": 1,
+ "unit": "year",
},
- 'height_ft': {
- 'min': float(coaster_stats['min_height_ft'] or 0),
- 'max': float(coaster_stats['max_height_ft'] or 500),
- 'step': 5,
- 'unit': 'feet'
+ "height_ft": {
+ "min": float(coaster_stats["min_height_ft"] or 0),
+ "max": float(coaster_stats["max_height_ft"] or 500),
+ "step": 5,
+ "unit": "feet",
},
- 'length_ft': {
- 'min': float(coaster_stats['min_length_ft'] or 0),
- 'max': float(coaster_stats['max_length_ft'] or 10000),
- 'step': 100,
- 'unit': 'feet'
+ "length_ft": {
+ "min": float(coaster_stats["min_length_ft"] or 0),
+ "max": float(coaster_stats["max_length_ft"] or 10000),
+ "step": 100,
+ "unit": "feet",
},
- 'speed_mph': {
- 'min': float(coaster_stats['min_speed_mph'] or 0),
- 'max': float(coaster_stats['max_speed_mph'] or 150),
- 'step': 5,
- 'unit': 'mph'
+ "speed_mph": {
+ "min": float(coaster_stats["min_speed_mph"] or 0),
+ "max": float(coaster_stats["max_speed_mph"] or 150),
+ "step": 5,
+ "unit": "mph",
},
- 'inversions': {
- 'min': coaster_stats['min_inversions'] or 0,
- 'max': coaster_stats['max_inversions'] or 20,
- 'step': 1,
- 'unit': 'inversions'
+ "inversions": {
+ "min": coaster_stats["min_inversions"] or 0,
+ "max": coaster_stats["max_inversions"] or 20,
+ "step": 1,
+ "unit": "inversions",
},
},
- 'total_count': Ride.objects.count(),
+ "total_count": Ride.objects.count(),
}
def _get_category_label(self, category: str) -> str:
"""Convert category code to human-readable label."""
category_labels = {
- 'RC': 'Roller Coaster',
- 'DR': 'Dark Ride',
- 'FR': 'Flat Ride',
- 'WR': 'Water Ride',
- 'TR': 'Transport Ride',
- 'OT': 'Other',
+ "RC": "Roller Coaster",
+ "DR": "Dark Ride",
+ "FR": "Flat Ride",
+ "WR": "Water Ride",
+ "TR": "Transport Ride",
+ "OT": "Other",
}
if category in category_labels:
return category_labels[category]
@@ -718,14 +695,14 @@ class SmartRideLoader:
def _get_status_label(self, status: str) -> str:
"""Convert status code to human-readable label."""
status_labels = {
- 'OPERATING': 'Operating',
- 'CLOSED_TEMP': 'Temporarily Closed',
- 'SBNO': 'Standing But Not Operating',
- 'CLOSING': 'Closing Soon',
- 'CLOSED_PERM': 'Permanently Closed',
- 'UNDER_CONSTRUCTION': 'Under Construction',
- 'DEMOLISHED': 'Demolished',
- 'RELOCATED': 'Relocated',
+ "OPERATING": "Operating",
+ "CLOSED_TEMP": "Temporarily Closed",
+ "SBNO": "Standing But Not Operating",
+ "CLOSING": "Closing Soon",
+ "CLOSED_PERM": "Permanently Closed",
+ "UNDER_CONSTRUCTION": "Under Construction",
+ "DEMOLISHED": "Demolished",
+ "RELOCATED": "Relocated",
}
if status in status_labels:
return status_labels[status]
@@ -735,19 +712,19 @@ class SmartRideLoader:
def _get_rc_type_label(self, rc_type: str) -> str:
"""Convert roller coaster type to human-readable label."""
rc_type_labels = {
- 'SITDOWN': 'Sit Down',
- 'INVERTED': 'Inverted',
- 'SUSPENDED': 'Suspended',
- 'FLOORLESS': 'Floorless',
- 'FLYING': 'Flying',
- 'WING': 'Wing',
- 'DIVE': 'Dive',
- 'SPINNING': 'Spinning',
- 'WILD_MOUSE': 'Wild Mouse',
- 'BOBSLED': 'Bobsled',
- 'PIPELINE': 'Pipeline',
- 'FOURTH_DIMENSION': '4th Dimension',
- 'FAMILY': 'Family',
+ "SITDOWN": "Sit Down",
+ "INVERTED": "Inverted",
+ "SUSPENDED": "Suspended",
+ "FLOORLESS": "Floorless",
+ "FLYING": "Flying",
+ "WING": "Wing",
+ "DIVE": "Dive",
+ "SPINNING": "Spinning",
+ "WILD_MOUSE": "Wild Mouse",
+ "BOBSLED": "Bobsled",
+ "PIPELINE": "Pipeline",
+ "FOURTH_DIMENSION": "4th Dimension",
+ "FAMILY": "Family",
}
if rc_type in rc_type_labels:
return rc_type_labels[rc_type]
@@ -757,9 +734,9 @@ class SmartRideLoader:
def _get_track_material_label(self, material: str) -> str:
"""Convert track material to human-readable label."""
material_labels = {
- 'STEEL': 'Steel',
- 'WOOD': 'Wood',
- 'HYBRID': 'Hybrid (Steel/Wood)',
+ "STEEL": "Steel",
+ "WOOD": "Wood",
+ "HYBRID": "Hybrid (Steel/Wood)",
}
if material in material_labels:
return material_labels[material]
@@ -769,15 +746,15 @@ class SmartRideLoader:
def _get_propulsion_system_label(self, propulsion_system: str) -> str:
"""Convert propulsion system to human-readable label."""
propulsion_labels = {
- 'CHAIN': 'Chain Lift',
- 'LSM': 'Linear Synchronous Motor',
- 'LIM': 'Linear Induction Motor',
- 'HYDRAULIC': 'Hydraulic Launch',
- 'PNEUMATIC': 'Pneumatic Launch',
- 'CABLE': 'Cable Lift',
- 'FLYWHEEL': 'Flywheel Launch',
- 'GRAVITY': 'Gravity',
- 'NONE': 'No Propulsion System',
+ "CHAIN": "Chain Lift",
+ "LSM": "Linear Synchronous Motor",
+ "LIM": "Linear Induction Motor",
+ "HYDRAULIC": "Hydraulic Launch",
+ "PNEUMATIC": "Pneumatic Launch",
+ "CABLE": "Cable Lift",
+ "FLYWHEEL": "Flywheel Launch",
+ "GRAVITY": "Gravity",
+ "NONE": "No Propulsion System",
}
if propulsion_system in propulsion_labels:
return propulsion_labels[propulsion_system]
diff --git a/backend/apps/rides/services/location_service.py b/backend/apps/rides/services/location_service.py
index 745cd0bb..a9688b5a 100644
--- a/backend/apps/rides/services/location_service.py
+++ b/backend/apps/rides/services/location_service.py
@@ -69,9 +69,7 @@ class RideLocationService:
return ride_location
@classmethod
- def update_ride_location(
- cls, ride_location: RideLocation, **updates
- ) -> RideLocation:
+ def update_ride_location(cls, ride_location: RideLocation, **updates) -> RideLocation:
"""
Update ride location with validation.
@@ -149,9 +147,7 @@ class RideLocationService:
if park:
queryset = queryset.filter(ride__park=park)
- return list(
- queryset.select_related("ride", "ride__park").order_by("point__distance")
- )
+ return list(queryset.select_related("ride", "ride__park").order_by("point__distance"))
@classmethod
def get_ride_navigation_info(cls, ride_location: RideLocation) -> dict[str, Any]:
@@ -249,9 +245,7 @@ class RideLocationService:
# Rough conversion: 1 degree latitude ≈ 111,000 meters
# 1 degree longitude varies by latitude, but we'll use a rough approximation
lat_offset = offset[0] / 111000 # North offset in degrees
- lon_offset = offset[1] / (
- 111000 * abs(park_location.latitude) * 0.01
- ) # East offset
+ lon_offset = offset[1] / (111000 * abs(park_location.latitude) * 0.01) # East offset
estimated_lat = park_location.latitude + lat_offset
estimated_lon = park_location.longitude + lon_offset
@@ -277,9 +271,7 @@ class RideLocationService:
return updated_count
# Get all rides in the park that don't have precise coordinates
- ride_locations = RideLocation.objects.filter(
- ride__park=park, point__isnull=True
- ).select_related("ride")
+ ride_locations = RideLocation.objects.filter(ride__park=park, point__isnull=True).select_related("ride")
for ride_location in ride_locations:
# Try to search for the specific ride within the park area
@@ -312,22 +304,15 @@ class RideLocationService:
# Look for results that might be the ride
for result in results:
display_name = result.get("display_name", "").lower()
- if (
- ride_location.ride.name.lower() in display_name
- and park.name.lower() in display_name
- ):
+ if ride_location.ride.name.lower() in display_name and park.name.lower() in display_name:
# Update the ride location
- ride_location.set_coordinates(
- float(result["lat"]), float(result["lon"])
- )
+ ride_location.set_coordinates(float(result["lat"]), float(result["lon"]))
ride_location.save()
updated_count += 1
break
except Exception as e:
- logger.warning(
- f"Error updating ride location for {ride_location.ride.name}: {str(e)}"
- )
+ logger.warning(f"Error updating ride location for {ride_location.ride.name}: {str(e)}")
continue
return updated_count
@@ -346,9 +331,7 @@ class RideLocationService:
area_map = {}
ride_locations = (
- RideLocation.objects.filter(ride__park=park)
- .select_related("ride")
- .order_by("park_area", "ride__name")
+ RideLocation.objects.filter(ride__park=park).select_related("ride").order_by("park_area", "ride__name")
)
for ride_location in ride_locations:
diff --git a/backend/apps/rides/services/media_service.py b/backend/apps/rides/services/media_service.py
index ef61a823..2cebe014 100644
--- a/backend/apps/rides/services/media_service.py
+++ b/backend/apps/rides/services/media_service.py
@@ -143,11 +143,7 @@ class RideMediaService:
Returns:
List of RidePhoto instances
"""
- return list(
- ride.photos.filter(photo_type=photo_type, is_approved=True).order_by(
- "-created_at"
- )
- )
+ return list(ride.photos.filter(photo_type=photo_type, is_approved=True).order_by("-created_at"))
@staticmethod
def set_primary_photo(ride: Ride, photo: RidePhoto) -> bool:
@@ -218,9 +214,7 @@ class RideMediaService:
photo.image.delete(save=False)
photo.delete()
- logger.info(
- f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}"
- )
+ logger.info(f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}")
return True
except Exception as e:
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
@@ -272,9 +266,7 @@ class RideMediaService:
if RideMediaService.approve_photo(photo, approved_by):
approved_count += 1
- logger.info(
- f"Bulk approved {approved_count} photos by user {approved_by.username}"
- )
+ logger.info(f"Bulk approved {approved_count} photos by user {approved_by.username}")
return approved_count
@staticmethod
@@ -289,9 +281,7 @@ class RideMediaService:
List of construction RidePhoto instances ordered by date taken
"""
return list(
- ride.photos.filter(photo_type="construction", is_approved=True).order_by(
- "date_taken", "created_at"
- )
+ ride.photos.filter(photo_type="construction", is_approved=True).order_by("date_taken", "created_at")
)
@staticmethod
diff --git a/backend/apps/rides/services/ranking_service.py b/backend/apps/rides/services/ranking_service.py
index 9dba45b0..7d277afb 100644
--- a/backend/apps/rides/services/ranking_service.py
+++ b/backend/apps/rides/services/ranking_service.py
@@ -53,9 +53,7 @@ class RideRankingService:
Dictionary with statistics about the ranking calculation
"""
start_time = timezone.now()
- self.logger.info(
- f"Starting ranking calculation for category: {category or 'ALL'}"
- )
+ self.logger.info(f"Starting ranking calculation for category: {category or 'ALL'}")
try:
with transaction.atomic():
@@ -87,9 +85,7 @@ class RideRankingService:
self._cleanup_old_data()
duration = (timezone.now() - start_time).total_seconds()
- self.logger.info(
- f"Ranking calculation completed in {duration:.2f} seconds"
- )
+ self.logger.info(f"Ranking calculation completed in {duration:.2f} seconds")
return {
"status": "success",
@@ -113,9 +109,7 @@ class RideRankingService:
"""
queryset = (
Ride.objects.filter(status="OPERATING", reviews__is_published=True)
- .annotate(
- review_count=Count("reviews", filter=Q(reviews__is_published=True))
- )
+ .annotate(review_count=Count("reviews", filter=Q(reviews__is_published=True)))
.filter(review_count__gt=0)
)
@@ -124,9 +118,7 @@ class RideRankingService:
return list(queryset.distinct())
- def _calculate_all_comparisons(
- self, rides: list[Ride]
- ) -> dict[tuple[int, int], RidePairComparison]:
+ def _calculate_all_comparisons(self, rides: list[Ride]) -> dict[tuple[int, int], RidePairComparison]:
"""
Calculate pairwise comparisons for all ride pairs.
@@ -146,15 +138,11 @@ class RideRankingService:
processed += 1
if processed % 100 == 0:
- self.logger.debug(
- f"Processed {processed}/{total_pairs} comparisons"
- )
+ self.logger.debug(f"Processed {processed}/{total_pairs} comparisons")
return comparisons
- def _calculate_pairwise_comparison(
- self, ride_a: Ride, ride_b: Ride
- ) -> RidePairComparison | None:
+ def _calculate_pairwise_comparison(self, ride_a: Ride, ride_b: Ride) -> RidePairComparison | None:
"""
Calculate the pairwise comparison between two rides.
@@ -163,15 +151,11 @@ class RideRankingService:
"""
# Get mutual riders (users who have rated both rides)
ride_a_reviewers = set(
- RideReview.objects.filter(ride=ride_a, is_published=True).values_list(
- "user_id", flat=True
- )
+ RideReview.objects.filter(ride=ride_a, is_published=True).values_list("user_id", flat=True)
)
ride_b_reviewers = set(
- RideReview.objects.filter(ride=ride_b, is_published=True).values_list(
- "user_id", flat=True
- )
+ RideReview.objects.filter(ride=ride_b, is_published=True).values_list("user_id", flat=True)
)
mutual_riders = ride_a_reviewers & ride_b_reviewers
@@ -183,16 +167,12 @@ class RideRankingService:
# Get ratings from mutual riders
ride_a_ratings = {
review.user_id: review.rating
- for review in RideReview.objects.filter(
- ride=ride_a, user_id__in=mutual_riders, is_published=True
- )
+ for review in RideReview.objects.filter(ride=ride_a, user_id__in=mutual_riders, is_published=True)
}
ride_b_ratings = {
review.user_id: review.rating
- for review in RideReview.objects.filter(
- ride=ride_b, user_id__in=mutual_riders, is_published=True
- )
+ for review in RideReview.objects.filter(ride=ride_b, user_id__in=mutual_riders, is_published=True)
}
# Count wins and ties
@@ -212,12 +192,8 @@ class RideRankingService:
ties += 1
# Calculate average ratings from mutual riders
- ride_a_avg = (
- sum(ride_a_ratings.values()) / len(ride_a_ratings) if ride_a_ratings else 0
- )
- ride_b_avg = (
- sum(ride_b_ratings.values()) / len(ride_b_ratings) if ride_b_ratings else 0
- )
+ ride_a_avg = sum(ride_a_ratings.values()) / len(ride_a_ratings) if ride_a_ratings else 0
+ ride_b_avg = sum(ride_b_ratings.values()) / len(ride_b_ratings) if ride_b_ratings else 0
# Create or update comparison record
comparison, created = RidePairComparison.objects.update_or_create(
@@ -228,16 +204,8 @@ class RideRankingService:
"ride_b_wins": ride_b_wins if ride_a.id < ride_b.id else ride_a_wins,
"ties": ties,
"mutual_riders_count": len(mutual_riders),
- "ride_a_avg_rating": (
- Decimal(str(ride_a_avg))
- if ride_a.id < ride_b.id
- else Decimal(str(ride_b_avg))
- ),
- "ride_b_avg_rating": (
- Decimal(str(ride_b_avg))
- if ride_a.id < ride_b.id
- else Decimal(str(ride_a_avg))
- ),
+ "ride_a_avg_rating": (Decimal(str(ride_a_avg)) if ride_a.id < ride_b.id else Decimal(str(ride_b_avg))),
+ "ride_b_avg_rating": (Decimal(str(ride_b_avg)) if ride_a.id < ride_b.id else Decimal(str(ride_a_avg))),
},
)
@@ -294,16 +262,12 @@ class RideRankingService:
# Calculate winning percentage (ties count as 0.5)
total_comparisons = wins + losses + ties
if total_comparisons > 0:
- winning_percentage = Decimal(
- str((wins + 0.5 * ties) / total_comparisons)
- )
+ winning_percentage = Decimal(str((wins + 0.5 * ties) / total_comparisons))
else:
winning_percentage = Decimal("0.5")
# Get average rating and reviewer count
- ride_stats = RideReview.objects.filter(
- ride=ride, is_published=True
- ).aggregate(
+ ride_stats = RideReview.objects.filter(ride=ride, is_published=True).aggregate(
avg_rating=Avg("rating"), reviewer_count=Count("user", distinct=True)
)
@@ -356,11 +320,7 @@ class RideRankingService:
tied_group = [rankings[i]]
j = i + 1
- while (
- j < len(rankings)
- and rankings[j]["winning_percentage"]
- == rankings[i]["winning_percentage"]
- ):
+ while j < len(rankings) and rankings[j]["winning_percentage"] == rankings[i]["winning_percentage"]:
tied_group.append(rankings[j])
j += 1
@@ -462,9 +422,7 @@ class RideRankingService:
cutoff_date = timezone.now() - timezone.timedelta(days=days_to_keep)
# Delete old snapshots
- deleted_snapshots = RankingSnapshot.objects.filter(
- snapshot_date__lt=cutoff_date.date()
- ).delete()
+ deleted_snapshots = RankingSnapshot.objects.filter(snapshot_date__lt=cutoff_date.date()).delete()
if deleted_snapshots[0] > 0:
self.logger.info(f"Deleted {deleted_snapshots[0]} old ranking snapshots")
@@ -486,9 +444,7 @@ class RideRankingService:
)
# Get ranking history
- history = RankingSnapshot.objects.filter(ride=ride).order_by(
- "-snapshot_date"
- )[:30]
+ history = RankingSnapshot.objects.filter(ride=ride).order_by("-snapshot_date")[:30]
return {
"current_rank": ranking.rank,
@@ -501,32 +457,18 @@ class RideRankingService:
"last_calculated": ranking.last_calculated,
"head_to_head": [
{
- "opponent": (
- comp.ride_b if comp.ride_a_id == ride.id else comp.ride_a
- ),
+ "opponent": (comp.ride_b if comp.ride_a_id == ride.id else comp.ride_a),
"result": (
"win"
if (
- (
- comp.ride_a_id == ride.id
- and comp.ride_a_wins > comp.ride_b_wins
- )
- or (
- comp.ride_b_id == ride.id
- and comp.ride_b_wins > comp.ride_a_wins
- )
+ (comp.ride_a_id == ride.id and comp.ride_a_wins > comp.ride_b_wins)
+ or (comp.ride_b_id == ride.id and comp.ride_b_wins > comp.ride_a_wins)
)
else (
"loss"
if (
- (
- comp.ride_a_id == ride.id
- and comp.ride_a_wins < comp.ride_b_wins
- )
- or (
- comp.ride_b_id == ride.id
- and comp.ride_b_wins < comp.ride_a_wins
- )
+ (comp.ride_a_id == ride.id and comp.ride_a_wins < comp.ride_b_wins)
+ or (comp.ride_b_id == ride.id and comp.ride_b_wins < comp.ride_a_wins)
)
else "tie"
)
diff --git a/backend/apps/rides/services/search.py b/backend/apps/rides/services/search.py
index b8076881..c183b366 100644
--- a/backend/apps/rides/services/search.py
+++ b/backend/apps/rides/services/search.py
@@ -127,9 +127,7 @@ class RideSearchService:
# Apply text search with ranking
if filters.get("global_search"):
- queryset, search_rank = self._apply_full_text_search(
- queryset, filters["global_search"]
- )
+ queryset, search_rank = self._apply_full_text_search(queryset, filters["global_search"])
search_metadata["search_applied"] = True
search_metadata["search_term"] = filters["global_search"]
else:
@@ -176,9 +174,7 @@ class RideSearchService:
"applied_filters": self._get_applied_filters_summary(filters),
}
- def _apply_full_text_search(
- self, queryset, search_term: str
- ) -> tuple[models.QuerySet, models.Expression]:
+ def _apply_full_text_search(self, queryset, search_term: str) -> tuple[models.QuerySet, models.Expression]:
"""
Apply PostgreSQL full-text search with ranking and fuzzy matching.
"""
@@ -201,17 +197,14 @@ class RideSearchService:
search_query = SearchQuery(search_term, config="english")
# Calculate search rank
- search_rank = SearchRank(
- search_vector, search_query, weights=self.SEARCH_RANK_WEIGHTS
- )
+ search_rank = SearchRank(search_vector, search_query, weights=self.SEARCH_RANK_WEIGHTS)
# Apply trigram similarity for fuzzy matching on name
trigram_similarity = TrigramSimilarity("name", search_term)
# Combine full-text search with trigram similarity
queryset = queryset.annotate(trigram_similarity=trigram_similarity).filter(
- Q(search_vector=search_query)
- | Q(trigram_similarity__gte=self.TRIGRAM_SIMILARITY_THRESHOLD)
+ Q(search_vector=search_query) | Q(trigram_similarity__gte=self.TRIGRAM_SIMILARITY_THRESHOLD)
)
# Use the greatest of search rank and trigram similarity for final ranking
@@ -219,36 +212,22 @@ class RideSearchService:
return queryset, final_rank
- def _apply_basic_info_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_basic_info_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply basic information filters."""
# Category filter (multi-select)
if filters.get("category"):
- categories = (
- filters["category"]
- if isinstance(filters["category"], list)
- else [filters["category"]]
- )
+ categories = filters["category"] if isinstance(filters["category"], list) else [filters["category"]]
queryset = queryset.filter(category__in=categories)
# Status filter (multi-select)
if filters.get("status"):
- statuses = (
- filters["status"]
- if isinstance(filters["status"], list)
- else [filters["status"]]
- )
+ statuses = filters["status"] if isinstance(filters["status"], list) else [filters["status"]]
queryset = queryset.filter(status__in=statuses)
# Park filter (multi-select)
if filters.get("park"):
- parks = (
- filters["park"]
- if isinstance(filters["park"], list)
- else [filters["park"]]
- )
+ parks = filters["park"] if isinstance(filters["park"], list) else [filters["park"]]
if isinstance(parks[0], str): # If slugs provided
queryset = queryset.filter(park__slug__in=parks)
else: # If IDs provided
@@ -256,11 +235,7 @@ class RideSearchService:
# Park area filter (multi-select)
if filters.get("park_area"):
- areas = (
- filters["park_area"]
- if isinstance(filters["park_area"], list)
- else [filters["park_area"]]
- )
+ areas = filters["park_area"] if isinstance(filters["park_area"], list) else [filters["park_area"]]
if isinstance(areas[0], str): # If slugs provided
queryset = queryset.filter(park_area__slug__in=areas)
else: # If IDs provided
@@ -297,9 +272,7 @@ class RideSearchService:
return queryset
- def _apply_height_safety_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_height_safety_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply height and safety requirement filters."""
# Minimum height range
@@ -320,9 +293,7 @@ class RideSearchService:
return queryset
- def _apply_performance_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_performance_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply performance metric filters."""
# Capacity range
@@ -337,13 +308,9 @@ class RideSearchService:
if filters.get("duration_range"):
duration_range = filters["duration_range"]
if duration_range.get("min") is not None:
- queryset = queryset.filter(
- ride_duration_seconds__gte=duration_range["min"]
- )
+ queryset = queryset.filter(ride_duration_seconds__gte=duration_range["min"])
if duration_range.get("max") is not None:
- queryset = queryset.filter(
- ride_duration_seconds__lte=duration_range["max"]
- )
+ queryset = queryset.filter(ride_duration_seconds__lte=duration_range["max"])
# Rating range
if filters.get("rating_range"):
@@ -355,17 +322,13 @@ class RideSearchService:
return queryset
- def _apply_relationship_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_relationship_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply relationship filters (manufacturer, designer, ride model)."""
# Manufacturer filter (multi-select)
if filters.get("manufacturer"):
manufacturers = (
- filters["manufacturer"]
- if isinstance(filters["manufacturer"], list)
- else [filters["manufacturer"]]
+ filters["manufacturer"] if isinstance(filters["manufacturer"], list) else [filters["manufacturer"]]
)
if isinstance(manufacturers[0], str): # If slugs provided
queryset = queryset.filter(manufacturer__slug__in=manufacturers)
@@ -374,11 +337,7 @@ class RideSearchService:
# Designer filter (multi-select)
if filters.get("designer"):
- designers = (
- filters["designer"]
- if isinstance(filters["designer"], list)
- else [filters["designer"]]
- )
+ designers = filters["designer"] if isinstance(filters["designer"], list) else [filters["designer"]]
if isinstance(designers[0], str): # If slugs provided
queryset = queryset.filter(designer__slug__in=designers)
else: # If IDs provided
@@ -386,11 +345,7 @@ class RideSearchService:
# Ride model filter (multi-select)
if filters.get("ride_model"):
- models_list = (
- filters["ride_model"]
- if isinstance(filters["ride_model"], list)
- else [filters["ride_model"]]
- )
+ models_list = filters["ride_model"] if isinstance(filters["ride_model"], list) else [filters["ride_model"]]
if isinstance(models_list[0], str): # If slugs provided
queryset = queryset.filter(ride_model__slug__in=models_list)
else: # If IDs provided
@@ -398,9 +353,7 @@ class RideSearchService:
return queryset
- def _apply_roller_coaster_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_roller_coaster_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply roller coaster specific filters."""
queryset = self._apply_numeric_range_filter(
queryset, filters, "height_ft_range", "rollercoasterstats__height_ft"
@@ -426,14 +379,8 @@ class RideSearchService:
# Coaster type filter (multi-select)
if filters.get("coaster_type"):
- types = (
- filters["coaster_type"]
- if isinstance(filters["coaster_type"], list)
- else [filters["coaster_type"]]
- )
- queryset = queryset.filter(
- rollercoasterstats__roller_coaster_type__in=types
- )
+ types = filters["coaster_type"] if isinstance(filters["coaster_type"], list) else [filters["coaster_type"]]
+ queryset = queryset.filter(rollercoasterstats__roller_coaster_type__in=types)
# Propulsion system filter (multi-select)
if filters.get("propulsion_system"):
@@ -457,18 +404,12 @@ class RideSearchService:
if filters.get(filter_key):
range_filter = filters[filter_key]
if range_filter.get("min") is not None:
- queryset = queryset.filter(
- **{f"{field_name}__gte": range_filter["min"]}
- )
+ queryset = queryset.filter(**{f"{field_name}__gte": range_filter["min"]})
if range_filter.get("max") is not None:
- queryset = queryset.filter(
- **{f"{field_name}__lte": range_filter["max"]}
- )
+ queryset = queryset.filter(**{f"{field_name}__lte": range_filter["max"]})
return queryset
- def _apply_company_filters(
- self, queryset, filters: dict[str, Any]
- ) -> models.QuerySet:
+ def _apply_company_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
"""Apply company-related filters."""
# Manufacturer roles filter
@@ -518,13 +459,9 @@ class RideSearchService:
return queryset.order_by("-search_rank", "name")
# Apply the sorting
- return queryset.order_by(
- sort_field, "name"
- ) # Always add name as secondary sort
+ return queryset.order_by(sort_field, "name") # Always add name as secondary sort
- def _add_search_highlights(
- self, results: list[Ride], search_term: str
- ) -> list[Ride]:
+ def _add_search_highlights(self, results: list[Ride], search_term: str) -> list[Ride]:
"""Add search highlights to results using SearchHeadline."""
if not search_term or not results:
@@ -601,9 +538,7 @@ class RideSearchService:
else:
raise ValueError(f"Unknown filter key: {filter_key}")
- def get_search_suggestions(
- self, query: str, limit: int = 10
- ) -> list[dict[str, Any]]:
+ def get_search_suggestions(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
"""
Get search suggestions for autocomplete functionality.
"""
@@ -686,17 +621,11 @@ class RideSearchService:
# Apply context filters to narrow down options
if context_filters:
temp_filters = context_filters.copy()
- temp_filters.pop(
- filter_type, None
- ) # Remove the filter we're getting options for
+ temp_filters.pop(filter_type, None) # Remove the filter we're getting options for
base_queryset = self._apply_all_filters(base_queryset, temp_filters)
if filter_type == "park":
- return list(
- base_queryset.values("park__name", "park__slug")
- .distinct()
- .order_by("park__name")
- )
+ return list(base_queryset.values("park__name", "park__slug").distinct().order_by("park__name"))
elif filter_type == "manufacturer":
return list(
diff --git a/backend/apps/rides/services/status_service.py b/backend/apps/rides/services/status_service.py
index 2a299047..80401c38 100644
--- a/backend/apps/rides/services/status_service.py
+++ b/backend/apps/rides/services/status_service.py
@@ -3,7 +3,6 @@ Services for ride status transitions and management.
Following Django styleguide pattern for business logic encapsulation.
"""
-
from django.contrib.auth.models import AbstractBaseUser
from django.db import transaction
@@ -34,9 +33,7 @@ class RideStatusService:
return ride
@staticmethod
- def close_ride_temporarily(
- *, ride_id: int, user: AbstractBaseUser | None = None
- ) -> Ride:
+ def close_ride_temporarily(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
"""
Temporarily close a ride.
@@ -56,9 +53,7 @@ class RideStatusService:
return ride
@staticmethod
- def mark_ride_sbno(
- *, ride_id: int, user: AbstractBaseUser | None = None
- ) -> Ride:
+ def mark_ride_sbno(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
"""
Mark a ride as SBNO (Standing But Not Operating).
@@ -111,9 +106,7 @@ class RideStatusService:
return ride
@staticmethod
- def close_ride_permanently(
- *, ride_id: int, user: AbstractBaseUser | None = None
- ) -> Ride:
+ def close_ride_permanently(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
"""
Permanently close a ride.
diff --git a/backend/apps/rides/services_core.py b/backend/apps/rides/services_core.py
index fb3e7ce1..8cbcbb64 100644
--- a/backend/apps/rides/services_core.py
+++ b/backend/apps/rides/services_core.py
@@ -139,9 +139,7 @@ class RideService:
return ride
@staticmethod
- def close_ride_temporarily(
- *, ride_id: int, user: UserType | None = None
- ) -> Ride:
+ def close_ride_temporarily(*, ride_id: int, user: UserType | None = None) -> Ride:
"""
Temporarily close a ride.
@@ -161,9 +159,7 @@ class RideService:
return ride
@staticmethod
- def mark_ride_sbno(
- *, ride_id: int, user: UserType | None = None
- ) -> Ride:
+ def mark_ride_sbno(*, ride_id: int, user: UserType | None = None) -> Ride:
"""
Mark a ride as SBNO (Standing But Not Operating).
@@ -216,9 +212,7 @@ class RideService:
return ride
@staticmethod
- def close_ride_permanently(
- *, ride_id: int, user: UserType | None = None
- ) -> Ride:
+ def close_ride_permanently(*, ride_id: int, user: UserType | None = None) -> Ride:
"""
Permanently close a ride.
@@ -258,9 +252,7 @@ class RideService:
return ride
@staticmethod
- def relocate_ride(
- *, ride_id: int, new_park_id: int, user: UserType | None = None
- ) -> Ride:
+ def relocate_ride(*, ride_id: int, new_park_id: int, user: UserType | None = None) -> Ride:
"""
Relocate a ride to a new park.
@@ -336,12 +328,7 @@ class RideService:
"""
from apps.moderation.services import ModerationService
- result = {
- 'manufacturers': [],
- 'designers': [],
- 'ride_models': [],
- 'total_submissions': 0
- }
+ result = {"manufacturers": [], "designers": [], "ride_models": [], "total_submissions": 0}
# Check for new manufacturer
manufacturer_name = form_data.get("manufacturer_search")
@@ -354,8 +341,8 @@ class RideService:
reason=f"New manufacturer suggested: {manufacturer_name}",
)
if submission:
- result['manufacturers'].append(submission.id)
- result['total_submissions'] += 1
+ result["manufacturers"].append(submission.id)
+ result["total_submissions"] += 1
# Check for new designer
designer_name = form_data.get("designer_search")
@@ -368,8 +355,8 @@ class RideService:
reason=f"New designer suggested: {designer_name}",
)
if submission:
- result['designers'].append(submission.id)
- result['total_submissions'] += 1
+ result["designers"].append(submission.id)
+ result["total_submissions"] += 1
# Check for new ride model
ride_model_name = form_data.get("ride_model_search")
@@ -386,7 +373,7 @@ class RideService:
reason=f"New ride model suggested: {ride_model_name}",
)
if submission:
- result['ride_models'].append(submission.id)
- result['total_submissions'] += 1
+ result["ride_models"].append(submission.id)
+ result["total_submissions"] += 1
return result
diff --git a/backend/apps/rides/signals.py b/backend/apps/rides/signals.py
index 17c58d2c..9f1fa594 100644
--- a/backend/apps/rides/signals.py
+++ b/backend/apps/rides/signals.py
@@ -13,6 +13,7 @@ logger = logging.getLogger(__name__)
# Computed Field Maintenance
# =============================================================================
+
def update_ride_search_text(ride):
"""
Update ride's search_text computed field.
@@ -25,7 +26,7 @@ def update_ride_search_text(ride):
try:
ride._populate_computed_fields()
- ride.save(update_fields=['search_text'])
+ ride.save(update_fields=["search_text"])
logger.debug(f"Updated search_text for ride {ride.pk}")
except Exception as e:
logger.exception(f"Failed to update search_text for ride {ride.pk}: {e}")
@@ -47,22 +48,16 @@ def handle_ride_status(sender, instance, **kwargs):
if today >= instance.closing_date and instance.status == "CLOSING":
target_status = instance.post_closing_status or "SBNO"
- logger.info(
- f"Ride {instance.pk} closing date reached, "
- f"transitioning to {target_status}"
- )
+ logger.info(f"Ride {instance.pk} closing date reached, " f"transitioning to {target_status}")
# Try to use FSM transition method if available
- transition_method_name = f'transition_to_{target_status.lower()}'
+ transition_method_name = f"transition_to_{target_status.lower()}"
if hasattr(instance, transition_method_name):
# Check if transition is allowed before attempting
- if hasattr(instance, 'can_proceed'):
- can_proceed = getattr(instance, f'can_transition_to_{target_status.lower()}', None)
+ if hasattr(instance, "can_proceed"):
+ can_proceed = getattr(instance, f"can_transition_to_{target_status.lower()}", None)
if can_proceed and callable(can_proceed) and not can_proceed():
- logger.warning(
- f"FSM transition to {target_status} not allowed "
- f"for ride {instance.pk}"
- )
+ logger.warning(f"FSM transition to {target_status} not allowed " f"for ride {instance.pk}")
# Fall back to direct status change
instance.status = target_status
instance.status_since = instance.closing_date
@@ -72,13 +67,9 @@ def handle_ride_status(sender, instance, **kwargs):
method = getattr(instance, transition_method_name)
method()
instance.status_since = instance.closing_date
- logger.info(
- f"Applied FSM transition to {target_status} for ride {instance.pk}"
- )
+ logger.info(f"Applied FSM transition to {target_status} for ride {instance.pk}")
except Exception as e:
- logger.exception(
- f"Failed to apply FSM transition for ride {instance.pk}: {e}"
- )
+ logger.exception(f"Failed to apply FSM transition for ride {instance.pk}: {e}")
# Fall back to direct status change
instance.status = target_status
instance.status_since = instance.closing_date
@@ -101,23 +92,20 @@ def validate_closing_status(sender, instance, **kwargs):
if instance.status == "CLOSING":
# Ensure post_closing_status is set
if not instance.post_closing_status:
- logger.warning(
- f"Ride {instance.pk} entering CLOSING without post_closing_status set"
- )
+ logger.warning(f"Ride {instance.pk} entering CLOSING without post_closing_status set")
# Default to SBNO if not set
instance.post_closing_status = "SBNO"
# Ensure closing_date is set
if not instance.closing_date:
- logger.warning(
- f"Ride {instance.pk} entering CLOSING without closing_date set"
- )
+ logger.warning(f"Ride {instance.pk} entering CLOSING without closing_date set")
# Default to today's date
instance.closing_date = timezone.now().date()
# FSM transition signal handlers
+
def handle_ride_transition_to_closing(instance, source, target, user, **kwargs):
"""
Validate transition to CLOSING status.
@@ -134,20 +122,15 @@ def handle_ride_transition_to_closing(instance, source, target, user, **kwargs):
Returns:
True if transition should proceed, False to abort.
"""
- if target != 'CLOSING':
+ if target != "CLOSING":
return True
if not instance.post_closing_status:
- logger.error(
- f"Cannot transition ride {instance.pk} to CLOSING: "
- "post_closing_status not set"
- )
+ logger.error(f"Cannot transition ride {instance.pk} to CLOSING: " "post_closing_status not set")
return False
if not instance.closing_date:
- logger.warning(
- f"Ride {instance.pk} transitioning to CLOSING without closing_date"
- )
+ logger.warning(f"Ride {instance.pk} transitioning to CLOSING without closing_date")
return True
@@ -166,45 +149,35 @@ def apply_post_closing_status(instance, user=None):
Returns:
True if status was applied, False otherwise.
"""
- if instance.status != 'CLOSING':
- logger.debug(
- f"Ride {instance.pk} not in CLOSING state, skipping"
- )
+ if instance.status != "CLOSING":
+ logger.debug(f"Ride {instance.pk} not in CLOSING state, skipping")
return False
target_status = instance.post_closing_status
if not target_status:
- logger.warning(
- f"Ride {instance.pk} in CLOSING but no post_closing_status set"
- )
+ logger.warning(f"Ride {instance.pk} in CLOSING but no post_closing_status set")
return False
# Try to use FSM transition
- transition_method_name = f'transition_to_{target_status.lower()}'
+ transition_method_name = f"transition_to_{target_status.lower()}"
if hasattr(instance, transition_method_name):
try:
method = getattr(instance, transition_method_name)
method(user=user)
instance.post_closing_status = None
- instance.save(update_fields=['post_closing_status'])
- logger.info(
- f"Applied post_closing_status {target_status} to ride {instance.pk}"
- )
+ instance.save(update_fields=["post_closing_status"])
+ logger.info(f"Applied post_closing_status {target_status} to ride {instance.pk}")
return True
except Exception as e:
- logger.exception(
- f"Failed to apply post_closing_status for ride {instance.pk}: {e}"
- )
+ logger.exception(f"Failed to apply post_closing_status for ride {instance.pk}: {e}")
return False
else:
# Direct status change
instance.status = target_status
instance.post_closing_status = None
instance.status_since = timezone.now().date()
- instance.save(update_fields=['status', 'post_closing_status', 'status_since'])
- logger.info(
- f"Applied post_closing_status {target_status} to ride {instance.pk} (direct)"
- )
+ instance.save(update_fields=["status", "post_closing_status", "status_since"])
+ logger.info(f"Applied post_closing_status {target_status} to ride {instance.pk} (direct)")
return True
@@ -212,7 +185,8 @@ def apply_post_closing_status(instance, user=None):
# Computed Field Maintenance Signal Handlers
# =============================================================================
-@receiver(post_save, sender='parks.Park')
+
+@receiver(post_save, sender="parks.Park")
def update_ride_search_text_on_park_change(sender, instance, **kwargs):
"""
Update ride search_text when park name or location changes.
@@ -227,7 +201,7 @@ def update_ride_search_text_on_park_change(sender, instance, **kwargs):
logger.exception(f"Failed to update ride search_text on park change: {e}")
-@receiver(post_save, sender='parks.Company')
+@receiver(post_save, sender="parks.Company")
def update_ride_search_text_on_company_change(sender, instance, **kwargs):
"""
Update ride search_text when manufacturer/designer name changes.
@@ -248,7 +222,7 @@ def update_ride_search_text_on_company_change(sender, instance, **kwargs):
logger.exception(f"Failed to update ride search_text on company change: {e}")
-@receiver(post_save, sender='rides.RideModel')
+@receiver(post_save, sender="rides.RideModel")
def update_ride_search_text_on_ride_model_change(sender, instance, **kwargs):
"""
Update ride search_text when ride model name changes.
diff --git a/backend/apps/rides/tasks.py b/backend/apps/rides/tasks.py
index f758a21c..24321575 100644
--- a/backend/apps/rides/tasks.py
+++ b/backend/apps/rides/tasks.py
@@ -36,9 +36,7 @@ def check_overdue_closings() -> dict:
# Query rides that need transition
today = timezone.now().date()
- overdue_rides = Ride.objects.filter(
- status="CLOSING", closing_date__lte=today
- ).select_for_update()
+ overdue_rides = Ride.objects.filter(status="CLOSING", closing_date__lte=today).select_for_update()
processed = 0
succeeded = 0
@@ -109,9 +107,7 @@ def _get_system_user():
logger.info("Created system user for automated tasks")
except Exception as e:
# If creation fails, try to get moderator or admin user
- logger.warning(
- "Failed to create system user, falling back to moderator: %s", str(e)
- )
+ logger.warning("Failed to create system user, falling back to moderator: %s", str(e))
try:
system_user = User.objects.filter(is_staff=True).first()
if not system_user:
diff --git a/backend/apps/rides/tests.py b/backend/apps/rides/tests.py
index d2857f72..94a6d59f 100644
--- a/backend/apps/rides/tests.py
+++ b/backend/apps/rides/tests.py
@@ -36,53 +36,40 @@ class RideTransitionTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.user = User.objects.create_user(
- username='testuser',
- email='test@example.com',
- password='testpass123',
- role='USER'
+ username="testuser", email="test@example.com", password="testpass123", role="USER"
)
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.admin = User.objects.create_user(
- username='admin',
- email='admin@example.com',
- password='testpass123',
- role='ADMIN'
+ username="admin", email="admin@example.com", password="testpass123", role="ADMIN"
)
# Create operator and park
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
# Create manufacturer
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
"""Helper to create a Ride with specified status."""
defaults = {
- 'name': 'Test Ride',
- 'slug': 'test-ride',
- 'description': 'A test ride',
- 'park': self.park,
- 'manufacturer': self.manufacturer
+ "name": "Test Ride",
+ "slug": "test-ride",
+ "description": "A test ride",
+ "park": self.park,
+ "manufacturer": self.manufacturer,
}
defaults.update(kwargs)
return Ride.objects.create(status=status, **defaults)
@@ -93,34 +80,34 @@ class RideTransitionTests(TestCase):
def test_operating_to_closed_temp_transition(self):
"""Test transition from OPERATING to CLOSED_TEMP."""
- ride = self._create_ride(status='OPERATING')
- self.assertEqual(ride.status, 'OPERATING')
+ ride = self._create_ride(status="OPERATING")
+ self.assertEqual(ride.status, "OPERATING")
ride.transition_to_closed_temp(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_TEMP')
+ self.assertEqual(ride.status, "CLOSED_TEMP")
def test_operating_to_sbno_transition(self):
"""Test transition from OPERATING to SBNO."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_operating_to_closing_transition(self):
"""Test transition from OPERATING to CLOSING."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.transition_to_closing(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSING')
+ self.assertEqual(ride.status, "CLOSING")
# -------------------------------------------------------------------------
# Under construction transitions
@@ -128,14 +115,14 @@ class RideTransitionTests(TestCase):
def test_under_construction_to_operating_transition(self):
"""Test transition from UNDER_CONSTRUCTION to OPERATING."""
- ride = self._create_ride(status='UNDER_CONSTRUCTION')
- self.assertEqual(ride.status, 'UNDER_CONSTRUCTION')
+ ride = self._create_ride(status="UNDER_CONSTRUCTION")
+ self.assertEqual(ride.status, "UNDER_CONSTRUCTION")
ride.transition_to_operating(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
# -------------------------------------------------------------------------
# Closed temp transitions
@@ -143,34 +130,34 @@ class RideTransitionTests(TestCase):
def test_closed_temp_to_operating_transition(self):
"""Test transition from CLOSED_TEMP to OPERATING (reopen)."""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
ride.transition_to_operating(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
def test_closed_temp_to_sbno_transition(self):
"""Test transition from CLOSED_TEMP to SBNO."""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_closed_temp_to_closed_perm_transition(self):
"""Test transition from CLOSED_TEMP to CLOSED_PERM."""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
ride.transition_to_closed_perm(user=self.moderator)
ride.closing_date = date.today()
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
# -------------------------------------------------------------------------
# SBNO transitions
@@ -178,23 +165,23 @@ class RideTransitionTests(TestCase):
def test_sbno_to_operating_transition(self):
"""Test transition from SBNO to OPERATING (revival)."""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
ride.transition_to_operating(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
def test_sbno_to_closed_perm_transition(self):
"""Test transition from SBNO to CLOSED_PERM."""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
ride.transition_to_closed_perm(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
# -------------------------------------------------------------------------
# Closing transitions
@@ -202,23 +189,23 @@ class RideTransitionTests(TestCase):
def test_closing_to_closed_perm_transition(self):
"""Test transition from CLOSING to CLOSED_PERM."""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.transition_to_closed_perm(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
def test_closing_to_sbno_transition(self):
"""Test transition from CLOSING to SBNO."""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
# -------------------------------------------------------------------------
# Closed perm transitions (to final states)
@@ -226,23 +213,23 @@ class RideTransitionTests(TestCase):
def test_closed_perm_to_demolished_transition(self):
"""Test transition from CLOSED_PERM to DEMOLISHED."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
ride.transition_to_demolished(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_closed_perm_to_relocated_transition(self):
"""Test transition from CLOSED_PERM to RELOCATED."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
ride.transition_to_relocated(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
# -------------------------------------------------------------------------
# Invalid transitions (final states)
@@ -250,28 +237,28 @@ class RideTransitionTests(TestCase):
def test_demolished_cannot_transition(self):
"""Test that DEMOLISHED state cannot transition further."""
- ride = self._create_ride(status='DEMOLISHED')
+ ride = self._create_ride(status="DEMOLISHED")
with self.assertRaises(TransitionNotAllowed):
ride.transition_to_operating(user=self.moderator)
def test_relocated_cannot_transition(self):
"""Test that RELOCATED state cannot transition further."""
- ride = self._create_ride(status='RELOCATED')
+ ride = self._create_ride(status="RELOCATED")
with self.assertRaises(TransitionNotAllowed):
ride.transition_to_operating(user=self.moderator)
def test_operating_cannot_directly_demolish(self):
"""Test that OPERATING cannot directly transition to DEMOLISHED."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
ride.transition_to_demolished(user=self.moderator)
def test_operating_cannot_directly_relocate(self):
"""Test that OPERATING cannot directly transition to RELOCATED."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
with self.assertRaises(TransitionNotAllowed):
ride.transition_to_relocated(user=self.moderator)
@@ -282,84 +269,76 @@ class RideTransitionTests(TestCase):
def test_open_wrapper_method(self):
"""Test the open() wrapper method."""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
ride.open(user=self.user)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
def test_close_temporarily_wrapper_method(self):
"""Test the close_temporarily() wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.close_temporarily(user=self.user)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_TEMP')
+ self.assertEqual(ride.status, "CLOSED_TEMP")
def test_mark_sbno_wrapper_method(self):
"""Test the mark_sbno() wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.mark_sbno(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_mark_closing_wrapper_method(self):
"""Test the mark_closing() wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
closing = date(2025, 12, 31)
- ride.mark_closing(
- closing_date=closing,
- post_closing_status='DEMOLISHED',
- user=self.moderator
- )
+ ride.mark_closing(closing_date=closing, post_closing_status="DEMOLISHED", user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSING')
+ self.assertEqual(ride.status, "CLOSING")
self.assertEqual(ride.closing_date, closing)
- self.assertEqual(ride.post_closing_status, 'DEMOLISHED')
+ self.assertEqual(ride.post_closing_status, "DEMOLISHED")
def test_mark_closing_requires_post_closing_status(self):
"""Test that mark_closing() requires post_closing_status."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
with self.assertRaises(ValidationError):
- ride.mark_closing(
- closing_date=date(2025, 12, 31),
- post_closing_status='',
- user=self.moderator
- )
+ ride.mark_closing(closing_date=date(2025, 12, 31), post_closing_status="", user=self.moderator)
def test_close_permanently_wrapper_method(self):
"""Test the close_permanently() wrapper method."""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
ride.close_permanently(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
def test_demolish_wrapper_method(self):
"""Test the demolish() wrapper method."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
ride.demolish(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_relocate_wrapper_method(self):
"""Test the relocate() wrapper method."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
ride.relocate(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
# ============================================================================
@@ -373,37 +352,30 @@ class RidePostClosingTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
"""Helper to create a Ride."""
defaults = {
- 'name': 'Test Ride',
- 'slug': 'test-ride',
- 'description': 'A test ride',
- 'park': self.park,
- 'manufacturer': self.manufacturer
+ "name": "Test Ride",
+ "slug": "test-ride",
+ "description": "A test ride",
+ "park": self.park,
+ "manufacturer": self.manufacturer,
}
defaults.update(kwargs)
return Ride.objects.create(status=status, **defaults)
@@ -411,111 +383,85 @@ class RidePostClosingTests(TestCase):
def test_apply_post_closing_status_to_demolished(self):
"""Test apply_post_closing_status transitions to DEMOLISHED."""
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=yesterday,
- post_closing_status='DEMOLISHED'
- )
+ ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="DEMOLISHED")
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_apply_post_closing_status_to_relocated(self):
"""Test apply_post_closing_status transitions to RELOCATED."""
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=yesterday,
- post_closing_status='RELOCATED'
- )
+ ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="RELOCATED")
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
def test_apply_post_closing_status_to_sbno(self):
"""Test apply_post_closing_status transitions to SBNO."""
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=yesterday,
- post_closing_status='SBNO'
- )
+ ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="SBNO")
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_apply_post_closing_status_to_closed_perm(self):
"""Test apply_post_closing_status transitions to CLOSED_PERM."""
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=yesterday,
- post_closing_status='CLOSED_PERM'
- )
+ ride = self._create_ride(status="CLOSING", closing_date=yesterday, post_closing_status="CLOSED_PERM")
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
def test_apply_post_closing_status_not_yet_reached(self):
"""Test apply_post_closing_status does nothing if date not reached."""
tomorrow = date.today() + timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=tomorrow,
- post_closing_status='DEMOLISHED'
- )
+ ride = self._create_ride(status="CLOSING", closing_date=tomorrow, post_closing_status="DEMOLISHED")
ride.apply_post_closing_status(user=self.moderator)
ride.refresh_from_db()
# Status should remain CLOSING since date hasn't been reached
- self.assertEqual(ride.status, 'CLOSING')
+ self.assertEqual(ride.status, "CLOSING")
def test_apply_post_closing_status_requires_closing_status(self):
"""Test apply_post_closing_status requires CLOSING status."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
with self.assertRaises(ValidationError) as ctx:
ride.apply_post_closing_status(user=self.moderator)
- self.assertIn('CLOSING', str(ctx.exception))
+ self.assertIn("CLOSING", str(ctx.exception))
def test_apply_post_closing_status_requires_closing_date(self):
"""Test apply_post_closing_status requires closing_date."""
- ride = self._create_ride(
- status='CLOSING',
- post_closing_status='DEMOLISHED'
- )
+ ride = self._create_ride(status="CLOSING", post_closing_status="DEMOLISHED")
ride.closing_date = None
ride.save()
with self.assertRaises(ValidationError) as ctx:
ride.apply_post_closing_status(user=self.moderator)
- self.assertIn('closing_date', str(ctx.exception))
+ self.assertIn("closing_date", str(ctx.exception))
def test_apply_post_closing_status_requires_post_closing_status(self):
"""Test apply_post_closing_status requires post_closing_status."""
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(
- status='CLOSING',
- closing_date=yesterday
- )
+ ride = self._create_ride(status="CLOSING", closing_date=yesterday)
ride.post_closing_status = None
ride.save()
with self.assertRaises(ValidationError) as ctx:
ride.apply_post_closing_status(user=self.moderator)
- self.assertIn('post_closing_status', str(ctx.exception))
+ self.assertIn("post_closing_status", str(ctx.exception))
# ============================================================================
@@ -529,64 +475,54 @@ class RideTransitionHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.moderator = User.objects.create_user(
- username='moderator',
- email='moderator@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="moderator", email="moderator@example.com", password="testpass123", role="MODERATOR"
)
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
- def _create_ride(self, status='OPERATING'):
+ def _create_ride(self, status="OPERATING"):
"""Helper to create a Ride."""
return Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
manufacturer=self.manufacturer,
- status=status
+ status=status,
)
def test_transition_creates_state_log(self):
"""Test that transitions create StateLog entries."""
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.transition_to_closed_temp(user=self.moderator)
ride.save()
ride_ct = ContentType.objects.get_for_model(ride)
- log = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).first()
+ log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first()
self.assertIsNotNone(log)
- self.assertEqual(log.state, 'CLOSED_TEMP')
+ self.assertEqual(log.state, "CLOSED_TEMP")
self.assertEqual(log.by, self.moderator)
def test_multiple_transitions_create_multiple_logs(self):
"""Test that multiple transitions create multiple log entries."""
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride_ct = ContentType.objects.get_for_model(ride)
# First transition
@@ -597,29 +533,23 @@ class RideTransitionHistoryTests(TestCase):
ride.transition_to_operating(user=self.moderator)
ride.save()
- logs = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).order_by("timestamp")
self.assertEqual(logs.count(), 2)
- self.assertEqual(logs[0].state, 'CLOSED_TEMP')
- self.assertEqual(logs[1].state, 'OPERATING')
+ self.assertEqual(logs[0].state, "CLOSED_TEMP")
+ self.assertEqual(logs[1].state, "OPERATING")
def test_transition_log_includes_user(self):
"""Test that transition logs include the user who made the change."""
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride_ct = ContentType.objects.get_for_model(ride)
- log = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).first()
+ log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first()
self.assertEqual(log.by, self.moderator)
@@ -628,19 +558,15 @@ class RideTransitionHistoryTests(TestCase):
from django_fsm_log.models import StateLog
yesterday = date.today() - timedelta(days=1)
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = yesterday
- ride.post_closing_status = 'DEMOLISHED'
+ ride.post_closing_status = "DEMOLISHED"
ride.save()
ride.apply_post_closing_status(user=self.moderator)
ride_ct = ContentType.objects.get_for_model(ride)
- log = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id,
- state='DEMOLISHED'
- ).first()
+ log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id, state="DEMOLISHED").first()
self.assertIsNotNone(log)
self.assertEqual(log.by, self.moderator)
@@ -657,31 +583,27 @@ class RideBusinessLogicTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
def test_ride_creates_with_valid_park(self):
"""Test ride can be created with valid park."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
self.assertEqual(ride.park, self.park)
@@ -689,36 +611,33 @@ class RideBusinessLogicTests(TestCase):
def test_ride_slug_auto_generated(self):
"""Test that ride slug is auto-generated from name."""
ride = Ride.objects.create(
- name='My Amazing Roller Coaster',
- description='A test ride',
- park=self.park,
- manufacturer=self.manufacturer
+ name="My Amazing Roller Coaster", description="A test ride", park=self.park, manufacturer=self.manufacturer
)
- self.assertEqual(ride.slug, 'my-amazing-roller-coaster')
+ self.assertEqual(ride.slug, "my-amazing-roller-coaster")
def test_ride_url_generated(self):
"""Test that frontend URL is generated on save."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
- self.assertIn('test-park', ride.url)
- self.assertIn('test-ride', ride.url)
+ self.assertIn("test-park", ride.url)
+ self.assertIn("test-ride", ride.url)
def test_opening_year_computed_from_opening_date(self):
"""Test that opening_year is computed from opening_date."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
manufacturer=self.manufacturer,
- opening_date=date(2020, 6, 15)
+ opening_date=date(2020, 6, 15),
)
self.assertEqual(ride.opening_year, 2020)
@@ -726,38 +645,31 @@ class RideBusinessLogicTests(TestCase):
def test_search_text_populated(self):
"""Test that search_text is populated on save."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A thrilling roller coaster',
+ name="Test Ride",
+ slug="test-ride",
+ description="A thrilling roller coaster",
park=self.park,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
- self.assertIn('test ride', ride.search_text)
- self.assertIn('thrilling roller coaster', ride.search_text)
- self.assertIn('test park', ride.search_text)
- self.assertIn('test manufacturer', ride.search_text)
+ self.assertIn("test ride", ride.search_text)
+ self.assertIn("thrilling roller coaster", ride.search_text)
+ self.assertIn("test park", ride.search_text)
+ self.assertIn("test manufacturer", ride.search_text)
def test_ride_slug_unique_within_park(self):
"""Test that ride slugs are unique within a park."""
Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='First ride',
- park=self.park,
- manufacturer=self.manufacturer
+ name="Test Ride", slug="test-ride", description="First ride", park=self.park, manufacturer=self.manufacturer
)
# Creating another ride with same name should get different slug
ride2 = Ride.objects.create(
- name='Test Ride',
- description='Second ride',
- park=self.park,
- manufacturer=self.manufacturer
+ name="Test Ride", description="Second ride", park=self.park, manufacturer=self.manufacturer
)
- self.assertNotEqual(ride2.slug, 'test-ride')
- self.assertTrue(ride2.slug.startswith('test-ride'))
+ self.assertNotEqual(ride2.slug, "test-ride")
+ self.assertTrue(ride2.slug.startswith("test-ride"))
# ============================================================================
@@ -771,55 +683,51 @@ class RideMoveTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park1 = Park.objects.create(
- name='Park One',
- slug='park-one',
- description='First park',
+ name="Park One",
+ slug="park-one",
+ description="First park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.park2 = Park.objects.create(
- name='Park Two',
- slug='park-two',
- description='Second park',
+ name="Park Two",
+ slug="park-two",
+ description="Second park",
operator=self.operator,
- timezone='America/Los_Angeles'
+ timezone="America/Los_Angeles",
)
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
def test_move_ride_to_different_park(self):
"""Test moving a ride to a different park."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park1,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
changes = ride.move_to_park(self.park2)
ride.refresh_from_db()
self.assertEqual(ride.park, self.park2)
- self.assertEqual(changes['old_park']['id'], self.park1.id)
- self.assertEqual(changes['new_park']['id'], self.park2.id)
+ self.assertEqual(changes["old_park"]["id"], self.park1.id)
+ self.assertEqual(changes["new_park"]["id"], self.park2.id)
def test_move_ride_updates_url(self):
"""Test that moving a ride updates the URL."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park1,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
old_url = ride.url
@@ -827,27 +735,27 @@ class RideMoveTests(TestCase):
ride.refresh_from_db()
self.assertNotEqual(ride.url, old_url)
- self.assertIn('park-two', ride.url)
- self.assertTrue(changes['url_changed'])
+ self.assertIn("park-two", ride.url)
+ self.assertTrue(changes["url_changed"])
def test_move_ride_handles_slug_conflict(self):
"""Test that moving a ride handles slug conflicts in destination park."""
# Create ride in park1
ride1 = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park1,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
# Create ride with same slug in park2
Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='Another test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="Another test ride",
park=self.park2,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
# Move ride1 to park2
@@ -856,8 +764,8 @@ class RideMoveTests(TestCase):
ride1.refresh_from_db()
self.assertEqual(ride1.park, self.park2)
# Slug should have been modified to avoid conflict
- self.assertNotEqual(ride1.slug, 'test-ride')
- self.assertTrue(changes['slug_changed'])
+ self.assertNotEqual(ride1.slug, "test-ride")
+ self.assertTrue(changes["slug_changed"])
# ============================================================================
@@ -871,34 +779,30 @@ class RideSlugHistoryTests(TestCase):
def setUp(self):
"""Set up test fixtures."""
self.operator = ParkCompany.objects.create(
- name='Test Operator',
- description='Test operator company',
- roles=['OPERATOR']
+ name="Test Operator", description="Test operator company", roles=["OPERATOR"]
)
self.park = Park.objects.create(
- name='Test Park',
- slug='test-park',
- description='A test park',
+ name="Test Park",
+ slug="test-park",
+ description="A test park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
self.manufacturer = Company.objects.create(
- name='Test Manufacturer',
- description='Test manufacturer company',
- roles=['MANUFACTURER']
+ name="Test Manufacturer", description="Test manufacturer company", roles=["MANUFACTURER"]
)
def test_get_by_slug_finds_current_slug(self):
"""Test get_by_slug finds ride by current slug."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
- found_ride, is_historical = Ride.get_by_slug('test-ride', park=self.park)
+ found_ride, is_historical = Ride.get_by_slug("test-ride", park=self.park)
self.assertEqual(found_ride, ride)
self.assertFalse(is_historical)
@@ -906,24 +810,24 @@ class RideSlugHistoryTests(TestCase):
def test_get_by_slug_with_park_filter(self):
"""Test get_by_slug filters by park."""
ride = Ride.objects.create(
- name='Test Ride',
- slug='test-ride',
- description='A test ride',
+ name="Test Ride",
+ slug="test-ride",
+ description="A test ride",
park=self.park,
- manufacturer=self.manufacturer
+ manufacturer=self.manufacturer,
)
# Should find ride in correct park
- found_ride, is_historical = Ride.get_by_slug('test-ride', park=self.park)
+ found_ride, is_historical = Ride.get_by_slug("test-ride", park=self.park)
self.assertEqual(found_ride, ride)
# Should not find ride in different park
other_park = Park.objects.create(
- name='Other Park',
- slug='other-park',
- description='Another park',
+ name="Other Park",
+ slug="other-park",
+ description="Another park",
operator=self.operator,
- timezone='America/New_York'
+ timezone="America/New_York",
)
with self.assertRaises(Ride.DoesNotExist):
- Ride.get_by_slug('test-ride', park=other_park)
+ Ride.get_by_slug("test-ride", park=other_park)
diff --git a/backend/apps/rides/tests/test_ride_workflows.py b/backend/apps/rides/tests/test_ride_workflows.py
index b6024fba..dc2f9648 100644
--- a/backend/apps/rides/tests/test_ride_workflows.py
+++ b/backend/apps/rides/tests/test_ride_workflows.py
@@ -25,42 +25,33 @@ class RideOpeningWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='ride_user',
- email='ride_user@example.com',
- password='testpass123',
- role='USER'
+ username="ride_user", email="ride_user@example.com", password="testpass123", role="USER"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
"""Helper to create a ride with park."""
from apps.parks.models import Company, Park
from apps.rides.models import Ride
# Create manufacturer
- manufacturer = Company.objects.create(
- name=f'Manufacturer {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
+ manufacturer = Company.objects.create(name=f"Manufacturer {timezone.now().timestamp()}", roles=["MANUFACTURER"])
# Create park with operator
- operator = Company.objects.create(
- name=f'Operator {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ operator = Company.objects.create(name=f"Operator {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Test Park {timezone.now().timestamp()}',
- slug=f'test-park-{timezone.now().timestamp()}',
+ name=f"Test Park {timezone.now().timestamp()}",
+ slug=f"test-park-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Test Ride {timezone.now().timestamp()}',
- 'slug': f'test-ride-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Test Ride {timezone.now().timestamp()}",
+ "slug": f"test-ride-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -71,16 +62,16 @@ class RideOpeningWorkflowTests(TestCase):
Flow: UNDER_CONSTRUCTION → OPERATING
"""
- ride = self._create_ride(status='UNDER_CONSTRUCTION')
+ ride = self._create_ride(status="UNDER_CONSTRUCTION")
- self.assertEqual(ride.status, 'UNDER_CONSTRUCTION')
+ self.assertEqual(ride.status, "UNDER_CONSTRUCTION")
# Ride opens
ride.transition_to_operating(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
class RideMaintenanceWorkflowTests(TestCase):
@@ -89,38 +80,29 @@ class RideMaintenanceWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='maint_user',
- email='maint@example.com',
- password='testpass123',
- role='USER'
+ username="maint_user", email="maint@example.com", password="testpass123", role="USER"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Maint {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Maint {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Maint {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Maint {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Maint {timezone.now().timestamp()}',
- slug=f'park-maint-{timezone.now().timestamp()}',
+ name=f"Park Maint {timezone.now().timestamp()}",
+ slug=f"park-maint-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Maint {timezone.now().timestamp()}',
- 'slug': f'ride-maint-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Maint {timezone.now().timestamp()}",
+ "slug": f"ride-maint-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -131,21 +113,21 @@ class RideMaintenanceWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_TEMP → OPERATING
"""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
# Close for maintenance
ride.transition_to_closed_temp(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_TEMP')
+ self.assertEqual(ride.status, "CLOSED_TEMP")
# Reopen after maintenance
ride.transition_to_operating(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
class RideSBNOWorkflowTests(TestCase):
@@ -154,38 +136,29 @@ class RideSBNOWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='sbno_mod',
- email='sbno_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="sbno_mod", email="sbno_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr SBNO {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op SBNO {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr SBNO {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op SBNO {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park SBNO {timezone.now().timestamp()}',
- slug=f'park-sbno-{timezone.now().timestamp()}',
+ name=f"Park SBNO {timezone.now().timestamp()}",
+ slug=f"park-sbno-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride SBNO {timezone.now().timestamp()}',
- 'slug': f'ride-sbno-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride SBNO {timezone.now().timestamp()}",
+ "slug": f"ride-sbno-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -196,14 +169,14 @@ class RideSBNOWorkflowTests(TestCase):
Flow: OPERATING → SBNO
"""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
# Mark as SBNO
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_ride_sbno_from_closed_temp(self):
"""
@@ -211,14 +184,14 @@ class RideSBNOWorkflowTests(TestCase):
Flow: OPERATING → CLOSED_TEMP → SBNO
"""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
# Extended to SBNO
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_ride_revival_from_sbno(self):
"""
@@ -226,14 +199,14 @@ class RideSBNOWorkflowTests(TestCase):
Flow: SBNO → OPERATING
"""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
# Revive the ride
ride.transition_to_operating(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
def test_sbno_to_closed_perm(self):
"""
@@ -241,14 +214,14 @@ class RideSBNOWorkflowTests(TestCase):
Flow: SBNO → CLOSED_PERM
"""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
# Confirm permanent closure
ride.transition_to_closed_perm(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
class RideScheduledClosureWorkflowTests(TestCase):
@@ -257,38 +230,29 @@ class RideScheduledClosureWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='closing_mod',
- email='closing_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="closing_mod", email="closing_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Closing {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Closing {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Closing {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Closing {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Closing {timezone.now().timestamp()}',
- slug=f'park-closing-{timezone.now().timestamp()}',
+ name=f"Park Closing {timezone.now().timestamp()}",
+ slug=f"park-closing-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Closing {timezone.now().timestamp()}',
- 'slug': f'ride-closing-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Closing {timezone.now().timestamp()}",
+ "slug": f"ride-closing-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -299,19 +263,19 @@ class RideScheduledClosureWorkflowTests(TestCase):
Flow: OPERATING → CLOSING (with closing_date and post_closing_status)
"""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
closing_date = (timezone.now() + timedelta(days=30)).date()
# Mark as closing
ride.transition_to_closing(user=self.moderator)
ride.closing_date = closing_date
- ride.post_closing_status = 'DEMOLISHED'
+ ride.post_closing_status = "DEMOLISHED"
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSING')
+ self.assertEqual(ride.status, "CLOSING")
self.assertEqual(ride.closing_date, closing_date)
- self.assertEqual(ride.post_closing_status, 'DEMOLISHED')
+ self.assertEqual(ride.post_closing_status, "DEMOLISHED")
def test_closing_to_closed_perm(self):
"""
@@ -319,9 +283,9 @@ class RideScheduledClosureWorkflowTests(TestCase):
Flow: CLOSING → CLOSED_PERM
"""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = timezone.now().date()
- ride.post_closing_status = 'CLOSED_PERM'
+ ride.post_closing_status = "CLOSED_PERM"
ride.save()
# Transition when closing date reached
@@ -329,7 +293,7 @@ class RideScheduledClosureWorkflowTests(TestCase):
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
def test_closing_to_sbno(self):
"""
@@ -337,9 +301,9 @@ class RideScheduledClosureWorkflowTests(TestCase):
Flow: CLOSING → SBNO
"""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = timezone.now().date()
- ride.post_closing_status = 'SBNO'
+ ride.post_closing_status = "SBNO"
ride.save()
# Transition to SBNO
@@ -347,7 +311,7 @@ class RideScheduledClosureWorkflowTests(TestCase):
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
class RideDemolitionWorkflowTests(TestCase):
@@ -356,38 +320,29 @@ class RideDemolitionWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='demo_ride_mod',
- email='demo_ride_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="demo_ride_mod", email="demo_ride_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='CLOSED_PERM', **kwargs):
+ def _create_ride(self, status="CLOSED_PERM", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Demo {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Demo {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Demo {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Demo {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Demo {timezone.now().timestamp()}',
- slug=f'park-demo-{timezone.now().timestamp()}',
+ name=f"Park Demo {timezone.now().timestamp()}",
+ slug=f"park-demo-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Demo {timezone.now().timestamp()}',
- 'slug': f'ride-demo-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Demo {timezone.now().timestamp()}",
+ "slug": f"ride-demo-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -398,20 +353,20 @@ class RideDemolitionWorkflowTests(TestCase):
Flow: CLOSED_PERM → DEMOLISHED
"""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
# Demolish
ride.transition_to_demolished(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_demolished_is_final_state(self):
"""Test that demolished rides cannot transition further."""
from django_fsm import TransitionNotAllowed
- ride = self._create_ride(status='DEMOLISHED')
+ ride = self._create_ride(status="DEMOLISHED")
# Cannot transition from demolished
with self.assertRaises(TransitionNotAllowed):
@@ -424,38 +379,29 @@ class RideRelocationWorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='reloc_ride_mod',
- email='reloc_ride_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="reloc_ride_mod", email="reloc_ride_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='CLOSED_PERM', **kwargs):
+ def _create_ride(self, status="CLOSED_PERM", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Reloc {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Reloc {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Reloc {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Reloc {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Reloc {timezone.now().timestamp()}',
- slug=f'park-reloc-{timezone.now().timestamp()}',
+ name=f"Park Reloc {timezone.now().timestamp()}",
+ slug=f"park-reloc-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Reloc {timezone.now().timestamp()}',
- 'slug': f'ride-reloc-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Reloc {timezone.now().timestamp()}",
+ "slug": f"ride-reloc-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -466,20 +412,20 @@ class RideRelocationWorkflowTests(TestCase):
Flow: CLOSED_PERM → RELOCATED
"""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
# Relocate
ride.transition_to_relocated(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
def test_relocated_is_final_state(self):
"""Test that relocated rides cannot transition further."""
from django_fsm import TransitionNotAllowed
- ride = self._create_ride(status='RELOCATED')
+ ride = self._create_ride(status="RELOCATED")
# Cannot transition from relocated
with self.assertRaises(TransitionNotAllowed):
@@ -492,145 +438,129 @@ class RideWrapperMethodTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='wrapper_ride_user',
- email='wrapper_ride@example.com',
- password='testpass123',
- role='USER'
+ username="wrapper_ride_user", email="wrapper_ride@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='wrapper_ride_mod',
- email='wrapper_ride_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="wrapper_ride_mod", email="wrapper_ride_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Wrapper {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Wrapper {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Wrapper {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Wrapper {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Wrapper {timezone.now().timestamp()}',
- slug=f'park-wrapper-{timezone.now().timestamp()}',
+ name=f"Park Wrapper {timezone.now().timestamp()}",
+ slug=f"park-wrapper-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Wrapper {timezone.now().timestamp()}',
- 'slug': f'ride-wrapper-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Wrapper {timezone.now().timestamp()}",
+ "slug": f"ride-wrapper-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
def test_close_temporarily_wrapper(self):
"""Test close_temporarily wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
- if hasattr(ride, 'close_temporarily'):
+ if hasattr(ride, "close_temporarily"):
ride.close_temporarily(user=self.user)
else:
ride.transition_to_closed_temp(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_TEMP')
+ self.assertEqual(ride.status, "CLOSED_TEMP")
def test_mark_sbno_wrapper(self):
"""Test mark_sbno wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
- if hasattr(ride, 'mark_sbno'):
+ if hasattr(ride, "mark_sbno"):
ride.mark_sbno(user=self.moderator)
else:
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
def test_mark_closing_wrapper(self):
"""Test mark_closing wrapper method."""
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
closing_date = (timezone.now() + timedelta(days=30)).date()
- if hasattr(ride, 'mark_closing'):
- ride.mark_closing(
- closing_date=closing_date,
- post_closing_status='DEMOLISHED',
- user=self.moderator
- )
+ if hasattr(ride, "mark_closing"):
+ ride.mark_closing(closing_date=closing_date, post_closing_status="DEMOLISHED", user=self.moderator)
else:
ride.transition_to_closing(user=self.moderator)
ride.closing_date = closing_date
- ride.post_closing_status = 'DEMOLISHED'
+ ride.post_closing_status = "DEMOLISHED"
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSING')
+ self.assertEqual(ride.status, "CLOSING")
def test_open_wrapper(self):
"""Test open wrapper method."""
- ride = self._create_ride(status='CLOSED_TEMP')
+ ride = self._create_ride(status="CLOSED_TEMP")
- if hasattr(ride, 'open'):
+ if hasattr(ride, "open"):
ride.open(user=self.user)
else:
ride.transition_to_operating(user=self.user)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'OPERATING')
+ self.assertEqual(ride.status, "OPERATING")
def test_close_permanently_wrapper(self):
"""Test close_permanently wrapper method."""
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
- if hasattr(ride, 'close_permanently'):
+ if hasattr(ride, "close_permanently"):
ride.close_permanently(user=self.moderator)
else:
ride.transition_to_closed_perm(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'CLOSED_PERM')
+ self.assertEqual(ride.status, "CLOSED_PERM")
def test_demolish_wrapper(self):
"""Test demolish wrapper method."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
- if hasattr(ride, 'demolish'):
+ if hasattr(ride, "demolish"):
ride.demolish(user=self.moderator)
else:
ride.transition_to_demolished(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_relocate_wrapper(self):
"""Test relocate wrapper method."""
- ride = self._create_ride(status='CLOSED_PERM')
+ ride = self._create_ride(status="CLOSED_PERM")
- if hasattr(ride, 'relocate'):
+ if hasattr(ride, "relocate"):
ride.relocate(user=self.moderator)
else:
ride.transition_to_relocated(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
class RidePostClosingStatusAutomationTests(TestCase):
@@ -639,90 +569,81 @@ class RidePostClosingStatusAutomationTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.moderator = User.objects.create_user(
- username='auto_mod',
- email='auto_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="auto_mod", email="auto_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='CLOSING', **kwargs):
+ def _create_ride(self, status="CLOSING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Auto {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Auto {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Auto {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Auto {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Auto {timezone.now().timestamp()}',
- slug=f'park-auto-{timezone.now().timestamp()}',
+ name=f"Park Auto {timezone.now().timestamp()}",
+ slug=f"park-auto-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Auto {timezone.now().timestamp()}',
- 'slug': f'ride-auto-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Auto {timezone.now().timestamp()}",
+ "slug": f"ride-auto-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
def test_apply_post_closing_status_demolished(self):
"""Test apply_post_closing_status transitions to DEMOLISHED."""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = timezone.now().date()
- ride.post_closing_status = 'DEMOLISHED'
+ ride.post_closing_status = "DEMOLISHED"
ride.save()
# Apply post-closing status if method exists
- if hasattr(ride, 'apply_post_closing_status'):
+ if hasattr(ride, "apply_post_closing_status"):
ride.apply_post_closing_status(user=self.moderator)
else:
ride.transition_to_demolished(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'DEMOLISHED')
+ self.assertEqual(ride.status, "DEMOLISHED")
def test_apply_post_closing_status_relocated(self):
"""Test apply_post_closing_status transitions to RELOCATED."""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = timezone.now().date()
- ride.post_closing_status = 'RELOCATED'
+ ride.post_closing_status = "RELOCATED"
ride.save()
- if hasattr(ride, 'apply_post_closing_status'):
+ if hasattr(ride, "apply_post_closing_status"):
ride.apply_post_closing_status(user=self.moderator)
else:
ride.transition_to_relocated(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'RELOCATED')
+ self.assertEqual(ride.status, "RELOCATED")
def test_apply_post_closing_status_sbno(self):
"""Test apply_post_closing_status transitions to SBNO."""
- ride = self._create_ride(status='CLOSING')
+ ride = self._create_ride(status="CLOSING")
ride.closing_date = timezone.now().date()
- ride.post_closing_status = 'SBNO'
+ ride.post_closing_status = "SBNO"
ride.save()
- if hasattr(ride, 'apply_post_closing_status'):
+ if hasattr(ride, "apply_post_closing_status"):
ride.apply_post_closing_status(user=self.moderator)
else:
ride.transition_to_sbno(user=self.moderator)
ride.save()
ride.refresh_from_db()
- self.assertEqual(ride.status, 'SBNO')
+ self.assertEqual(ride.status, "SBNO")
class RideStateLogTests(TestCase):
@@ -731,44 +652,32 @@ class RideStateLogTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
- username='ride_log_user',
- email='ride_log_user@example.com',
- password='testpass123',
- role='USER'
+ username="ride_log_user", email="ride_log_user@example.com", password="testpass123", role="USER"
)
cls.moderator = User.objects.create_user(
- username='ride_log_mod',
- email='ride_log_mod@example.com',
- password='testpass123',
- role='MODERATOR'
+ username="ride_log_mod", email="ride_log_mod@example.com", password="testpass123", role="MODERATOR"
)
- def _create_ride(self, status='OPERATING', **kwargs):
+ def _create_ride(self, status="OPERATING", **kwargs):
from apps.parks.models import Company, Park
from apps.rides.models import Ride
- manufacturer = Company.objects.create(
- name=f'Mfr Log {timezone.now().timestamp()}',
- roles=['MANUFACTURER']
- )
- operator = Company.objects.create(
- name=f'Op Log {timezone.now().timestamp()}',
- roles=['OPERATOR']
- )
+ manufacturer = Company.objects.create(name=f"Mfr Log {timezone.now().timestamp()}", roles=["MANUFACTURER"])
+ operator = Company.objects.create(name=f"Op Log {timezone.now().timestamp()}", roles=["OPERATOR"])
park = Park.objects.create(
- name=f'Park Log {timezone.now().timestamp()}',
- slug=f'park-log-{timezone.now().timestamp()}',
+ name=f"Park Log {timezone.now().timestamp()}",
+ slug=f"park-log-{timezone.now().timestamp()}",
operator=operator,
- status='OPERATING',
- timezone='America/New_York'
+ status="OPERATING",
+ timezone="America/New_York",
)
defaults = {
- 'name': f'Ride Log {timezone.now().timestamp()}',
- 'slug': f'ride-log-{timezone.now().timestamp()}',
- 'park': park,
- 'manufacturer': manufacturer,
- 'status': status
+ "name": f"Ride Log {timezone.now().timestamp()}",
+ "slug": f"ride-log-{timezone.now().timestamp()}",
+ "park": park,
+ "manufacturer": manufacturer,
+ "status": status,
}
defaults.update(kwargs)
return Ride.objects.create(**defaults)
@@ -778,7 +687,7 @@ class RideStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride_ct = ContentType.objects.get_for_model(ride)
# Perform transition
@@ -786,13 +695,10 @@ class RideStateLogTests(TestCase):
ride.save()
# Check log was created
- log = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).first()
+ log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first()
self.assertIsNotNone(log, "StateLog entry should be created")
- self.assertEqual(log.state, 'CLOSED_TEMP')
+ self.assertEqual(log.state, "CLOSED_TEMP")
self.assertEqual(log.by, self.user)
def test_multiple_transitions_logged(self):
@@ -800,7 +706,7 @@ class RideStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride_ct = ContentType.objects.get_for_model(ride)
# First transition: OPERATING -> SBNO
@@ -812,21 +718,18 @@ class RideStateLogTests(TestCase):
ride.save()
# Check multiple logs created
- logs = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).order_by("timestamp")
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
- self.assertEqual(logs[0].state, 'SBNO')
- self.assertEqual(logs[1].state, 'OPERATING')
+ self.assertEqual(logs[0].state, "SBNO")
+ self.assertEqual(logs[1].state, "OPERATING")
def test_sbno_revival_workflow_logged(self):
"""Test that SBNO revival workflow is logged."""
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='SBNO')
+ ride = self._create_ride(status="SBNO")
ride_ct = ContentType.objects.get_for_model(ride)
# Revival: SBNO -> OPERATING
@@ -834,13 +737,10 @@ class RideStateLogTests(TestCase):
ride.save()
# Check log was created
- log = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).first()
+ log = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).first()
self.assertIsNotNone(log, "StateLog entry should be created")
- self.assertEqual(log.state, 'OPERATING')
+ self.assertEqual(log.state, "OPERATING")
self.assertEqual(log.by, self.moderator)
def test_full_lifecycle_logged(self):
@@ -848,7 +748,7 @@ class RideStateLogTests(TestCase):
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride_ct = ContentType.objects.get_for_model(ride)
# Lifecycle: OPERATING -> CLOSED_TEMP -> SBNO -> CLOSED_PERM -> DEMOLISHED
@@ -865,37 +765,31 @@ class RideStateLogTests(TestCase):
ride.save()
# Check all logs created
- logs = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).order_by("timestamp")
self.assertEqual(logs.count(), 4, "Should have 4 log entries")
states = [log.state for log in logs]
- self.assertEqual(states, ['CLOSED_TEMP', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED'])
+ self.assertEqual(states, ["CLOSED_TEMP", "SBNO", "CLOSED_PERM", "DEMOLISHED"])
def test_scheduled_closing_workflow_logged(self):
"""Test that scheduled closing workflow creates logs."""
from django.contrib.contenttypes.models import ContentType
from django_fsm_log.models import StateLog
- ride = self._create_ride(status='OPERATING')
+ ride = self._create_ride(status="OPERATING")
ride_ct = ContentType.objects.get_for_model(ride)
# Scheduled closing workflow: OPERATING -> CLOSING -> CLOSED_PERM
ride.transition_to_closing(user=self.moderator)
ride.closing_date = (timezone.now() + timedelta(days=30)).date()
- ride.post_closing_status = 'DEMOLISHED'
+ ride.post_closing_status = "DEMOLISHED"
ride.save()
ride.transition_to_closed_perm(user=self.moderator)
ride.save()
- logs = StateLog.objects.filter(
- content_type=ride_ct,
- object_id=ride.id
- ).order_by('timestamp')
+ logs = StateLog.objects.filter(content_type=ride_ct, object_id=ride.id).order_by("timestamp")
self.assertEqual(logs.count(), 2, "Should have 2 log entries")
- self.assertEqual(logs[0].state, 'CLOSING')
- self.assertEqual(logs[1].state, 'CLOSED_PERM')
+ self.assertEqual(logs[0].state, "CLOSING")
+ self.assertEqual(logs[1].state, "CLOSED_PERM")
diff --git a/backend/apps/rides/views.py b/backend/apps/rides/views.py
index f8970f76..66aef276 100644
--- a/backend/apps/rides/views.py
+++ b/backend/apps/rides/views.py
@@ -98,9 +98,7 @@ def show_coaster_fields(request: HttpRequest) -> HttpResponse:
return render(request, "rides/partials/coaster_fields.html")
-def ride_status_actions(
- request: HttpRequest, park_slug: str, ride_slug: str
-) -> HttpResponse:
+def ride_status_actions(request: HttpRequest, park_slug: str, ride_slug: str) -> HttpResponse:
"""
Return FSM status actions for ride moderators.
@@ -131,9 +129,7 @@ def ride_status_actions(
)
-def ride_header_badge(
- request: HttpRequest, park_slug: str, ride_slug: str
-) -> HttpResponse:
+def ride_header_badge(request: HttpRequest, park_slug: str, ride_slug: str) -> HttpResponse:
"""
Return the header status badge partial for a ride.
@@ -205,9 +201,7 @@ class RideDetailView(HistoryMixin, DetailView):
return context
-class RideCreateView(
- LoginRequiredMixin, ParkContextRequired, RideFormMixin, CreateView
-):
+class RideCreateView(LoginRequiredMixin, ParkContextRequired, RideFormMixin, CreateView):
"""
View for creating a new ride.
@@ -389,9 +383,7 @@ class RideListView(ListView):
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
- context["category_choices"] = [
- (choice.value, choice.label) for choice in choices
- ]
+ context["category_choices"] = [(choice.value, choice.label) for choice in choices]
# Add filter summary for display
if filter_form.is_valid():
@@ -512,10 +504,7 @@ def get_search_suggestions(request: HttpRequest) -> HttpResponse:
if query:
# Get common ride names
matching_names = (
- Ride.objects.filter(name__icontains=query)
- .values("name")
- .annotate(count=Count("id"))
- .order_by("-count")[:3]
+ Ride.objects.filter(name__icontains=query).values("name").annotate(count=Count("id")).order_by("-count")[:3]
)
for match in matching_names:
@@ -663,18 +652,14 @@ class RideRankingsView(ListView):
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
- context["category_choices"] = [
- (choice.value, choice.label) for choice in choices
- ]
+ context["category_choices"] = [(choice.value, choice.label) for choice in choices]
context["selected_category"] = self.request.GET.get("category", "all")
context["min_riders"] = self.request.GET.get("min_riders", "")
# Add statistics
if self.object_list:
context["total_ranked"] = RideRanking.objects.count()
- context["last_updated"] = (
- self.object_list[0].last_calculated if self.object_list else None
- )
+ context["last_updated"] = self.object_list[0].last_calculated if self.object_list else None
return context
@@ -688,9 +673,9 @@ class RideRankingDetailView(DetailView):
def get_queryset(self):
"""Get ride with ranking data."""
- return Ride.objects.select_related(
- "park", "manufacturer", "ranking"
- ).prefetch_related("comparisons_as_a", "comparisons_as_b", "ranking_history")
+ return Ride.objects.select_related("park", "manufacturer", "ranking").prefetch_related(
+ "comparisons_as_a", "comparisons_as_b", "ranking_history"
+ )
def get_context_data(self, **kwargs):
"""Add ranking details to context."""
@@ -704,14 +689,10 @@ class RideRankingDetailView(DetailView):
context.update(ranking_details)
# Get recent movement
- recent_snapshots = RankingSnapshot.objects.filter(
- ride=self.object
- ).order_by("-snapshot_date")[:7]
+ recent_snapshots = RankingSnapshot.objects.filter(ride=self.object).order_by("-snapshot_date")[:7]
if len(recent_snapshots) >= 2:
- context["rank_change"] = (
- recent_snapshots[0].rank - recent_snapshots[1].rank
- )
+ context["rank_change"] = recent_snapshots[0].rank - recent_snapshots[1].rank
context["previous_rank"] = recent_snapshots[1].rank
else:
context["not_ranked"] = True
diff --git a/backend/apps/support/models.py b/backend/apps/support/models.py
index b2817fb4..7d36861c 100644
--- a/backend/apps/support/models.py
+++ b/backend/apps/support/models.py
@@ -5,30 +5,30 @@ from apps.core.history import TrackedModel
class Ticket(TrackedModel):
- STATUS_OPEN = 'open'
- STATUS_IN_PROGRESS = 'in_progress'
- STATUS_CLOSED = 'closed'
+ STATUS_OPEN = "open"
+ STATUS_IN_PROGRESS = "in_progress"
+ STATUS_CLOSED = "closed"
STATUS_CHOICES = [
- (STATUS_OPEN, 'Open'),
- (STATUS_IN_PROGRESS, 'In Progress'),
- (STATUS_CLOSED, 'Closed'),
+ (STATUS_OPEN, "Open"),
+ (STATUS_IN_PROGRESS, "In Progress"),
+ (STATUS_CLOSED, "Closed"),
]
- CATEGORY_GENERAL = 'general'
- CATEGORY_BUG = 'bug'
- CATEGORY_PARTNERSHIP = 'partnership'
- CATEGORY_PRESS = 'press'
- CATEGORY_DATA = 'data'
- CATEGORY_ACCOUNT = 'account'
+ CATEGORY_GENERAL = "general"
+ CATEGORY_BUG = "bug"
+ CATEGORY_PARTNERSHIP = "partnership"
+ CATEGORY_PRESS = "press"
+ CATEGORY_DATA = "data"
+ CATEGORY_ACCOUNT = "account"
CATEGORY_CHOICES = [
- (CATEGORY_GENERAL, 'General Inquiry'),
- (CATEGORY_BUG, 'Bug Report'),
- (CATEGORY_PARTNERSHIP, 'Partnership'),
- (CATEGORY_PRESS, 'Press/Media'),
- (CATEGORY_DATA, 'Data Correction'),
- (CATEGORY_ACCOUNT, 'Account Issue'),
+ (CATEGORY_GENERAL, "General Inquiry"),
+ (CATEGORY_BUG, "Bug Report"),
+ (CATEGORY_PARTNERSHIP, "Partnership"),
+ (CATEGORY_PRESS, "Press/Media"),
+ (CATEGORY_DATA, "Data Correction"),
+ (CATEGORY_ACCOUNT, "Account Issue"),
]
user = models.ForeignKey(
@@ -37,7 +37,7 @@ class Ticket(TrackedModel):
null=True,
blank=True,
related_name="tickets",
- help_text="User who submitted the ticket (optional)"
+ help_text="User who submitted the ticket (optional)",
)
category = models.CharField(
@@ -45,18 +45,13 @@ class Ticket(TrackedModel):
choices=CATEGORY_CHOICES,
default=CATEGORY_GENERAL,
db_index=True,
- help_text="Category of the ticket"
+ help_text="Category of the ticket",
)
subject = models.CharField(max_length=255)
message = models.TextField()
email = models.EmailField(help_text="Contact email", blank=True)
- status = models.CharField(
- max_length=20,
- choices=STATUS_CHOICES,
- default=STATUS_OPEN,
- db_index=True
- )
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_OPEN, db_index=True)
class Meta(TrackedModel.Meta):
verbose_name = "Ticket"
@@ -71,4 +66,3 @@ class Ticket(TrackedModel):
if self.user and not self.email:
self.email = self.user.email
super().save(*args, **kwargs)
-
diff --git a/backend/apps/support/serializers.py b/backend/apps/support/serializers.py
index 701c621a..9990745d 100644
--- a/backend/apps/support/serializers.py
+++ b/backend/apps/support/serializers.py
@@ -7,8 +7,8 @@ from .models import Ticket
class TicketSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
- category_display = serializers.CharField(source='get_category_display', read_only=True)
- status_display = serializers.CharField(source='get_status_display', read_only=True)
+ category_display = serializers.CharField(source="get_category_display", read_only=True)
+ status_display = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
model = Ticket
@@ -29,8 +29,7 @@ class TicketSerializer(serializers.ModelSerializer):
def validate(self, data):
# Ensure email is provided if user is anonymous
- request = self.context.get('request')
- if request and not request.user.is_authenticated and not data.get('email'):
+ request = self.context.get("request")
+ if request and not request.user.is_authenticated and not data.get("email"):
raise serializers.ValidationError({"email": "Email is required for guests."})
return data
-
diff --git a/backend/apps/support/views.py b/backend/apps/support/views.py
index 94d028da..ff869ce4 100644
--- a/backend/apps/support/views.py
+++ b/backend/apps/support/views.py
@@ -11,9 +11,10 @@ class TicketViewSet(viewsets.ModelViewSet):
Only Staff can LIST/RETRIEVE/UPDATE all.
Users can LIST/RETRIEVE their own.
"""
+
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
- permission_classes = [permissions.AllowAny] # We handle granular perms in get_queryset/perform_create
+ permission_classes = [permissions.AllowAny] # We handle granular perms in get_queryset/perform_create
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["status", "category"]
ordering_fields = ["created_at", "status"]
@@ -25,7 +26,7 @@ class TicketViewSet(viewsets.ModelViewSet):
return Ticket.objects.all()
if user.is_authenticated:
return Ticket.objects.filter(user=user)
- return Ticket.objects.none() # Guests can't list tickets
+ return Ticket.objects.none() # Guests can't list tickets
def perform_create(self, serializer):
if self.request.user.is_authenticated:
diff --git a/backend/config/django/base.py b/backend/config/django/base.py
index 1fc59729..a1083874 100644
--- a/backend/config/django/base.py
+++ b/backend/config/django/base.py
@@ -42,16 +42,12 @@ DEBUG = config("DEBUG", default=True, cast=bool)
# Allowed hosts (comma-separated in .env)
ALLOWED_HOSTS = config(
- "ALLOWED_HOSTS",
- default="localhost,127.0.0.1",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
+ "ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# CSRF trusted origins (comma-separated in .env)
CSRF_TRUSTED_ORIGINS = config(
- "CSRF_TRUSTED_ORIGINS",
- default="",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
+ "CSRF_TRUSTED_ORIGINS", default="", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# =============================================================================
diff --git a/backend/config/django/local.py b/backend/config/django/local.py
index f13c838b..f03e34ab 100644
--- a/backend/config/django/local.py
+++ b/backend/config/django/local.py
@@ -149,10 +149,7 @@ LOGGING = {
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
- "format": (
- "%(levelname)s %(asctime)s %(module)s %(process)d "
- "%(thread)d %(message)s"
- ),
+ "format": ("%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s"),
},
},
"handlers": {
diff --git a/backend/config/django/production.py b/backend/config/django/production.py
index 9a0ba147..4281c280 100644
--- a/backend/config/django/production.py
+++ b/backend/config/django/production.py
@@ -20,16 +20,10 @@ from .base import * # noqa: F401,F403
DEBUG = False
# Allowed hosts must be explicitly set in production
-ALLOWED_HOSTS = config(
- "ALLOWED_HOSTS",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
-)
+ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()])
# CSRF trusted origins for production
-CSRF_TRUSTED_ORIGINS = config(
- "CSRF_TRUSTED_ORIGINS",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
-)
+CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()])
# =============================================================================
# Security Settings for Production
@@ -75,9 +69,7 @@ if redis_url:
"PARSER_CLASS": "redis.connection.HiredisParser",
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
- "max_connections": config(
- "REDIS_MAX_CONNECTIONS", default=100, cast=int
- ),
+ "max_connections": config("REDIS_MAX_CONNECTIONS", default=100, cast=int),
"timeout": 20,
"socket_keepalive": True,
"retry_on_timeout": True,
@@ -119,9 +111,7 @@ if redis_url:
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Update STORAGES for Django 4.2+
-STORAGES["staticfiles"]["BACKEND"] = ( # noqa: F405
- "whitenoise.storage.CompressedManifestStaticFilesStorage"
-)
+STORAGES["staticfiles"]["BACKEND"] = "whitenoise.storage.CompressedManifestStaticFilesStorage" # noqa: F405
# =============================================================================
# Production REST Framework Settings
@@ -148,8 +138,7 @@ LOGGING = {
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": (
- "%(levelname)s %(asctime)s %(module)s %(process)d "
- "%(thread)d %(message)s %(pathname)s %(lineno)d"
+ "%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s %(pathname)s %(lineno)d"
),
},
"simple": {
@@ -257,9 +246,7 @@ if SENTRY_DSN:
RedisIntegration(),
],
environment=config("SENTRY_ENVIRONMENT", default="production"),
- traces_sample_rate=config(
- "SENTRY_TRACES_SAMPLE_RATE", default=0.1, cast=float
- ),
+ traces_sample_rate=config("SENTRY_TRACES_SAMPLE_RATE", default=0.1, cast=float),
send_default_pii=False, # Don't send PII to Sentry
attach_stacktrace=True,
)
diff --git a/backend/config/settings/cache.py b/backend/config/settings/cache.py
index d4934390..47c16e56 100644
--- a/backend/config/settings/cache.py
+++ b/backend/config/settings/cache.py
@@ -46,15 +46,13 @@ CACHES = {
# Connection pooling for better performance
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
- "max_connections": config(
- "REDIS_MAX_CONNECTIONS", default=100, cast=int
- ),
+ "max_connections": config("REDIS_MAX_CONNECTIONS", default=100, cast=int),
"timeout": config("REDIS_CONNECTION_TIMEOUT", default=20, cast=int),
"socket_keepalive": True,
"socket_keepalive_options": {
- 1: 1, # TCP_KEEPIDLE: Start keepalive after 1s idle
- 2: 1, # TCP_KEEPINTVL: Send probes every 1s
- 3: 3, # TCP_KEEPCNT: Close after 3 failed probes
+ 1: 1, # TCP_KEEPIDLE: Start keepalive after 1s idle
+ 2: 1, # TCP_KEEPINTVL: Send probes every 1s
+ 3: 3, # TCP_KEEPCNT: Close after 3 failed probes
},
"retry_on_timeout": True,
"health_check_interval": 30,
@@ -62,14 +60,11 @@ CACHES = {
# Compress cached data to save memory
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
# Graceful degradation if Redis is unavailable
- "IGNORE_EXCEPTIONS": config(
- "REDIS_IGNORE_EXCEPTIONS", default=True, cast=bool
- ),
+ "IGNORE_EXCEPTIONS": config("REDIS_IGNORE_EXCEPTIONS", default=True, cast=bool),
},
"KEY_PREFIX": config("CACHE_KEY_PREFIX", default="thrillwiki"),
"VERSION": 1,
},
-
# Session cache - separate for security isolation
# Uses a different Redis database (db 2)
"sessions": {
@@ -80,16 +75,13 @@ CACHES = {
"PARSER_CLASS": "redis.connection.HiredisParser",
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
- "max_connections": config(
- "REDIS_SESSIONS_MAX_CONNECTIONS", default=50, cast=int
- ),
+ "max_connections": config("REDIS_SESSIONS_MAX_CONNECTIONS", default=50, cast=int),
"timeout": 10,
"socket_keepalive": True,
},
},
"KEY_PREFIX": "sessions",
},
-
# API cache - high concurrency for API responses
# Uses a different Redis database (db 3)
"api": {
@@ -100,9 +92,7 @@ CACHES = {
"PARSER_CLASS": "redis.connection.HiredisParser",
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
- "max_connections": config(
- "REDIS_API_MAX_CONNECTIONS", default=100, cast=int
- ),
+ "max_connections": config("REDIS_API_MAX_CONNECTIONS", default=100, cast=int),
"timeout": 15,
"socket_keepalive": True,
"retry_on_timeout": True,
@@ -126,14 +116,10 @@ SESSION_CACHE_ALIAS = "sessions"
SESSION_COOKIE_AGE = config("SESSION_COOKIE_AGE", default=3600, cast=int)
# Update session on each request (sliding expiry)
-SESSION_SAVE_EVERY_REQUEST = config(
- "SESSION_SAVE_EVERY_REQUEST", default=True, cast=bool
-)
+SESSION_SAVE_EVERY_REQUEST = config("SESSION_SAVE_EVERY_REQUEST", default=True, cast=bool)
# Session persists until cookie expires (not browser close)
-SESSION_EXPIRE_AT_BROWSER_CLOSE = config(
- "SESSION_EXPIRE_AT_BROWSER_CLOSE", default=False, cast=bool
-)
+SESSION_EXPIRE_AT_BROWSER_CLOSE = config("SESSION_EXPIRE_AT_BROWSER_CLOSE", default=False, cast=bool)
# =============================================================================
# Cache Middleware Settings
@@ -141,6 +127,4 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = config(
# For Django's cache middleware (UpdateCacheMiddleware/FetchFromCacheMiddleware)
CACHE_MIDDLEWARE_SECONDS = config("CACHE_MIDDLEWARE_SECONDS", default=300, cast=int)
-CACHE_MIDDLEWARE_KEY_PREFIX = config(
- "CACHE_MIDDLEWARE_KEY_PREFIX", default="thrillwiki"
-)
+CACHE_MIDDLEWARE_KEY_PREFIX = config("CACHE_MIDDLEWARE_KEY_PREFIX", default="thrillwiki")
diff --git a/backend/config/settings/database.py b/backend/config/settings/database.py
index a1d28c2e..4346b2b5 100644
--- a/backend/config/settings/database.py
+++ b/backend/config/settings/database.py
@@ -26,10 +26,7 @@ from decouple import config
# =============================================================================
# Parse DATABASE_URL environment variable into Django database settings
-DATABASE_URL = config(
- "DATABASE_URL",
- default="postgis://thrillwiki_user:thrillwiki@localhost:5432/thrillwiki_test_db"
-)
+DATABASE_URL = config("DATABASE_URL", default="postgis://thrillwiki_user:thrillwiki@localhost:5432/thrillwiki_test_db")
# Parse the database URL
db_config = dj_database_url.parse(DATABASE_URL)
@@ -84,14 +81,8 @@ if "postgis" in DATABASE_URL or "postgresql" in DATABASE_URL:
# macOS with Homebrew (default)
# Linux: /usr/lib/x86_64-linux-gnu/libgdal.so
# Docker: Usually handled by the image
-GDAL_LIBRARY_PATH = config(
- "GDAL_LIBRARY_PATH",
- default="/opt/homebrew/lib/libgdal.dylib"
-)
-GEOS_LIBRARY_PATH = config(
- "GEOS_LIBRARY_PATH",
- default="/opt/homebrew/lib/libgeos_c.dylib"
-)
+GDAL_LIBRARY_PATH = config("GDAL_LIBRARY_PATH", default="/opt/homebrew/lib/libgdal.dylib")
+GEOS_LIBRARY_PATH = config("GEOS_LIBRARY_PATH", default="/opt/homebrew/lib/libgeos_c.dylib")
# =============================================================================
# Read Replica Configuration (Optional)
diff --git a/backend/config/settings/email.py b/backend/config/settings/email.py
index d5f37d68..b3e12f49 100644
--- a/backend/config/settings/email.py
+++ b/backend/config/settings/email.py
@@ -21,10 +21,7 @@ from decouple import config
# - ForwardEmail: django_forwardemail.backends.ForwardEmailBackend (production)
# - SMTP: django.core.mail.backends.smtp.EmailBackend (custom SMTP)
-EMAIL_BACKEND = config(
- "EMAIL_BACKEND",
- default="django_forwardemail.backends.ForwardEmailBackend"
-)
+EMAIL_BACKEND = config("EMAIL_BACKEND", default="django_forwardemail.backends.ForwardEmailBackend")
# =============================================================================
# ForwardEmail Configuration
@@ -32,10 +29,7 @@ EMAIL_BACKEND = config(
# ForwardEmail is a privacy-focused email service that supports custom domains
# https://forwardemail.net/
-FORWARD_EMAIL_BASE_URL = config(
- "FORWARD_EMAIL_BASE_URL",
- default="https://api.forwardemail.net"
-)
+FORWARD_EMAIL_BASE_URL = config("FORWARD_EMAIL_BASE_URL", default="https://api.forwardemail.net")
FORWARD_EMAIL_API_KEY = config("FORWARD_EMAIL_API_KEY", default="")
FORWARD_EMAIL_DOMAIN = config("FORWARD_EMAIL_DOMAIN", default="")
@@ -62,10 +56,7 @@ EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD", default="")
EMAIL_TIMEOUT = config("EMAIL_TIMEOUT", default=30, cast=int)
# Default from email address
-DEFAULT_FROM_EMAIL = config(
- "DEFAULT_FROM_EMAIL",
- default="ThrillWiki "
-)
+DEFAULT_FROM_EMAIL = config("DEFAULT_FROM_EMAIL", default="ThrillWiki ")
# =============================================================================
# Email Subject Prefix
diff --git a/backend/config/settings/local.py b/backend/config/settings/local.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/config/settings/logging.py b/backend/config/settings/logging.py
index 27001221..1238174c 100644
--- a/backend/config/settings/logging.py
+++ b/backend/config/settings/logging.py
@@ -46,10 +46,7 @@ LOGGING_FORMATTERS = {
# JSON format for production - machine parseable for log aggregation
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
- "format": (
- "%(levelname)s %(asctime)s %(module)s %(process)d "
- "%(thread)d %(message)s"
- ),
+ "format": ("%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s"),
},
# Simple format for console output
"simple": {
diff --git a/backend/config/settings/rest_framework.py b/backend/config/settings/rest_framework.py
index 5e524718..95a67970 100644
--- a/backend/config/settings/rest_framework.py
+++ b/backend/config/settings/rest_framework.py
@@ -82,15 +82,11 @@ REST_FRAMEWORK = {
CORS_ALLOW_CREDENTIALS = True
# Allow all origins (not recommended for production)
-CORS_ALLOW_ALL_ORIGINS = config(
- "CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool
-)
+CORS_ALLOW_ALL_ORIGINS = config("CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool)
# Specific allowed origins (comma-separated)
CORS_ALLOWED_ORIGINS = config(
- "CORS_ALLOWED_ORIGINS",
- default="",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
+ "CORS_ALLOWED_ORIGINS", default="", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# Allowed HTTP headers for CORS requests
@@ -129,33 +125,27 @@ CORS_EXPOSE_HEADERS = [
# API Rate Limiting
# =============================================================================
-API_RATE_LIMIT_PER_MINUTE = config(
- "API_RATE_LIMIT_PER_MINUTE", default=60, cast=int
-)
-API_RATE_LIMIT_PER_HOUR = config(
- "API_RATE_LIMIT_PER_HOUR", default=1000, cast=int
-)
+API_RATE_LIMIT_PER_MINUTE = config("API_RATE_LIMIT_PER_MINUTE", default=60, cast=int)
+API_RATE_LIMIT_PER_HOUR = config("API_RATE_LIMIT_PER_HOUR", default=1000, cast=int)
# =============================================================================
# SimpleJWT Settings
# =============================================================================
# JWT token configuration for authentication
+
# Import SECRET_KEY for signing tokens
# This will be set by base.py before this module is imported
def get_secret_key():
"""Get SECRET_KEY lazily to avoid circular imports."""
return config("SECRET_KEY")
+
SIMPLE_JWT = {
# Token lifetimes
# Short access tokens (15 min) provide better security
- "ACCESS_TOKEN_LIFETIME": timedelta(
- minutes=config("JWT_ACCESS_TOKEN_LIFETIME_MINUTES", default=15, cast=int)
- ),
- "REFRESH_TOKEN_LIFETIME": timedelta(
- days=config("JWT_REFRESH_TOKEN_LIFETIME_DAYS", default=7, cast=int)
- ),
+ "ACCESS_TOKEN_LIFETIME": timedelta(minutes=config("JWT_ACCESS_TOKEN_LIFETIME_MINUTES", default=15, cast=int)),
+ "REFRESH_TOKEN_LIFETIME": timedelta(days=config("JWT_REFRESH_TOKEN_LIFETIME_DAYS", default=7, cast=int)),
# Token rotation and blacklisting
# Rotate refresh tokens on each use and blacklist old ones
"ROTATE_REFRESH_TOKENS": True,
@@ -177,9 +167,7 @@ SIMPLE_JWT = {
# User identification
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
- "USER_AUTHENTICATION_RULE": (
- "rest_framework_simplejwt.authentication.default_user_authentication_rule"
- ),
+ "USER_AUTHENTICATION_RULE": ("rest_framework_simplejwt.authentication.default_user_authentication_rule"),
# Token classes
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
@@ -211,9 +199,7 @@ REST_AUTH = {
# SameSite cookie attribute (Lax is compatible with OAuth flows)
"JWT_AUTH_SAMESITE": "Lax",
"JWT_AUTH_RETURN_EXPIRATION": True,
- "JWT_TOKEN_CLAIMS_SERIALIZER": (
- "rest_framework_simplejwt.serializers.TokenObtainPairSerializer"
- ),
+ "JWT_TOKEN_CLAIMS_SERIALIZER": ("rest_framework_simplejwt.serializers.TokenObtainPairSerializer"),
}
# =============================================================================
diff --git a/backend/config/settings/secrets.py b/backend/config/settings/secrets.py
index 8336680b..634cc195 100644
--- a/backend/config/settings/secrets.py
+++ b/backend/config/settings/secrets.py
@@ -31,17 +31,13 @@ logger = logging.getLogger("security")
# =============================================================================
# Enable secret rotation checking (set to True in production)
-SECRET_ROTATION_ENABLED = config(
- "SECRET_ROTATION_ENABLED", default=False, cast=bool
-)
+SECRET_ROTATION_ENABLED = config("SECRET_ROTATION_ENABLED", default=False, cast=bool)
# Secret version for tracking rotations
SECRET_KEY_VERSION = config("SECRET_KEY_VERSION", default="1")
# Secret expiry warning threshold (days before expiry to start warning)
-SECRET_EXPIRY_WARNING_DAYS = config(
- "SECRET_EXPIRY_WARNING_DAYS", default=30, cast=int
-)
+SECRET_EXPIRY_WARNING_DAYS = config("SECRET_EXPIRY_WARNING_DAYS", default=30, cast=int)
# =============================================================================
# Required Secrets Registry
@@ -104,10 +100,7 @@ def validate_secret_strength(name: str, value: str, min_length: int = 10) -> boo
return False
if len(value) < min_length:
- logger.error(
- f"Secret '{name}' is too short ({len(value)} chars, "
- f"minimum {min_length})"
- )
+ logger.error(f"Secret '{name}' is too short ({len(value)} chars, " f"minimum {min_length})")
return False
# Check for placeholder values
@@ -123,9 +116,7 @@ def validate_secret_strength(name: str, value: str, min_length: int = 10) -> boo
value_lower = value.lower()
for pattern in placeholder_patterns:
if pattern in value_lower:
- logger.warning(
- f"Secret '{name}' appears to contain a placeholder value"
- )
+ logger.warning(f"Secret '{name}' appears to contain a placeholder value")
return False
return True
@@ -148,9 +139,7 @@ def validate_secret_key(secret_key: str) -> bool:
bool: True if valid, False otherwise
"""
if len(secret_key) < 50:
- logger.error(
- f"SECRET_KEY is too short ({len(secret_key)} chars, minimum 50)"
- )
+ logger.error(f"SECRET_KEY is too short ({len(secret_key)} chars, minimum 50)")
return False
has_upper = any(c.isupper() for c in secret_key)
@@ -159,10 +148,7 @@ def validate_secret_key(secret_key: str) -> bool:
has_special = any(not c.isalnum() for c in secret_key)
if not all([has_upper, has_lower, has_digit, has_special]):
- logger.warning(
- "SECRET_KEY should contain uppercase, lowercase, digits, "
- "and special characters"
- )
+ logger.warning("SECRET_KEY should contain uppercase, lowercase, digits, " "and special characters")
# Don't fail, just warn - some generated keys may not have all
return True
@@ -193,7 +179,7 @@ def get_secret(
value = config(name, default=default)
except UndefinedValueError:
if required:
- raise ValueError(f"Required secret '{name}' is not set")
+ raise ValueError(f"Required secret '{name}' is not set") from None
return default
if value and min_length > 0 and not validate_secret_strength(name, value, min_length):
@@ -231,7 +217,7 @@ def validate_required_secrets(raise_on_error: bool = False) -> list[str]:
msg = f"Required secret '{name}' is not set: {rules['description']}"
errors.append(msg)
if raise_on_error:
- raise ValueError(msg)
+ raise ValueError(msg) from None
return errors
@@ -257,9 +243,7 @@ def check_secret_expiry() -> list[str]:
version = int(SECRET_KEY_VERSION)
# If version is very old, suggest rotation
if version < 2:
- warnings_list.append(
- "SECRET_KEY version is old. Consider rotating secrets."
- )
+ warnings_list.append("SECRET_KEY version is old. Consider rotating secrets.")
except ValueError:
pass
@@ -316,8 +300,7 @@ class EnvironmentSecretProvider(SecretProvider):
def set_secret(self, name: str, value: str) -> bool:
"""Environment variables are read-only at runtime."""
logger.warning(
- f"Cannot set secret '{name}' in environment provider. "
- "Update your .env file or environment variables."
+ f"Cannot set secret '{name}' in environment provider. " "Update your .env file or environment variables."
)
return False
@@ -385,4 +368,4 @@ def run_startup_validation() -> None:
raise ValueError("SECRET_KEY does not meet security requirements")
except UndefinedValueError:
if not debug_mode:
- raise ValueError("SECRET_KEY is required in production")
+ raise ValueError("SECRET_KEY is required in production") from None
diff --git a/backend/config/settings/security.py b/backend/config/settings/security.py
index 9344993e..db3d42bd 100644
--- a/backend/config/settings/security.py
+++ b/backend/config/settings/security.py
@@ -35,15 +35,11 @@ TURNSTILE_VERIFY_URL = config(
# X-XSS-Protection: Enables browser's built-in XSS filter
# Note: Modern browsers are deprecating this in favor of CSP, but it's still
# useful for older browsers
-SECURE_BROWSER_XSS_FILTER = config(
- "SECURE_BROWSER_XSS_FILTER", default=True, cast=bool
-)
+SECURE_BROWSER_XSS_FILTER = config("SECURE_BROWSER_XSS_FILTER", default=True, cast=bool)
# X-Content-Type-Options: Prevents MIME type sniffing attacks
# When True, adds "X-Content-Type-Options: nosniff" header
-SECURE_CONTENT_TYPE_NOSNIFF = config(
- "SECURE_CONTENT_TYPE_NOSNIFF", default=True, cast=bool
-)
+SECURE_CONTENT_TYPE_NOSNIFF = config("SECURE_CONTENT_TYPE_NOSNIFF", default=True, cast=bool)
# X-Frame-Options: Protects against clickjacking attacks
# DENY = Never allow framing (most secure)
@@ -53,24 +49,18 @@ X_FRAME_OPTIONS = config("X_FRAME_OPTIONS", default="DENY")
# Referrer-Policy: Controls how much referrer information is sent
# strict-origin-when-cross-origin = Send full URL for same-origin,
# only origin for cross-origin, nothing for downgrade
-SECURE_REFERRER_POLICY = config(
- "SECURE_REFERRER_POLICY", default="strict-origin-when-cross-origin"
-)
+SECURE_REFERRER_POLICY = config("SECURE_REFERRER_POLICY", default="strict-origin-when-cross-origin")
# Cross-Origin-Opener-Policy: Prevents cross-origin attacks via window references
# same-origin = Document can only be accessed by windows from same origin
-SECURE_CROSS_ORIGIN_OPENER_POLICY = config(
- "SECURE_CROSS_ORIGIN_OPENER_POLICY", default="same-origin"
-)
+SECURE_CROSS_ORIGIN_OPENER_POLICY = config("SECURE_CROSS_ORIGIN_OPENER_POLICY", default="same-origin")
# =============================================================================
# HSTS (HTTP Strict Transport Security) Configuration
# =============================================================================
# Include subdomains in HSTS policy
-SECURE_HSTS_INCLUDE_SUBDOMAINS = config(
- "SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True, cast=bool
-)
+SECURE_HSTS_INCLUDE_SUBDOMAINS = config("SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True, cast=bool)
# HSTS max-age in seconds (31536000 = 1 year, recommended minimum)
SECURE_HSTS_SECONDS = config("SECURE_HSTS_SECONDS", default=31536000, cast=int)
@@ -82,9 +72,7 @@ SECURE_HSTS_PRELOAD = config("SECURE_HSTS_PRELOAD", default=False, cast=bool)
# URLs exempt from SSL redirect (e.g., health checks)
# Format: comma-separated list of URL patterns
SECURE_REDIRECT_EXEMPT = config(
- "SECURE_REDIRECT_EXEMPT",
- default="",
- cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
+ "SECURE_REDIRECT_EXEMPT", default="", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# Redirect all HTTP requests to HTTPS
@@ -93,9 +81,7 @@ SECURE_SSL_REDIRECT = config("SECURE_SSL_REDIRECT", default=False, cast=bool)
# Header used by proxy to indicate HTTPS
# Common values: ('HTTP_X_FORWARDED_PROTO', 'https')
_proxy_ssl_header = config("SECURE_PROXY_SSL_HEADER", default="")
-SECURE_PROXY_SSL_HEADER = (
- tuple(_proxy_ssl_header.split(",")) if _proxy_ssl_header else None
-)
+SECURE_PROXY_SSL_HEADER = tuple(_proxy_ssl_header.split(",")) if _proxy_ssl_header else None
# =============================================================================
# Session Cookie Security
@@ -143,9 +129,7 @@ AUTHENTICATION_BACKENDS = [
AUTH_PASSWORD_VALIDATORS = [
{
- "NAME": (
- "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
- ),
+ "NAME": ("django.contrib.auth.password_validation.UserAttributeSimilarityValidator"),
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
diff --git a/backend/config/settings/storage.py b/backend/config/settings/storage.py
index dc33615d..a657c3ca 100644
--- a/backend/config/settings/storage.py
+++ b/backend/config/settings/storage.py
@@ -37,19 +37,13 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
# WhiteNoise serves static files efficiently without a separate web server
# Compression quality for Brotli/Gzip (1-100, higher = better but slower)
-WHITENOISE_COMPRESSION_QUALITY = config(
- "WHITENOISE_COMPRESSION_QUALITY", default=90, cast=int
-)
+WHITENOISE_COMPRESSION_QUALITY = config("WHITENOISE_COMPRESSION_QUALITY", default=90, cast=int)
# Cache max-age for static files (1 year for immutable content)
-WHITENOISE_MAX_AGE = config(
- "WHITENOISE_MAX_AGE", default=31536000, cast=int
-)
+WHITENOISE_MAX_AGE = config("WHITENOISE_MAX_AGE", default=31536000, cast=int)
# Don't fail on missing manifest entries (graceful degradation)
-WHITENOISE_MANIFEST_STRICT = config(
- "WHITENOISE_MANIFEST_STRICT", default=False, cast=bool
-)
+WHITENOISE_MANIFEST_STRICT = config("WHITENOISE_MANIFEST_STRICT", default=False, cast=bool)
# Additional MIME types
WHITENOISE_MIMETYPES = {
@@ -59,11 +53,26 @@ WHITENOISE_MIMETYPES = {
# Skip compressing already compressed formats
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = [
- "jpg", "jpeg", "png", "gif", "webp", # Images
- "zip", "gz", "tgz", "bz2", "tbz", "xz", "br", # Archives
- "swf", "flv", # Flash
- "woff", "woff2", # Fonts
- "mp3", "mp4", "ogg", "webm", # Media
+ "jpg",
+ "jpeg",
+ "png",
+ "gif",
+ "webp", # Images
+ "zip",
+ "gz",
+ "tgz",
+ "bz2",
+ "tbz",
+ "xz",
+ "br", # Archives
+ "swf",
+ "flv", # Flash
+ "woff",
+ "woff2", # Fonts
+ "mp3",
+ "mp4",
+ "ogg",
+ "webm", # Media
]
# =============================================================================
@@ -103,20 +112,14 @@ STORAGES = {
# Maximum size (in bytes) of file to upload into memory (2.5MB)
# Files larger than this are written to disk
-FILE_UPLOAD_MAX_MEMORY_SIZE = config(
- "FILE_UPLOAD_MAX_MEMORY_SIZE", default=2621440, cast=int
-)
+FILE_UPLOAD_MAX_MEMORY_SIZE = config("FILE_UPLOAD_MAX_MEMORY_SIZE", default=2621440, cast=int)
# Maximum size (in bytes) of request data (10MB)
# This limits the total size of POST request body
-DATA_UPLOAD_MAX_MEMORY_SIZE = config(
- "DATA_UPLOAD_MAX_MEMORY_SIZE", default=10485760, cast=int
-)
+DATA_UPLOAD_MAX_MEMORY_SIZE = config("DATA_UPLOAD_MAX_MEMORY_SIZE", default=10485760, cast=int)
# Maximum number of GET/POST parameters (1000)
-DATA_UPLOAD_MAX_NUMBER_FIELDS = config(
- "DATA_UPLOAD_MAX_NUMBER_FIELDS", default=1000, cast=int
-)
+DATA_UPLOAD_MAX_NUMBER_FIELDS = config("DATA_UPLOAD_MAX_NUMBER_FIELDS", default=1000, cast=int)
# File upload permissions (0o644 = rw-r--r--)
FILE_UPLOAD_PERMISSIONS = 0o644
diff --git a/backend/config/settings/third_party.py b/backend/config/settings/third_party.py
index ee053b16..e74453e0 100644
--- a/backend/config/settings/third_party.py
+++ b/backend/config/settings/third_party.py
@@ -33,9 +33,7 @@ ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
ACCOUNT_LOGIN_METHODS = {"email", "username"}
# Email verification settings
-ACCOUNT_EMAIL_VERIFICATION = config(
- "ACCOUNT_EMAIL_VERIFICATION", default="mandatory"
-)
+ACCOUNT_EMAIL_VERIFICATION = config("ACCOUNT_EMAIL_VERIFICATION", default="mandatory")
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True
@@ -114,12 +112,8 @@ CELERY_BROKER_URL = config("REDIS_URL", default="redis://localhost:6379/1")
CELERY_RESULT_BACKEND = config("REDIS_URL", default="redis://localhost:6379/1")
# Task settings for test environments
-CELERY_TASK_ALWAYS_EAGER = config(
- "CELERY_TASK_ALWAYS_EAGER", default=False, cast=bool
-)
-CELERY_TASK_EAGER_PROPAGATES = config(
- "CELERY_TASK_EAGER_PROPAGATES", default=False, cast=bool
-)
+CELERY_TASK_ALWAYS_EAGER = config("CELERY_TASK_ALWAYS_EAGER", default=False, cast=bool)
+CELERY_TASK_EAGER_PROPAGATES = config("CELERY_TASK_EAGER_PROPAGATES", default=False, cast=bool)
# =============================================================================
# Health Check Configuration
@@ -165,16 +159,10 @@ CLOUDFLARE_IMAGES = {
"DEFAULT_VARIANT": config("CLOUDFLARE_IMAGES_DEFAULT_VARIANT", default="public"),
"UPLOAD_TIMEOUT": config("CLOUDFLARE_IMAGES_UPLOAD_TIMEOUT", default=300, cast=int),
"WEBHOOK_SECRET": config("CLOUDFLARE_IMAGES_WEBHOOK_SECRET", default=""),
- "CLEANUP_EXPIRED_HOURS": config(
- "CLOUDFLARE_IMAGES_CLEANUP_HOURS", default=24, cast=int
- ),
- "MAX_FILE_SIZE": config(
- "CLOUDFLARE_IMAGES_MAX_FILE_SIZE", default=10 * 1024 * 1024, cast=int
- ),
+ "CLEANUP_EXPIRED_HOURS": config("CLOUDFLARE_IMAGES_CLEANUP_HOURS", default=24, cast=int),
+ "MAX_FILE_SIZE": config("CLOUDFLARE_IMAGES_MAX_FILE_SIZE", default=10 * 1024 * 1024, cast=int),
"ALLOWED_FORMATS": ["jpeg", "png", "gif", "webp"],
- "REQUIRE_SIGNED_URLS": config(
- "CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS", default=False, cast=bool
- ),
+ "REQUIRE_SIGNED_URLS": config("CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS", default=False, cast=bool),
"DEFAULT_METADATA": {},
}
@@ -183,21 +171,13 @@ CLOUDFLARE_IMAGES = {
# =============================================================================
# Settings for the road trip planning service using OpenStreetMap
-ROADTRIP_CACHE_TIMEOUT = config(
- "ROADTRIP_CACHE_TIMEOUT", default=3600 * 24, cast=int
-) # 24 hours for geocoding
-ROADTRIP_ROUTE_CACHE_TIMEOUT = config(
- "ROADTRIP_ROUTE_CACHE_TIMEOUT", default=3600 * 6, cast=int
-) # 6 hours for routes
+ROADTRIP_CACHE_TIMEOUT = config("ROADTRIP_CACHE_TIMEOUT", default=3600 * 24, cast=int) # 24 hours for geocoding
+ROADTRIP_ROUTE_CACHE_TIMEOUT = config("ROADTRIP_ROUTE_CACHE_TIMEOUT", default=3600 * 6, cast=int) # 6 hours for routes
ROADTRIP_MAX_REQUESTS_PER_SECOND = config(
"ROADTRIP_MAX_REQUESTS_PER_SECOND", default=1, cast=int
) # Respect OSM rate limits
-ROADTRIP_USER_AGENT = config(
- "ROADTRIP_USER_AGENT", default="ThrillWiki/1.0 (https://thrillwiki.com)"
-)
-ROADTRIP_REQUEST_TIMEOUT = config(
- "ROADTRIP_REQUEST_TIMEOUT", default=10, cast=int
-) # seconds
+ROADTRIP_USER_AGENT = config("ROADTRIP_USER_AGENT", default="ThrillWiki/1.0 (https://thrillwiki.com)")
+ROADTRIP_REQUEST_TIMEOUT = config("ROADTRIP_REQUEST_TIMEOUT", default=10, cast=int) # seconds
ROADTRIP_MAX_RETRIES = config("ROADTRIP_MAX_RETRIES", default=3, cast=int)
ROADTRIP_BACKOFF_FACTOR = config("ROADTRIP_BACKOFF_FACTOR", default=2, cast=int)
@@ -206,9 +186,7 @@ ROADTRIP_BACKOFF_FACTOR = config("ROADTRIP_BACKOFF_FACTOR", default=2, cast=int)
# =============================================================================
# django-autocomplete-light settings
-AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = config(
- "AUTOCOMPLETE_BLOCK_UNAUTHENTICATED", default=False, cast=bool
-)
+AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = config("AUTOCOMPLETE_BLOCK_UNAUTHENTICATED", default=False, cast=bool)
# =============================================================================
# Frontend Configuration
@@ -226,7 +204,5 @@ TURNSTILE_SECRET = config("TURNSTILE_SECRET", default="")
# Skip Turnstile validation in development if keys not set
TURNSTILE_SKIP_VALIDATION = config(
- "TURNSTILE_SKIP_VALIDATION",
- default=not TURNSTILE_SECRET, # Skip if no secret
- cast=bool
+ "TURNSTILE_SKIP_VALIDATION", default=not TURNSTILE_SECRET, cast=bool # Skip if no secret
)
diff --git a/backend/config/settings/validation.py b/backend/config/settings/validation.py
index 86a11ca7..6fb040a9 100644
--- a/backend/config/settings/validation.py
+++ b/backend/config/settings/validation.py
@@ -160,19 +160,13 @@ def validate_email(value: str) -> bool:
def validate_type(value: Any, expected_type: type) -> bool:
"""Validate that a value is of the expected type."""
- if expected_type == bool:
+ if expected_type is bool:
# Special handling for boolean strings
- return isinstance(value, bool) or str(value).lower() in (
- "true", "false", "1", "0", "yes", "no"
- )
+ return isinstance(value, bool) or str(value).lower() in ("true", "false", "1", "0", "yes", "no")
return isinstance(value, expected_type)
-def validate_range(
- value: Any,
- min_value: Any | None = None,
- max_value: Any | None = None
-) -> bool:
+def validate_range(value: Any, min_value: Any | None = None, max_value: Any | None = None) -> bool:
"""Validate that a value is within a specified range."""
if min_value is not None and value < min_value:
return False
@@ -215,11 +209,11 @@ def validate_variable(name: str, rules: dict) -> list[str]:
var_type = rules.get("type", str)
default = rules.get("default")
- if var_type == bool:
+ if var_type is bool:
value = config(name, default=default, cast=bool)
- elif var_type == int:
+ elif var_type is int:
value = config(name, default=default, cast=int)
- elif var_type == float:
+ elif var_type is float:
value = config(name, default=default, cast=float)
else:
value = config(name, default=default)
@@ -233,29 +227,21 @@ def validate_variable(name: str, rules: dict) -> list[str]:
# Type validation
if not validate_type(value, rules.get("type", str)):
- errors.append(
- f"{name}: Expected type {rules['type'].__name__}, "
- f"got {type(value).__name__}"
- )
+ errors.append(f"{name}: Expected type {rules['type'].__name__}, " f"got {type(value).__name__}")
# Length validation (for strings)
if isinstance(value, str):
min_length = rules.get("min_length", 0)
max_length = rules.get("max_length")
if not validate_length(value, min_length, max_length):
- errors.append(
- f"{name}: Length must be between {min_length} and "
- f"{max_length or 'unlimited'}"
- )
+ errors.append(f"{name}: Length must be between {min_length} and " f"{max_length or 'unlimited'}")
# Range validation (for numbers)
- if isinstance(value, (int, float)):
+ if isinstance(value, int | float):
min_value = rules.get("min_value")
max_value = rules.get("max_value")
if not validate_range(value, min_value, max_value):
- errors.append(
- f"{name}: Value must be between {min_value} and {max_value}"
- )
+ errors.append(f"{name}: Value must be between {min_value} and {max_value}")
# Custom validator
validator_name = rules.get("validator")
@@ -285,13 +271,9 @@ def validate_cross_rules() -> list[str]:
try:
value = config(var_name, default=None)
if value is not None and not check_fn(value):
- errors.append(
- f"{rule['name']}: {var_name} {message}"
- )
+ errors.append(f"{rule['name']}: {var_name} {message}")
except Exception:
- errors.append(
- f"{rule['name']}: Could not validate {var_name}"
- )
+ errors.append(f"{rule['name']}: Could not validate {var_name}")
except Exception as e:
errors.append(f"Cross-validation error for {rule['name']}: {e}")
@@ -343,9 +325,7 @@ def validate_all_settings(raise_on_error: bool = False) -> dict:
logger.error(f"Configuration error: {error}")
if raise_on_error:
- raise ValueError(
- f"Configuration validation failed: {result['errors']}"
- )
+ raise ValueError(f"Configuration validation failed: {result['errors']}")
# Log warnings
for warning in result["warnings"]:
@@ -372,9 +352,7 @@ def run_startup_validation() -> None:
for error in result["errors"]:
warnings.warn(f"Configuration error: {error}", stacklevel=2)
else:
- raise ValueError(
- "Configuration validation failed. Check logs for details."
- )
+ raise ValueError("Configuration validation failed. Check logs for details.")
# =============================================================================
diff --git a/backend/ensure_admin.py b/backend/ensure_admin.py
index 6391f2ae..793a263e 100644
--- a/backend/ensure_admin.py
+++ b/backend/ensure_admin.py
@@ -7,10 +7,11 @@ sys.path.append(os.path.join(os.path.dirname(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
django.setup()
-from django.contrib.auth import get_user_model
+from django.contrib.auth import get_user_model # noqa: E402
User = get_user_model()
+
def ensure_admin():
username = "admin"
email = "admin@example.com"
@@ -23,12 +24,13 @@ def ensure_admin():
else:
print(f"Superuser {username} already exists.")
u = User.objects.get(username=username)
- if not u.is_staff or not u.is_superuser or u.role != 'ADMIN':
+ if not u.is_staff or not u.is_superuser or u.role != "ADMIN":
u.is_staff = True
u.is_superuser = True
- u.role = 'ADMIN'
+ u.role = "ADMIN"
u.save()
print("Updated existing user to ADMIN/Superuser.")
+
if __name__ == "__main__":
ensure_admin()
diff --git a/backend/scripts/benchmark_queries.py b/backend/scripts/benchmark_queries.py
index 98f1e451..4f749c44 100644
--- a/backend/scripts/benchmark_queries.py
+++ b/backend/scripts/benchmark_queries.py
@@ -30,6 +30,7 @@ if not settings.DEBUG:
def benchmark(name: str, iterations: int = 5):
"""Decorator to benchmark a function."""
+
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> dict[str, Any]:
@@ -48,17 +49,19 @@ def benchmark(name: str, iterations: int = 5):
query_counts.append(len(context.captured_queries))
return {
- 'name': name,
- 'avg_time_ms': statistics.mean(times),
- 'min_time_ms': min(times),
- 'max_time_ms': max(times),
- 'std_dev_ms': statistics.stdev(times) if len(times) > 1 else 0,
- 'avg_queries': statistics.mean(query_counts),
- 'min_queries': min(query_counts),
- 'max_queries': max(query_counts),
- 'iterations': iterations,
+ "name": name,
+ "avg_time_ms": statistics.mean(times),
+ "min_time_ms": min(times),
+ "max_time_ms": max(times),
+ "std_dev_ms": statistics.stdev(times) if len(times) > 1 else 0,
+ "avg_queries": statistics.mean(query_counts),
+ "min_queries": min(query_counts),
+ "max_queries": max(query_counts),
+ "iterations": iterations,
}
+
return wrapper
+
return decorator
@@ -67,7 +70,9 @@ def print_benchmark_result(result: dict[str, Any]) -> None:
print(f"\n{'='*60}")
print(f"Benchmark: {result['name']}")
print(f"{'='*60}")
- print(f" Time (ms): avg={result['avg_time_ms']:.2f}, min={result['min_time_ms']:.2f}, max={result['max_time_ms']:.2f}")
+ print(
+ f" Time (ms): avg={result['avg_time_ms']:.2f}, min={result['min_time_ms']:.2f}, max={result['max_time_ms']:.2f}"
+ )
print(f" Std Dev (ms): {result['std_dev_ms']:.2f}")
print(f" Queries: avg={result['avg_queries']:.1f}, min={result['min_queries']}, max={result['max_queries']}")
print(f" Iterations: {result['iterations']}")
@@ -86,7 +91,7 @@ def run_benchmarks() -> list[dict[str, Any]]:
parks = Park.objects.optimized_for_list()[:50]
for park in parks:
_ = park.operator
- _ = park.coaster_count_calculated if hasattr(park, 'coaster_count_calculated') else None
+ _ = park.coaster_count_calculated if hasattr(park, "coaster_count_calculated") else None
return list(parks)
results.append(bench_park_list_optimized())
@@ -167,22 +172,22 @@ def run_benchmarks() -> list[dict[str, Any]]:
def print_summary(results: list[dict[str, Any]]) -> None:
"""Print a summary table of all benchmarks."""
- print("\n" + "="*80)
+ print("\n" + "=" * 80)
print("BENCHMARK SUMMARY")
- print("="*80)
+ print("=" * 80)
print(f"{'Benchmark':<45} {'Avg Time (ms)':<15} {'Avg Queries':<15}")
- print("-"*80)
+ print("-" * 80)
for result in results:
print(f"{result['name']:<45} {result['avg_time_ms']:<15.2f} {result['avg_queries']:<15.1f}")
- print("="*80)
+ print("=" * 80)
if True: # Always run when executed
- print("\n" + "="*80)
+ print("\n" + "=" * 80)
print("THRILLWIKI QUERY PERFORMANCE BENCHMARKS")
- print("="*80)
+ print("=" * 80)
print("\nRunning benchmarks...")
try:
@@ -200,4 +205,5 @@ if True: # Always run when executed
except Exception as e:
print(f"\nError running benchmarks: {e}")
import traceback
+
traceback.print_exc()
diff --git a/backend/stubs/environ.pyi b/backend/stubs/environ.pyi
index 1d8b44bd..1df17f37 100644
--- a/backend/stubs/environ.pyi
+++ b/backend/stubs/environ.pyi
@@ -1,3 +1,4 @@
+# ruff: noqa: B008
"""Type stubs for django-environ to fix Pylance type checking issues."""
import builtins
diff --git a/backend/templates/base/base.html b/backend/templates/base/base.html
index 2604f422..a71f31f1 100644
--- a/backend/templates/base/base.html
+++ b/backend/templates/base/base.html
@@ -54,7 +54,7 @@
-
+
@@ -64,7 +64,7 @@
{# Use title block directly #}
- {% block page_title %}{% block title %}ThrillWiki{% endblock %}{% endblock %}
+ {% block title %}ThrillWiki{% endblock %}
diff --git a/backend/test_avatar_upload.py b/backend/test_avatar_upload.py
index 42830782..432f0d1d 100644
--- a/backend/test_avatar_upload.py
+++ b/backend/test_avatar_upload.py
@@ -29,13 +29,7 @@ def step1_get_upload_url():
print("Step 1: Requesting upload URL...")
url = f"{API_BASE}/cloudflare-images/api/upload-url/"
- data = {
- "metadata": {
- "type": "avatar",
- "userId": "7627" # Replace with your user ID
- },
- "require_signed_urls": False
- }
+ data = {"metadata": {"type": "avatar", "userId": "7627"}, "require_signed_urls": False} # Replace with your user ID
response = requests.post(url, json=data, headers=HEADERS)
print(f"Status: {response.status_code}")
@@ -54,11 +48,9 @@ def step2_upload_image(upload_url):
# Create a simple test image (1x1 pixel PNG)
# This is a minimal valid PNG file
- png_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x12IDATx\x9cc```bPPP\x00\x02\xd2\x00\x00\x00\x05\x00\x01\r\n-\xdb\x00\x00\x00\x00IEND\xaeB`\x82'
+ png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x12IDATx\x9cc```bPPP\x00\x02\xd2\x00\x00\x00\x05\x00\x01\r\n-\xdb\x00\x00\x00\x00IEND\xaeB`\x82"
- files = {
- 'file': ('test_avatar.png', png_data, 'image/png')
- }
+ files = {"file": ("test_avatar.png", png_data, "image/png")}
# Upload to Cloudflare (no auth headers needed for direct upload)
response = requests.post(upload_url, files=files)
@@ -76,9 +68,7 @@ def step3_save_avatar(cloudflare_id):
print("\nStep 3: Saving avatar reference...")
url = f"{API_BASE}/accounts/profile/avatar/save/"
- data = {
- "cloudflare_image_id": cloudflare_id
- }
+ data = {"cloudflare_image_id": cloudflare_id}
response = requests.post(url, json=data, headers=HEADERS)
print(f"Status: {response.status_code}")
diff --git a/backend/tests/accessibility/test_wcag_compliance.py b/backend/tests/accessibility/test_wcag_compliance.py
index 0d096f81..e34471e5 100644
--- a/backend/tests/accessibility/test_wcag_compliance.py
+++ b/backend/tests/accessibility/test_wcag_compliance.py
@@ -33,12 +33,14 @@ try:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
+
HAS_SELENIUM = True
except ImportError:
HAS_SELENIUM = False
try:
from axe_selenium_python import Axe
+
HAS_AXE = True
except ImportError:
HAS_AXE = False
@@ -53,7 +55,7 @@ def skip_if_no_browser():
return unittest.skip("Selenium not installed")
if not HAS_AXE:
return unittest.skip("axe-selenium-python not installed")
- if os.environ.get('CI') and not os.environ.get('BROWSER_TESTS'):
+ if os.environ.get("CI") and not os.environ.get("BROWSER_TESTS"):
return unittest.skip("Browser tests disabled in CI")
return lambda func: func
@@ -73,14 +75,12 @@ class AccessibilityTestMixin:
dict: Axe results containing violations and passes
"""
if url_name:
- url = f'{self.live_server_url}{reverse(url_name)}'
+ url = f"{self.live_server_url}{reverse(url_name)}"
elif not url:
raise ValueError("Either url_name or url must be provided")
self.driver.get(url)
- WebDriverWait(self.driver, 10).until(
- EC.presence_of_element_located((By.TAG_NAME, "body"))
- )
+ WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
axe = Axe(self.driver)
axe.inject()
@@ -96,20 +96,13 @@ class AccessibilityTestMixin:
results: Axe audit results
page_name: Name of page for error messages
"""
- critical_violations = [
- v for v in results.get('violations', [])
- if v.get('impact') in ('critical', 'serious')
- ]
+ critical_violations = [v for v in results.get("violations", []) if v.get("impact") in ("critical", "serious")]
if critical_violations:
- violation_details = "\n".join([
- f"- {v['id']}: {v['description']} (impact: {v['impact']})"
- for v in critical_violations
- ])
- self.fail(
- f"Critical accessibility violations found on {page_name}:\n"
- f"{violation_details}"
+ violation_details = "\n".join(
+ [f"- {v['id']}: {v['description']} (impact: {v['impact']})" for v in critical_violations]
)
+ self.fail(f"Critical accessibility violations found on {page_name}:\n" f"{violation_details}")
def assert_wcag_aa_compliant(self, results, page_name="page"):
"""
@@ -119,17 +112,13 @@ class AccessibilityTestMixin:
results: Axe audit results
page_name: Name of page for error messages
"""
- violations = results.get('violations', [])
+ violations = results.get("violations", [])
if violations:
- violation_details = "\n".join([
- f"- {v['id']}: {v['description']} (impact: {v['impact']})"
- for v in violations
- ])
- self.fail(
- f"WCAG 2.1 AA violations found on {page_name}:\n"
- f"{violation_details}"
+ violation_details = "\n".join(
+ [f"- {v['id']}: {v['description']} (impact: {v['impact']})" for v in violations]
)
+ self.fail(f"WCAG 2.1 AA violations found on {page_name}:\n" f"{violation_details}")
@skip_if_no_browser()
@@ -148,11 +137,11 @@ class WCAGComplianceTests(AccessibilityTestMixin, LiveServerTestCase):
# Configure Chrome for headless testing
chrome_options = Options()
- chrome_options.add_argument('--headless')
- chrome_options.add_argument('--no-sandbox')
- chrome_options.add_argument('--disable-dev-shm-usage')
- chrome_options.add_argument('--disable-gpu')
- chrome_options.add_argument('--window-size=1920,1080')
+ chrome_options.add_argument("--headless")
+ chrome_options.add_argument("--no-sandbox")
+ chrome_options.add_argument("--disable-dev-shm-usage")
+ chrome_options.add_argument("--disable-gpu")
+ chrome_options.add_argument("--window-size=1920,1080")
try:
cls.driver = webdriver.Chrome(options=chrome_options)
@@ -162,38 +151,38 @@ class WCAGComplianceTests(AccessibilityTestMixin, LiveServerTestCase):
@classmethod
def tearDownClass(cls):
- if hasattr(cls, 'driver'):
+ if hasattr(cls, "driver"):
cls.driver.quit()
super().tearDownClass()
def test_homepage_accessibility(self):
"""Test homepage WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='home')
+ results = self.run_axe_audit(url_name="home")
self.assert_no_critical_violations(results, "homepage")
def test_park_list_accessibility(self):
"""Test park list page WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='parks:park_list')
+ results = self.run_axe_audit(url_name="parks:park_list")
self.assert_no_critical_violations(results, "park list")
def test_ride_list_accessibility(self):
"""Test ride list page WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='rides:global_ride_list')
+ results = self.run_axe_audit(url_name="rides:global_ride_list")
self.assert_no_critical_violations(results, "ride list")
def test_manufacturer_list_accessibility(self):
"""Test manufacturer list page WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='rides:manufacturer_list')
+ results = self.run_axe_audit(url_name="rides:manufacturer_list")
self.assert_no_critical_violations(results, "manufacturer list")
def test_login_page_accessibility(self):
"""Test login page WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='account_login')
+ results = self.run_axe_audit(url_name="account_login")
self.assert_no_critical_violations(results, "login page")
def test_signup_page_accessibility(self):
"""Test signup page WCAG 2.1 AA compliance."""
- results = self.run_axe_audit(url_name='account_signup')
+ results = self.run_axe_audit(url_name="account_signup")
self.assert_no_critical_violations(results, "signup page")
@@ -207,77 +196,66 @@ class HTMLAccessibilityTests(TestCase):
def test_homepage_has_main_landmark(self):
"""Verify homepage has a main landmark."""
- response = self.client.get(reverse('home'))
- self.assertContains(response, ']*>', content)
+
+ img_tags = re.findall(r"
]*>", content)
for img in img_tags:
- self.assertIn(
- 'alt=',
- img,
- f"Image missing alt attribute: {img[:100]}"
- )
+ self.assertIn("alt=", img, f"Image missing alt attribute: {img[:100]}")
def test_form_fields_have_labels(self):
"""Verify form fields have associated labels."""
- response = self.client.get(reverse('account_login'))
- content = response.content.decode('utf-8')
+ response = self.client.get(reverse("account_login"))
+ content = response.content.decode("utf-8")
# Find input elements (excluding hidden and submit)
import re
- inputs = re.findall(
- r']*type=["\'](?!hidden|submit)[^"\']*["\'][^>]*>',
- content
- )
+
+ inputs = re.findall(r']*type=["\'](?!hidden|submit)[^"\']*["\'][^>]*>', content)
for inp in inputs:
# Each input should have id attribute for label association
- self.assertTrue(
- 'id=' in inp or 'aria-label' in inp,
- f"Input missing id or aria-label: {inp[:100]}"
- )
+ self.assertTrue("id=" in inp or "aria-label" in inp, f"Input missing id or aria-label: {inp[:100]}")
def test_buttons_are_accessible(self):
"""Verify buttons have accessible names."""
- response = self.client.get(reverse('home'))
- content = response.content.decode('utf-8')
+ response = self.client.get(reverse("home"))
+ content = response.content.decode("utf-8")
import re
+
# Find button elements
- buttons = re.findall(r'', content, re.DOTALL)
+ buttons = re.findall(r"", content, re.DOTALL)
for button in buttons:
# Button should have text content or aria-label
- has_text = bool(re.search(r'>([^<]+)<', button))
- has_aria = 'aria-label' in button
+ has_text = bool(re.search(r">([^<]+)<", button))
+ has_aria = "aria-label" in button
- self.assertTrue(
- has_text or has_aria,
- f"Button missing accessible name: {button[:100]}"
- )
+ self.assertTrue(has_text or has_aria, f"Button missing accessible name: {button[:100]}")
class KeyboardNavigationTests(TestCase):
@@ -289,49 +267,35 @@ class KeyboardNavigationTests(TestCase):
def test_interactive_elements_are_focusable(self):
"""Verify interactive elements don't have tabindex=-1."""
- response = self.client.get(reverse('home'))
- content = response.content.decode('utf-8')
+ response = self.client.get(reverse("home"))
+ content = response.content.decode("utf-8")
# Links and buttons should not have tabindex=-1 (unless intentionally hidden)
import re
- problematic = re.findall(
- r'<(a|button)[^>]*tabindex=["\']?-1["\']?[^>]*>',
- content
- )
+
+ problematic = re.findall(r'<(a|button)[^>]*tabindex=["\']?-1["\']?[^>]*>', content)
# Filter out elements that are legitimately hidden
for elem in problematic:
- self.assertIn(
- 'aria-hidden',
- elem,
- f"Interactive element has tabindex=-1 without aria-hidden: {elem}"
- )
+ self.assertIn("aria-hidden", elem, f"Interactive element has tabindex=-1 without aria-hidden: {elem}")
def test_modals_have_escape_handler(self):
"""Verify modal templates include escape key handling."""
from django.template.loader import get_template
- template = get_template('components/modals/modal_inner.html')
+ template = get_template("components/modals/modal_inner.html")
source = template.template.source
- self.assertIn(
- 'escape',
- source.lower(),
- "Modal should handle Escape key"
- )
+ self.assertIn("escape", source.lower(), "Modal should handle Escape key")
def test_dropdowns_have_keyboard_support(self):
"""Verify dropdown menus support keyboard navigation."""
- response = self.client.get(reverse('home'))
- content = response.content.decode('utf-8')
+ response = self.client.get(reverse("home"))
+ content = response.content.decode("utf-8")
# Check for aria-expanded on dropdown triggers
- if 'dropdown' in content.lower() or 'menu' in content.lower():
- self.assertIn(
- 'aria-expanded',
- content,
- "Dropdown should have aria-expanded attribute"
- )
+ if "dropdown" in content.lower() or "menu" in content.lower():
+ self.assertIn("aria-expanded", content, "Dropdown should have aria-expanded attribute")
class ARIAAttributeTests(TestCase):
@@ -343,42 +307,26 @@ class ARIAAttributeTests(TestCase):
"""Verify modal has role=dialog."""
from django.template.loader import get_template
- template = get_template('components/modals/modal_inner.html')
+ template = get_template("components/modals/modal_inner.html")
source = template.template.source
- self.assertIn(
- 'role="dialog"',
- source,
- "Modal should have role=dialog"
- )
+ self.assertIn('role="dialog"', source, "Modal should have role=dialog")
def test_modal_has_aria_modal(self):
"""Verify modal has aria-modal=true."""
from django.template.loader import get_template
- template = get_template('components/modals/modal_inner.html')
+ template = get_template("components/modals/modal_inner.html")
source = template.template.source
- self.assertIn(
- 'aria-modal="true"',
- source,
- "Modal should have aria-modal=true"
- )
+ self.assertIn('aria-modal="true"', source, "Modal should have aria-modal=true")
def test_breadcrumb_has_navigation_role(self):
"""Verify breadcrumbs use nav element with aria-label."""
from django.template.loader import get_template
- template = get_template('components/navigation/breadcrumbs.html')
+ template = get_template("components/navigation/breadcrumbs.html")
source = template.template.source
- self.assertIn(
- '