Django Rest Framework: How to properly test a ViewSet? - django

I'm new to testing in DRF. I have the following ViewSet where I'm only allowing the 'List' and 'Retrieve' actions:
class RecipeViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
permission_classes = [permissions.AllowAny]
queryset = Recipe.objects.filter(visible=True).order_by('-created')
pagination_class = RecipeListPagination
lookup_url_kwarg = 'recipe_id'
def get_serializer_class(self):
if self.action == 'list':
return RecipeListSerializer
elif self.action == 'retrieve':
return RecipeDetailSerializer
These are the tests I've written so far:
class RecipeViewSetTestCase(test.APITestCase):
def setUp(self):
# Create objects
self.category = RecipeCategory.objects.create(name="meals")
Recipe.objects.create(name="Pepperoni Pizza", category=self.category, description="It's rounded")
Recipe.objects.create(name="Beef Ragu", category=self.category, description="It's moo'")
# Get urls
self.recipe = Recipe.objects.first()
self.list_url = reverse('recipes-list')
self.detail_url = reverse('recipes-detail', kwargs={'recipe_id': self.recipe.id})
def test_recipe_list(self):
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 2)
def test_recipe_detail(self):
response = self.client.get(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], self.recipe.name)
def test_recipe_create_not_allowed(self):
response = self.client.post(self.list_url)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_recipe_delete_not_allowed(self):
response = self.client.delete(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_recipe_update_not_allowed(self):
response = self.client.put(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_recipe_partial_update_not_allowed(self):
response = self.client.patch(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
I'm not sure if it's necessary to write a test for each method that shouldn't be allowed, or if there is a better way.
Also, I'm not sure of what else I should be testing inside this viewset.
Does anyone with experience in testing DRF knows how to properly test viewsets?
Many thanks!

Related

Why is my Django test failing when the view actually works?

I have a class based view that works as designed in the browser. I'm trying to write unit tests for the view and they keep failing. I'm wondering why. The view (the UserPassesTest is whether the user is a superuser or not):
class EditUserView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
"""handles get and post for adding a new AEUser"""
template_name = 'editUser.html'
title = 'Edit User'
def get(self, request, *args, **kwargs):
"""handles the GET"""
post_url = reverse('edit_user', args=[kwargs['user_id']])
usr = get_object_or_404(AEUser, pk=kwargs['user_id'])
form = EditUserForm(initial={'is_active':usr.is_active, 'is_superuser':usr.is_superuser}, \
user=usr, request=request)
return render(request, self.template_name, \
{'title_text':self.title, 'post_url':post_url, 'form':form})
The Test Case:
class TestEditUser(TestCase):
"""test the AddUser view"""
#classmethod
def setUpTestData(cls):
cls.user = AEUser.objects.create_user(username='shawn', email='shawn#gmail.com', password='test')
cls.user.is_superuser = True
cls.user.save()
def setUp(self):
self.client = Client()
def test_get(self):
"""tests the GET"""
self.client.login(username=self.user.username, password=self.user.password)
get_url = reverse('edit_user', args=[self.user.id])
response = self.client.get(get_url, follow=True)
self.assertEqual(self.user.is_superuser, True)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'editUser.html')
I have 3 asserts in the test case. If I comment out the last two, and only assert that the user is a superuser, the test passes. For whatever reason, though, on the other two asserts, I get failures. The error I receive is:
AssertionError: False is not true : Template 'editUser.html' was not a template used to render the response. Actual template(s) used: 404.html, base.html, which leads me to believe the get_object_or_404 call is what's triggering the failure. Where am I going wrong with this test case? Thanks!
Test should be:
class TestEditUser(TestCase):
"""test the AddUser view"""
#classmethod
def setUpTestData(cls):
cls.user = AEUser.objects.create_user(username='shawn', email='shawn#gmail.com', password='test')
cls.user.is_superuser = True
cls.user.save()
def setUp(self):
self.client = Client()
def test_get(self):
"""tests the GET"""
self.client.login(username=self.user.username, password='test')
get_url = reverse('edit_user', args=[self.user.id])
response = self.client.get(get_url, follow=True)
self.assertEqual(self.user.is_superuser, True)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'editUser.html')

Context error while testing RESTful API URL in Django

Dear community and forum,
I am in charge of developing RESTful API URLs for a website project. However, when it comes to testing I have got that error:
self = HyperlinkedIdentityField('api:request')
value = <ResourceRequest: JoKLLwwKqxrkrwWmcjOWzIscGzpsbgWJqRAOZabnwxQpiEDRfifeZhvzpRRp...ewyOFcaQVhchYNVIhUoiWBzKMrFYvYQBMNRZsLFfOZSjclHUXwyXZQHxjMtbHvWefMIlyZqvTvXqiu>
def to_representation(self, value):
assert 'request' in self.context, (
"`%s` requires the request in the serializer"
" context. Add `context={'request': request}` when instantiating "
"the serializer." % self.__class__.__name__
)
E AssertionError: `HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.
/usr/lib/python2.7/site-packages/rest_framework/relations.py:351: AssertionError
The serialiser is (sorry for the ugly code so far):
class ResourceRequestSerializer(serializers.ModelSerializer):
# context = self.kwargs.get('context', None)
# request = kwargs['context']['request']
# print(kwargs)
view_name = 'api:request'
url = serializers.HyperlinkedIdentityField(view_name)
# print(self)
# url = URLField(view_name=view_name, read_only=True, many=True)
# url = serializers.SerializerMethodField()
support_level = serializers.SerializerMethodField()
originator = BriefUCLProfileSerializer(source='originator.ucl_profile')
sponsor = BriefUCLProfileSerializer()
versioned_dependencies = serializers.StringRelatedField(many=True)
types_of_work = serializers.StringRelatedField(many=True)
previous = serializers.SerializerMethodField()
status_history = RequestStatusChangeSerializer(many=True)
For the test:
# Change the status through a POST request
response = self.app.post(
reverse(
'api:request',
args=[request1.pk],
),
params={
'context': request1,
'format': 'json',
'status': ResourceRequest.STATUS_APPROVED,
},
# context=request1,
headers=self.auth_headers,
)
I am still wondering if the context has to be passed from within the serialiser or from the test.
Here is the view too:
class ResourceRequestAPIView(RetrieveAPIView):
"""Retrieve an individual resource request by pk and optionally update status"""
serializer_class = ResourceRequestSerializer
permission_classes = (IsAuthenticated,)
authentication_classes = (TokenAuthentication, SessionAuthentication)
def get_object(self):
try:
return ResourceRequest.objects.get(pk=self.kwargs.get('pk'))
except ResourceRequest.DoesNotExist:
raise Http404
def post(self, request, *args, **kwargs):
resource_request = self.get_object()
status = request.data['status']
if status != ResourceRequest.STATUS_SUBMITTED:
# Change the status
resource_request.set_status(status,
request.user)
# And send emails
request_url = request.build_absolute_uri(
reverse('api:request', args=[resource_request.pk])
)
send_emails(resource_request,
request_url=request_url,
)
serializer = ResourceRequestSerializer(resource_request)
return Response(serializer.data)
Any help greatly appreciated !
Thank you
Roland
Right, so with that viewset in hand, you'll have to initialize the serializer with the context:
serializer = ResourceRequestSerializer(
resource_request,
context=self.get_serializer_context(),
)
get_serializer_context() is provided by default by DRF viewsets.

Post request handling in Django Rest framework

I am using Django Rest Framework, currently to pull some data from the backend we are using Get request, but due to URL limit going high we are planning to implement a Post request. To do this firstly the backend Django Rest API has to be made available to serve a post request.
I am new to Django and I don't find a post or get method in the code, all i can say is we are using viewsets, I tried using "#detail_route(methods=['post'])" but this didn't work, what am I doing wrong here?
class XViewSet(viewsets.ViewSet):
renderer_classes = ''
def retrieve(self, request, pk=None):
try:
pk = int(pk)
except ValueError:
raise InvalidParameterError(parameter_name='id', invalid_value=pk)
queryset = models.X.objects.all()
x = get_object_or_404(queryset, pk=pk)
pipelines = request.query_params.getlist('pipeline[]')
callsets =
callset_ids =
serializer = serializers.XSerializer(x, context={'request': request})
requested_samples = [z[1:] for z in request.query_params.getlist('s')]
filtered_calls = []
serialized_data = serializer.data
unfiltered_calls = serialized_data.get('calls')
if unfiltered_calls:
for serialized_calls in unfiltered_calls:
if serialized_calls['callset'] in callset_ids:
unfiltered_calls = serialized_calls['data']
for call in unfiltered_calls:
if call['y'] in requested_y:
filtered_calls.append(call)
break
serialized_data['calls'] = filtered_calls
return Response(serialized_data, status=status.HTTP_200_OK)
def list(self, request):
qp = self.request.query_params
validated_qp =
# generate the query
query_object =
query =
# execute the query
cursor = connections['default'].cursor()
cursor.execute(query)
qs = utils.dictfetchall(cursor)
# sanitize query results
if 't' in validated_qp:
return_data =
else:
for x in qs:
if 'calls' in x:
x['calls'] =
else:
x['calls'] = {}
return_data =
resp = Response(return_data, status=status.HTTP_200_OK)
if validated_qp.get(''):
resp['content-disposition'] =
return resp
You can use class-based views to handle the requirements,
from rest_framework.views import APIView
class MyAPI(APIView):
def get(selfself, request):
# do stuff with get
return Response(data="return msg or data")
def post(self, request):
post_data = request.data
# do something with `post_data`
return Response(data="return msg or data")
UPDATE : Using ViewSet
ViewSet class provide us create() methode to create new model instances. So we can override that to handle post data coming to the view. Just add a create() under your view class as below
class XViewSet(viewsets.ViewSet):
renderer_classes = ''
def create(self, request): # Here is the new update comes <<<<
post_data = request.data
# do something with post data
return Response(data="return data")
def retrieve(self, request, pk=None):
# your code
return Response(serialized_data, status=status.HTTP_200_OK)
def list(self, request):
# your code
return resp

AttributeError: '' object has no attribute 'page' DRF Pagination

This is my question asked 2 days back. I used Louis Barranqueiro's answer to solve my problem.
Now I want to add current page number as well as page_size in the serialized data. I know I have to customize the get_paginated_response method in PageNumberPagination class, but when I do that I get this error:
My code
def get_paginated_response(self, data, request):
# import pdb
# pdb.set_trace()
return Response(OrderedDict([
('next', self.get_next_link()),
('current', self.get_current_link()),
('previous', self.get_previous_link()),
('results', data)
]))
def get_queryset(self, request):
product_sync_ts = self.request.GET.get('product_sync_ts', None)
if product_sync_ts:
product = Product.objects.filter(....)
)
# return self.get_paginated_response(product, self.request)
return Response(product)
else:
content = {'details': "Bad Request"}
raise APIException400(request, content)
def get(self, request, format=None):
products = self.get_queryset(request)
serializer = SyncedProductSerializer(instance={'products': products})
# product = self.paginate_queryset(serializer, request)
return self.get_paginated_response(serializer, request)
# return self.get_paginated_response(serializer.data, request)
Error:
File "/Users/Documents/content-api/venv/lib/python2.7/site-packages/rest_framework/pagination.py", line 242, in get_next_link
if not self.page.has_next()
AttributeError: 'PaginatedProductList' object has no attribute 'page'
Some one might wanna try:
REST_FRAMEWORK = {
'PAGE_SIZE': 20,
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',}
on settings.py
with a view like
class GeneralManagementAPIView(generics.ListAPIView):
queryset = GeneralManagements.objects.all()
permission_classes = (IsAuthenticatedOrReadOnly,)
renderer_classes = (GeneralManagementJSONRenderer,)
serializer_class = GeneralManagementSerializer
def get_queryset(self):
return GeneralManagements.objects.all()
def list(self, request):
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
print("request ", request)
serializer_context = {'request': request}
serializer = self.serializer_class(
page, context=serializer_context, many=True
)
print("serializer ", serializer, "serializer.data", serializer.data )
return self.get_paginated_response(serializer.data)
You should call paginate_queryset before calling get_paginated_response
Note that the paginate_queryset method may set state on the pagination instance, that may later be used by the get_paginated_response method.
https://www.django-rest-framework.org/api-guide/pagination/
I am just adding my answer to help other users with the same problem they are facing.
Adding to the above statement, it also has a solution to your question in a simpler way.
Just pass PageNumberPagination and add request in self.paginate_queryset method.
class PaginatedProductList(APIView, PageNumberPagination):
def get(self, request):
products = Product.objects.filter(....)
return self.paginate_queryset(products, request)
So finally I found out how to solve this problem..
Below is the code(Simple and silly mistakes that I was doing)
class PaginatedProductList(APIView, PageNumberPagination):
page_size = 1000 #---crucial line
max_page_size = 1000
def get_paginated_response(self, data, page, page_num):
return Response(OrderedDict([
('count', self.page.paginator.count),
('current', page),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('page_size', page_num),
('results', data)
]))
def get_queryset(self, request):
product_sync_ts = self.request.GET.get('product_sync_ts', None)
if product_sync_ts:
product = Product.objects.filter(...)
)
return self.paginate_queryset(product, self.request)
raise APIException400(request, {'details': "Bad Request"})
def get(self, request):
page = self.request.GET.get('page', 1)
page_size = self.request.GET.get('page_size', 1000)
products = self.get_queryset(request)
serializer = SyncedProductSerializer(instance={'products': products})
return self.get_paginated_response(serializer.data, page, page_size)
Other than inheriting PageNumberPagination class in view, try defining your pagination class outside your view(as a separate class) inheriting from PageNumberPagination. and mention that class name as pagination class = YourClassName. inside your view
class PaginationClass(PageNumberPagination):
page_size = 2
and in view,
class GeneralManagementAPIView(generics.ListAPIView):
pagination_class = PaginationClass

Tastypie to respond for every requests in Json format

I am working on Api for my project, i'm using Tastypie 9.9.0. I want the response in Json format for PUT, POST and DELETE operations.
The existing responses like STATUS 201 CREATED, STATUS 204 NO CONTENT, STATUS 410 GONE is fine.
It must respond in a custom format.
for example
1. {
"resource_name": "user",
"action":"password_reset",
"status": "success"
}
2. {
"resource_name": "todo",
"action":"insert",
"status":"sucess",
}
3. {
"resource_name": "todo",
"action":"delete",
"status":"sucess",
}
this is the code i was working on. I dont know how to add custom response messages
class ToDoResource(ModelResource):
user = fields.ToOneField(UserResource, 'user')
class Meta:
queryset = ToDo.objects.all()
fields=['alert_time','description','status','user']
resource_name = 'todo'
filtering = {
'user': ALL_WITH_RELATIONS,
'alert_time': ['exact', 'range', 'gt', 'gte', 'lt', 'lte'],
}
serializer = Serializer()
authentication= MyApiKeyAuthentication()
authorization=Authorization()
always_return_data = True
allowed_methods = ['post','get','put','delete']
def obj_create(self, bundle,request=None, **kwargs):
if not request.user.is_superuser:
try:
bundle.data.pop('user')
except:
pass
return super(ToDoResource, self).obj_create(bundle, request, user=request.user)
def create_response(self, request, data):
"""
Extracts the common "which-format/serialize/return-response" cycle.
Mostly a useful shortcut/hook.
"""
desired_format = self.determine_format(request)
serialized = self.serialize(request, data, desired_format)
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
def apply_authorization_limits(self, request, object_list):
return object_list.filter(user=request.user)
You can add/modify custom data in get_list(request, **kwargs) and/or get_object(request, **kwargs)
For example,
import json
from django.http import HttpResponse
class ToDoResource(ModelResource):
# ... the rest of code
def get_list(self, request, **kwargs):
resp = super(ToDoResource, self).get_list(request, **kwargs)
data = json.loads(resp.content)
# ... the rest of code
data['meta']['resource_name'] = self._meta.resource_name
data['meta']['action'] = request.method
data['meta']['status'] = ANY_STATUS
# ... the rest of code
data = json.dumps(data)
return HttpResponse(data, mimetype='application/json', status=ANY_STATUS)