So I have typical generic view:
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
def get_queryset(self):
area = Area.objects.get(pk=self.kwargs.get('area_id'))
area_tree = area.get_tree(parent=area) #returns queryset
category = Category.objects.get(pk=self.kwargs.get('category_id'))
queryset = Foo.objects.filter(area__in=area_tree, category=category)
return queryset
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
My problem is, if i try get area or category objects, which doesn't exist, browser throws me error:
Area matching query does not exist.
How can I make it so, that when Area matching query does not exist, I get standard rest framework 404 response?
The problem here is that get_queryset doesn't really expect any failures. In your case, although you are returning a queryset, you seem to be hitting the database with the Area.objects.get(pk=self.kwargs.get('area_id')) call. When this fails, it violates the I/O defined by get_queryset which isn't expecting the Area.DoesNotExist exception. So it fails and you end up with a Django 500 error.
What you need to ensure is that the get_queryset method, returns a queryset, preferably without making any calls to the DB (I say preferably, since there is no such rule that says it shouldn't hit the DB, but its generally understood that get_queryset wont be the one to actually perform the DB query). Then if you must, you can freely perform any get operations on your DB inside the get_object with the get_object_or_404 shortcut. Since get_object_or_404 raises an Http404 exception and get_object knows how to handle this exception, it will gracefully return the 404 page that you are expecting.
If you can ensure your area.get_tree implementation can work with a parent queryset, instead of a parent object, then you could do something like this:
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
def get_queryset(self):
area = Area.objects.filter(pk=self.kwargs.get('area_id'))
area_tree = area.get_tree(parent=area) #returns queryset
category = Category.objects.filter(pk=self.kwargs.get('category_id'))
queryset = Foo.objects.filter(area__in=area_tree, category__in=category)
return queryset
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
If you are unable to get area.tree to work without a queryset, then you can delay some of your get_queryset logic to get_object. Like so:
from django.http import Http404
class FooListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
lookup_fields = ('area_id', 'category_id', )
queryset = Foo.objects.all()
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
area = get_object_or_404(Area, **{'pk': self.kwargs.get('area_id')})
area_tree = area.get_tree(parent=area)
category = get_object_or_404(Category, **{'pk': self.kwargs.get('category_id')})
queryset = queryset.filter(area__in=area_tree, category=category)
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter)
Related
I have my custom API view and I want to use Search filter in this view, but generic filter dodn't work so i want to create custom one but It doesn't work and I dont know where is problem.
view
class TaskIndexAPIView(APIView):
filter_backends = (CustomSearchFilter,)
search_fields = ('name', 'description', 'user__username')
def get_queryset(self):
return Task.objects.all()
def get(self, request):
tasks = self.get_queryset()
for i in tasks:
if i.date <= date.today():
i.delayed = 'This task is delayed'
i.save()
else:
i.delayed = ''
i.save()
serializer = IndexSerializer(tasks, many=True)
return Response(serializer.data)
My custom search filter
search_filter
class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request):
if request.get_queryset.get('name', 'user'):
return ['name', 'user']
return super(CustomSearchFilter, self).get_search_fields(view, request)
In your context, the difference between APIView and generic view is, the generic view has a method called filter_queryset() which handles the filtering and searching operations.
So here in your view, we need to include the same.
class TaskIndexAPIView(APIView):
filter_backends = (CustomSearchFilter,)
search_fields = ('name', 'description', 'user__username')
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need
to call it either from a list view, or from a custom `get_object`
method if you want to apply the configured filtering backend to the
default queryset.
"""
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def get_queryset(self):
return Task.objects.all()
def get(self, request):
the_filtered_qs = self.filter_queryset(self.get_queryset())
tasks = the_filtered_qs
for i in tasks:
if i.date <= date.today():
i.delayed = 'This task is delayed'
i.save()
else:
i.delayed = ''
i.save()
serializer = IndexSerializer(tasks, many=True)
return Response(serializer.data)
you will get the filtered queryset as
the_filtered_qs = self.filter_queryset(self.get_queryset())
Using Django Class Based Views, I already have a ListView for Order objects, and I created a FormView to perform advanced searches on these orderers.
However, I'm not sure how to pass the filtered queryset of the FormView to the ListView.
Here is the code, with commented sections to explain the issue:
class OrdersListView(PermissionRequiredCanHandleOrders,
SelectRelatedMixin, PrefetchRelatedMixin,
ModelInContextMixin, SubSectionLastOrders,
RestaurantOrdersOnly,
ListView):
model = Order
paginator_class = DiggPaginator
paginate_by = 15
select_related = ('convive__user',)
prefetch_related = ('orderoperation_set',)
# will use the template named order_list.html
class OrdersAdvancedSearchView(PermissionRequiredCanHandleOrders,
ModelInContextMixin, SubSectionLastOrders,
RestaurantOrdersOnly, RestaurantMixin,
FormView):
model = Order
template_name = "orders/order_advanced_search.html"
form_class = OrderAdvancedSearchForm
def form_valid(self, form):
data = form.cleaned_data
queryset = Order.objects.all()
# Here, I'm using the form content to filter the queryset
# queryset = queryset.filter(some_attribute__in=data['stuff'])
# MY PAIN POINT IS HERE: what I'm supposed to do here ?
# my queryset is filtered, and I need to call OrderListView
# with it.
return super().form_valid(form)
You should use the ListView to filter the queryset with the GET request parameters. For this, add the form to your context and process it when getting the queryset:
def get_queryset(self):
self.form = OrderAdvancedSearchForm(data=self.request.GET or None)
if self.request.GET and form.is_valid():
# filter using the form's cleaned_data
queryset = super().get_queryset().filter(...)
else:
queryset = super().get_queryset()
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # this calls self.get_queryset() which assigns self.form
context['form'] = self.form
return context
Now in your template you can just render the same form, except its method should be "GET" not "POST".
I have a APIView class for showing all the rents and posting and delete etc. Now i want search feature so i tried to use DjangoFilterBackend but it is not working. I see in documentation, it has been used with ListAPIView but how can i use it in APIView.
class Rent(APIView):
"""
List all the rents if token is not provided else a token specific rent
"""
serializer_class = RentSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('city', 'place', 'property_category',)
search_fields = ('=city', '=place')
def get(self, request, token=None, format=None):
reply={}
try:
rents = Rental.objects.all()
if token:
rent = Rental.objects.get(token=token)
reply['data'] = self.serializer_class(rent).data
else:
reply['data'] = self.serializer_class(rents, many=True).data
except Rental.DoesNotExist:
return error.RequestedResourceNotFound().as_response()
except:
return error.UnknownError().as_response()
else:
return Response(reply, status.HTTP_200_OK)
when i search the rent with the following parameters in the url, i get all the rents, instead i should get only those rents that lies in city Kathmandu and place koteshwor
http://localhost:8000/api/v1/rents?city=Kathmandu&place=Koteshwor
To use the functionality of DjangoFilterBackend, you could incorporate the filter_queryset method from GenericViewSet, which is the DRF class that inherits from APIView and leads to all specific 'generic' view classes in DRF. It looks like this:
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need
to call it either from a list view, or from a custom `get_object`
method if you want to apply the configured filtering backend to the
default queryset.
"""
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py
Here If you are using APIView, There is nothing to do with filters.So you have to do like
get_data = request.query_params #or request.GET check both
Then
Rental.objects.filter(city=get_data['city'], place=get_data['place'])
In case someone is wondering how can we integrate django_filters filter_class with api_views:
#api_view(['GET'])
#permission_classes([permissions.IsAuthenticated])
def filter_data(request, format=None):
qs = models.YourModal.objects.all()
filtered_data = filters.YourFilter(request.GET, queryset=qs)
filtered_qs = filtered_data.qs
....
return response.Ok(yourData)
Adding to #ChidG's answer. All you need to do is override the DjangoFilterBackend's filter_queryset method, which is the entry point for the filter, and pass it the instance of your APIView. The important point to note here is you must declare filter_fields or filter_class on the view in order to get the filter to work. Otherwise it just return your queryset unfiltered.
If you're more curious about how this works, the class is located at django_filters.rest_framework.backends.py
In this example, the url would look something like {base_url}/foo?is_active=true
from django_filters.rest_framework import DjangoFilterBackend
class FooFilter(DjangoFilterBackend):
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset, request=request).qs
return queryset
class Foo(APIView):
permission_classes = (AllowAny,)
filter_fields = ('name', 'is_active')
def get(self, request, format=None):
queryset = Foo.objects.all()
ff = FooFilter()
filtered_queryset = ff.filter_queryset(request, queryset, self)
if filtered_queryset.exists():
serializer = FooSerializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response([], status=status.HTTP_200_OK)
I have multiple API which historically work using id as the lookup field:
/api/organization/10
I have a frontend consuming those api.
I'm building a new interface and for some reasons, I would like to use a slug instead an id:
/api/organization/my-orga
The API is built with Django Rest Framework. Except the change of lookup field, the api behavior should stay the same.
Is there a solution to allow my API to work with both a slug and a pk ? Those two path should give them same results:
/api/organization/10
/api/organization/my-orga
Here is my API definition:
# urls.py
router = DefaultRouter()
router.register(r'organization', Organization)
urlpatterns = router.urls
#view.py
class Organization(viewsets.ModelViewSet):
queryset = OrganisationGroup.objects.all()
serializer_class = OrganizationSerializer
# serializer.py
class OrganizationSerializer(PermissionsSerializer):
class Meta:
model = Organization
Try this
from django.db.models import Q
import operator
from functools import reduce
from django.shortcuts import get_object_or_404
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
filter[field] = self.kwargs[field]
q = reduce(operator.or_, (Q(x) for x in filter.items()))
return get_object_or_404(queryset, q)
Then in View
class Organization(MultipleFieldLookupMixin, viewsets.ModelViewSet):
queryset = OrganisationGroup.objects.all()
serializer_class = OrganizationSerializer
lookup_fields = ('pk', 'another field')
I solved the similar problem by overriding retrieve method and check pk field's value against any pattern. For example if it consists of only numbers.
def retrieve(self, request, *args, **kwargs):
if kwargs['pk'].isdigit():
return super(Organization, self).retrieve(request, *args, **kwargs)
else:
# get and return object however you want here.
I know you asked this question quite a time ago, but here is the complete solution i got from all answers, considering both views and urls:
Put this in your views.py: (With a little edit from drf)
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
if self.kwargs.get(field, None):
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
Then inherit your view from this Mixin and add fields you want to lookup_fields. Like this:
class YourDetailView(MultipleFieldLookupMixin, RetrieveUpdateAPIView):
...
lookup_fields = ['pk', 'slug','code']
And in urls.py:
re_path(r'^organization/(?P<pk>[0-9]+)/$',
YourDetailView),
re_path(r'^organization/(?P<slug>[-a-zA-Z0-9_]+)/$',
YourDetailView),
re_path(r'^organization/sth_else/(?P<code>[0-9]+)/$',
YourDetailView),
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields.
filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter) # Lookup the object
class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_fields = ('account', 'username')
I think best way is to override the get_object(self) method
class Organization(generics.RetrieveAPIView):
serializer_class = OrganizationSerializer
queryset = Organization.objects.all()
multiple_lookup_fields = ['pk', 'slug']
def get_object(self):
queryset = self.get_queryset()
filter = {}
for field in self.multiple_lookup_fields:
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
return obj
There are a lot of answers here already, but none provide a full description including the mixin, view, and url configuration. This answer does.
This is the mixin that works best, it is slightly modified from https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins to not error out on non-existing fields.
class MultipleFieldLookupMixin:
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
Source: https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins
Modified to not error out for not providing all fields in the url.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs.get(field): # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
Now add the view as follows, it is important to have the Mixin first, otherwise the get_object method is not overwritten:
class RudAPIView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
...
lookup_fields = ['pk', 'other_field']
Now, for the urls, we use default converters. It is important int comes first as that one will actually check if it is an int, and if not fallback to str. If you have more complex fields, you need to resort to regex.
path('efficiency/<int:pk>/', views.RudAPIView.as_view(), name='something-rud'),
path('efficiency/<string:other_field>/', views.RudAPIView.as_view(), name='something-rud'),
I think the fundamental answer is that this would not be good REST/API design and just isn't something DRF would enable.
The official docs have an example for this at https://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins
Also, you need to modify the urls.py adding a new route for the same view, but with the new field name.
If you still would like to use Viewsets without breaking it apart, here you go.
(Test passed on my end)
import operator
from functools import reduce
from django.db.models import Q
from django.shortcuts import get_object_or_404
class MultipleFieldLookupMixin(object):
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filters = {}
pk_fields = ["pk", "id"]
for field in self.lookup_fields:
identifier = self.kwargs[self.lookup_field]
if (field in pk_fields and identifier.isdigit()) or field not in pk_fields:
filters[field] = self.kwargs[self.lookup_field]
q = reduce(operator.or_, (Q(x) for x in filters.items()))
obj = get_object_or_404(queryset, q)
self.check_object_permissions(self.request, obj)
return obj
This is my latest version that supports primary key fields that not necessary are strings, I think is more resilient.
import operator
from functools import reduce
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
class MultipleFieldLookupMixin:
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filters = {}
for field in self.lookup_fields:
try:
# Validate the data type we got is a valid data type for the field we are setting
self.get_serializer_class().Meta.model._meta.get_field(field).to_python(
self.kwargs[self.lookup_field]
)
filters[field] = self.kwargs[self.lookup_field]
except ValidationError:
continue
query = reduce(operator.or_, (Q(x) for x in filters.items()))
obj = get_object_or_404(queryset, query)
self.check_object_permissions(self.request, obj)
return obj
Here's the situation. I got a list on my Django REST API: /playerslist/
It returns me a list of players just like this one:
http://pastebin.com/JYA39gHT
This is exactly what I want for the moment. But now, I need this:
Going for /playerslist/1/ gives me different infos for the Player Number 1. The list is here only for listing players with basic informations. But I need detailed view for players, containing info from other models and with different serialization, it must be a basic issue, but as I'm totally new to Django and Python in general, I must misunderstanding something.
Here is my Viewset:
class PlayersListViewSet(viewsets.ModelViewSet):
queryset = Player.objects.all()
serializer_class = PlayersListSerializer
http_method_names = ['get', 'post']
pagination_class = None
filter_backends = [filters.OrderingFilter]
ordering_fields = ['name']
def get_queryset(self):
queryset = Player.objects.all()
team_id = self.request.query_params.get('team', None)
if team_id:
try:
queryset = queryset.filter(team=team_id)
except ValueError:
raise exceptions.ParseError()
return queryset
How can I achieve this ? Must I use #detail_route to have something like playerslist/1/detail ? I've already tried but DRF's documentation only show a single example and it's not clear at all for me.
You can override the methods retrieve (returning one instance) or list (returning list obviously) as shown in first example in http://www.django-rest-framework.org/api-guide/viewsets/.
class PlayersListViewSet(viewsets.ModelViewSet):
queryset = Player.objects.all()
serializer_class = PlayersListSerializer
http_method_names = ['get', 'post']
pagination_class = None
filter_backends = [filters.OrderingFilter]
ordering_fields = ['name']
def get_queryset(self):
queryset = Player.objects.all()
team_id = self.request.query_params.get('team', None)
if team_id:
try:
queryset = queryset.filter(team=team_id)
except ValueError:
raise exceptions.ParseError()
return queryset
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = PlayerDetailSerializer(instance)
return Response(serializer.data)
Where PlayerDetailSerializer is another serializer with different fields (whatever you need) and there is no need to specify it in serializer_class.
To get different results when you do a 'detail' view, you want to change the serializer when doing a 'retrieve' call. I've done this with a custom mixin for a ModelViewSet, which expects a special "detail_serializer_class":
class DifferentDetailSerializerMixin(object):
"""
For a viewset, mix this in to use a different serializer class
for individual 'retrieve' views, different from the standard
serializer for lists.
"""
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.detail_serializer_class(instance, context=self.get_serializer_context())
return Response(serializer.data)
Your viewset is simply:
class PlayersListViewSet(DifferentDetailSerializerMixin, viewsets.ModelViewSet):
queryset = Player.objects.all()
serializer_class = PlayersListSerializer
detail_serializer_class = PlayersDetailSerializer
filter_backends = [filters.OrderingFilter]
ordering_fields = ['name']
Here, PlayersDetailSerializer is another Serializer that has more fields that you want to return.
As an aside, if you want to support optional filtering by teams, I would strongly recommend using django-filter. That way you don't have to worry about validation etc. Once installed, it's simply a case of adding this to your viewset:
filter_backends = (filters.OrderingFilter, filters.DjangoFilterBackend, )
filter_fields = ['team']
See the docs for more info.