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)
Related
So, I would like top stop ursing urlpatterns and just use router. But instead of using ID of an object I'm using UUID instead and I'm using it with urlpatterns and dont't find some way to use it with routers.
this is my current model:
class Board(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200, blank=False, null=False)
this is my core app urls.py:
...
router = DefaultRouter()
router.register(r'boards', BoardViewSet)
router.register(r'y', yViewSet)
router.register(r'z', zViewSet, basename='z')
urlpatterns = [
path('', include(router.urls)),
path('board-list/<uuid:pk>/', BoardViewSet.as_view({'get': 'list'}), name='boards'),
]
and this is the project urls.py:
from django.contrib import admin
from django.urls import path, include
from core.urls import router as api_router
routes = []
routes.extend(api_router.urls)
urlpatterns = [
path('api/', include((routes, 'board_microservice'), namespace='v1')),
path('admin/', admin.site.urls),
]
the application usage is ok, but I have some troubles with test.
i.e:
this works well:
url = reverse('v1:board-list')
response = api_client().get(
url
)
and it isn't working:
board = baker.make(Board)
url = reverse('v1:board-list', kwargs={"pk": board.id})
response = api_client().get(url)
I receive
django.urls.exceptions.NoReverseMatch: Reverse for 'board-list' with keyword arguments
and I think I can replace urlpatterns by router to solve it and turns it more simple
There is any way to do it with router?
You haven't shared your view, but it seems you are using a ModelViewSet and you seem to be looking for the retrieve endpoint rather than list (judging from using a pk). If so then you want to use -detail instead of -list:
url = reverse('v1:board-detail', kwargs={"pk": board.id})
Board_list is not what you want to call... Try board_get instead.
Board list takes no argument. But get does.. To get a particular board.
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.
I've created a REST API accepting a POST request in Django. The request itself works great when calling from a React front end, there should be nothing wrong with the API itself.
I want to create integration test for the API.
I've written the below test, which is basically copy/paste from the Django Documentation adapted to my API:
from django.urls import reverse, path
from rest_framework import status
from rest_framework.test import APITestCase
class SendLoginEmailTest(APITestCase):
def test_post_withValidData_loginEmailIsSent(self):
url = reverse('api_private:send_login_email')
data = {'email': 'validemail#email.com', 'token': '4d4ca980-cb0c-425d-8d2c-7e38cb33f38e'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
I run the test using the command:
python3 manage.py test
The test fails with the following exception:
django.urls.exceptions.NoReverseMatch: Reverse for 'send_login_email' not found. 'send_login_email' is not a valid view function or pattern name.
Any ideas what I'm doing wrong?
== Update ==
Content of urls.py.
from django.urls import path
from server.api_private.views import SendLoginEmail, ValidateEmail, IsEmailValidated, ProjectSave
app_name = "api_private"
urlpatterns = [
path('send_login_email/', SendLoginEmail.as_view()),
path('validate_email/', ValidateEmail.as_view()),
path('is_email_validated/', IsEmailValidated.as_view()),
path('project_save/', ProjectSave.as_view())
]
You didn't add names to your urls. reverse functions is looking for names. Add names to all URLs that you are going to use with reverse function.
urlpatterns = [
path('send_login_email/', SendLoginEmail.as_view(), name='send_logic_email'),
...,
]
Background
OK, I have a library wagtail_references which, like many snippet libraries, uses wagtail hooks to add admin views into the wagtail CMS.
Say I have, in my wagtail_hooks.py
#hooks.register('register_admin_urls')
def register_admin_urls():
return [
url(r'^references/', include(admin_urls, namespace='wagtailreferences')),
]
And the views that get registered are in views/reference.py:
#permission_checker.require('add')
def add(request):
Reference = get_reference_model()
ReferenceForm = get_reference_form(Reference)
if request.method == 'POST':
# STUFF I WANT TO TEST BECAUSE IT DOESN'T WORK PROPERLY
else:
form = ReferenceForm(user=request.user)
return render(request, "wagtail_references/references/add.html", {'form': form})
So in my test_stuff.py file, I'd have:
class TestReferenceIndexView(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
def post(self, params=None):
params = params if params else {}
return self.client.post(reverse('wagtailreferences:add'), params)
def test_simple(self):
response = self.post()
self.assertEqual(response.status_code, 201)
The Problem
But test_simple fails, because of course the urls for the view its testing are hooked in dynamically, not defined in urls.py. I get:
django.urls.exceptions.NoReverseMatch: 'wagtailreferences' is not a registered namespace
The question
How can I test endpoints whose URLs are registered by wagtail hooks?
I've tried
Registering the hooks manually in the test case, like:
class TestReferenceIndexView(TestCase, WagtailTestUtils):
def setUp(self):
self.register_hook('register_admin_urls', register_admin_urls)
DOH! I hadn't registered the admin urls in my test app.
tests/urls.py looked like this:
from django.conf.urls import include, url
from wagtail.core import urls as wagtail_urls
urlpatterns = [
url(r'', include(wagtail_urls)),
]
But now looks like this:
from django.conf.urls import include, url
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
urlpatterns = [
url(r'^admin/', include(wagtailadmin_urls)),
url(r'', include(wagtail_urls)),
]
Fixed. Sigh.
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)),
...
]