É possível fazer um `in` `lookup_type` através do analisador de URL do django-filter?
-
21-12-2019 - |
Pergunta
estou a usar filtro Django com Django-rest-framework e estou tentando instanciar um filtro que aceita listas de números para filtrar a consulta definida
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
Se eu passar uma lista de números inteiros separados por vírgula, o filtro será completamente ignorado.
Se eu passar um único número inteiro, ele passa pelo django-filter para o validador de formulário do django e reclama:
'Decimal' object is not iterable
Existe uma maneira de criar um objeto Django-filter que possa manipular uma lista de números inteiros e filtrar adequadamente o conjunto de consultas?
Solução
Para o bem ou para o mal, criei um filtro personalizado para isso:
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
Que é usado como:
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
Agora minha interface aceita listas de números inteiros delimitadas por vírgulas.
Outras dicas
Eu sei que este é um post antigo, mas agora existe uma solução melhor.A alteração que a torna correta é publicada aqui.
Eles adicionaram um BaseInFilter
e um BaseRangeFilter
.A documentação é aqui.
Visão geral, o BaseFilter verifica o CSV e, quando misturado com outro filtro, faz o que você está pedindo.Seu código agora pode ser escrito como:
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
pass
class MyModelViewSet(viewsets.ModelViewSet):
ids = NumberInFilter(name='id', lookup_expr='in')
class Meta:
model = MyModel
fields = ['ids']
Aqui está uma solução completa:
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
De acordo com uma postagem no problemas com filtro Django:
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"))
Eu pessoalmente usei isso sem nenhum problema em meus projetos e funciona sem a necessidade de criar um filtro por tipo.
Com base na resposta do @yndolok, cheguei a uma solução geral.Acho que filtrar por uma lista de ids é uma tarefa muito comum e por isso deveria ser incluída no 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)
Solução de atualização:
de django_filters importe rest_framework como filtros
nome-->nome_campo
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
Como respondi aqui DjangoFilterBackend com vários IDs, agora é muito fácil fazer um filtro que aceite lista e valide o conteúdo
Por exemplo:
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', ]
Isto aceitará uma lista de números inteiros de um parâmetro get.Por exemplo /endpoint/?id_in=1,2,3