How to route to specific viewset methods Django - python

I've been studying the viewset and router docs for Django and I can't figure out how to set up a route to access the method on a viewset.
For example, here is my urls.py:
from rest_framework.routers import DefaultRouter
from users.views import (UserViewSet, testing)
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="users")
urlpatterns = [
path('testing', testing)
]
And then this is my views file in my user directory
#csrf_exempt
class UserViewSet:
def create(self):
return JsonResponse({
'the create endpoint'
})
#csrf_exempt
def testing(request):
return JsonResponse({
"title": "My json res"
})
Using postman, I can hit the endpoint example.com/testing and have the json response logged out. However, I try to hit example.com/users/create and I get a 404. I thought the basename propery when registering the viewset with the router would group all of the methods inside that class under that route path and then the methods would all be their own endpoint. Am I thinking about this incorrectly? Any help would be lovely as I am new to Django. I've mostly done Express and Laravel. Thanks!

You didn't convert your router into a urlpatterns list, so you won't be able to access your viewset regardless.
To convert the router:
from rest_framework.routers import DefaultRouter
from users.views import (UserViewSet, testing)
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="users")
urlpatterns = [
path('testing', testing),
*router.urls,
]
In Django Rest Framework, a viewset's create method is executed during a POST request to a particular uri. In your case, this uri would be /users.
If you would like to add an additional method that triggers at /users/create, you will need to use the action decorator:
from rest_framework import viewsets
from rest_framework.response import JsonResponse
class UserViewSet(viewsets.ViewSet):
#action(methods=['GET'], url_path='create')
def my_custom_action(self):
return JsonResponse({
'the create endpoint'
})
Since create is a reserved method on DRF viewsets, you will need to name the method something else (in the example, my_custom_action), and set the url_path parameter accordingly.
If you were to omit the url_path, the path would default to the name of the method, eg. /users/my_custom_action.

Related

Unable to POST data with the Django REST Framework : Not Found

