Phase 4: Page() collects component media; drop manual scripts= threading

Page() now calls collect_media(content) and emits the ModuleScript /
StaticScript tags itself, so views no longer thread scripts= for
component-owned JS. The list views (game/session/purchase/device/
platform/playevent) compose with Fragment(filter_bar, content) instead of
mark_safe(str(filter_bar) + str(content)) — keeping the node tree intact
so the filter bar's media (filter_bar.js + search_select.js +
range_slider.js, and date_range_picker.js on purchases) reaches Page().
The stats views drop _STATS_SCRIPTS; YearPicker's datepicker.umd.js is
collected from its declared media.

The scripts= argument remains for page-specific glue not owned by a
component (the add-form helpers add_game.js / add_purchase.js /
add_session.js, alongside search_select.js for their form widgets).

Adds regression tests asserting the list and stats pages auto-load their
widget scripts with no scripts= in the view, and documents the node/
media model in CLAUDE.md.

https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
This commit is contained in:
Claude
2026-06-13 07:32:35 +00:00
parent 0819ddb87d
commit 2d3ae4e04f
10 changed files with 57 additions and 53 deletions
+2 -6
View File
@@ -2,9 +2,9 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.safestring import mark_safe
from common.components import (
Fragment,
A,
AddForm,
Button,
@@ -12,7 +12,6 @@ from common.components import (
Icon,
paginated_table_content,
DeviceFilterBar,
ModuleScript,
)
from common.layout import render_page
from common.time import dateformat, local_strftime
@@ -76,14 +75,11 @@ def list_devices(request: HttpRequest) -> HttpResponse:
preset_list_url=reverse("games:list_presets") + "?mode=devices",
preset_save_url=reverse("games:save_preset") + "?mode=devices",
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage devices",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
+2 -4
View File
@@ -11,6 +11,7 @@ from django.urls import reverse
from django.utils.safestring import SafeText, mark_safe
from common.components import (
Fragment,
H1,
A,
AddForm,
@@ -145,14 +146,11 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse:
preset_list_url=reverse("games:list_presets"),
preset_save_url=reverse("games:save_preset"),
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage games",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
+4 -10
View File
@@ -13,16 +13,14 @@ from django.urls import reverse
from django.utils.timezone import localtime
from django.utils.timezone import now as timezone_now
from common.components import StaticScript
from common.layout import render_page
from common.time import format_duration
from games.models import Game, Platform, Purchase, Session
from games.views.stats_content import stats_content
from games.views.stats_data import compute_stats
# Flowbite-datepicker UMD bundle (vendored, v2.0.0), hoisted into the stats
# pages for YearPicker.
_STATS_SCRIPTS = StaticScript("datepicker.umd.js")
# The Flowbite-datepicker UMD bundle is declared as media on the YearPicker
# component, so Page() loads it automatically on the stats pages.
def model_counts(request: HttpRequest) -> dict[str, bool]:
@@ -76,9 +74,7 @@ def use_custom_redirect(
def stats_alltime(request: HttpRequest) -> HttpResponse:
request.session["return_path"] = request.path
data = compute_stats(None)
return render_page(
request, stats_content(data), title=data["title"], scripts=_STATS_SCRIPTS
)
return render_page(request, stats_content(data), title=data["title"])
@login_required
@@ -92,9 +88,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
return HttpResponseRedirect(reverse("games:stats_alltime"))
request.session["return_path"] = request.path
data = compute_stats(year)
return render_page(
request, stats_content(data), title=data["title"], scripts=_STATS_SCRIPTS
)
return render_page(request, stats_content(data), title=data["title"])
@login_required
+2 -6
View File
@@ -2,9 +2,9 @@ from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.safestring import mark_safe
from common.components import (
Fragment,
A,
AddForm,
Button,
@@ -12,7 +12,6 @@ from common.components import (
Icon,
paginated_table_content,
PlatformFilterBar,
ModuleScript,
)
from common.layout import render_page
from common.time import dateformat, local_strftime
@@ -83,14 +82,11 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
preset_list_url=reverse("games:list_presets") + "?mode=platforms",
preset_save_url=reverse("games:save_preset") + "?mode=platforms",
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage platforms",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
+2 -5
View File
@@ -9,9 +9,9 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.safestring import mark_safe
from common.components import (
Fragment,
A,
AddForm,
Button,
@@ -151,14 +151,11 @@ def list_playevents(request: HttpRequest) -> HttpResponse:
preset_list_url=reverse("games:list_presets") + "?mode=playevents",
preset_save_url=reverse("games:save_preset") + "?mode=playevents",
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage play events",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)
+3 -6
View File
@@ -14,6 +14,7 @@ from django.utils.safestring import SafeText, mark_safe
from django.views.decorators.http import require_POST
from common.components import (
Fragment,
A,
AddForm,
Button,
@@ -129,22 +130,18 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
elided_page_range=elided_page_range,
request=request,
)
from common.components import ModuleScript, PurchaseFilterBar
from common.components import PurchaseFilterBar
filter_bar = PurchaseFilterBar(
filter_json=filter_json,
preset_list_url=reverse("games:list_presets"),
preset_save_url=reverse("games:save_preset"),
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage purchases",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("date_range_picker.js")
+ ModuleScript("filter_bar.js"),
)
+2 -4
View File
@@ -11,6 +11,7 @@ from django.utils import timezone
from django.utils.safestring import SafeText, mark_safe
from common.components import (
Fragment,
A,
AddForm,
Button,
@@ -176,14 +177,11 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
preset_list_url=reverse("games:list_presets"),
preset_save_url=reverse("games:save_preset"),
)
content = mark_safe(str(filter_bar) + str(content))
content = Fragment(filter_bar, content)
return render_page(
request,
content,
title="Manage sessions",
scripts=ModuleScript("range_slider.js")
+ ModuleScript("search_select.js")
+ ModuleScript("filter_bar.js"),
)