"""Core application views."""
import json
import logging
import os
import time
from django.conf import settings
from django.db import connection
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.http import require_GET, require_http_methods
[docs]
logger = logging.getLogger(__name__)
def _get_manifest_paths() -> list[str]:
"""Get possible locations for the asset manifest file"""
return [
# Docker container path with mounted frontend build
"/app/frontend/build/asset-manifest.json",
# Docker container path (copied from frontend build)
os.path.join(settings.STATIC_ROOT, "asset-manifest.json"),
os.path.join(settings.STATIC_ROOT, "frontend", "asset-manifest.json"),
# Local development path in static files
os.path.join(settings.BASE_DIR, "static", "asset-manifest.json"),
os.path.join(settings.BASE_DIR, "static", "frontend", "asset-manifest.json"),
# Frontend build directory (development)
os.path.join(
settings.BASE_DIR.parent,
"frontend",
"build",
"asset-manifest.json",
),
# Alternative static location
os.path.join(settings.BASE_DIR, "staticfiles", "asset-manifest.json"),
os.path.join(
settings.BASE_DIR,
"staticfiles",
"frontend",
"asset-manifest.json",
),
]
def _parse_manifest(manifest: dict) -> tuple[str, str]:
"""Parse manifest data to extract main JS and CSS files"""
files = manifest.get("files", {})
entrypoints = manifest.get("entrypoints", [])
logger.debug(
"Parsing manifest - files keys: %s, entrypoints: %s",
list(files.keys()) if files else "none",
entrypoints if entrypoints else "none",
)
main_js = ""
main_css = ""
if files:
# New format: files object with keys
main_js = files.get("main.js", "")
main_css = files.get("main.css", "")
logger.debug("Files format - main.js: %s, main.css: %s", main_js, main_css)
elif entrypoints:
# Old format: entrypoints array
for entry in entrypoints:
if entry.endswith(".js") and not main_js:
main_js = entry
elif entry.endswith(".css") and not main_css:
main_css = entry
logger.debug(
"Entrypoints format - main.js: %s, main.css: %s",
main_js,
main_css,
)
else:
logger.debug("No files or entrypoints found in manifest")
return main_js, main_css
def _normalize_asset_paths(main_js: str, main_css: str) -> dict[str, str]:
"""Normalize asset paths for Django static file serving"""
# Remove /static/ prefix if present since Django will add it
main_js = main_js.replace("/static/", "").replace("static/", "")
main_css = main_css.replace("/static/", "").replace("static/", "")
# The files should already be in the correct structure from React build
# Don't add frontend/ prefix since we're copying directly to static root
return {
"main_js": main_js,
"main_css": main_css,
}
def _find_static_files_directly() -> dict[str, str]:
"""Find static files directly in directories when manifest is unavailable"""
static_dirs = [
os.path.join(settings.STATIC_ROOT, "js") if settings.STATIC_ROOT else None,
os.path.join(settings.STATIC_ROOT, "css") if settings.STATIC_ROOT else None,
os.path.join(settings.BASE_DIR, "static", "js"),
os.path.join(settings.BASE_DIR, "static", "css"),
os.path.join(settings.BASE_DIR, "staticfiles", "js"),
os.path.join(settings.BASE_DIR, "staticfiles", "css"),
]
js_file = ""
css_file = ""
for static_dir in static_dirs:
if static_dir and os.path.exists(static_dir):
try:
files = os.listdir(static_dir)
logger.debug(
"Checking directory %s: found %d files: %s",
static_dir,
len(files),
files[:5],
)
if "js" in static_dir:
js_files = [f for f in files if f.endswith(".js") and "main" in f]
if js_files:
js_file = f"js/{js_files[0]}"
logger.debug("Found JS file: %s", js_file)
else:
logger.debug(
"No main JS files found in %s (found: %s)",
static_dir,
[f for f in files if f.endswith(".js")],
)
elif "css" in static_dir:
css_files = [f for f in files if f.endswith(".css") and "main" in f]
if css_files:
css_file = f"css/{css_files[0]}"
logger.debug("Found CSS file: %s", css_file)
else:
logger.debug(
"No main CSS files found in %s (found: %s)",
static_dir,
[f for f in files if f.endswith(".css")],
)
except (OSError, IndexError) as e:
logger.debug("Error accessing directory %s: %s", static_dir, e)
continue
elif static_dir:
logger.debug("Directory does not exist: %s", static_dir)
if js_file or css_file:
logger.debug(
"Direct file search successful - JS: %s, CSS: %s",
js_file,
css_file,
)
return {
"main_js": js_file or "js/main.js",
"main_css": css_file or "css/main.css",
}
logger.debug(
"Direct file search failed - no JS or CSS files found in any directory",
)
return {}
[docs]
def get_react_assets() -> dict[str, str]:
"""Read React's asset-manifest.json to get the correct filenames with hashes"""
logger.debug("Starting asset discovery process")
# Try to find and parse manifest files
for manifest_path in _get_manifest_paths():
logger.debug("Checking manifest path: %s", manifest_path)
try:
if os.path.exists(manifest_path):
logger.debug("Manifest file found at: %s", manifest_path)
with open(manifest_path) as f:
manifest = json.load(f)
main_js, main_css = _parse_manifest(manifest)
if main_js or main_css:
result = _normalize_asset_paths(main_js, main_css)
logger.debug(
"Successfully loaded assets from manifest - JS: %s, CSS: %s",
result.get("main_js"),
result.get("main_css"),
)
return result
else:
logger.debug("Manifest found but no main JS/CSS files detected")
else:
logger.debug("Manifest file does not exist: %s", manifest_path)
except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
logger.debug("Failed to load manifest from %s: %s", manifest_path, e)
continue
logger.debug("No valid manifest found, falling back to direct file search")
# Fallback: try to find files directly in static directories
direct_files = _find_static_files_directly()
if direct_files:
logger.debug("Direct file search successful: %s", direct_files)
return direct_files
# Final fallback for development
logger.warning("No static files found, using development fallback paths")
return {
"main_js": "js/main.js",
"main_css": "css/main.css",
}
[docs]
def home(request: HttpRequest) -> HttpResponse:
assets = get_react_assets()
logger.debug("Loading assets: %s", assets)
try:
return render(request, "index.html", {"assets": assets})
except Exception as e:
# During testing or when template fails, provide a simple fallback response
logger.warning("Template rendering failed, using fallback: %s", e)
return HttpResponse(
"<!DOCTYPE html><html><head><title>Coalition Builder</title></head><body>"
'<div id="root">Loading...</div></body></html>',
content_type="text/html",
)
@require_GET
[docs]
def robots_txt(request: HttpRequest) -> HttpResponse:
"""Serve the robots.txt file to prevent search engine indexing"""
return HttpResponse("User-agent: *\nDisallow: /\n", content_type="text/plain")
@require_http_methods(["GET", "HEAD"])
[docs]
def health_check(request: HttpRequest) -> JsonResponse:
"""
Dedicated health check endpoint for the Django backend.
This endpoint checks:
1. Application status
2. Database connectivity
3. Available memory and system resources
Returns a JSON response with health status information.
"""
start_time = time.time()
# Check database connection
db_status = "healthy"
db_response_time = 0
try:
db_check_start = time.time()
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
db_response_time = round((time.time() - db_check_start) * 1000)
except Exception as e:
db_status = "unhealthy"
logger.error("Health check database connection failed: %s", e)
# Get memory info
try:
import psutil
process = psutil.Process(os.getpid())
memory_info = process.memory_info()
memory = {
"rss": f"{memory_info.rss / (1024 * 1024):.2f}MB",
"vms": f"{memory_info.vms / (1024 * 1024):.2f}MB",
}
except ImportError:
# psutil not available
memory = {"status": "psutil not installed"}
except Exception as e:
logger.error("Error retrieving memory information: %s", e)
memory = {"error": "An error occurred while retrieving memory information"}
# Build response
health_data = {
"status": "healthy" if db_status == "healthy" else "unhealthy",
"timestamp": timezone.now().isoformat(),
"application": {
"name": f"{settings.ORGANIZATION_NAME} API",
"debug": settings.DEBUG,
},
"database": {
"status": db_status,
"responseTime": f"{db_response_time}ms",
"engine": settings.DATABASES["default"]["ENGINE"],
"name": str(settings.DATABASES["default"]["NAME"]),
},
"memory": memory,
"responseTime": f"{round((time.time() - start_time) * 1000)}ms",
}
status_code = 200 if health_data["status"] == "healthy" else 503
return JsonResponse(
health_data,
status=status_code,
headers={"Cache-Control": "no-store, max-age=0"},
)