I'm trying to figure out why the Django REST Framework throws a 404 Not Found when I POST data with the code below, because when I load the browsable API with the URL it correctly displays the object list with the HTML form to POST data.
The Django project that serve the API run in a Docker container as well as the client, but in a separate Docker host.
How could I fix the issue ?
Server
Console logs
django-1 | Not Found: /api/strategy/target/
django-1 | [26/Sep/2022 14:27:05] "POST /api/strategy/target/ HTTP/1.1" 404 23
project/project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path("api/strategy/", include("strategy.urls")),
]
strategy/urls.py
from django.urls import path, include
from rest_framework import routers
from strategy.api.views import TargetViewSet
router = routers.DefaultRouter()
router.register("target", TargetViewSet, basename="targets-list")
urlpatterns = [
path('', include(router.urls)),
]
strategy/views.py
from rest_framework import viewsets
from strategy.api.serializers import TargetSerializer
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAdminUser
# Create new model
#permission_classes([IsAdminUser])
class TargetViewSet(viewsets.ModelViewSet):
serializer_class = TargetSerializer
queryset = Target.objects.all()
Client
res = requests.post("http://1.2.3.4:8001/api/strategy/target/",
data=data,
headers={'Authorization': 'Bearer {0}'.format(token)}
)
router.register("target", TargetViewSet, basename="targets-list")
The defination of the router is correct but I think the basename you have there is the problem. Since you have a queryset defined in your views, you can remove the basename completely. You only need to use basename if you define your own get_queryset function.
So try this and let's see if it works out for you.
router.register("target", TargetViewSet")

how to Create Nested router urs in rest_framework?

i have created a url using NestedSimpleRouter. And the url that i have created is that :
http://localhost:8000/api/category/1/subcategory/
And In above url 1 in the id of category .And my problem is that
i want to create the url like this
http://localhost:8000/api/category/1/subcategory/1/interest
this 1 is subcategory_id
So How can i define Nested url for this url .My urls.py is given below:
urls.py:
from django.urls import path,include
from rest_framework_nested import routers
from campaignapp import views
router = routers.DefaultRouter()
router.register('campaign',views.CampaignViewSet)
router.register('interestbundle',views.InterestBundleViewSet)
router.register('category',views.CategoryViewSet,'category')
category_router = routers.NestedSimpleRouter(router, r'category', lookup='category')
category_router.register(r'subcategory', views.SubcategoryViewSet, base_name='category-subcategory')
urlpatterns = [
path('',include(router.urls)),
path('',include(category_router.urls))
]
You can add extra actions to your viewset using action or list_route or detail_route(older version of django rest framework) decorator. This decorator is applicable all ViewSets which inherit from ViewSetMixin class.

Django Write Authentication Redirect for Blackboxed Route URLs

I'm using a django library called django-dashing that has a predefined set of urls that render a dashboard. I import them like this
urlpatterns = [
...
url(r'^dashboard/', include(dashing_router.urls)),
...
]
I want the router to be accessible only by administrators, which I can do with some config settings within django-dashing. However, when a non-admin user attempts to access /dashboard/, I want to redirect them to django's /admin/ panel to have them log in, instead of throwing the 403 that django-dashing does.
Since the django-dashing views are effectively blackboxed, I was wondering if there was a way to write a 'pre-view' that would intercept the request to /dashboard/, run some code – specifically, doing the appropriate redirects – and then continue onto the actual dashboard.
I know this would be easy enough to do by writing two urls, like /dashboard-auth/ which redirects to /dashboard/, but I don't want the user to have to go to one URL to get to another
Any suggestions?
A Django simple custom middleware is another option...
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
class DashingRedirectMiddleware(object):
def process_request(self, request):
if request.path.startswith('/dashing/') and not request.user.is_staff:
return HttpResponseRedirect(reverse('admin:login'))
return
Don't forget to add this middleware to your DJANGO SETTINGS...
MIDDLEWARE_CLASSES = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'yourapp.middleware.DashingRedirectMiddleware',
...
]
Or something like that.
The way I would do it is by overridding dashing's default router. All of the urls are generated dynamically by the Router class, so by overriding the get_urls method, you can wrap each function in the staff_member_required decorator.
from django.contrib.admin.views.decorators import staff_member_required
from django.conf.urls import url
from dashing.utils import Router
from dashing.views import Dashboard
class AdminRequiredRouter(Router):
def get_urls(self):
urlpatterns = [
url(r'^$', staff_member_required(Dashboard.as_view()), name='dashboard'),
]
for widget, basename, parameters in self.registry:
urlpatterns += [
url(r'/'.join((
r'^widgets/{}'.format(basename),
r'/'.join((r'(?P<{}>{})'.format(parameter, regex)
for parameter, regex in parameters.items())),
)),
staff_member_required(widget.as_view()),
name='widget_{}'.format(basename)),
]
return urlpatterns
router = AdminRequiredRouter()
Then include your router instead of dashing's
from .router import router
urlpatterns = [
...
url(r'^dashboard/', include(router.urls)),
...
]
If you are willing to look inside the 'black box' of the dashing urls, you can see that the /dashboard/ is handled by the Dashboard view. You could subclass this view, and catch the PermissionDenied error.
from django.core.exceptions import PermissionDenied
from dashing.views import Dashboard
class MyDashboard(Dashboard):
def get(self, request, *args, **kwargs):
try:
return super(MyDashboard, self).get(request, *args, **kwargs)
except PermissionDenied:
return redirect('/admin/')
Then add your view above the dashing urls:
urlpatterns = [
...
url(r'^dashboard/$', MyDashboard.as_view())
url(r'^dashboard/', include(dashing_router.urls)),
...
]

Stuck creating a Django REST Framework ViewSet

