How to add custom pagination with parameters in DRF? - django-rest-framework

I have different ViewSets for CRUD operations with entities in my project. I want to return paginated response and I created custom pagination where I overrided get_paginated_response method. For now it looks like this:
class CustomPagination(PageNumberPagination):
page = DEFAULT_PAGE
page_size = DEFAULT_PAGE_SIZE
page_size_query_param = 'page_size'
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'total': self.page.paginator.count,
'page': int(self.request.GET.get('page', DEFAULT_PAGE)), # can not set default = self.page
'page_size': int(self.request.GET.get('page_size', self.page_size)),
'results': data
})
What I want to do is to add parameters to pagination so that fields in response would be different for different models, e.g. not "results" but "comments" for Comment model, "recipes" for Recipe model.
I've introduced a variable entity_name in my get_paginated_response method to do the following:
def get_paginated_response(self, data, entity_name: str):
return Response({
'links': {
f'next_{entity_name}_link': self.get_next_link(),
f'previous_{entity_name}_link': self.get_previous_link()
},
'total': self.page.paginator.count,
'page': int(self.request.GET.get('page', DEFAULT_PAGE)), # can not set default = self.page
'page_size': int(self.request.GET.get('page_size', self.page_size)),
entity_name: data
})
I've also redefined list action in my viewset in order to pass certain value of entity_name variable:
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(
data=serializer.data,
entity_name='comment'
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
But I am getting TypeError with this code, it is said that GenericAPIView.get_paginated_response() got an unexpected keyword argument 'entity_name'
What should I do to solve it? How to pass parameters correctly?

Related

How to get data from post request in generics.ListAPIView DRF?

There is some view:
class Search(CategoryDetail):
serializer_class = ProductSerializer
def get_queryset(self):
# print(self.request)
return self.get_filters(None)
It inherits from another ListAPIView and does something.
From front on Vue.js throught post i pass some query parameter:
async Search(){
axios
.post('api/products/search', {'query': this.query})
.then(response =>{
this.products = response.data
})
.catch(error=>{
console.log(error)
})
}
Initially, i performed work with this post request through the search view function:
#api_view(['POST'])
def search(request):
query = request.data.get('query')
if query:
products = Product.objects.filter(Q(name__icontains=query) | Q(description__icontains = query))
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
else:
return Response({"products": []})
But now i need to change the view function to a class, but in the class i need to get the same query = request.data.get('query'), however, when i try, the Bad Request error is constantly returned.
ListAPIView doesn't accept post requests. You should pass your query as a GET request query parameter and reach it in the get_queryset method to filter your results.
class ProductList(generics.ListCreateAPIView):
serializer_class = ProductSerializer
def get_queryset(self):
query = self.request.query_params.get('query')
if query is not None:
return Product.objects.filter(Q(name__icontains=query) | Q(description__icontains = query))
else:
Product.objects.none()
If you must use POST data and return the list of objects, you can implement your own APIView and use its post method.
class PrdocutList(APIView):
def post(self, request, format=None):
query = request.data.get('query')
if query:
products = Product.objects.filter(Q(name__icontains=query) | Q(description__icontains = query))
else:
products = Product.objects.none()
serializer = ProductSerializer(products, many=True)
return Response(serializer.data)
You can also implement your own filter_backend.

Django REST framework - unique_together, serializer validators, and only create if unique_together has been satisfied or update if not?

I was wondering, I have the following within the Meta class of a model:
class Meta:
unique_together = ['owner', 'question', 'answer']
Within my API viewset, I perform the create as the following:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer_class()(data=request.data, context={'request': request,}, many=False)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Which is very formulaic Django REST Framework thus far.
The serializer is as follows:
class SurveyResultCreateOrUpdateSerializer(serializers.ModelSerializer):
owner = serializers.PrimaryKeyRelatedField(
read_only=True,
default=serializers.CurrentUserDefault()
)
class Meta:
model = SurveyResult
fields = [
'uuid', 'id', 'question', 'answer', 'owner',
]
read_only_fields = [
'uuid', 'id', 'owner',
]
validators = [
UniqueTogetherValidator(
queryset=SurveyResult.objects.all(),
fields=['owner', 'question', 'answer']
)
]
So, when trying to create if an object where the validation fails, the error is raised. Brillaint. This is expected behaviour.
However, when the error is raised, I would like to immediately perform an update attempt if the unique together validation has been thrown...?
What would be the "pattern" way of doing this? Should I knock the serializer.is_valid() method to an if else statement, and on the else...or can I catch the raised exception only if the exception is a unique together error? Should I perform a separate update call from the client dependent on the response? Is there a way of creating an "perform_create_or_update()" function within the viewset? Is there a create_or_update viewset method?
My starting point would be something like this:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer_class()(data=request.data, context={'request': request,}, many=False)
try:
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except exceptions.ValidationError as error:
if 'unique' in error.get_codes()['non_field_errors'] and len(error.get_codes()['non_field_errors']) == 1:
print("We should try and update")
return Response(

Retrieve Single Object in Modelviewset

I was using CBV, ModelViewSet but somehow my API Keep showing all the Objects inside, how i get my ModelViewSet retrieve a single object that base on my sku ?
api/views.py
class SamAPIViewSet(viewsets.ModelViewSet):
queryset = SamModels.objects.all()
serializer_class = SamSerializers
lookup_field = 'pk'
def get_queryset(self):
return self.queryset
def get_object(self):
sku_id = self.kwargs['pk']
return self.get_queryset().filter(id=sku_id)
def list(self, request):
products = SamModels.objects.all()
serializers = self.get_serializer(products, many=True)
return Response(serializers.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializers = self.get_serializer(instance)
return Response(serializers.data)
api/urls.py
sam_api_list = SamAPIViewSet.as_view({
'get': 'list'
})
sam_api_detail = SamAPIViewSet.as_view({
'get': 'retrieve'
})
urlpatterns = [
url(r'sam', sam_api_list, name='api-sam'),
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
]
sam/1, it return all of my objects, i am not sure what wrong, my object has name, id, sku and how do i change my view to get retrieve single object base on my sku and not my id ?
Your routes are upside down. Try this:
urlpatterns = [
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
url(r'sam', sam_api_list, name='api-sam'),
]
Django will select the first expression that matches. If r'sam' is above r'sam/int:pk' then it will select r'sam' without going down to the next entry
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
url(r'sam', sam_api_list, name='api-sam'),
to
path(r'sam/<str:pk>', sam_api_detail, name='api-sam-detail'),
path(r'sam', sam_api_list, name='api-sam'),
remove
def get_queryset(self):
return self.queryset
def get_object(self):
sku_id = self.kwargs['pk']
return self.get_queryset().filter(id=sku_id)
and change lookup_field = 'pk' to lookup_field = 'sku'

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)

Getting "AttributeError: 'QuerySet' object has no attribute '_meta'" on django rest "PUT" method

I am trying to update the record with PUT method, Getting AttributeError: 'QuerySet' object has no attribute '_meta'.
My models.py:
class TableInfo(models.Model):
table_name = models.CharField(max_length=10)
columns = JSONField(null=False)
serializer.py:
class TableInfoSerializer(serializers.ModelSerializer):
class Meta:
model = TableInfo
fields = '__all__'
views.py :
#api_view(['GET','PUT'])
def table_info(request):
try:
queryset = TableInfo.objects.all()
print("1")
except TableInfo.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
print("2")
serializer_class = TableInfoSerializer(queryset, many=True)
return Response(serializer_class.data)
elif request.method == 'PUT':
print(request.data)
serializer = TableInfoSerializer(queryset, data=request.data)
if serializer.is_valid():
serializer.save()
print("4")
return HttpResponse(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
code is breaking at if serializer.is_valid():
On "GET" I am getting the result. Please help me with "PUT" method.
This error happens with PUT because the serializer tries to access the Meta class on the model instance it is updating, but fails because you are not passing a model instance - you're passing a queryset as indicated in the comments.
So you need to pass an instance, and to specify which instance you would normally pass the instance id via the URL. For that you would be best to separate out your views, and create a table_detail view for retrieving and updating a specific instance.
#api_view(['GET','PUT'])
def table_detail(request, pk):
try:
table_info = TableInfo.objects.get(pk=pk) # Lookup a specific object
except TableInfo.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer_class = TableInfoSerializer(table_info)
return Response(serializer_class.data)
elif request.method == 'PUT':
serializer = TableInfoSerializer(table_info, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Your table_info view can then just handle the list operation.
#api_view(['GET'])
def table_info(request):
if request.method == 'GET':
queryset = TableInfo.objects.all()
serializer_class = TableInfoSerializer(queryset, many=True)
return Response(serializer_class.data)

Resources