I have added a method to my viewset as follows:
class CustomImageViewSet(viewsets.ModelViewSet):
queryset = CustomImage.objects.all()
serializer_class = CustomImageSerializer
lookup_field = 'id'
#action(detail=True, methods=['get'], url_path='sepia/')
def sepia(self, request, id):
# do something
data = image_to_string(image)
return HttpResponse(data, content_type="image/png", status=status.HTTP_200_OK)
Since it is not a default or overridden request method, I am not sure how can I proceed writing a test for it. Any suggestions?
You're not clear on what the test should test but you can test the response status_code for example like this:
def test_sepia_api():
api_client = APIClient()
response = api_client.get(path="{path_to_your_api}/sepia/")
assert response.status_code == 200
I noticed you were using pytest. I'll assume you've got pytest-django too then (it really does make everything easier). I like using request factory since it's generally faster if you've got authentication needs.
def test_me(self, user, rf):
view = CustomImageViewSet()
request = rf.get("")
request.user = user # If you need authentication
view.request = request
response = view.sepia(request, 123)
assert response.data == BLAH
Related
I'm currently struggling to make this current unit-test pass:
def test_markNotifications(self):
request_url = f'Notifications/mark_notifications/'
view = NotificationsViewSet.as_view(actions={'post': 'mark_notifications'})
request = self.factory.post(request_url)
request.POST = {'id_notifs': "1"}
force_authenticate(request, user=self.user)
response = view(request)
self.assertEqual(response.status_code, 200)
Here's the associated view:
#action(detail=False, methods=['POST'])
def mark_notifications(self, request, pk=None):
"""
Put Notifications as already read.
"""
id_notifs = request.POST.get("id_notifs")
if not id_notifs:
return Response("Missing parameters.", status=400)
id_notifs = str(id_notifs).split(",")
print(id_notifs)
for id in id_notifs:
notif = Notification.objects.filter(pk=id).first()
if not notif:
return Response("No existant notification with the given id.", status=400)
notif.isRead = True
notif.save()
return Response("Notifications have been marked as read.", status=200)
The problem is that even though I'm passing "id_notifs" through the request in test, I'm getting None when I do id_notifs = request.POST.get("id_notifs").
It seems that the id_notifs I'm passing in the POST request are neither in the body and the form-data. In this context, I have no idea on how to access them.
Looking forward some help, thanks.
I have defined the following custome action for my ViewSet Agenda:
class AgendaViewSet(viewsets.ModelViewSet):
"""
A simple viewset to retrieve all the Agendas
"""
queryset = Agenda.objects.all()
serializer_class = AgendaSerializer
#action(detail=False, methods=['GET'])
def get_user_agenda(self, request, pk=None):
print('here1')
id = request.GET.get("id_user")
if not id:
return Response("No id in the request.", status=400)
id = int(id)
user = User.objects.filter(pk=id)
if not user:
return Response("No existant user with the given id.", status=400)
response = self.queryset.filter(UserRef__in=user)
if not response:
return Response("No existant Agenda.", status=400)
serializer = AgendaSerializer(response, many=True)
return Response(serializer.data)
Here, I'd like to unit-test my custom action named "get_user_agenda".
However, when I'm testing, the debug output("here1") doesn't show up, and it always returns 200 as a status_code.
Here's my test:
def test_GetUserAgenda(self):
request_url = f'Agenda/get_user_agenda/'
view = AgendaViewSet.as_view(actions={'get': 'retrieve'})
request = self.factory.get(request_url, {'id_user': 15})
response = view(request)
self.assertEqual(response.status_code, 400)
Note that:
self.factory = APIRequestFactory()
Am I missing something?
Sincerely,
You will have to use the method name of the custom action and not retrieve so:
view = AgendaViewSet.as_view(actions={'get': 'get_user_agenda'})
You have to specify request url
#action(detail=False, methods=['GET'], url_path='get_user_agenda')
def get_user_agenda(self, request, pk=None):
And in my opinion it would be better to use detail=True, and get pk from url.
For example: 'Agenda/pk_here/get_user_agenda/'
Django 2.2
I am writing tests for API using APIRequestFactory. The code that hits
/some_endpoint and /some_endpoint/<item_id> already works, and so does the test that tests /some_endpoint. However the test to test /some_endpoint/<item_id> does not work because I can not find a working way to pass that <item_id> value to the view code. Please not it's not /some_endpoint/<some_keyword>=<item_id> , it's "flat" in my case i.e. there's no keyword. The problem is <item_id> does not make it into the view code (it's always None in the classview in get_queryset method)
I tried to pass it as **kwargs, it does not arrive either ( see here). But that probably would not work anyway without keyword.
I tried to switch to use of Client instead of APIRequestFactory, same result. But I would rather get it working with APIRequestFactory unless it does not work this way in general. Below is the code.
test.py
def test_getByLongId(self) :
factory = APIRequestFactory()
item = Item.active.get(id=1)
print(item.longid)
#it prints correct longid here
request = factory.get("/item/%s" % item.longid)
view = ItemList.as_view()
force_authenticate(request, user=self.user)
response = view(request)
urls.py
urlpatterns = [
...
...
url(item/(?P<item_id>[a-zA-Z0-9-]+)/$', views.ItemList.as_view(), name='item-detail'),
...
...
]
views.py
class ItemList(generics.ListAPIView):
permission_classes = (IsBotOrReadOnly,)
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = ItemSerializer
schema = AutoSchema(
manual_fields=[
coreapi.Field("longid"),
]
)
def get_queryset(self):
"""
Optionally restricts the returned SampleSequencing to a given barcode.
"""
longid = self.kwargs.get('item_id', None)
print(longid)
#prints correct longid when executed by the webserver code and prints None when executed by the test
queryset = Item.active.filter(longid=longid)
return queryset
You have to pass item_id into the view():
def test_by_long_id(self) :
factory = APIRequestFactory()
item = Item.active.get(id=1)
print(item.longid)
#it prints correct longid here
request = factory.get("/item/%s" % item.longid)
view = ItemList.as_view()
force_authenticate(request, user=self.user)
response = view(request, item_id=item.longid)
or use APIClient:
from rest_framework.test import APIClient
# ...
#
def test_item_client(self):
item = Item.active.get(id=1)
client = APIClient()
url = '/item/%s/' % item.id
response = client.get(url)
I am trying to get my endpoint to return a uri-list when asked for that and a json string as default. I am testing this in a unit test looking a bit like:
[...]
headers = {'Accept': 'text/uri-list'}
response = self.client.get('/api/v1/licenses/', headers=headers)
[...]
I have written a URIListRenderer like this:
from rest_framework import renderers
class URIListRenderer(renderers.BaseRenderer):
media_type = 'text/uri-list'
def render(self, data, media_type='None', renderer_context=None):
return "\n".join(data).encode()
Next I am trying to get my Response in my View to be rendered using my renderer:
class RestLicenses(APIView):
"""
List all licenses, or create a new license
"""
permission_classes = (IsAuthenticated,)
parser_classes = (MultiPartParser,)
renderer_classes = (JSONRenderer, URIListRenderer,)
def get(self, request, format=None,):
models = LicenseModel.objects.all()
if len(models) == 0 :
return Response('[]',status=204)
if request.META.get('headers') is not None :
if request.META.get('headers').get('Accept') == 'text/uri-list' :
result = [];
for m in models :
result.append(reverse('downloadLicense', args=[m.pk], request=request))
return Response(result, status=200)
serializer = LicenseJSONSerializer(request, models, many=True)
serializer.is_valid()
return HttpResponse(JSONRenderer().render(serializer.data), content_type='application/json', status=200)
But it seems impossible to get it to choose any other renderer than the first one in the list. How do I make it choose my URIListRenderer and not the json one?
Your unit test is not setting the headers correctly. As described here, you should use CGI style headers when using the Django test client:
response = self.client.get('/api/v1/licenses/', HTTP_ACCEPT='text/uri-list')
The content negotiation uses the real HTTP Accept header. In your code, you check that "headers" is set, but that's not the real HTTP Accept header. It should be:
if request.META.get('HTTP_ACCEPT') == "text/uri-list":
...
This is a block of code from the method finalize_response:
if isinstance(response, Response):
if not getattr(request, 'accepted_renderer', None):
neg = self.perform_content_negotiation(request, force=True)
request.accepted_renderer, request.accepted_media_type = neg
response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type
response.renderer_context = self.get_renderer_context()
As you can see, before it performs content negotiaton, it checks to see if the view has already set the renderer needed and if not tries to perform the negotiation by itself.
So you can do this in your get method:
if request.META.get('headers') is not None :
if request.META.get('headers').get('Accept') == 'text/uri-list' :
request.accepted_renderer = URIListRenderer
result = [];
for m in models :
result.append(reverse('downloadLicense', args=[m.pk], request=request))
return Response(result, status=200)
Another thing you should probably do is to place your custom renderer class before JSONRenderer in the renderer_classes list. In that way, it will first check the special case before the more general case during content negotiation. Is suspect that the request format also matches JSOnRender and it overshadows the custom renderer. Hope this helps
I have a ModelViewSet with an extra action that needs to behave differently than the rest of the viewset with respect to authentication and authorization.
E.g.
class MyModelViewSet(viewsets.ModelViewSet):
authentication_classes = [SomeClass]
permission_classes = [AnotherClass]
queryset = Model.objects.some_important_ones()
serializer_class = MyModelSerializer
#list_route(methods=['post', 'options'], authentication_classes=[])
def action(self, request, *args, **kwargs):
response_data = so_something_specific(request)
return Response(response_data, status=status.HTTP_201_CREATED)
The testing set up is
#pytest.fixture
def request():
return APIRequestFactory()
def test_the_data_was_created(request):
req = request.post('/api/my-model/action/', payload={}, format='json')
view = MyModelViewSet.as_view({'post': 'action'})
res = view(req)
assert res.status_code == 201
So the issue is that the response always complains about authentication not being provided but the action doesn't actually need it!
The repr looks like this:
<Response status_code=401, "text/html; charset=utf-8">
Content
'{"detail":"Authentication credentials were not provided."}'
Question is, how might I go about getting the callable to operate as it should?
This ended up being really simple. Looking at the router source, I just needed to pass in the initialization keyword arguments to match what the #list_route decorator was doing like so:
view = MyModelViewSet.as_view({'post': 'action'}, **{'authentication_classes': []})