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

set status code in middleware #47

Merged
merged 2 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/source/form-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Form Submission

By default, Turbo will intercept all clicks on links and form submission, as for form submission, if the form validation fail on the server side, Turbo expects the server return `422 Unprocessable Entity`.

In Django, however, failed form submission would still return HTTP `200`, which would cause some issues when working with Turbo Drive.

[https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission](https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission)

How to solve this issue?

`turbo_helper.middleware.TurboMiddleware` can detect POST request from Turbo and change the response status code to `422` if the form validation failed.

It should work out of the box for `Turbo 8+` on the frontend.

So developers do not need to manually set the status code to `422` in the view.
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ Topics
:maxdepth: 2

install.md
form-submission.md
dom_id.md
turbo_frame.md
turbo_stream.md
real-time-updates.md
extend-turbo-stream.md
multi-format.md
signal-decorator.md
redirect.md
test.md
2 changes: 1 addition & 1 deletion docs/source/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ MIDDLEWARE = [
]
```

With the `TurboMiddleware` we have `request.turbo` object which we can access in Django view or template.
With the `TurboMiddleware` we have `request.turbo` object which we can access in Django view or template. It will also help us to change the response status code to `422` if the POST request come from Turbo and the form validation failed.

If the request originates from a turbo-frame, we can get the value from the `request.turbo.frame`

Expand Down
11 changes: 0 additions & 11 deletions docs/source/redirect.md

This file was deleted.

23 changes: 22 additions & 1 deletion src/turbo_helper/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import http
import threading
from typing import Callable

Expand Down Expand Up @@ -53,9 +54,13 @@ def __bool__(self):


class TurboMiddleware:
"""Adds `turbo` attribute to request:
"""
Task 1: Adds `turbo` attribute to request:
1. `request.turbo` : True if request contains turbo header
2. `request.turbo.frame`: DOM ID of requested Turbo-Frame (or None)

Task 2: Auto change status code for Turbo Drive
https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission
"""

def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
Expand All @@ -64,5 +69,21 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
def __call__(self, request: HttpRequest) -> HttpResponse:
with SetCurrentRequest(request):
request.turbo = SimpleLazyObject(lambda: TurboData(request))

response = self.get_response(request)

if (
request.method == "POST"
and request.headers.get("X-Turbo-Request-Id")
and response.get("Content-Type") != "text/vnd.turbo-stream.html"
):
if response.status_code == http.HTTPStatus.OK:
response.status_code = http.HTTPStatus.UNPROCESSABLE_ENTITY

if response.status_code in (
http.HTTPStatus.MOVED_PERMANENTLY,
http.HTTPStatus.FOUND,
):
response.status_code = http.HTTPStatus.SEE_OTHER

return response
94 changes: 86 additions & 8 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import http

import pytest
from django.http import HttpResponse
from django.http import (
HttpResponse,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)

from turbo_helper.middleware import TurboMiddleware
from turbo_helper.response import TurboStreamResponse


@pytest.fixture
Expand All @@ -11,23 +18,94 @@ def get_response():

class TestTurboMiddleware:
def test_accept_header_not_found(self, rf, get_response):
req = rf.get("/", HTTP_ACCEPT="text/html")
headers = {
"ACCEPT": "text/html",
}
headers = {f"HTTP_{key.upper()}": value for key, value in headers.items()}
req = rf.get("/", **headers)
TurboMiddleware(get_response)(req)
assert not req.turbo
assert req.turbo.frame is None

def test_accept_header_found(self, rf, get_response):
req = rf.get("/", HTTP_ACCEPT="text/vnd.turbo-stream.html")
headers = {
"ACCEPT": "text/vnd.turbo-stream.html",
}
headers = {f"HTTP_{key.upper()}": value for key, value in headers.items()}
req = rf.get("/", **headers)
TurboMiddleware(get_response)(req)
assert req.turbo
assert req.turbo.frame is None

def test_turbo_frame(self, rf, get_response):
req = rf.get(
"/",
HTTP_ACCEPT="text/vnd.turbo-stream.html",
HTTP_TURBO_FRAME="my-playlist",
)
headers = {
"ACCEPT": "text/vnd.turbo-stream.html",
"TURBO_FRAME": "my-playlist",
}
headers = {
f"HTTP_{key.upper()}": value for key, value in headers.items()
} # Add "HTTP_" prefix
req = rf.get("/", **headers)
TurboMiddleware(get_response)(req)
assert req.turbo
assert req.turbo.frame == "my-playlist"


class TestTurboMiddlewareAutoChangeStatusCode:
def test_post_failed_form_submission(self, rf):
headers = {
"ACCEPT": "text/vnd.turbo-stream.html",
"X-Turbo-Request-Id": "d4165765-488b-41a0-82b6-39126c40e3e0",
}
headers = {
f"HTTP_{key.upper()}": value for key, value in headers.items()
} # Add "HTTP_" prefix
req = rf.post("/", **headers)

def form_submission(request):
# in Django, failed form submission will return 200
return HttpResponse()

resp = TurboMiddleware(form_submission)(req)

assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY

@pytest.mark.parametrize(
"response_class", [HttpResponseRedirect, HttpResponsePermanentRedirect]
)
def test_post_succeed_form_submission(self, rf, response_class):
headers = {
"ACCEPT": "text/vnd.turbo-stream.html",
"X-Turbo-Request-Id": "d4165765-488b-41a0-82b6-39126c40e3e0",
}
headers = {
f"HTTP_{key.upper()}": value for key, value in headers.items()
} # Add "HTTP_" prefix
req = rf.post("/", **headers)

def form_submission(request):
# in Django, failed form submission will return 301, 302
return response_class("/success/")

resp = TurboMiddleware(form_submission)(req)

assert resp.status_code == http.HTTPStatus.SEE_OTHER

def test_post_turbo_stream(self, rf, get_response):
"""
Do not change if response is TurboStreamResponse
"""
headers = {
"ACCEPT": "text/vnd.turbo-stream.html",
"X-Turbo-Request-Id": "d4165765-488b-41a0-82b6-39126c40e3e0",
}
headers = {
f"HTTP_{key.upper()}": value for key, value in headers.items()
} # Add "HTTP_" prefix
req = rf.post("/", **headers)

def form_submission(request):
return TurboStreamResponse()

resp = TurboMiddleware(form_submission)(req)
assert resp.status_code == http.HTTPStatus.OK
Loading