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

1""" 

2Template rendering helpers for FastAPI. 

3 

4Provides render_template() that injects request-specific context 

5(session, CSRF token, flash messages) into every template render, 

6matching Flask's behavior. 

7""" 

8 

9from fastapi import Request 

10from loguru import logger 

11from starlette.responses import HTMLResponse 

12 

13from ..template_config import templates 

14from .csrf import generate_csrf_token 

15from .flash import get_flashed_messages 

16 

17 

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. 

25 

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. 

29 

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. 

35 

36 Returns: 

37 HTMLResponse with rendered template. 

38 """ 

39 ctx = context or {} 

40 

41 # Inject request-specific globals that templates expect 

42 ctx.setdefault("request", request) 

43 ctx.setdefault("session", request.session) 

44 

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 

50 

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 ) 

57 

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 

64 

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 

69 

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 ) 

91 

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) 

99 

100 return templates.TemplateResponse( 

101 request=request, 

102 name=name, 

103 context=ctx, 

104 status_code=status_code, 

105 )