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?