I am stuck trying to work out how to create a Django REST Framework ViewSet.
The API calls I have inherited look like this:
/api/v1/user/<user_id>/like_count
/api/v1/user/<user_id>/friends/
/api/v1/user/login
/api/v1/user/logout/
In my base urls.py I have the following:
urlpatterns = patterns('',
url(r'^api/v1/', include('api.urls')),
url(r'^$', TemplateView.as_view(template_name='base.html'), name='home'),
url(r'^docs/', include('rest_framework_swagger.urls'))
)
I have an app called api. In the api urls.py I have:
from django.conf.urls import url, include
from rest_framework import routers
from api import views
router = routers.DefaultRouter()
router.register(r'user', views.UserViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
In my api/views.py file I want to create a UserViewSet class that handles all the possible variants of the url calls.
First I can't work out if I should use:
class UserViewSet(viewsets.ModelViewSet):
or...
class UserViewSet(APIView):
If I understand it correctly I can cater for the
/api/v1/user/login
/api/v1/user/logout
calls using something like:
class UserViewSet(viewsets.APIView):
def login(self, request, format=None):
...
def logout(self,request, format=None):
But I can't work out how to cater for the other variants that have the <user-id> in the url.
Is there a recommended way to do this?
Some API calls have a trailing '/' and some don't. It is what I have been given (to fit in with an existing mobile app).
EDIT: By the way, I have done the DRF tutorial twice now and still can't see how to do this.
EDIT2: I am really struggling to understand the DRF documentation for this. Can anyone provide some example code that deals with my use case?
Using a ViewSet or Views will depend on what other actions you need for your user resource apart from those you have in your urls. ModelViewSet is built from mixins for listing, updating etc. You can combine those to achieve what you want or you can just go with a combination of views.
Of course you can combine a viewset with views if you have to.
For the /api/v1/user/<user_id>/friends/ endpoint you can look at DRF docs or at DRF Nested Routers.
The like_count can be implemented with a view that returns just that single value.
Auth is a different topic entirely --- you can look at this.
viewsets.py
class UserViewset(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = UserSerializer
queryset = User.objects.all()
urls.py
from .viewsets import UserViewSet
user_list = UserViewSet.as_view({'get':'list'})
user_detail = UserViewSet.as_view({'get':'retrieve'})
urlpatterns= [
url(r'^user/(?P<id>\d+)/(?P<friends>[-\w\d]+)/$', user_detail, name='user-detail')
]
In the serializers.py adde the ParameterisedHyperlinkedIdentityField as a serializer.
serializers.py
class UserSerializer(serializers.HyperlinkedModelSerializer):
url = ParameterisedHyperlinkedIdentityField(view_name='user-detail', lookup_fields=(('id', 'id'), ('friends', 'friends')), read_only=True)
class Meta:
model = User

get router url name when testing in Django Rest Framework

I have some problem. I use routers in Django Rest Framework and I want to test some api methods.
In urls.py:
router = DefaultRouter()
router.register(r'my-list', MyViewSet, base_name="my_list")
urlpatterns = [
url(r'^api/', include(router.urls,
namespace='api'), ),
]
So, in tests.py I want to use something like reverse. Now I use
response = self.client.get('/api/my-list/')
Its a hard coded string, if I use :
response = self.client.get(reverse('api:my_list')
I have an error:
django.core.urlresolvers.NoReverseMatch: Reverse for 'my_list' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []
How to fix that?
Thanks!
DRF adds suffixes in viewsets for different URLs - list, detail and possibly custom URLs. You can see that in source code and in docs. So in your case the actual reverse should be something like:
reverse('api:my_list-list') # for list URL. e.g. /api/my-list/
reverse('api:my_list-detail') # for detail URL. e.g. /api/my-list/<pk>/
That is why its also probably better to use a resource name as a router base_name. For example base_name='user' vs base_name='users_list'.
Update 2021.
I have added more details from from #miki725 answer.
There are some details that needs to have some considerations such as app_name parameter that need to be placed within the myappname.urls.
Therefore the urls.py should look like this:
# django imports
from django.urls import path, include
# drf imports
from rest_framework import routers
from myappname.viewsets import UserViewSet
# In the example used in the question the app_name is 'api'
app_name = 'myappname' # <---- Needed when testing API URLS..
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, basename='user') # <---- Here already have -list and -detail by default.
urlpatterns = [
path('', include(router.urls)),
]
tests.py
from myappname.models import User
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
class TestApi(APITestCase):
def setUp(self):
self.headerInfo = {'content-type': 'application/json'}
# example of query to be used on URL.
self.user = User.objects.create(
username = 'anyname',
created_at=timezone.now(),
created_by='testname',
)
self.user.save()
# payload to be used to test PUT method for example...
self.user_data = {
'username': 'othername',
'created_at': timezone.now(),
'created_by': 'othertestname'
}
# again: In the example used in the question the app_name is 'api'
# that's why reverse('api:my-list')...
self.url_user_list = reverse('myappname:user-list') # <------
self.url_user_detail = reverse('myappname:user-detail',
kwargs={'pk': self.user.pk}) # <------
"""
Test User endpont.
"""
def test_get_user(self):
"""GET method"""
response = self.client.get(self.url_user_list,
self.user_data,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_user(self):
""" test POST method for User endpoint"""
response = self.client.post(
self.url_user_list, self.user_data,
# headers=self.headerInfo,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_update_user(self):
data = {
'username': 'someothername',
'created_at': timezone.now(),
'created_by': 'someothertestname'
}
response = self.client.put(self.url_user_detail, data, headers=self.headerInfo)
self.assertEqual(response.status_code, status.HTTP_200_OK)

Categories