how to reverse the URL of a ViewSet's custom action in django restframework
Question:
I have defined a custom action for a ViewSet
from rest_framework import viewsets
class UserViewSet(viewsets.ModelViewSet):
@action(methods=['get'], detail=False, permission_classes=[permissions.AllowAny])
def gender(self, request):
....
And the viewset is registered to url in the conventional way
from django.conf.urls import url, include
from rest_framework import routers
from api import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet, base_name='myuser')
urlpatterns = [
url(r'^', include(router.urls)),
]
The URL /api/users/gender/
works. But I don’t know how to get it using reverse
in unit test. (I can surely hard code this URL, but it’ll be nice to get it from code)
According to the django documentation, the following code should work
reverse('admin:app_list', kwargs={'app_label': 'auth'})
# '/admin/auth/'
But I tried the following and they don’t work
reverse('myuser-list', kwargs={'app_label':'gender'})
# errors out
reverse('myuser-list', args=('gender',))
# '/api/users.gender'
In the django-restframework documentation, there is a function called reverse_action
. However, my attempts didn’t work
from api.views import UserViewSet
a = UserViewSet()
a.reverse_action('gender') # error out
from django.http import HttpRequest
req = HttpRequest()
req.method = 'GET'
a.reverse_action('gender', request=req) # still error out
What is the proper way to reverse the URL of that action?
Answers:
You can use reverse
just add to viewset’s basename action:
reverse('myuser-gender')
See related part of docs.
Based on DRF Doc.
from rest_framework import routers
router = routers.DefaultRouter()
view = UserViewSet()
view.basename = router.get_default_basename(UserViewSet)
view.request = None
or you can set request if you want.
view.request = req
In the end, you can get a reverse action URL and use it.
url = view.reverse_action('gender', args=[])
You can print all reversible url names of a given router
at startup with the get_urls()
method
In urls.py after the last item is registered to the router = DefaultRouter()
variable, you can add:
#router = routers.DefaultRouter()
#router.register(r'users', UserViewSet)
#...
import pprint
pprint.pprint(router.get_urls())
The next time your app is initialized it will print to stdout something like
[<RegexURLPattern user-list ^users/$>,
<RegexURLPattern user-list ^users.(?P<format>[a-z0-9]+)/?$>,
<RegexURLPattern user-detail ^users/(?P<pk>[^/.]+)/$>,
<RegexURLPattern user-detail ^users/(?P<pk>[^/.]+).(?P<format>[a-z0-9]+)/?$>,
#...
]
where ‘user-list’ and ‘user-detail’ are the names that can be given to reverse(...)
If you want to use UserViewset().reverse_action()
specifically, you can do this by assigning a basename
and request = None
to your ViewSet:
from rest_framework import viewsets
class UserViewSet(viewsets.ModelViewSet):
basename = 'user'
request = None
@action(methods=['get'], detail=False, permission_classes=[permissions.AllowAny])
def gender(self, request):
....
and in urls.py:
router.register('user', UserViewSet, basename=UserViewSet.basename)
Then calling url = UserViewset().reverse_action('gender')
or url = UserViewset().reverse_action(UserViewSet().gender.url_name)
will return the correct url.
Edit:
Above method only works when calling reverse_action()
only once because the ViewSetMixin.as_view()
method overrides basename
on instantiation. This can be solved by adding a custom subclass of GenericViewSet
(or ModelViewSet
if preferred) like so:
from django.utils.decorators import classonlymethod
from rest_framework.viewsets import GenericViewSet
class ReversibleViewSet(GenericViewSet):
basename = None
request = None
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
basename = cls.basename
view = super().as_view(actions, **initkwargs)
cls.basename = basename
return view
and subclassing this class for the specific ViewSets, setting basename as desired.
The answer is reverse('myuser-gender')
.
Note! But remember that DRF will replace _
in the action name with -
. That means if the action name is my_pretty_action
in the reverse you should use reverse(app-my-pretty-action)
.
I have defined a custom action for a ViewSet
from rest_framework import viewsets
class UserViewSet(viewsets.ModelViewSet):
@action(methods=['get'], detail=False, permission_classes=[permissions.AllowAny])
def gender(self, request):
....
And the viewset is registered to url in the conventional way
from django.conf.urls import url, include
from rest_framework import routers
from api import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet, base_name='myuser')
urlpatterns = [
url(r'^', include(router.urls)),
]
The URL /api/users/gender/
works. But I don’t know how to get it using reverse
in unit test. (I can surely hard code this URL, but it’ll be nice to get it from code)
According to the django documentation, the following code should work
reverse('admin:app_list', kwargs={'app_label': 'auth'})
# '/admin/auth/'
But I tried the following and they don’t work
reverse('myuser-list', kwargs={'app_label':'gender'})
# errors out
reverse('myuser-list', args=('gender',))
# '/api/users.gender'
In the django-restframework documentation, there is a function called reverse_action
. However, my attempts didn’t work
from api.views import UserViewSet
a = UserViewSet()
a.reverse_action('gender') # error out
from django.http import HttpRequest
req = HttpRequest()
req.method = 'GET'
a.reverse_action('gender', request=req) # still error out
What is the proper way to reverse the URL of that action?
You can use reverse
just add to viewset’s basename action:
reverse('myuser-gender')
See related part of docs.
Based on DRF Doc.
from rest_framework import routers
router = routers.DefaultRouter()
view = UserViewSet()
view.basename = router.get_default_basename(UserViewSet)
view.request = None
or you can set request if you want.
view.request = req
In the end, you can get a reverse action URL and use it.
url = view.reverse_action('gender', args=[])
You can print all reversible url names of a given router
at startup with the get_urls()
method
In urls.py after the last item is registered to the router = DefaultRouter()
variable, you can add:
#router = routers.DefaultRouter()
#router.register(r'users', UserViewSet)
#...
import pprint
pprint.pprint(router.get_urls())
The next time your app is initialized it will print to stdout something like
[<RegexURLPattern user-list ^users/$>,
<RegexURLPattern user-list ^users.(?P<format>[a-z0-9]+)/?$>,
<RegexURLPattern user-detail ^users/(?P<pk>[^/.]+)/$>,
<RegexURLPattern user-detail ^users/(?P<pk>[^/.]+).(?P<format>[a-z0-9]+)/?$>,
#...
]
where ‘user-list’ and ‘user-detail’ are the names that can be given to reverse(...)
If you want to use UserViewset().reverse_action()
specifically, you can do this by assigning a basename
and request = None
to your ViewSet:
from rest_framework import viewsets
class UserViewSet(viewsets.ModelViewSet):
basename = 'user'
request = None
@action(methods=['get'], detail=False, permission_classes=[permissions.AllowAny])
def gender(self, request):
....
and in urls.py:
router.register('user', UserViewSet, basename=UserViewSet.basename)
Then calling url = UserViewset().reverse_action('gender')
or url = UserViewset().reverse_action(UserViewSet().gender.url_name)
will return the correct url.
Edit:
Above method only works when calling reverse_action()
only once because the ViewSetMixin.as_view()
method overrides basename
on instantiation. This can be solved by adding a custom subclass of GenericViewSet
(or ModelViewSet
if preferred) like so:
from django.utils.decorators import classonlymethod
from rest_framework.viewsets import GenericViewSet
class ReversibleViewSet(GenericViewSet):
basename = None
request = None
@classonlymethod
def as_view(cls, actions=None, **initkwargs):
basename = cls.basename
view = super().as_view(actions, **initkwargs)
cls.basename = basename
return view
and subclassing this class for the specific ViewSets, setting basename as desired.
The answer is reverse('myuser-gender')
.
Note! But remember that DRF will replace _
in the action name with -
. That means if the action name is my_pretty_action
in the reverse you should use reverse(app-my-pretty-action)
.