DRF: Pagination without queryset - django-rest-framework

I am trying to make use of Django Rest Framework's pagination mechanisms in my case without success.
class TransactionView(viewsets.ViewSet):
serializer_class = TransactionSerializer
def list(self, request):
# fetching data from external API...
serializer = self.serializer_class(data=list_of_json, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class TransactionSerializer(serializers.Serializer):
# Serializer (transaction's) fields ...
def create(self, validated_data):
return APITransaction(**validated_data)
class APITransaction(object):
def __init__(self, arg1, arg2, ...):
self.arg1 = arg1
...
The problem is that registering the pagination_class (like I have done for the rest of my resources which are represented by Models), doesn't work since the data are created/fetched on the fly, thus I don't have a Model/queryset.
Any ideas on how I could use DRF's pagination mechanism?

Here's the class I wound up creating and using locally for this sort of thing. (Thanks to stelios above for the initial clues.) Of course the contents of "data" must be JSONable.
from typing import List, Any
from collections import OrderedDict
from django.core.paginator import Paginator
from django.http.response import JsonResponse
from rest_framework.request import Request
class ListPaginator:
def __init__(self, request: Request):
# Hidden HtmlRequest properties/methods found in Request.
self._url_scheme = request.scheme
self._host = request.get_host()
self._path_info = request.path_info
def paginate_list(self, data: List[Any], page_size: int, page_number: int) -> JsonResponse:
paginator = Paginator(data, page_size)
page = paginator.page(page_number)
previous_url = None
next_url = None
if self._host and self._path_info:
if page.has_previous():
previous_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.previous_page_number())
if page.has_next():
next_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.next_page_number())
response_dict = OrderedDict([
('count', len(data)),
('next', next_url),
('previous', previous_url),
('results', page.object_list)
])
return JsonResponse(response_dict, status=200, safe=False)

You can't reuse existing DRF's pagination because they are supposed to work with queryset.
However, you may roll your own class by inheriting BasePagination though I haven't done myself.

Related

How to send PUT request to ModelViewSet without passing a primary key in the url?

I am particularly interested in using ModelViewSet
for solving the challenge of updating the logged in user's profile. I am using the following definition:
from rest_framework import viewsets
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
return UserProfile.objects.filter(user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
By overriding get_queryset, I am able to get the expected behavior, i.e. when I access the endpoints (say profile/), I get the profile of the logged in user. However, if I have to update the profile, I can only access PUT by going to profile/6/. Is there a way I can get ModelViewSet to expose PUT at the same endpoint i.e. profile/?
You can register ModelViewSet HTTP method under any path you want.
path(
"profile",
ProfileRetrieveUpdate.as_view(
{"put": "partial_update", "get": "retrieve"}
),
name="profile-retrieve-update",
)
You will have to adjust other things as well as you don't provide pk.
Instead of overriding get_queryset method you need to adjust get_object for your needs.
from rest_framework import viewsets
from rest_framework.generics import get_object_or_404
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = UserProfile.objects.all()
def get_object(self):
obj = get_object_or_404(self.queryset, user=self.request.user)
self.check_object_permissions(self.request, obj)
return obj
def perform_update(self, serializer):
serializer.save(user=self.request.user)
Now if you PUT profile/ it should work as expected.

DRF name undefined in custom serializer

When I try to hit my api/atoms/ endpoint in the browser, I am getting a name undefined error in the views.py file, but it has a base name in urls.
Note: this is a non-model serializer and a ViewSet.
error
...views.py", line 74, in list
instance = atom.values(), many=True)
NameError: name 'atoms' is not defined
views.py
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(
instance = atoms.values(), many=True) #<-------------
return Response(serializer.data)
urls.py
# for viewsets in views.py
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'atoms', views.AtomViewSet, base_name='atoms')
urlpatterns = [
path('', views.api_root),
path('', include(router.urls)),
]
serializer.py
class AtomSerializer(serializers.Serializer):
uid = UniqueIdProperty()
created_at = DateTimeProperty()
updated_at = DateTimeProperty()
charge = IntegerProperty()
mass = FloatProperty()
def create(self, validated_data):
return Atom(id=None, **validated_data)
def update(self, instance, validated_data):
for field, value in validated_data.items():
setattr(instance, field, value)
return instance
This is a basic python NameError exception raised when a local or global name is not found.
The variable atoms is not defined in the list() method or globally, that's why the python interpreter raised the exception.
In your code, you'd write atoms.values(), which forces me to think that you are dealing with a QuerySet, which might be an Atom model.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(instance=Atom.objects.all(), many=True)
return Response(serializer.data)
Note: this is a non-model serializer and a ViewSet.
You are doing create and update operations in your AtomSerializer class, and those are directly connected to the model. I don't see any particular reason that pulls you back from using a ModelSerializer here. Apart from that, you are using the routers, which become a good choice when you deal with the CRUD operations, hence I strongly suggest you use the combination of ModelViewset and ModelSerializer in your code.
In your views.py you did not define atom, you need to define it first before using it or else you will get that error.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
# You need to define the atom first before passing it to your AtomSerializer
atoms = [] # or atom = Atom.objects.all()
serializer = AtomSerializer(
data=atoms,
many=True
)
return Response(serializer.data)

Django Filter Backend not working in restful api in django

