reverse() in a test only returns relative URL in a test for django-rest-framework and that causes a 404 - django-rest-framework

I am trying to test the endpoints for my API by using this guide. Specifically, this block is supposed to test the get request:
class GetAllPuppiesTest(TestCase):
""" Test module for GET all puppies API """
def setUp(self):
Puppy.objects.create(
name='Casper', age=3, breed='Bull Dog', color='Black')
Puppy.objects.create(
name='Muffin', age=1, breed='Gradane', color='Brown')
Puppy.objects.create(
name='Rambo', age=2, breed='Labrador', color='Black')
Puppy.objects.create(
name='Ricky', age=6, breed='Labrador', color='Brown')
def test_get_all_puppies(self):
# get API response
response = client.get(reverse('get_post_puppies'))
# get data from db
puppies = Puppy.objects.all()
serializer = PuppySerializer(puppies, many=True)
self.assertEqual(response.data, serializer.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
When I try to adapt this to my own test, it looks like this:
from ..models import DemanderFeature, DemanderFeatureCollection
from rest_framework import status
from django.test import TestCase, Client
from django.urls import reverse
from ..serializers import DemanderFeatureCollectionSerializer
class GetAllDemanderFeatureCollections(TestCase):
def setUp(self):
DemanderFeatureCollection.objects.create(name='testdemanderfeaturecollection0')
DemanderFeatureCollection.objects.create(name='testdemanderfeaturecollection1')
def test_get_all_demandercollections(self):
# get API response
response = client.get(reverse('demandercollections-list'))
# get data from db
demanderfeaturecollections = DemanderFeatureCollection.objects.all()
serializer = DemanderFeatureCollectionSerializer(demanderfeaturecollections, many=True)
self.assertEqual(response.data, serializer.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
The problem is however the reverse() method is only returning the relative URL, (/demandercollections/) and then client.get(reverse(...)) returns a 404. I don't understand how I can force it to use the actual explicit URL during testing.
I'm using Django 3.
My main urls.py looks like this:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include("app.urls")),
path('api-auth/', include('rest_framework.urls')),
]
And my module urls.py looks like this:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from app import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'demanders', views.DemanderFeatureViewSet)
router.register(r'demandercollections', views.DemanderFeatureCollectionViewSet, basename="demandercollections")
router.register(r'producers', views.ProducerFeatureViewSet)
router.register(r'producercollections', views.ProducerFeatureCollectionViewSet)
router.register(r'pathfinderrunconfigurations', views.PathfinderRunConfigurationViewSet)
router.register(r'users', views.UserViewSet)
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]
The DemanderCollectionViewSet in views.py looks like this:
class DemanderFeatureCollectionViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
queryset = DemanderFeatureCollection.objects.all()
serializer_class = DemanderFeatureCollectionSerializer
lookup_field = 'name'
#action(detail=True, methods=["get"])
def geojson(self, request, *args, **kwargs):
demanders = DemanderFeature.objects.filter(demandercollection=self.get_object())
return Response(serialize('geojson', demanders, geometry_field='geom', fields=('name',)))
#action(detail=True, methods=["patch"])
def commit(self, request, *args, **kwargs):
demandercollection = self.get_object()
if not request.data["committed"]:
# User is trying to "uncommit", do not allow this
return Response("You may not un-commit a DemanderCollection. You must copy it and make modifications on the copy.", status=status.HTTP_400_BAD_REQUEST)
demandercollection.committed = True
demandercollection.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get_queryset(self):
user = get_object_or_404(User, username=self.request.user)
return DemanderFeatureCollection.objects.filter(deleted=False).filter(owner=user).order_by("name")
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def destroy(self, request, *args, **kwargs):
demandercollection = self.get_object()
demandercollection.deleted = True
demandercollection.save()
return Response(f"Successfully deleted DemanderCollection.")
POST ANSWER EDIT
not only was the accepted answer indeed the culprit, but it also revealed that the DemanderFeatureCollection objects being created must also be created with an owner attribute, and the client object must call its login() method to a valid user credential pair.
The test class therefore had to be updated to look like this:
class GetAllDemanderFeatureCollections(TestCase):
""" Test module for GET all puppies API """
def setUp(self):
self.test_user = User.objects.create_user('test_user', 'a#b.com', 'test_user')
self.other_user = User.objects.create_user('other_user', 'a#b.com', 'other_user')
client.login(username="test_user", password="test_user")
DemanderFeatureCollection.objects.create(name='testdemanderfeaturecollection0', owner=self.test_user)
DemanderFeatureCollection.objects.create(name='testdemanderfeaturecollection1', owner=self.test_user)
DemanderFeatureCollection.objects.create(name='otherdemanderfeaturecollection0', owner=self.other_user)
def test_get_all_demandercollections_for_user(self):
# get API response
response = client.get(reverse('demandercollections-list'))
# get data from db
demanderfeaturecollections = DemanderFeatureCollection.objects.filter(owner=self.test_user).all()
serializer = DemanderFeatureCollectionSerializer(demanderfeaturecollections, many=True)
self.assertEqual(response.data, serializer.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)

