Skip to content

Commit

Permalink
set status code in middleware (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-yin authored Aug 16, 2024
1 parent 72e02e7 commit 9cb5329
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 22 deletions.
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

0 comments on commit 9cb5329

Please sign in to comment.