¿Cómo usar ModelMultipleChoiceFilter?

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!