How to do a search in the DRF only for the selected field? - django-rest-framework

I can’t understand how to do a search in the DRF only for the selected field.
The documentation on https://www.django-rest-framework.org/api-guide/filtering/ states that need to create a subclass to override the functions get_search_fields (). Everything is clear with this.
It is not clear how to make a request.
If just do a search:
http://... /api/v1/?search=keywords
In this case, it searches for all fields specified in:
search_fields = ['title', 'article']
What needs to be specified in the request in order to search in title field or article field?
http://... /api/v1/?search=keywords..?
Fragment of the views.py responsible for the search:
from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request):
if request.query_params.get('title_only'):
return ['title']
elif request.query_params.get('article_only'):
return ['article']
return super(CustomSearchFilter, self).get_search_fields(view, request)
class MyListView(generics.ListAPIView):
serializer_class = MySerializer
filter_backends = [CustomSearchFilter, filters.OrderingFilter]
ordering_fields = ['id', 'title', 'article']
search_fields = ['title', 'article']
def get_queryset(self):
queryset = Blog.objects.all()
return queryset

You can use DjangoFilterBackend
Then you'll only need to specify the fields, something like this
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['category', 'in_stock']
Then you can filter using query params like this
http://example.com/api/products?category=clothing&in_stock=True
If you need more complex filter then have a look at django-filter documentations : https://django-filter.readthedocs.io/en/master/ref/filters.html#lookup-expr
Reference : https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend

How:
http://127.0.0.1:8000/api/v1/?search=Sometitle&title_only=title_only
Why:
# filters.py
from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter):
""""Dynamically change search fields based on request content."""
def get_search_fields(self, view, request):
"""
Search only on the title or the article body if the query parameter,
title_only or article_only is in the request.
"""
# request.query_params is a more correctly named synonym for request.GET
# request.GET a dictionary-like object containing
# all given HTTP GET parameters
# Get the value of the "title_only" item
# How does the querydict looks like
# <QueryDict: {'search': ['Sometitle'], 'title_only': ['title_only']}>
# if exist return search_fields with ['title'] only
if request.query_params.get('title_only'):
return ['title']
elif request.query_params.get('article_only'):
return ['article']
return super(CustomSearchFilter, self).get_search_fields(view, request)
Links:
query_params
django.http.HttpRequest.GET

Related

DRF ViewSet - dealing with query params

I want to change a queryset in my ViewSet depending on query parameters.
I see that there is a list of tags in query params, but when I try extract them I get just last tag as a string. And I have no idea why and how it should work. Can someone explain it for me, please?
class RecipeViewSet(ModelViewSet):
pagination_class = PageNumberPagination
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
def get_serializer_class(self):
if self.action in ['list', 'retrieve']:
return RecipeListSerializer
return RecipeCreateSerializer
def get_queryset(self):
queryset = Recipe.objects.all()
params = self.request.query_params
tags = params.get("tags")
print("params:")
print(params) # <QueryDict: {'page': ['1'], 'limit': ['6'], 'tags': ['breakfast', 'lunch', 'dinner']}>
print("tags:")
print(type(tags)) # <class 'str'>
print(tags) # I get only str - "dinner"
if tags:
queryset = Recipe.objects.filter(tags__slug__in=tags).distinct()
return queryset
For getting lists you need to use getlist. In your case it would look like this:
params.getlist("tags[]")
This is because you're working with an instance of type QueryDict and not dict. You can find more info here.

Django: customizing the field types needed for create and retrieve serializers