I am trying to use django-filters in have search queries in url using params
view.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .models import RecruiterRegister
from .serializers import RecruiterRegisterSerializer
# from rest_framework import filters
from rest_framework import generics
class RecruiterRegisterList(APIView):
queryset=RecruiterRegister.objects.all()
filter_backends = (DjangoFilterBackend,SearchFilter ,)
filter_fields = ('email','password')
search_fields = ('email','password')
def get(self, request, format=None):
recruiterRegisters = RecruiterRegister.objects.all()
# self.filter_backends = (DjangoFilterBackend,)
# self.filter_fields = ('password', )
serializer = RecruiterRegisterSerializer(recruiterRegisters, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = RecruiterRegisterSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
recruiterRegisters = self.get_object()
recruiterRegisters.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
but I am not able to get filtered data. All the api data appears.
url sample
http://127.0.0.1:8000/recruiterRegister/?password=random2
edited RecruiterRegisterList class:
class RecruiterRegisterList(generics.ListAPIView):
queryset=RecruiterRegister.objects.all()
serializer_class = RecruiterRegisterSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('email','password')
# search_fields = ('email','password')
def list(self, request ):
queryset = self.get_queryset()
serializer = RecruiterRegisterSerializer(queryset, many=True)
return Response(serializer.data)
First question is - why would you ever want to filter or search by password? that is a very unhealthy and unsecure design. Passwords are not stored in their raw formats but hashed so the filters will never hit anyway
Now, even if you really wanted to do that, filtering using filter_backends is implemented in the GenericAPIView and not available in APIView so you may want to use the ListAPIview from rest_framework.generics instead.
Lastly, even if you used the ListAPIView, it still wouldn't work because you're overriding the get method and not calling self.filter_queryset().
Use the ListAPIView and use the already implemented list() method and it will work
I'm adding only the line bellow & working fine:
filter_backends = self.filter_queryset(queryset)
And your code like:
class RecruiterRegisterList(generics.ListAPIView):
queryset=RecruiterRegister.objects.all()
serializer_class = RecruiterRegisterSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('email','password')
search_fields = ('email','password')
def list(self, request ):
queryset = self.get_queryset()
filter_backends = self.filter_queryset(queryset)
serializer = RecruiterRegisterSerializer(filter_backends, many=True)
return Response(serializer.data)

How get request user data in django rest to representation function

I need to access request.user in to_representation function, I tried self.context['request'] but self.context is empty dictionary. Is there anyway to access request.user or any way that I can push this data to this function?
def to_representation(self, obj):
print(self.context)
#output is an empty dictionary {}
UPDATE
class RetrieveView(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
uid = kwargs.get('uid')
try:
item = self.model.nodes.get(uid=uid)
except Exception as e:
# error response
serializer = self.serializer_class(item)
return HttpSuccessResponse(SuccessResponse(serializer.data).to_json(), status=status.HTTP_200_OK).send()
class TopicRetrieveView(single_result.RetrieveView):
model = Topic
serializer_class = topic.TopicSerializer
ALL CODES are from django rest framwork Generic views generic.py
serializer_class is attribute we set in class definition or we need to override get_serializer_class function. It will handle in this function:
def get_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.serializer_class`.
You may want to override this if you need to provide different
serializations depending on the incoming request.
(Eg. admins get full serialization, others get basic serialization)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
get_serializer_class will used in get_serializer function:
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
and context will fill by get_serializer_context function.
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
So correct usage is serializer = self.get_serializer(item) because it will use serializer_class for serializing item and fill context with extra information that may be helpful. serializer = self.serializer_class(item) can be used for just serializing item with no more extra information.

Django REST Framework - Extending ListAPIView with custom PUT method

I developed APIs using Django REST Framework for an inventory management application.
The endpoint to GET the list of products includes query parameters to filter the list. See below.
Product List View:
class ProductListAPIView(ListAPIView):
serializer_class = ProductListSerializer
queryset = Product.objects.all()
permission_classes = [DjangoModelPermissionsWithView]
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = [
'sku',
'product_name',
...
]
filter_class = ProductFilter
pagination_class = ProductPageNumberPagination
ordering = ['-id']
ordering_fields = [
'id',
'sku',
'product_name',
...
]
def get_serializer_context(self, *args, **kwargs):
return {"request": self.request}
I have created another view to handle requests in order export the products to PDF, CSV, etc:
class ProductExportAPIView(APIView):
def put(self, request, *args, **kwargs):
# We use the seriaziler only to validate request.data
serializer = ProductExportSerializer(data=request.data)
if serializer.is_valid():
user_id = request.user.pk
file_key = request.data.get('file_key')
file_name = request.data.get('file_name', '')
extra_args = request.data.get('extra_args', {})
product_ids = request.data.get('product_ids')
# NOTE THAT export_file IS A CELERY TASK
export_file.delay(user_id, file_key, file_name, product_ids, extra_args)
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The API it's working fine, but it works only if the user selects the products - the product_ids field is used to provide the list of products to be exported.
I would like to let the users export ALL the products via ProductExportAPIView by providing the query params that I'm using with ProductListAPIView rather than providing product_ids.
product_ids should be an optional field to be used only to export a few products.
How I can enable query parameters filtering on my ProductExportAPIView, there is a way to do this without hardcoding it? Can I extend ProductListAPIView with the PUT method to export products?
In order to use the same query parameters defined in ProductListAPIView, now ProductExportAPIView extends ProductListAPIView, so it inherits everything I needed:
class ProductExportAPIView(ProductListAPIView):
permission_classes = [AllowAny]
http_method_names = ['put'] # disable GET method inherited from ProductListAPIView
def put(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# We use the serializer only to validate request.data
serializer = ProductExportSerializer(data=request.data)
if serializer.is_valid():
user_id = request.user.pk
file_key = request.data.get('file_key')
file_name = request.data.get('file_name', '')
extra_args = request.data.get('extra_args', {})
product_ids = request.data.get('product_ids', [])
if len(product_ids)==0:
product_ids = [p.pk for p in queryset]
export_file.delay(user_id, file_key, file_name, product_ids, extra_args)
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Resources