diff --git a/HISTORY.rst b/HISTORY.rst index aaceaa99..e441a5da 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,10 @@ History * Add ``django_htmx.http.retarget()`` for setting the ``HX-Retarget`` header added in `htmx 1.6.1 `__. +* Add ``HttpResponseLocation`` for sending a response with the ``HX-Location`` header. + + Thanks to Ben Beecher in `PR #239 `__. + 1.12.2 (2022-08-31) ------------------- diff --git a/docs/http.rst b/docs/http.rst index daa7069b..6d330861 100644 --- a/docs/http.rst +++ b/docs/http.rst @@ -3,6 +3,9 @@ HTTP .. currentmodule:: django_htmx.http +Response classes +---------------- + .. autoclass:: HttpResponseClientRedirect htmx can trigger a client side redirect when it receives a response with the |HX-Redirect header|__. @@ -44,6 +47,34 @@ HTTP return HttpResponseClientRefresh() ... +.. autoclass:: HttpResponseLocation + + An HTTP response class for sending the |HX-Location header|__. + This header makes htmx make a client-side “boosted” request, acting like a client side redirect with a page reload. + + .. |HX-Location header| replace:: ``HX-Location`` header + __ https://htmx.org/headers/hx-location/ + + ``redirect_to`` should be the URL to redirect to, as per Django’s |HttpResponseRedirect|__. + + .. |HttpResponseRedirect| replace:: ``HttpResponseRedirect`` + __ https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpResponseRedirect + + ``source``, ``event``, ``target``, ``swap``, ``values``, and ``headers`` are all optional, with meaning as `documented by htmx `__. + + For example: + + .. code-block:: python + + from django_htmx.http import HttpResponseLocation + + + def wait_for_completion(request, action_id): + ... + if action.completed: + return HttpResponseLocation(f"/action/{action.id}/completed/") + ... + .. autoclass:: HttpResponseStopPolling When using a `polling trigger `__, htmx will stop polling when it encounters a response with the special HTTP status code 286. @@ -80,6 +111,9 @@ HTTP return render("event-finished.html", status=HTMX_STOP_POLLING) ... +Response modifying functions +---------------------------- + .. autofunction:: push_url Set the |HX-Push-Url header|__ of ``response`` and return it. diff --git a/src/django_htmx/http.py b/src/django_htmx/http.py index 7ddf1088..a71cc372 100644 --- a/src/django_htmx/http.py +++ b/src/django_htmx/http.py @@ -48,6 +48,51 @@ def __init__(self) -> None: 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, + 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 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) diff --git a/tests/test_http.py b/tests/test_http.py index 22eadda7..7304596e 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from uuid import UUID import pytest @@ -9,6 +10,7 @@ from django_htmx.http import HttpResponseClientRedirect from django_htmx.http import HttpResponseClientRefresh +from django_htmx.http import HttpResponseLocation from django_htmx.http import HttpResponseStopPolling from django_htmx.http import push_url from django_htmx.http import reswap @@ -41,6 +43,40 @@ def test_repr(self): ) +class HttpResponseLocationTests(SimpleTestCase): + def test_success(self): + response = HttpResponseLocation("/home/") + + assert response.status_code == 200 + assert "Location" not in response + spec = json.loads(response["HX-Location"]) + assert spec == {"path": "/home/"} + + def test_success_complete(self): + response = HttpResponseLocation( + "/home/", + source="#button", + event="doubleclick", + target="#main", + swap="innerHTML", + headers={"year": "2022"}, + values={"banner": "true"}, + ) + + assert response.status_code == 200 + assert "Location" not in response + spec = json.loads(response["HX-Location"]) + assert spec == { + "path": "/home/", + "source": "#button", + "event": "doubleclick", + "target": "#main", + "swap": "innerHTML", + "headers": {"year": "2022"}, + "values": {"banner": "true"}, + } + + class HttpResponseClientRefreshTests(SimpleTestCase): def test_success(self): response = HttpResponseClientRefresh()