feat(modals): layout context processor for HX-Modal header
Switches the Jinja base layout to _modal_partial.html (yield-only) when the HX-Modal:1 request header is set, otherwise base.html. Foundation for URL-addressable modals (spec 2026-05-17-url-addressable-modals). Guards with has_request_context() so the processor is safe when render_template_string is called from app_context() without a request (e.g. test_timeago_filter_registered_on_app). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d05d00449f
commit
82c3f041ce
3 changed files with 37 additions and 0 deletions
|
|
@ -61,6 +61,13 @@ def create_app(test_config: dict[str, object] | None = None) -> Flask:
|
||||||
|
|
||||||
app.add_template_filter(format_time_html, "timeago")
|
app.add_template_filter(format_time_html, "timeago")
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_base_layout() -> dict[str, str]:
|
||||||
|
from flask import has_request_context
|
||||||
|
|
||||||
|
is_modal = has_request_context() and request.headers.get("HX-Modal") == "1"
|
||||||
|
return {"base_layout": "_modal_partial.html" if is_modal else "base.html"}
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def csrf_protect() -> Response | None:
|
def csrf_protect() -> Response | None:
|
||||||
if "csrf_token" not in session:
|
if "csrf_token" not in session:
|
||||||
|
|
|
||||||
1
l4d2web/l4d2web/templates/_modal_partial.html
Normal file
1
l4d2web/l4d2web/templates/_modal_partial.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{% block content %}{% endblock %}
|
||||||
29
l4d2web/tests/test_url_addressable_modals.py
Normal file
29
l4d2web/tests/test_url_addressable_modals.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from flask import render_template_string
|
||||||
|
|
||||||
|
from l4d2web.app import create_app
|
||||||
|
|
||||||
|
|
||||||
|
def _make_app(tmp_path, monkeypatch, db_name: str):
|
||||||
|
db_url = f"sqlite:///{tmp_path/db_name}"
|
||||||
|
monkeypatch.setenv("DATABASE_URL", db_url)
|
||||||
|
return create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_layout_is_modal_partial_when_hx_modal_header_set(tmp_path, monkeypatch):
|
||||||
|
app = _make_app(tmp_path, monkeypatch, "layout-modal.db")
|
||||||
|
with app.test_request_context("/", headers={"HX-Modal": "1"}):
|
||||||
|
assert render_template_string("{{ base_layout }}") == "_modal_partial.html"
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_layout_is_base_html_for_normal_request(tmp_path, monkeypatch):
|
||||||
|
app = _make_app(tmp_path, monkeypatch, "layout-default.db")
|
||||||
|
with app.test_request_context("/"):
|
||||||
|
assert render_template_string("{{ base_layout }}") == "base.html"
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_layout_does_not_react_to_plain_hx_request_header(tmp_path, monkeypatch):
|
||||||
|
# HTMX sets HX-Request on every request including the build-status poll;
|
||||||
|
# only HX-Modal should switch the layout.
|
||||||
|
app = _make_app(tmp_path, monkeypatch, "layout-hxreq.db")
|
||||||
|
with app.test_request_context("/", headers={"HX-Request": "true"}):
|
||||||
|
assert render_template_string("{{ base_layout }}") == "base.html"
|
||||||
Loading…
Reference in a new issue