In get_queryset() method of DemanderFeatureCollectionViewSet class you are filtering the model instances with owner field against the logged-in user.
In your test-cases, you are creating the DemanderFeatureCollection instances without linking the user and hence DRF raising an HTTP 404 error. So, attaching the user to the instance and making the request with the same user will give you a proper response from the API.

Related

Django Rest Framework - How to implement a view to return a javascript file?

I need to serve/return javascript files from a Django Rest Framework api, so in any other website client I can do:
<script src="https://myserveraddress.com/django-api/my-view/my-js-file.js"></script>
and import the content from it to my clients websites by coding this tag on them.
How can i do the view and the url pattern for it?
I want to use DRF because of the CORS policy.
Thanks in advance!
use django HttpResponse and return it
return HttpResponse("<script src="https://myserveraddress.com/django-api/my-view/my-js-file.js"></script>",content_type="application/x-javascript")
HttpResponse comes from django -> from django.http import HttpResponse
Thanks, #mehran heydarian for your contribution.
With your tip and looking inside the rest_framework views module, using views.APIView, I have solved my problem and implemented both the view and the url. Additionaly to the view I have implemented a filter, so I can pass an argument on the url to be filtered on a query:
# views.py
from django.http import HttpResponse
from rest_framework import views
from . models import MYMODEL,
class MYMODELApiView(views.APIView):
#classmethod
def get_extra_actions(cls):
return []
#api_view(('GET',))
def my_def(request, some_string):
"""
some code to implement a condition
for validation
"""
def get_queryset(some_string):
"""
This view returns a string.
"""
if condition:
res = MYMODEL.objects.filter(some_field_from_model=some_string).values('file')
the_file = open('/myserver/directory_before_static/' + res[0]['file'], 'r')
file_content = the_file.read()
the_file.close()
return file_content
else:
the_file = open('/myserver/directory_before_static/static/another_directory/another.js', 'r')
file_content = the_file.read()
the_file.close()
return file_content
file_content = get_queryset(some_string)
return HttpResponse(file_content, content_type="application/javascript", charset='utf-8')
# urls.py
from django.urls import re_path
from .views import MYMODELApiView
urlpatterns = [
...
re_path('^my_url/(?P<some_string>.+)/$', MYMODELApiView.my_def, name='my_url'),
...
]
This works fine to me.

Multiple get methods in same class in Django views

I have multiple get methods in the same class. How do I create the urls?
Views.py
class DashboardData(viewsets.ViewSet):
#action(detail=True, methods=['get'])
def get_total(self, request):
Total = File.objects.all().count()
return Response(Total, status=status.HTTP_200_OK)
def get_done(self, request):
Done = File.objects.filter(entry=True).count()
return Response(Done, status=status.HTTP_200_OK)
def get_not_done(self, request):
NotDone = File.objects.filter(entry=False).count()
return Response(NotDone, status=status.HTTP_200_OK)
def get_pending(self, request):
Pending = File.objects.filter(entry=False).count()
return Response(Pending, status=status.HTTP_200_OK)
For example:
http://baser_url/dashboard/total_count should call the get_total() method
http://baser_url/dashboard/done_count should call the done_count() method.
If you want to route view functions more explicitly, then other way of doing so is:
urlpatterns = [
path('dashboard/total_count/', DashboardData.as_view({'get': 'get_total'})),
...,
path('dashboard/done_count/', DashboardData.as_view({'get': 'done_count'})),
]
You can register the methods as custom actions and set the url_path parameter.
Example:
class DashboardData(viewsets.ViewSet):
#action(methods=['get'], detail=False, url_path='total_count')
def get_total(self, request):
Total = File.objects.all().count()
return Response(Total, status=status.HTTP_200_OK)
#action(methods=['get'], detail=False, url_path='done_count')
def get_done(self, request):
Done = File.objects.filter(entry=True).count()
return Response(Done, status=status.HTTP_200_OK)
...
urls.py
router = SimpleRouter()
router.register('dashboard', views.DashboardData, basename='dashboarddata')
You can use the #action decorator, but with detail=False to add the url to the "root" of the viewset.
# views.py
class DashboardViewSet(viewsets.ViewSet):
#action(detail=False, url_path="download")
def get_download(self, request):
pass
# urls.py in your app
router = SimpleRouter()
router.register("dashboard", DashboardViewSet)
urlpatterns = router.urls
From your example it seems like you might be just fine making one single APIView for each function, and then adding them manually with path
class DashPendingView(APIView):
def get(self, request):
return Response(pending_things)
url_patterns = [
path("dashboard/pending/", DashPendingView.as_view(), "dash-pending")
]

DRF how to call an api without the the queruset

I am trying to create a rest API in DRF without any DB. I want the user to hit the API with the post data. Once the system receives the data I will do another API call and show the response. My code is:
serializers.py
class getCPHSerializers(serializers.Serializer):
cph_id = serializers.CharField(max_length=128, write_only=True, required=True)
views.py
class GETCPHDetails(generics.ListAPIView):
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, *args, **kwargs):
cphID = request.data.get('cphID',None)
errorList = []
if not cphID:
errorList.append({"message": "No CPH Found"})
if len(errorList) == 0:
param = {"searchQry": cphID}
apiResponse = requests.post("http://172.61.25.40:8000/api/newSearch", data=param )
return Response({"message":json.loads(apiResponse)})
I am getting an error
Expected a Response, HttpResponse or HttpStreamingResponse to be returned from the view, but received a <class 'NoneType'>
Any suggestion will be of great help. Thanks in advance.
You are seeing this error because you are not returning a response when
len(errorList) == 0 is False
Python will always return None if you don't return anything
from a method.

