1from __future__ import annotations
2
3from collections.abc import Callable
4from typing import Any
5
6from plain.http import ResponseBase
7from plain.utils.cache import patch_vary_headers
8from plain.views import TemplateView
9
10from .templates import render_template_fragment
11
12
13class HTMXView(TemplateView):
14 """View with HTMX-specific functionality."""
15
16 def render_template(self) -> str:
17 template = self.get_template()
18 context = self.get_template_context()
19
20 if self.is_htmx_request() and self.get_htmx_fragment_name():
21 return render_template_fragment(
22 template=template._jinja_template,
23 fragment_name=self.get_htmx_fragment_name(),
24 context=context,
25 )
26
27 return template.render(context)
28
29 def get_response(self) -> ResponseBase:
30 response = super().get_response()
31 # Tell browser caching to also consider the fragment header,
32 # not just the url/cookie.
33 patch_vary_headers(
34 response, ["HX-Request", "Plain-HX-Fragment", "Plain-HX-Action"]
35 )
36 return response
37
38 def get_request_handler(self) -> Callable[[], Any] | None:
39 if self.is_htmx_request() and self.request.method:
40 # You can use an htmx_{method} method on views
41 # (or htmx_{method}_{action} for specific actions)
42 method = f"htmx_{self.request.method.lower()}"
43
44 if action := self.get_htmx_action_name():
45 # If an action is specified, we throw an error if
46 # the associated method isn't found
47 return getattr(self, f"{method}_{action}")
48
49 if handler := getattr(self, method, None):
50 # If it's just an htmx post, for example,
51 # we can use a custom method or we can let it fall back
52 # to a regular post method if it's not found
53 return handler
54
55 return super().get_request_handler()
56
57 def is_htmx_request(self) -> bool:
58 return self.request.headers.get("HX-Request") == "true"
59
60 def get_htmx_fragment_name(self) -> str:
61 # A custom header that we pass with the {% htmxfragment %} tag
62 return self.request.headers.get("Plain-HX-Fragment", "")
63
64 def get_htmx_action_name(self) -> str:
65 return self.request.headers.get("Plain-HX-Action", "")