okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -0,0 +1,147 @@
from __future__ import annotations
import json
from typing import Any
from typing import Literal
from typing import TypeVar
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse
from django.http.response import HttpResponseBase
from django.http.response import HttpResponseRedirectBase
HTMX_STOP_POLLING = 286
class HttpResponseStopPolling(HttpResponse):
status_code = HTMX_STOP_POLLING
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._reason_phrase = "Stop Polling"
class HttpResponseClientRedirect(HttpResponseRedirectBase):
status_code = 200
def __init__(self, redirect_to: str, *args: Any, **kwargs: Any) -> None:
super().__init__(redirect_to, *args, **kwargs)
self["HX-Redirect"] = self["Location"]
del self["Location"]
@property
def url(self) -> str:
return self["HX-Redirect"]
class HttpResponseClientRefresh(HttpResponse):
def __init__(self) -> None:
super().__init__()
self["HX-Refresh"] = "true"
class HttpResponseLocation(HttpResponseRedirectBase):
status_code = 200
def __init__(
self,
redirect_to: str,
*args: Any,
source: str | None = None,
event: str | None = None,
target: str | None = None,
swap: Literal[
"innerHTML",
"outerHTML",
"beforebegin",
"afterbegin",
"beforeend",
"afterend",
"delete",
"none",
None,
] = None,
select: str | None = None,
values: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
**kwargs: Any,
) -> None:
super().__init__(redirect_to, *args, **kwargs)
spec: dict[str, str | dict[str, str]] = {
"path": self["Location"],
}
del self["Location"]
if source is not None:
spec["source"] = source
if event is not None:
spec["event"] = event
if target is not None:
spec["target"] = target
if swap is not None:
spec["swap"] = swap
if select is not None:
spec["select"] = select
if headers is not None:
spec["headers"] = headers
if values is not None:
spec["values"] = values
self["HX-Location"] = json.dumps(spec)
_HttpResponse = TypeVar("_HttpResponse", bound=HttpResponseBase)
def push_url(response: _HttpResponse, url: str | Literal[False]) -> _HttpResponse:
response["HX-Push-Url"] = "false" if url is False else url
return response
def replace_url(response: _HttpResponse, url: str | Literal[False]) -> _HttpResponse:
response["HX-Replace-Url"] = "false" if url is False else url
return response
def reswap(response: _HttpResponse, method: str) -> _HttpResponse:
response["HX-Reswap"] = method
return response
def retarget(response: _HttpResponse, target: str) -> _HttpResponse:
response["HX-Retarget"] = target
return response
def trigger_client_event(
response: _HttpResponse,
name: str,
params: dict[str, Any] | None = None,
*,
after: Literal["receive", "settle", "swap"] = "receive",
encoder: type[json.JSONEncoder] = DjangoJSONEncoder,
) -> _HttpResponse:
params = params or {}
if after == "receive":
header = "HX-Trigger"
elif after == "settle":
header = "HX-Trigger-After-Settle"
elif after == "swap":
header = "HX-Trigger-After-Swap"
else:
raise ValueError(
"Value for 'after' must be one of: 'receive', 'settle', or 'swap'."
)
if header in response:
value = response[header]
try:
data = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError(f"{header!r} value should be valid JSON.") from exc
data[name] = params
else:
data = {name: params}
response[header] = json.dumps(data, cls=encoder)
return response

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
from django.conf import settings
from django.templatetags.static import static
from django.utils.html import format_html
def django_htmx_script() -> str:
# Optimization: whilst the script has no behaviour outside of debug mode,
# don't include it.
if not settings.DEBUG:
return ""
return format_html(
'<script src="{}" data-debug="{}" defer></script>',
static("django-htmx.js"),
str(bool(settings.DEBUG)),
)

View File

@@ -0,0 +1,114 @@
from __future__ import annotations
import json
from collections.abc import Awaitable
from typing import Any
from typing import Callable
from urllib.parse import unquote
from urllib.parse import urlsplit
from urllib.parse import urlunsplit
from asgiref.sync import iscoroutinefunction
from asgiref.sync import markcoroutinefunction
from django.http import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.functional import cached_property
class HtmxMiddleware:
sync_capable = True
async_capable = True
def __init__(
self,
get_response: (
Callable[[HttpRequest], HttpResponseBase]
| Callable[[HttpRequest], Awaitable[HttpResponseBase]]
),
) -> None:
self.get_response = get_response
self.async_mode = iscoroutinefunction(self.get_response)
if self.async_mode:
# Mark the class as async-capable, but do the actual switch
# inside __call__ to avoid swapping out dunder methods
markcoroutinefunction(self)
def __call__(
self, request: HttpRequest
) -> HttpResponseBase | Awaitable[HttpResponseBase]:
if self.async_mode:
return self.__acall__(request)
request.htmx = HtmxDetails(request) # type: ignore [attr-defined]
return self.get_response(request)
async def __acall__(self, request: HttpRequest) -> HttpResponseBase:
request.htmx = HtmxDetails(request) # type: ignore [attr-defined]
return await self.get_response(request) # type: ignore [no-any-return, misc]
class HtmxDetails:
def __init__(self, request: HttpRequest) -> None:
self.request = request
def _get_header_value(self, name: str) -> str | None:
value = self.request.headers.get(name) or None
if value:
if self.request.headers.get(f"{name}-URI-AutoEncoded") == "true":
value = unquote(value)
return value
def __bool__(self) -> bool:
return self._get_header_value("HX-Request") == "true"
@cached_property
def boosted(self) -> bool:
return self._get_header_value("HX-Boosted") == "true"
@cached_property
def current_url(self) -> str | None:
return self._get_header_value("HX-Current-URL")
@cached_property
def current_url_abs_path(self) -> str | None:
url = self.current_url
if url is not None:
split = urlsplit(url)
if (
split.scheme == self.request.scheme
and split.netloc == self.request.get_host()
):
url = urlunsplit(split._replace(scheme="", netloc=""))
else:
url = None
return url
@cached_property
def history_restore_request(self) -> bool:
return self._get_header_value("HX-History-Restore-Request") == "true"
@cached_property
def prompt(self) -> str | None:
return self._get_header_value("HX-Prompt")
@cached_property
def target(self) -> str | None:
return self._get_header_value("HX-Target")
@cached_property
def trigger(self) -> str | None:
return self._get_header_value("HX-Trigger")
@cached_property
def trigger_name(self) -> str | None:
return self._get_header_value("HX-Trigger-Name")
@cached_property
def triggering_event(self) -> Any:
value = self._get_header_value("Triggering-Event")
if value is not None:
try:
value = json.loads(value)
except json.JSONDecodeError:
value = None
return value

View File

@@ -0,0 +1,22 @@
{
const data = document.currentScript.dataset;
const isDebug = data.debug === "True";
if (isDebug) {
document.addEventListener("htmx:beforeOnLoad", function (event) {
const xhr = event.detail.xhr;
if (xhr.status == 500 || xhr.status == 404) {
// Tell htmx to stop processing this response
event.stopPropagation();
document.children[0].innerHTML = xhr.response;
// Run Djangos inline script
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
(1, eval)(document.scripts[0].innerText);
// Need to directly call Djangos onload function since browser wont
window.onload();
}
});
}
}

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
from django import template
from django_htmx.jinja import django_htmx_script
register = template.Library()
register.simple_tag(django_htmx_script)