Coverage for src/local_deep_research/web/dependencies/template_helpers.py: 100%
33 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""
2Template rendering helpers for FastAPI.
4Provides render_template() that injects request-specific context
5(session, CSRF token, flash messages) into every template render,
6matching Flask's behavior.
7"""
9from fastapi import Request
10from loguru import logger
11from starlette.responses import HTMLResponse
13from ..template_config import templates
14from .csrf import generate_csrf_token
15from .flash import get_flashed_messages
18def render_template(
19 request: Request,
20 name: str,
21 context: dict | None = None,
22 status_code: int = 200,
23) -> HTMLResponse:
24 """Render a Jinja2 template with request-specific context injected.
26 This is the FastAPI equivalent of Flask's render_template(). It
27 automatically adds session, CSRF token, and flash messages to the
28 template context so templates work identically to Flask.
30 Args:
31 request: The current request.
32 name: Template name (e.g. "auth/login.html").
33 context: Template context variables.
34 status_code: HTTP status code.
36 Returns:
37 HTMLResponse with rendered template.
38 """
39 ctx = context or {}
41 # Inject request-specific globals that templates expect
42 ctx.setdefault("request", request)
43 ctx.setdefault("session", request.session)
45 # CSRF token callable — templates call {{ csrf_token() }}. Preserve an
46 # explicit route-provided callable, matching _LDRTemplates.TemplateResponse.
47 if "csrf_token" not in ctx:
48 token = generate_csrf_token(request) # gitleaks:allow
49 ctx["csrf_token"] = lambda: token
51 # Flash messages — templates call {{ get_flashed_messages(with_categories=true) }}
52 if "get_flashed_messages" not in ctx:
53 flashes = get_flashed_messages(request, with_categories=True)
54 ctx["get_flashed_messages"] = lambda with_categories=False: (
55 flashes if with_categories else [msg for _, msg in flashes]
56 )
58 # Make the active egress scope available to every template so base.html
59 # can render it onto <body data-scope=…>. Scope-aware CSS in styles.css
60 # picks it up to color the research card, chat input, etc. Falls back
61 # to the registered default scope if anything goes wrong — fail open
62 # visually, never crash a page render over a styling cue.
63 from ...security.egress.policy import DEFAULT_EGRESS_SCOPE
65 scope = DEFAULT_EGRESS_SCOPE
66 try:
67 from ...database.session_context import get_user_db_session
68 from ...utilities.db_utils import get_settings_manager
70 username = request.session.get("username") if request.session else None
71 if username:
72 with get_user_db_session(username) as db_session:
73 if db_session:
74 sm = get_settings_manager(db_session, username)
75 scope = (
76 sm.get_setting(
77 "policy.egress_scope", DEFAULT_EGRESS_SCOPE
78 )
79 or DEFAULT_EGRESS_SCOPE
80 )
81 # Canonicalise before it reaches the template: this collapses case
82 # and whitespace and maps invalid / retired values to the
83 # protective default. Without it a stored "STRICT" renders
84 # data-scope="STRICT", and base.html's body[data-scope="strict"]
85 # selector -- attribute values are case-sensitive -- silently stops
86 # matching, so the strict-scope visual cue disappears. Lost in the
87 # port; main applied it at app_factory.py:585.
88 from ...security.egress.policy import (
89 effective_scope_for_display,
90 )
92 scope = effective_scope_for_display(scope)
93 except Exception:
94 logger.debug(
95 "Failed to read egress scope for template context",
96 exc_info=True,
97 )
98 ctx.setdefault("egress_scope", scope)
100 return templates.TemplateResponse(
101 request=request,
102 name=name,
103 context=ctx,
104 status_code=status_code,
105 )