Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add redirect_middleware for handling htmx request redirects #406

Closed
Closed
28 changes: 28 additions & 0 deletions docs/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,31 @@ Middleware

The deserialized JSON representation of the event that triggered the request if it exists, or ``None``.
This header is set by the `event-header htmx extension <https://htmx.org/extensions/event-header/>`__, and contains details of the DOM event that triggered the request.


.. function:: redirect_middleware(get_response)

Middleware to handle redirects for htmx requests for login required views.

:param get_response: A function that takes a request and returns a response.
:type get_response: callable
:returns: A middleware function that takes a request and returns a response.

The middleware function checks if the request is an htmx request and if the response status code is 302 (redirection). If so, it parses the redirection location. If the redirection location is the login URL, it changes the redirection URL to point to the login redirect URL and changes the response status code to 204 (No Content). The modified response is then returned.

.. code-block:: python

def redirect_middleware(get_response):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy-pasting the source code into the docs isn't necessary. they would drift over time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, it does seem to be project-specific what should be done in cases like these. Thanks for taking the time to review!

def middleware(request):
response = get_response(request)
if request.headers.get("HX-Request") and response.status_code == 302:
location = urlparse(response["Location"])
if location.path == settings.LOGIN_URL:
redirect_url = (
f"{settings.LOGIN_URL}?next={settings.LOGIN_REDIRECT_URL}"
)
response["HX-Redirect"] = redirect_url
response.status_code = 204
return response

return middleware
20 changes: 20 additions & 0 deletions src/django_htmx/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from typing import Awaitable
from typing import Callable
from urllib.parse import unquote
from urllib.parse import urlparse
from urllib.parse import urlsplit
from urllib.parse import urlunsplit

from asgiref.sync import iscoroutinefunction
from asgiref.sync import markcoroutinefunction
from django.conf import settings
from django.http import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.functional import cached_property
Expand Down Expand Up @@ -112,3 +114,21 @@ def triggering_event(self) -> Any:
except json.JSONDecodeError:
value = None
return value


def redirect_middleware(get_response):
"""Middleware to handle redirects for htmx requests for login required views"""

def middleware(request):
response = get_response(request)
if request.headers.get("HX-Request") and response.status_code == 302:
location = urlparse(response["Location"])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi urlsplit is preferable https://www.youtube.com/watch?v=ABJvdsIANds

if location.path == settings.LOGIN_URL:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@login_required might use a different login_url, so this is fragile. Wrapping login_required would solve this

redirect_url = (
f"{settings.LOGIN_URL}?next={settings.LOGIN_REDIRECT_URL}"
)
Comment on lines +127 to +129
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd wnat to preserve next.

response["HX-Redirect"] = redirect_url
response.status_code = 204
return response

return middleware
24 changes: 24 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
from typing import Any
from typing import cast

from django.conf import settings
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.http.response import HttpResponseBase
from django.test import RequestFactory as BaseRequestFactory
from django.test import SimpleTestCase
from django.test.utils import override_settings

from django_htmx.middleware import HtmxDetails
from django_htmx.middleware import HtmxMiddleware
from django_htmx.middleware import redirect_middleware


class HtmxWSGIRequest(WSGIRequest):
Expand Down Expand Up @@ -179,3 +182,24 @@ async def dummy_async_view(request):

assert isinstance(response, HttpResponse)
assert bool(request.htmx) is True


class RedirectMiddlewareTests(SimpleTestCase):
def setUp(self):
self.factory = RequestFactory()
self.middleware = redirect_middleware(self.dummy_view)

def dummy_view(self, request):
return HttpResponse()

@override_settings(LOGIN_URL="/login/", LOGIN_REDIRECT_URL="/home/")
def test_redirect_middleware(self):
request = self.factory.get("/", HTTP_HX_REQUEST="true")
response = HttpResponse(status=302)
response["Location"] = settings.LOGIN_URL
response = self.middleware(request)
assert response.status_code == 204
assert (
response["HX-Redirect"]
== f"{settings.LOGIN_URL}?next={settings.LOGIN_REDIRECT_URL}"
)
Loading