adamchainz / django-htmx

Extensions for using Django with htmx.
https://django-htmx.readthedocs.io/
MIT License
1.61k stars 139 forks source link

Handle redirects when htmx == True #91

Open valberg opened 3 years ago

valberg commented 3 years ago

In a project I'm working on we logout priviliged users after some time. This has the downside of returning a HttpResponseRedirect when issuing a htmx request, ie. when a user tries to use an already open browser tab the next day.

For now we have solved the problem by writing the following middleware:

class HtmxRedirectMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        if request.htmx and isinstance(response, HttpResponseRedirect):
            # Set the HX-Redirect to the current location to imitate a reload
            response["HX-Redirect"] = response["Location"]
            # htmx only accepts 200's
            response.status_code = 200
        return response

This tells htmx to do a complete reload of the page leading to a redirect to the login page.

I think that the above approach might be a bit too generic - do we want to issue a redirect every time? For now it serves us fine since we are evaluating htmx in a small portion of our project.

The question is whether django-htmx should offer something similar?

adamchainz commented 3 years ago

I think that the above approach might be a bit too generic - do we want to issue a redirect every time?

This is my concern too. A server redirect isn't necessarily a call for an HTMX redirect...

Ideally you'd instead update your "if not logged in then redirect" code to generate the correct response for htmx immediately, rather than modifying it after the fact. Of course that's not always easy when you use e.g. django.contrib.auth.views.redirect_to_login.

I think we should let this one sit for a little, to think about it. I also hadn't thought about not-logged-in requests so I will look at working with them in my project.

valberg commented 3 years ago

Some more food for thought: I just did a quick search in the htmx source, and it looks as if 3xx status codes are ignored.

Ideally you'd instead update your "if not logged in then redirect" code to generate the correct response for htmx immediately, rather than modifying it after the fact.

We are using @login_required in this case, a specialised version of that might be a solution. But I'm not sure if it is the right one.

mfisco commented 2 years ago

I'm also working on a project where the majority requests require authentication. For the time being we simply customized django's LoginView

class AccountLoginView(LoginView):
    template_name = "accounts/login.html"

    def get(self, request, *args, **kwargs):
        """
        Triggers client-side redirect for htmx requests redirected to the login page.

        - Particularly useful when a user's session has expired but their client 
        is polling for a resource needing authentication. 

        """
        if request.htmx and REDIRECT_FIELD_NAME in request.GET:
            return HttpResponseClientRedirect(settings.LOGIN_URL)
        return super().get(request, *args, **kwargs)

Anyways, has there been any more ideas or thought given to adding this functionality to django-htmx?

suda commented 2 years ago

I was thinking, would it be worth it to add this as a decorator? I think this use case extends beyond just login (although that's exactly what brought me here 😅) and could be applied selectively vs being run on all requests via middleware like so:

def htmx_redirect(func):
    def wrapper(self, request, *args, **kwargs):
        response = func(self, request, *args, **kwargs)
        if request.htmx and isinstance(response, HttpResponseRedirect):
            response['HX-Redirect'] = response['Location']
            response.status_code = 204
        return response
    return wrapper
itsthejoker commented 2 years ago

I also just discovered this issue, and this what I wrote -- I apply it as a decorator on HTMX-only views and it works quite well for my uses, though doesn't handle the HX-Redirect header. I'm solving a different issue and can't delete my comment, so below is a general guard decorator for single views.

Click to expand! ```python def htmx_guard_redirect(redirect_name): """ Decorator for guarding HTMX-only function views. If a request comes in to this endpoint that did not originate from HTMX, respond with a redirect to the requested endpoint. Usage: @htmx_guard_redirect("homepage") def htmx_only_function(request): ... Takes an optional `test` boolean that bypasses the redirect. """ # https://stackoverflow.com/a/9030358 def _method_wrapper(view_method: Callable) -> Callable: def _arguments_wrapper( request: Request, *args, **kwargs ) -> HttpResponseRedirect | Callable: testing = False if "test" in kwargs.keys(): testing = kwargs.pop("test") if not request.htmx and not testing: return HttpResponseRedirect(reverse(redirect_name)) return view_method(request, *args, **kwargs) return _arguments_wrapper return _method_wrapper ```
scur-iolus commented 11 months ago

This blog post highlights that returning a HTTP 303 status code might sometimes be particularly convenient with HTMX. Indeed, when you perform actions using HTTP methods like PUT / DELETE / PATCH and you want to redirect after the action is completed, using a 303 response ensures that the subsequent request after the redirection will be done using a GET method.

I'd just like to emphasize this point, which hasn't yet been mentioned, although it seems to be closely related to the potential new functionality being discussed here.

BergLucas commented 8 months ago

Personally, I'm in favour of having such a mechanism in django-htmx.

I've just started learning HTMX, having already worked with Django for several years, and I was surprised not to find a solution in the documentation for managing authenticated routes like in classic Django.

In addition, since it's a middleware, it lets the user choose whether or not to activate it depending on whether it can work for their project.

Finally, it would allow many Django components to work directly without having to override some methods that are sometimes not designed to be easily overridden, such as handle_no_permission in LoginRequiredMixin for example.

In any case, thanks for the library, it works like a charm!