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