How to search from many fields? - django-rest-framework

I have custom filter to my viewset:
class OrderFilter(django_filters.rest_framework.FilterSet):
username = django_filters.CharFilter(name='user__username', lookup_expr='icontains')
client_name = django_filters.CharFilter(name='user__first_name', lookup_expr='icontains')
class Meta:
model = Order
exclude = ['pk']
And it works when I send query like this:
http://localhost:8000/orders/?username=testuser
or
http://localhost:8000/orders/?client_name=john
but I want to create only one query to search data containing search string in username, first_name and last_name. How to do it?

The general catch-all for complicated behavior that can't be expressed by a single filter is to use the method argument to a filter class (docs).
A possible implementation:
from django_filters import rest_framework as filters
from django.db.models import Q
class OrderFilter(filters.FilterSet):
search = filters.CharFilter(method='search_filter')
def search_filter(self, queryset, name, value):
return queryset.filter(Q(username__icontains=value)
| Q(first_name__icontains=value)
| Q(last_name__icontains=value))

Related

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

Apply filters to any or a certain many-to-many record

Consider these models:
from django.db import models
class Group(models.Model):
name = models.CharField()
class Person(models.Model):
height = models.PositiveIntegerField()
weight = models.PositiveIntegerField()
gender = models.CharField()
groups = models.ManyToManyField(Group, blank=True)
and the DRF views
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters import rest_framework as filters
from .serializers import GroupSerializer
from ..models import Group
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all().distinct()
serializer_class = GroupSerializer
filter_backends = (filters.DjangoFilterBackend,
SearchFilter, OrderingFilter)
filter_class = GroupFilter
A group can have 0,1,2 or more Persons, with 1 and 2 being the most common and where these 1 and 2 are clearly defined. Think of it as Facebook's chat: you have one-on-one chat most commonly, but sometimes you can have a group chat. When is one-one-one chat, 1 is sender, 2 is receiver.
I need to filter these records from DRF, when browsing the GroupViewSet and filter by Person attributes, where I can apply a group of filters to any Person or a certain Person.
For any person, no matter to which a certain condition is applied, is clear:
/api/group/?person__height__gt=100&person__weight__gt=200
But for a certain person, where a group of conditions apply to that person, in the URL, I could have something like:
/api/group/?person__0__height__gt=100&person__0__weight__gt=200&person__1__height__lte=200
And declare these into my custom FilterSet:
from django.db.models.constants import LOOKUP_SEP
class GroupFilter(filters.FilterSet):
person__0__height = filters.NumberFilter(method='person_filter')
person__0__height__gt = filters.NumberFilter(method='person_filter')
person__0__height__lt = filters.NumberFilter(method='person_filter')
# ... and so on for the rest of the possibilities
def person_filter(self, queryset, name, value):
m2mfield, index, field, *comparison = name.split(LOOKUP_SEP, 3)
# do subqueries based on the above and construct queryset filter.
But as you can imagine, this implies that I'll have a lot of boilerplate code. In my real models there are many fields and the above "solution" seems hacky to me.
So the question is: is there an easier/cleaner way to achieve the above filtering?
Maybe by dynamically declaring the person__0__height__gt attributes, for which I couldn't yet find a solution.
Note that I do not know the IDs of the Person entities upfront. Those person__0, person__1 are array indexes.
try this for cleaning code :
class GroupFilter(django_filters.FilterSet):
person_range = django_filters.NumericRangeFilter(field_name='person__0__height', lookup_expr='range')
person = django_filters.NumberFilter(field_name='person__0__height', lookup_expr='exact')
class Meta:
model = Group
fields = ('person_range','person',)
and call with url like this :
127.0.0.1:8000/yourpath/?person=180&person_range_min=130&person_range_max=210

DRF - in filter, use field-value instead of default pk / id

I'm trying to use DRF's filters so that the URL query is like so:
/roadname/?road=M5
not like so
/roadinfo/?road=1
I can't seem to do it when I've got a ForeignKey relationship.
I've tried using lookup_field with no luck (although not sure how this would work for multiple filter fields anyway - I don't think that's the answer). I've tried using a get_queryset() method in views as in the second example in the documentation. A comment I came across suggested that this is bad RESTApi practice - is it? How would a user know to type in '1' to get results for 'M5' in a front-end client?
I've set up two really simple models (and serializers, views, etc.) to try these out as below.
If I use RoadName, I have to type the name into the filter search box (rather than having a dropdown), but the url query is how I want it.
If I use RoadInfo (which has a ForeignField to RoadName), I get a drop down in the filter box, but the url query uses the ForeignKey pk.
My question: How can I set it so that when I use RoadInfo, the query uses the field value rather than the id/pk?
Models
from django.db import models
class RoadName(models.Model):
road = models.CharField(max_length=50)
def __str__(self):
return str(self.road)
class RoadInfo(models.Model):
road = models.ForeignKey(RoadName, on_delete='CASCADE')
# other data
def __str__(self):
return str(self.road)
Serializers
from traffic.models import *
from rest_framework import serializers
class RoadNameSerializer(serializers.ModelSerializer):
road = serializers.CharField()
class Meta:
model = RoadName
exclude = ('id',)
class RoadInfoSerializer(serializers.ModelSerializer):
road = RoadNameSerializer()
class Meta:
model = RoadInfo
exclude = ('id',)
Views
from traffic.serializers import *
from traffic.models import *
from django_filters import rest_framework as filters
from rest_framework import viewsets
class RoadNameViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadName.objects.all()
serializer_class = RoadNameSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'
class RoadInfoViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadInfo.objects.all()
serializer_class = RoadInfoSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'
The data M5 on the road attribute of RoadName model. It can be filtered by road__road from RoadInfo model.
So, Try /roadname/?road__road=M5