django graphene rate limit (throttling)

I am building a REST Api on Django RF. I need to set a requests limit from IP. It's easy to do that for a regular Api endpoint in views.py just adding following settings
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}
But I also have a Graphene django for a graphql api.
How can I set up a rate limit for that view? I have tried django-ratelimit, but it didn't work for me.
The issue was solved by customising GraphQL view to the following:
from graphene_django.views import GraphQLView
class CustomGraphQL(GraphQLView):
def parse_body(self, request):
if isinstance(request, Request):
return request.data
return super().parse_body(request)
#classmethod
def as_view(cls, *args, **kwargs):
view = super().as_view(*args, **kwargs)
view = authentication_classes((TokenAuthentication,))(view)
view = throttle_classes((AnonRateThrottle, AnonMinutesThrottle,
AnonSecondsThrottle, UserRateThrottle))(view)
view = api_view(['GET', 'POST'])(view)
return view

Is there a way to use inheritance in django viewsets or do I have to rewrite the functions?

I am writting an API in Django Rest Framework. I am using viewsets however regular methods create() and update() do not do it for me and I have to rewrite them.
Suppose that I need to do just one check to test if creation of an instance is legit, do I still have to rewrite the whole create function?
Currently I have this:
class LocationViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = LocationSerializer
def create(self, request, *args, **kwargs):
user = request.user
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if user.can_create_location():
serializer.save()
return Response(serializer.data)
else:
raise exceptions.PermissionDenied('You dont have permission to create this.')
Instead of rewritting the whole thing, is it possible to somehow use inheritance and do something like this?
class LocationViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = LocationSerializer
def create(self, request, *args, **kwargs):
user = request.user
if user.can_create_location():
return super().create(self, request, *args, **kwargs)
else:
raise exceptions.PermissionDenied('You dont have permission to create this.')
The above does not work as I get this error. I feel like this should be possible to do, but I don't know how. Thanks for your help!
AttributeError at /api/0/location
LocationViewSet' object has no attribute 'data'
Request Method: POST
Request URL: http://127.0.0.1:8000/api/0/location
Django Version: 2.1.7
Yes, it does work, and you're doing it almost correctly... as per your error: the function is trying to access LocationViewSet.data as you're passing self in the first argument, change that call to:
super().create(request, *args, **kwargs)
Python standard docs has some info on super() and a link to a more in depth explanation.

Resources