He estado tratando de que un ModelMultipleChoiceFilter funcione durante horas y he leído la documentación de los filtros DRF y Django.
Quiero poder filtrar un conjunto de sitios web en función de las tags que se les han asignado a través de ManyToManyField. Por ejemplo, quiero poder obtener una lista de sitios web que se han etiquetado como “Cocinando” o “Apicultura”.
Aquí está el fragmento relevante de mi models.py actual:
class SiteTag(models.Model): """Site Categories""" name = models.CharField(max_length=63) def __str__(self): return self.name class Website(models.Model): """A website""" domain = models.CharField(max_length=255, unique=True) description = models.CharField(max_length=2047) rating = models.IntegerField(default=1, choices=RATING_CHOICES) tags = models.ManyToManyField(SiteTag) added = models.DateTimeField(default=timezone.now()) updated = models.DateTimeField(default=timezone.now()) def __str__(self): return self.domain
Y mi fragmento de views.py actual:
class WebsiteFilter(filters.FilterSet): # With a simple CharFilter I can chain together a list of tags using &tag=foo&tag=bar - but only returns site for bar (sites for both foo and bar exist). tag = django_filters.CharFilter(name='tags__name') # THE PROBLEM: tags = django_filters.ModelMultipleChoiceFilter(name='name', queryset=SiteTag.objects.all(), lookup_type="eq") rating_min = django_filters.NumberFilter(name="rating", lookup_type="gte") rating_max = django_filters.NumberFilter(name="rating", lookup_type="lte") class Meta: model = Website fields = ('id', 'domain', 'rating', 'rating_min', 'rating_max', 'tag', 'tags') class WebsiteViewSet(viewsets.ModelViewSet): """API endpoint for sites""" queryset = Website.objects.all() serializer_class = WebsiteSerializer filter_class = WebsiteFilter filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) search_fields = ('domain',) ordering_fields = ('id', 'domain', 'rating',)
Acabo de realizar la prueba con la cadena de consulta [/path/to/sites]?tags=News
y estoy 100% seguro de que existen los registros adecuados, ya que funcionan (como se describe) con una ?tag
(Falta la consulta de s
).
Un ejemplo de las otras cosas que he probado es algo como:
tags = django_filters.ModelMultipleChoiceFilter(name='tags__name', queryset=Website.objects.all(), lookup_type="in")
¿Cómo puedo devolver cualquier sitio web que tenga una etiqueta de sitio que cumpla con el name == A OR name == B OR name == C
?
Me topé con esta pregunta mientras trataba de resolver un problema casi idéntico a ti mismo, y aunque podría haber escrito un filtro personalizado, tu pregunta me intrigó y tuve que profundizar más.
Resulta que un ModelMultipleChoiceFilter
solo hace un cambio en un Filter
normal, como se ve en el código fuente de django_filters
continuación:
class ModelChoiceFilter(Filter): field_class = forms.ModelChoiceField class ModelMultipleChoiceFilter(MultipleChoiceFilter): field_class = forms.ModelMultipleChoiceField
Es decir, cambia la field_class
a un ModelMultipleChoiceField
desde los ModelMultipleChoiceField
de Django.
ModelMultipleChoiceField
el código fuente de ModelMultipleChoiceField
, uno de los argumentos necesarios para __init__()
es queryset
, por lo que estaba en el camino correcto allí.
La otra pieza del rompecabezas proviene del método ModelMultipleChoiceField.clean()
, con una línea: key = self.to_field_name or 'pk'
. Lo que esto significa es que, de forma predeterminada, tomará el valor que le pase (por ejemplo, "cooking"
) y tratará de buscar Tag.objects.filter(pk="cooking")
, cuando obviamente queremos que lo vea. el nombre, y como podemos ver en esa línea, el campo con el que se compara es controlado por self.to_field_name
.
Afortunadamente, el django_filters
Filter.field()
incluye lo siguiente al crear una instancia del campo real.
self._field = self.field_class(required=self.required, label=self.label, widget=self.widget, **self.extra)
De particular interés es el **self.extra
, que viene del Filter.__init__()
: self.extra = kwargs
, así que todo lo que tenemos que hacer es pasar un to_field_name
kwarg adicional al ModelMultipleChoiceFilter
y se entregará al subyacente ModelMultipleChoiceField
.
Así que (¡salta aquí para ver la solución real!), El código real que deseas es
tags = django_filters.ModelMultipleChoiceFilter( name='sitetags__name', to_field_name='name', lookup_type='in', queryset=SiteTag.objects.all() )
¡Así que estuviste realmente cerca con el código que publicaste arriba! No sé si esta solución será relevante para usted, pero espero que pueda ayudar a alguien más en el futuro.
La solución que funcionó para mí fue usar un MultipleChoiceFilter
. En mi caso, tengo jueces que tienen razas, y quiero que mi API permita a las personas consultar, por ejemplo, jueces negros o blancos.
El filtro termina siendo:
race = filters.MultipleChoiceFilter( choices=Race.RACES, action=lambda queryset, value: queryset.filter(race__race__in=value) )
Race
es un campo de muchos a muchos fuera del Judge
:
class Race(models.Model): RACES = ( ('w', 'White'), ('b', 'Black or African American'), ('i', 'American Indian or Alaska Native'), ('a', 'Asian'), ('p', 'Native Hawaiian or Other Pacific Islander'), ('h', 'Hispanic/Latino'), ) race = models.CharField( choices=RACES, max_length=5, )
No soy un gran fanático de las funciones lambda
, pero tenía sentido aquí porque es una función muy pequeña. Básicamente, esto configura un MultipleChoiceFilter
que pasa los valores de los parámetros GET al campo de Race
modelo Race
. Se pasan como una lista, por lo que el parámetro in
funciona.
Entonces, mis usuarios pueden hacer:
/api/judges/?race=w&race=b
Y obtendrán jueces que se han identificado como blancos o negros.
PD: Sí, reconozco que este no es el conjunto completo de posibles razas. ¡Pero es lo que recoge el censo de Estados Unidos!