Django: @action Method \"GET\" not allowed

71 Views Asked by At

I have an issue with one of my ViewSets that I don't know what to do about. It seems the methods I want to add using action are not registered properly and I get a MethodNotAllowedError.

Here's a stripped-down version:

from drf_spectacular.utils import extend_schema
from rest_framework import mixins, viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response

from error_database.models import DeviceConfigType
from error_database.permissions import AllowAnyReadAccessOrHasWritePermission
from error_database.serializers import DeviceConfigTypeSerializer


class DeviceConfigTypeViewSet(
    mixins.CreateModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet
):
    queryset = DeviceConfigType.objects.all()
    serializer_class = DeviceConfigTypeSerializer
    lookup_field = "type"
    permission_classes = [AllowAnyReadAccessOrHasWritePermission]

    def get_queryset(self):
        return self.queryset.filter(release=self.kwargs['release_version_number'])

    def create(self, request, *args, **kwargs):
        # (some code)
        return Response(status=status.HTTP_201_CREATED)

    def destroy(self, request, *args, **kwargs):
        # (some code)
        return Response(status=status.HTTP_204_NO_CONTENT)

    @extend_schema(
        request=DeviceConfigTypeSerializer,
        responses={status.HTTP_201_CREATED: DeviceConfigTypeSerializer(many=True)},
    )
    @action(
        detail=True, methods=['get'],
        url_path=r'versions',
        url_name='config-type-list-versions',
    )
    def list_versions(self, request, *args, **kwargs) -> Response:
        # (some code)
        return Response(status=status.HTTP_200_OK)

    @action(
        detail=True,
        methods=['get'],
        url_path=r'versions/(?P<version_id>\d+\.\d{1,3})',
        url_name='config-type-version-get',
    )
    def get_version(self, request, *args, **kwargs):
        # (some code)
        return Response(status=status.HTTP_200_OK)

    @action(
        detail=True, methods=['delete'],
        url_path=r'versions/(?P<version_id>\d+\.\d{1,3})',
        url_name='config-type-version-delete',
    )
    def destroy_version(self, request, *args, **kwargs):
        # (some code)
        return Response(status=status.HTTP_204_NO_CONTENT)

The model in the background should handle two different keys: "config_type" and "version_id", with an API like this: /releases/{release_version_number}/config_type/{config_type}/versions/{version_id}

I have working tests for the ViewSets like this:

view = DeviceConfigTypeViewSet.as_view({'get': 'get_version'})
response = view(
    request,
    release_version_number=release.pk,
    type=config_type.type,
    version_id=version_number,
)

what works is:

  • GET | POST /releases/{release_version_number}/config_type/ -- get and create types
  • DELETE /releases/{release_version_number}/config_type/{config_type} -- delete a type
  • GET /releases/{release_version_number}/config_type/{config_type}/versions -- get versions for this type
  • DELETE /releases/{release_version_number}/config_type/{config_type}/versions/{version_id} -- delete specific version

What does NOT work:

  • GET /releases/{release_version_number}/config_type/{config_type}/versions/{version_id} -- get a specific version

"detail": "Method "GET" not allowed."

I've used both curl and a drf-spectacular-generated Swagger-UI to send requests and they fail the same way. I've also checked my url paths, url names etc. - it all seems to come down to my action not being registered properly. The Django logs just say:

"GET /releases/4.0/config_type/US/versions/1.000/ HTTP/1.1" 405 40

In my urls.py I'm registering it like this:

release_router = NestedSimpleRouter(router, r'releases', lookup='release')
config_type_router = NestedSimpleRouter(release_router, r'config_type', lookup='config')
urlpatterns = [
    path('', include(release_router.urls)),
    path('', include(config_type_router.urls)),
]

which gives me these URL patterns:

<URLPattern '^releases/(?P<release_version_number>[^/]+)/config_type/$' [name='release-config-type-list']>
<URLPattern '^releases/(?P<release_version_number>[^/]+)/config_type/versions/$' [name='release-config-type-config-type-list-versions']>
<URLPattern '^releases/(?P<release_version_number>[^/]+)/config_type/(?P<type>[^/.]+)/$' [name='release-config-type-detail']>
<URLPattern '^releases/(?P<release_version_number>[^/]+)/config_type/(?P<type>[^/.]+)/versions/(?P<version_id>\d+\.\d{1,3})/$' [name='release-config-type-config-type-version-delete']>
<URLPattern '^releases/(?P<release_version_number>[^/]+)/config_type/(?P<type>[^/.]+)/versions/(?P<version_id>\d+\.\d{1,3})/$' [name='release-config-type-config-type-version-get']>

My current workaround for this is to override the get() method of the ViewSet and call the requisite method like this:

def get(self, request, *args, **kwargs):
    version_id = kwargs.get('version_id', None)
    if version_id is not None:
        return self.get_version(request, *args, **kwargs)

... but I'd like to understand what I'm doing wrong here:

@action(
    detail=True,
    methods=['get'],
    url_path=r'versions/(?P<version_id>\d+\.\d{1,3})',
    url_name='config-type-version-get',
)
def get_version(self, request, *args, **kwargs):

From what I understand, this should register a GET method and route it to get_version - so why is it failing to do so?

0

There are 0 best solutions below