How do I specify multiple column name in a Django search field?

i use django filter backend
I define multiple columns in a search field, as follows:
class tableViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = table.objects.all()
serializer_class = WebSerializer
pagination_class = StandardResultsSetPagination
filter_backends = (DjangoFilterBackend,SearchFilter)
search_fields = ('name','family','tel',)
i want to make an api that handle some query like this:
select * from table1 where name like ('tom') and family like ('%anderson%') and tel like ('%0223654%')
Is there any way to specify the column name in the API?
For example:
http://127.0.0.1/api-user/table/search?name=tom&family=andeson&tel=0223455
You should rather use DjangoFilterBackend instead of SearchFilter if you want to filter per specific attribute.
As you are not looking for exact search, you will have to create a FilterSet and use filter_class in you view.
Something like:
class TableFilter(django_filters.FilterSet):
name = django_filters.CharFilter(name='name', lookup_expr='icontains')
family = django_filters.CharFilter(name='family', lookup_expr='icontains')
tel = django_filters.CharFilter(name='tel', lookup_expr='icontains')
class Meta:
model = table
fields = ['name', 'family', 'tel']
class tableViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing accounts.
"""
queryset = table.objects.all()
serializer_class = WebSerializer
pagination_class = StandardResultsSetPagination
filter_backends = (DjangoFilterBackend,SearchFilter)
filter_class = TableFilter
Specifying the field in the query string is unnecessary. The following request should work:
http://127.0.0.1/api-user/table?search=something
relevant docs

Possible to do an `in` `lookup_type` through the django-filter URL parser?

I'm using django-filter with django-rest-framework and I'm trying to instantiate a filter that accepts lists of numbers for filtering the query set down
class MyFilter(django_filters.FilterSet):
ids = django_filters.NumberFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
If I pass in a comma separated list of integers, the filter is ignored altogether.
If I pass in a single integer, it gets through django-filter into django's form validator and complains:
'Decimal' object is not iterable
Is there a way to create a django-filter object which can handle a list of integers and properly filter down the queryset?
For better or worse, I created a custom filter for this:
class IntegerListFilter(django_filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.name, self.lookup_type):integers})
return qs
Which is used like:
class MyFilter(django_filters.FilterSet):
ids = IntegerListFilter(name='id',lookup_type='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
Now my interface accepts comma-delimited lists of integers.
I know this is an old post, but there is now a better solution. The change that makes it correct is posted here.
They added a BaseInFilter and a BaseRangeFilter. The documentation is here.
Big picture, BaseFilter checks for CSV, and then when mixed with another filter it does what you are asking. Your code can now be written like:
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyModelViewSet(viewsets.ModelViewSet):
ids = NumberInFilter(name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['ids']
Here's a complete solution:
from django_filters import Filter, FilterSet
from rest_framework.filters import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .models import User
from .serializers import UserSerializer
class ListFilter(Filter):
def filter(self, qs, value):
if not value:
return qs
self.lookup_type = 'in'
values = value.split(',')
return super(ListFilter, self).filter(qs, values)
class UserFilter(FilterSet):
ids = ListFilter(name='id')
class Meta:
model = User
fields = ['ids']
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_class = UserFilter
According to a post in the django-filter issues:
from django_filters import Filter
from django_filters.fields import Lookup
class ListFilter(Filter):
def filter(self, qs, value):
return super(ListFilter, self).filter(qs, Lookup(value.split(u","), "in"))
I have personally used this without any issue in my projects, and it works without having to create a per-type filter.
Based on #yndolok answer I have come to a general solution. I think filtering by a list of ids is a very common task and therefore should be included in the FilterBackend:
class ListFilter(django_filters.Filter):
"""Class to filter from list of integers."""
def filter(self, qs, value):
"""Filter function."""
if not value:
return qs
self.lookup_type = 'in'
try:
map(int, value.split(','))
return super(ListFilter, self).filter(qs, value.split(','))
except ValueError:
return super(ListFilter, self).filter(qs, [None])
class FilterBackend(filters.DjangoFilterBackend):
"""A filter backend that includes ListFilter."""
def get_filter_class(self, view, queryset=None):
"""Append ListFilter to AutoFilterSet."""
filter_fields = getattr(view, 'filter_fields', None)
if filter_fields:
class AutoFilterSet(self.default_filter_set):
ids = ListFilter(name='id')
class Meta:
model = queryset.model
fields = list(filter_fields) + ["ids"]
return AutoFilterSet
else:
return super(FilterBackend, self).get_filter_class(view, queryset)
Uptodate solution:
from django_filters import rest_framework as filters
name-->field_name
lookup_type-->lookup_expr
class IntegerListFilter(filters.Filter):
def filter(self,qs,value):
if value not in (None,''):
integers = [int(v) for v in value.split(',')]
return qs.filter(**{'%s__%s'%(self.field_name, self.lookup_expr):integers})
return qs
class MyFilter(filters.FilterSet):
ids = IntegerListFilter(field_name='id',lookup_expr='in')
class Meta:
model = MyModel
fields = ('ids',)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = MyFilter
As I have answered here DjangoFilterBackend with multiple ids, it is now pretty easy to make a filter that accepts list and validates the contents
For Example:
from django_filters import rest_framework as filters
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyFilter(filters.FilterSet):
id_in = NumberInFilter(field_name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['id_in', ]
This will accept a list of integers from a get parameter. For example /endpoint/?id_in=1,2,3

Resources