DRF how to return list filtered by lookup w/ custom router and modelviewset - django-rest-framework

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'
)
]

Related

Accessing ViewSet object list to provide extra context to serializer

I am attempting to add context to a serializer within a ModelViewSet which is dependent on the current paged object list in context. I'll explain with an example.
I am building a viewsets.ModelViewSet that lists Users and a list of favorite_foods. However- the list of user's favorite foods in some external microservice accessible via API. Building a ViewSet to collect objects and performing HTTP requests on each is trivial, we can do something like this:
class UserViewSet(viewsets.ViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserSerializer(serializers.ModelSerializer):
favorite_foods = serializers.SerializerMethodField()
def get_favorite_foods(self, instance):
# performs an HTTP call and returns a list[] of foods.
return evil_coupled_microservice_client.get_user_food_list(instance)
class Meta:
model = User
fields = ('id', 'name', 'favorite_foods')
The issue is (aside from some ugly infrastructure coupling) this is going to make an HTTP request count equivalent to the page size. Instead- it would be great if I could prefetch the favorite food lists for all users on the page in a single HTTP call, and just add them into context, like this:
class UserViewSet(viewsets.ViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
users = <-- This is where I want to know what users are in the current filtered, paginated response.
users_food_dict = evil_coupled_microservice_client.get_many_users_food_list(users)
context.update({'usesr_foods': users_food_dict})
return context
However- it doesn't appear there is any way to fetch the object list that's going to be serialized. Although (I'm fairly sure) get_serializer_context is called after the queryset is filtered and paginated, I'm not sure how to access it without doing some really hacking re-compiling of the queryset based on the query_params and other pieces attached to the class.
I'll post my current solution. It's not terrible, but I'm still hoping for a cleaner built-in.
class UserViewSet(viewsets.ViewSet):
...
def list(self, request, *args, **kwargs):
# Overrwite `ListModelMixin` and store current set
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
self.current_queryset = page
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
self.current_queryset = queryset
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This is untested so far (not sure about functionality on Detail endpoints for instance) allows for the current_queryset to be fetched within the serializer context.

Django rest framework multi searching

everyone. I tried understand search.
I have url path('quiz/all/', QuizListView.as_view()),
View :
class QuizListView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizDetailSerializer
search_fields = ('description', 'title',)
filterset_fields = ['title', 'description',]
(method1)If I use search, for example /api/v1/quiz/all/?search=QI got all instances where title or description contains 'Q'
(method2)I can search /api/v1/quiz/all/?title=Q&description=d I got a instance which has exact title and description.
(method3)But I want to get list of all instances where title contains one value and description contains other value. For example, I want to write /api/v1/quiz/all/?title=Q&description=d and get list where title contains Q and description contains d.
Quiz1(title=Q, description=d)
Quiz2(title=Test, description=dd)
Quiz3(title=NewQ, description=Test_d)
For (method1,/api/v1/quiz/all/?search=Q) I got Quiz1, Quiz2, Quiz3
For (method2,/api/v1/quiz/all/?title=Q&description=d) I got Quiz1
For (method3,/api/v1/quiz/all/?title=Q&description=d) I would like to
get Quiz1 and Quiz3 (because they contain Q for title and d for description)
Thanks.
You'll need to create a custom FilterSet class and use the contains or icontains (if you want case insensitive) because the default is using exact and that's why you don't get back the result that you want. See docs here and here
# filters.py
class QuizFilter(django_filters.FilterSet):
class Meta:
model = Quiz
fields = {
'title': ['contains'], # or icontains
'description': ['contains'], # or icontains
}
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from .filters import QuizFilter
class QuizListView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizDetailSerializer
filter_backends = (DjangoFilterBackend, ) # add here other filters backends
filterset_class = QuizFilter

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

How to do a search in the DRF only for the selected field?

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

How do you override a ModelViewSet's get_queryset in Django Rest Framework 3?

I used to follow this pattern in Django Rest Framework (DRF) 2:
class Foo(models.Model):
user = models.ForeignKey(User)
class FooSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Foo
fields = ('url')
class FooViewset(viewsets.ModelViewSet):
def get_queryset(self):
return Foo.objects.filter(user=self.request.user)
serializer = FooSerializer
model = Foo # <-- the way a ModelViewSet is told what the object is in DRF 2
[ in urls.py]
from rest_framework import routers
router = routers.DefaultRouter()
router.register('Foo', views.FooViewSet)
In DRF 3, I now get:
AssertionError at /
`base_name` argument not specified, and could not automatically
determine the name from the viewset, as it does not have a
`.queryset` attribute.
How is get_queryset overridden for an instance of rest_framework.viewsets.ModelViewSet?
Figured this one out. The model field of the rest_framework.viewsets.ModelViewSet does seem to be AWOL in DRF3. Now, if you override get_queryset you need to specify a third parameter to routers.DefaultRouter().register which is the basename parameter. Then, the function won't go off and try to find it on the non-existent queryset field of the ModelViewSet.
e.g.
router = routers.DefaultRouter()
[...]
router.register('/rest/FooBar'/, views.FooBarViewSet, 'foobar-detail')
#^^^^^^^^^^^^^^^
In addition to Ross Rogers' answer, on current version (3.8.2), you can specify only the model's name instead of the handler. So, instead of:
router.register('/rest/FooBar', views.FooBarViewSet, base_name='foobar-list')
router.register('/rest/FooBar/{pk}', views.FooBarViewSet, base_name='foobar-detail')
You can just do:
router.register('/rest/FooBar', views.FooBarViewSet, base_name='foobar')
To overrite default queryset in DRF 3, just define queryset attribute whitin your FooViewSet class.
class FooViewset(viewsets.ModelViewSet):
queryset = Foo.objects.all()

Resources