django-rest-frameworkdjango-rest-viewsetshtmx

Redirect to ViewSet list view from update causes PUT method not allowed


Expanding on the Django REST Framework tutorial, I have switched to TemplateHTMLRenderer to work with HTML.

class SnippetViewSet(viewsets.ModelViewSet):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    premission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
    renderer_classes = (renderers.TemplateHTMLRenderer,)

    def retrieve(self, request, *args, **kwargs):
        snippet = get_object_or_404(Snippet, pk=kwargs.get('pk'))
        serializer = SnippetSerializer(snippet, context={'request': request})
        context = {'serializer': serializer, 'snippet': snippet }
        return Response(context, template_name='snippets/snippet_retrieve.html')

    def update(self, request, *args, **kwargs):
        snippet = get_object_or_404(Snippet, pk=kwargs.get('pk'))
        serializer = SnippetSerializer(snippet, data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        #return redirect(reverse('snippet-list', kwargs=kwargs, request=request))
        #serializer = SnippetSerializer()
        #return redirect(reverse('snippet-list', request=request))
        return HttpResponseRedirect(reverse('snippet-list'))

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

My retrieve method renders the following template:

{% load rest_framework %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/htmx.org@1.9.5" integrity="sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO" crossorigin="anonymous"></script>
    <title>Snippet Retrieve</title>
</head>
<body hx-boost="true">
    <h1>Snippet {{ snippet.pk }}</h1>
    <hr />
    <form>
        {% csrf_token %}
        {% render_form serializer %}
        <button hx-put="{% url 'snippet-detail' pk=snippet.pk %}"
            >Update
        </button>
    </form>
    <hr />
    <script>
        document.body.addEventListener('htmx:configRequest', (event) => {
            event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
        })
</script>
</body>
</html>

Routing looks like this:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views


router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, basename='snippet')
router.register(r'users', views.UserViewSet, basename='user')

The form sends a PUT request to be handled by the update method, and the data it updated correctly.

However, having updated the data, I should now return to the snippet-list via an HTTP redirection, but this fails with a 405 error:

[18/Sep/2023 19:08:46] "GET /snippets/5/ HTTP/1.1" 200 48984
[18/Sep/2023 19:08:50] "PUT /snippets/5/ HTTP/1.1" 302 0
Method Not Allowed: /snippets/
[18/Sep/2023 19:08:50] "PUT /snippets/ HTTP/1.1" 405 22

How do I correctly return a redirection to the list view from update?


Solution

  • The problem was that the redirect based on a status code 302 issues a new PUT request to the ViewSet list view. The list view can not handle the PUT request.

    Sending a 303 lets the client know the redirect should happen via a GET request, so the solution was:

    return HttpResponseRedirect(reverse('snippet-list'), status=303)

    That is, explicitly specify the 303 status code. Obviously, the list view must be implemented to handle the request as HTML.