I currently have the following serializer:
serializers.py
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = SurfInstructorSerializer(many=False)
surfers = SurferSerializer(many=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
def create(self, validated_data):
return SurfGroup(**validated_data)
And the following viewset create method (viewset inherited from viewsets.ViewSet as we need some bespoke customization, extra signals and actions etc):
viewsets.py
# Surf Group Create View:
def create(self, request, format=None):
serializer = SurfGroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response = responses.standardized_json_response(
message='Surf Group Objects Have Been Successfully Created',
data=serializer.data
)
return Response(data=response, status=status.HTTP_201_CREATED, headers=headers)
For the retrieve action, the serializer works well, and we have a nested instructor object in the response. However, I want to perform a create by passing in the instructor uuid attrbiute like (see content in the POST textarea):
Rather than a whole object...I was wondering how we achieve this? Is it best to just have two Serializers, one for performing the create, and one the retrieval?
def create(self, validated_data):
surf_group = SurfGroup(
instructor__uuid=validated_data['instructor'],
)
surf_group.save()
return surf_group
It is good question.
I work with this situations many times and it looks like one option is to have two serializers as you mean: 1 for list/retrieve and 1 for save.
Another option (for me) is to set serializer field input as UUID and output as another serializer data like this:
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = serializers.UUIDField()
surfers = SurferSerializer(many=True, read_only=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
# I use this validate method to transform uuid to object which will
# be bypassed to create method for easly save
def validate_instructor(self, instructor_uuid):
try:
return Instructor.objects.get(uuid=instructor_uuid)
except Instructor.DoesNotExist:
# Remember that you dont need to pass field_key: [errors] to ValidationError
# because validate_FIELD will automatically pass field_key for you !
raise ValidationError(['Instructor with the given uuid does not exist.'])
# Overwrite output data
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['instructor'] = SurfInstructorSerializer(instance=instance.instructor).data
return ret

Cannot generate post request for multiple data

I am trying to take input multiple data object in post request, but getting such error.
non_field_errors: [ Invalid data. Expected a dictionary, but got a list. ]
models.py
class OrderProduct(BaseModel):
product = models.ForeignKey(Product,on_delete=models.CASCADE)
order = models.ForeignKey(Order,on_delete=models.CASCADE)
order_product_price = models.FloatField(blank=False,null=False,default=0) # product may belong to offer do the price
order_product_qty = models.FloatField(default=1)
serializers.py
class OrderProductSerializer(serializers.ModelSerializer):
def update(self,instance,validated_data):
product = self.validated_data.pop('product')
order = self.validated_data.pop('order')
instance.orderproduct_qty =
self.validated_data.get('orderproduct_qty',instance.orderproduct_qty)
instance.product = product
instance.order = order
instance.save()
return instance
class Meta:
model = OrderProduct
fields = '__all__'
views.py
def post(self,request,*args,**kwargs):
if request.data['contact_number'] == '':
request.POST._mutable =True
request.data['contact_number'] = request.user.mobile_number
request.POST._mutable = False
serializer = OrderSerializer(data=request.data,many=isinstance(request.data,list),context={'request': request})
print(serializer)
if serializer.is_valid():
serializer.save(user = request.user,created_by = request.user)
return Response(serializer.data,status=status.HTTP_200_OK)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
urls.py
path('orderproduct/',views.OrderProductList.as_view()),
When you call serializer.save(). It's only perform create() action which is only create one and accept dictionary data type only. If you want to save multiple data like that, you will have to override the create function of the serializer class. You can do something similar like this or run a for loop.
serializers.py
def create(self, validate_data):
# Get the data objects you need to perform bulk create
order_products = OrderProduct.objects.bulk_create(validate_data)
return order_products
views.py
if serializer.is_valid(raise_exception=True):
# Replace the serializer.save() by this line to trigger the create method in serializer
self.perform_create(serializer)
return Response(...)

DRF how to return list filtered by lookup w/ custom router and modelviewset

I tried to search for answers as much as I can but I still don't know how to achieve my goal here.
My goal:
I need two api endpoints, one returns a list filtered by a lookup fields, and another returns an obj filtered by another field, both using GET method. For example:
<ip>/api/books/bycategory/{category_lookup}/ This endpoint will return a list of books filtered by a category
<ip>/api/books/byid/{id_lookup}/ This returns one book matches the specified id (not pk)
Since there's no built-in router that suits my needs here because the built-in ones don't provide url pattern with lookup that returns a list, so I figured I need to have a custom router of my own, so here's what I have:
class CustomRouter(routers.SimpleRouter):
routes = [
routers.DynamicRoute(
url=r'^{prefix}/{url_path}/{lookup}{trailing_slash}$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
)
]
router = CustomRouter()
router.register('books', BookViewSet)
and my serializer:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = BookKeeper
fields = '__all__'
Right until here I think I'm on the right track, but when it comes to the view, thats where i can't quite figure out. Right now I only have this incomplete viewset:
class BookViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BookKeeper.objects.all()
serializer_class = BookSerializer
#action(detail=True)
def bycategory(self, request):
lookup_field = 'category'
#action(detail=True)
def byid(self, request):
lookup_field = 'id'
My first question here is I "think" {url_path} in the router url matches the method name with #action specified in the viewset somehow and that how they are connected, am I correct?
Second question is how do I use {lookup} value in the view?
Third, what's the lookup_field for if I'm to use like:
def bycategory(self, request):
return Response(BookKeeper.objects.filter(category=<lookup_value>))
Lastly what should my viewset be like anyway?
Any input will be appreciated.
Second question is how do I use {lookup} value in the view?
You need two lookup_fields for the same set. You can do that by a custom Mixin class. But in four case, it is better not to use routers but custom urls, so edit like this:
# views.py
class BookViewSet(viewsets.ReadOnlyModelViewSet):
queryset = BookKeeper.objects.all()
serializer_class = BookSerializer
#action(detail=True)
def bycategory(self, request, category):
# do filtering by category
print(category)
#action(detail=True)
def byid(self, request, book_id):
# do filtering by book_id
print(book_id)
# urls.py
get_by_id = views.BookViewSet.as_view(
{
'get': 'byid'
}
)
get_by_category = views.BookViewSet.as_view(
{
'get': 'bycategory'
}
)
urlpatterns += [
url(
r'^api/books/byid/(?P<book_id>[0-9a-f-]+)/',
get_by_id,
name='get-by-id'
),url(
r'^api/books/bycategory/(?P<category>[0-9a-f-]+)/',
get_by_category,
name='get-by-category'
)
]

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