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': []})
Related
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/'
code: urls.py
path('tools/<int:rate>/', ToolsView.as_view(), name="get-all-tools"),
path('tools/post/', ToolsView.as_view(), name="save-tool"),
code: views.py
class ToolsView(APIView):
def get(self, request, rate):
objs = ToolsModel.objects.values_list()[:rate]
if objs is None:
return Response(status=status.HTTP_404_NOT_FOUND)
data = []
for obj in objs:
print(obj)
json = {}
json['toolid'] = obj[0]
json['tool_name'] = obj[1]
json['tool_from'] = obj[2]
json['tool_qty'] = obj[3]
json['tool_state'] = obj[4]
data.append(json)
return Response(data=data, status=status.HTTP_200_OK)
def post(self, request):
data = request.data
serialize = ToolsSerializer(data=data)
if serialize.is_valid():
serialize.save()
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_404_NOT_FOUND)
Whenever i call for tools/post/ intended to call post method
http://127.0.0.1:8000/help/tools/post/
i get
get() missing 1 required positional argument: 'rate'
but rate parameter is actually needed for 'tools/<int:rate>/' which invokes my get method ,example,
http://127.0.0.1:8000/help/tools/5/
Needed help with these. Thanks in advance!
if you put http://127.0.0.1:8000/help/tools/post/ into your browser, you will be performing a GET on the URL. In your urls.py you map that route to ToolsView.as_view(). This allows you to handle the different HTTP Methods via functions. So def post will get called when a POST is requested. But you are likely doing a GET, which will call the def get(...) method. But because you aren't passing in a rate it's missing from the function arguments, hence the error. To test the post method you need to perform a POST to http://127.0.0.1:8000/help/tools/post/.
Test Code:
import requests
requests.post("http://127.0.0.1:8000/help/tools/post/", data={})
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
I have pagination enabled by default, which is based on PageNumberPagination; this had sufficed until now as the API was only used by a front-end. Now we try to build automation on top of it, and I’d like to pass the full, unpaginated result set back to the client.
Is there a way to disable pagination for specific requests, e.g. if a request parameter is passed?
I used a similer appraoch to accepted answer
class Unpaginatable(PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
if request.query_params.get('get_all', False) == 'true':
return None
return super(BuildListPagination, self).paginate_queryset(queryset, request, view=view)
now if you pass ?get_all=true while making the request, you will get unpaginated response.
If you are using page number pagination style, the below solution may be better.
def paginate_queryset(self, queryset, request, view=None):
if 'page' not in request.query_params:
return None
return super().paginate_queryset(queryset, request, view)
So you simply send a request without the page query_param.
I actually did go with a custom pagination class:
class Unpaginatable(PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
if getattr(request, 'get_all', False):
return None
return super(BuildListPagination, self).paginate_queryset(queryset, request, view=view)
Now I just have to set request.get_all = True in my viewset and I get all the items.
thnaks to this answer,
Else, if you use limit/offset you can use this in Generic List API View class:
def paginate_queryset(self, queryset):
if 'limit' not in self.request.query_params:
return None
return super().paginate_queryset(queryset)
It worked with python 3.9/ Django 4.0, In my case, this method had no argument named request and view, so I fixed it.
this will also won't render paginator json, when the response is not paginated.
You can approximate this with a request limit set to an unfeasibly large number, but you need to configure Django first:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}
Then make a ridiculously high limit request:
GET https://api.example.org/accounts/?limit=999999999999
LimitOffsetPagination
- django-rest-framework.org
Actually DRF docs for this is very unclear, and there is no clear answer address this issue. After reading the source code, the followling code works for me.
from rest_framework.pagination import LimitOffsetPagination
class Unpaginated(LimitOffsetPagination):
def paginate_queryset(self, queryset, request, view=None):
self.count = self.get_count(queryset)
self.limit = self.get_limit(request)
self.offset = self.get_offset(request)
self.request = request
self.display_page_controls = False
return list(queryset)
class SomeViewSet(viewsets.ModelViewSet):
queryset = SomeModel.objects.all().order_by("-id")
pagination_class = Unpaginated
The key here is to override the paginate_queryset function in base class and return the whole queryset.
However, overriding this function is not documented at all in the docs or I just missed it.
https://www.django-rest-framework.org/api-guide/pagination/
https://github.com/encode/django-rest-framework/blob/master/rest_framework/pagination.py
I solve this problem in the following way:
make your own pagination class and inherit it from PageNumberPagination
redefine or add your code (with super()) in get_page_size function.
For me works this case:
def get_page_size(self, request):
super().get_page_size(request)
.......
self.page_size = self.max_page_size
return self.page_size
I have a generic ListCreateAPIView view. I've implemented a get_queryset function that performs a search. The function parses the query, extract tags and terms and returns a query set.
def get_queryset(self):
query = self.request.QUERY_PARAMS.get('query', None)
# No deleted items
queryset = Items.objects.filter(deleted__isnull=True)
if query is None:
return queryset
predicates = []
# Generate predicates from query
queryset = queryset.filter(reduce(__and__,predicates))
return queryset
What is the best way to add metadata to the response with data from the get_queryset function ?
I'm looking for something similar to the way pagination works.
{
query : {
terms : ['term1','term2'],
tags : ['tag1','tag2'] ,
}
results : [
{ name : 'item1', .... }
{ name : 'item2', .... }
]
}
EDIT
So i created a custom FilterBackend for the filtering and I now have an instance of the request and the response. Looking at the pagination code for django rest i see it's wrapping the results in serializer. The pagination is build into the view class so the fw invokes the serialization if a paginator is detected. Looking at the search api did not produce any new ideas.
My question remains, What is the best, and least intrusive way, of adding metadata from the filter backend to the response ?
One way i can think of (and one that i don't like) is to overload the matadata onto the request in the filter backend and override finalize_response in the view - without a doubt the worst way to do it.
I'm not sure it's the best way, but I would probably override get to simply intercept the response object and modify response.data however you wish. Something as simple as
from rest_framework import generics
class SomeModelList(generics.ListCreateAPIView):
"""
API endpoint representing a list of some things.
"""
model = SomeModel
serializer_class = SomeModelSerializer
def get(self, request, *args, **kwargs):
response = super(SomeModelList, self).get(request, *args, **kwargs)
# redefine response.data to include original query params
response.data = {
'query': dict(request.QUERY_PARAMS),
'results': response.data
}
return response
If you found yourself repeating this for multiple list views you could keep yourself DRY using a Mixin and include it in your list API classes:
from rest_framework import generics
from rest_framework.mixins import ListModelMixin
class IncludeQueryListMixin(ListModelMixin):
def list(self, request, *args, **kwargs):
response = super(IncludeQueryListMixin, self).list(request, *args, **kwargs)
# redefine response.data to include original query params
response.data = {
'query': dict(request.QUERY_PARAMS),
'results': response.data
}
return response
class SomeModelList(IncludeQueryListMixin, generics.ListCreateAPIView):
"""
API endpoint representing a list of some things.
"""
model = SomeModel
serializer_class = SomeModelSerializer