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?

Asked By: nos

||

Answers:

You can use reverse just add to viewset’s basename action:

reverse('myuser-gender') 

See related part of docs.

Answered By: neverwalkaloner

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=[])
Answered By: Ali ZahediGol

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(...)

Answered By: Anshik

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.

Answered By: Lukas Loos

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).

Answered By: Artem Dumanov