¿Cómo se filtra un serializador anidado en Django Rest Framework?

81

En Django Rest Framework, ¿cómo se filtra un serializador cuando está anidado en otro serializador?

Mis filtros se imponen en los conjuntos de vistas DRF, pero cuando llama a un serializador desde dentro de otro serializador, nunca se llama al conjunto de vistas del serializador anidado, por lo que los resultados anidados aparecen sin filtrar.

Intenté agregar un filtro en el conjunto de vistas de origen, pero no parece filtrar los resultados anidados porque los resultados anidados se llaman como una consulta previa separada. (El serializador anidado es una búsqueda inversa, como ve).

¿Es posible agregar una anulación de get_queryset () en el serializador anidado en sí (moviéndolo fuera del conjunto de vistas), para agregar el filtro allí? Yo también lo intenté, sin suerte.

Esto es lo que intenté, pero ni siquiera parece que lo llamen:

class QuestionnaireSerializer(serializers.ModelSerializer):
    edition = EditionSerializer(read_only=True)
    company = serializers.StringRelatedField(read_only=True)

    class Meta:
        model = Questionnaire

    def get_queryset(self):
        query = super(QuestionnaireSerializer, self).get_queryset(instance)
        if not self.request.user.is_staff:
            query = query.filter(user=self.request.user, edition__hide=False)
        return query
Juan
fuente
6
get_querysetes una clase en ModelViewSet, no en el serializador, por lo que no se llama
NotSimon

Respuestas:

96

Puede crear una subclase de ListSerializer y sobrescribir el to_representationmétodo.

De forma predeterminada, el to_representationmétodo llama data.all()al conjunto de consultas anidado. Por lo tanto, debe realizar data = data.filter(**your_filters)antes de llamar al método. Luego, debe agregar su ListSerializer subclasificado como list_serializer_class en el meta del serializador anidado.

  1. subclase ListSerializer, sobrescribiendo to_representationy luego llamando super
  2. agregue ListSerializer subclasificado como meta list_serializer_classen el serializador anidado

Aquí está el código relevante para su muestra.

class FilteredListSerializer(serializers.ListSerializer):

    def to_representation(self, data):
        data = data.filter(user=self.context['request'].user, edition__hide=False)
        return super(FilteredListSerializer, self).to_representation(data)


class EditionSerializer(serializers.ModelSerializer):

    class Meta:
        list_serializer_class = FilteredListSerializer
        model = Edition


class QuestionnaireSerializer(serializers.ModelSerializer):
    edition = EditionSerializer(read_only=True)
    company = serializers.StringRelatedField(read_only=True)

    class Meta:
        model = Questionnaire
en perspectiva
fuente
1
¡Eso hizo el truco! Aunque al final decidí que mis serializadores se estaban volviendo demasiado complejos y los refactoricé todos, lo que obligó al cliente a ejecutar algunas llamadas API más, pero simplificando enormemente mi aplicación.
John
3
Intentando utilizar esto como base para una solución a un problema similar; No estoy seguro de si realmente merece su propia pregunta. ¿Cómo pasaría una var desde QuestionnaireSerializerListSerializer? Para aproximar, necesito filtrar por ID de edición y por ID de cuestionario.
Brendan
3
Esto debería estar en la documentación de DRF. ¡Súper útil gracias!
Daniel van Flymen
7
En mi implementación, obtengo ¿ 'FilteredListSerializer' object has no attribute 'request'Alguien más obtiene lo mismo?
Dominooch
11
Para responder @Dominooch necesitas usar self.context ['request'] en lugar de self.request
rojoca
24

Probé muchas soluciones de SO y otros lugares.

Encontré solo una solución de trabajo para Django 2.0 + DRF 3.7.7.

Defina un método en el modelo que tenga una clase anidada. Elabore un filtro que se adapte a sus necesidades.

class Channel(models.Model):
    name = models.CharField(max_length=40)
    number = models.IntegerField(unique=True)
    active = models.BooleanField(default=True)

    def current_epg(self):
        return Epg.objects.filter(channel=self, end__gt=datetime.now()).order_by("end")[:6]


class Epg(models.Model):
    start = models.DateTimeField()
    end = models.DateTimeField(db_index=True)
    title = models.CharField(max_length=300)
    description = models.CharField(max_length=800)
    channel = models.ForeignKey(Channel, related_name='onair', on_delete=models.CASCADE)

.

class EpgSerializer(serializers.ModelSerializer):
    class Meta:
        model = Epg
        fields = ('channel', 'start', 'end', 'title', 'description',)


class ChannelSerializer(serializers.ModelSerializer):
    onair = EpgSerializer(many=True, read_only=True, source="current_epg")

    class Meta:
        model = Channel
        fields = ('number', 'name', 'onair',)

Preste atención source="current_epg"y entenderá el punto.

duddits
fuente
¡Si! Este comentario aprovecha la capacidad de la fuente para ser una función, que usted define en el modelo, ¡luego puede aprovechar el filtrado allí! ¡Frio!
zarigüeyas
¿Es posible pasar una cadena a la función en la clase?
AlexW
Solo necesitaba ordenar muchos campos relacionados. También probé muchas soluciones diferentes (juego de palabras). ¡Pero esta fue la única solución que funcionó para mí! ¡Gracias!
gabn88
Parece que esta es una solución más correcta en términos de filosofía de código de django que la respuesta aceptada. Django propone el enfoque ActiveModel ("modelos pesados"), por lo que el filtrado debe realizarse a nivel de modelo (o nivel de conjunto de vistas) y la serialización no debe saber nada sobre lógica empresarial.
oxfn
10

Si bien todas las respuestas anteriores funcionan, creo que el uso del Prefetchobjeto de Django es la forma más fácil de todas.

Supongamos que un Restaurantobj tiene muchos MenuItems, algunos de los cuales lo son is_remove == True, y solo desea los que no se eliminan.

En RestaurantViewSet, haz algo como

from django.db.models import Prefetch

queryset = Restaurant.objects.prefetch_related(
    Prefetch('menu_items', queryset=MenuItem.objects.filter(is_removed=False), to_attr='filtered_menu_items')
)

En RestaurantSerializer, haz algo como

class RestaurantSerializer(serializers.ModelSerializer):
    menu_items = MenuItemSerializer(source='filtered_menu_items', many=True, read_only=True)

Dennis Lau
fuente
Gran solución, estoy de acuerdo en que esta es la mejor manera de resolverlo.
Jordan
Esto debería estar en la parte superior. La solución superior actual filtra los datos con to_representation después de que ya se hayan extraído de la base de datos. Esta solución filtra los datos de la consulta y los recupera en una solicitud masiva. Mucho mejor para la mayoría de los casos.
Alex
7

Cuando se crea una instancia de un serializador y se pasa many = True, se creará una instancia de ListSerializer. La clase de serializador luego se convierte en un elemento secundario del ListSerializer principal

Este método toma el destino del campo como argumento de valor y debe devolver la representación que se debe usar para serializar el destino. El argumento de valor suele ser una instancia de modelo.

A continuación se muestra el ejemplo del serializador anidado

class UserSerializer(serializers.ModelSerializer):
    """ Here many=True is passed, So a ListSerializer instance will be 
     created"""
    system = SystemSerializer(many=True, read_only=True)

    class Meta:
        model = UserProfile
        fields = ('system', 'name')

class FilteredListSerializer(serializers.ListSerializer):
    
    """Serializer to filter the active system, which is a boolen field in 
       System Model. The value argument to to_representation() method is 
      the model instance"""
    
    def to_representation(self, data):
        data = data.filter(system_active=True)
        return super(FilteredListSerializer, self).to_representation(data)

class SystemSerializer(serializers.ModelSerializer):
    mac_id = serializers.CharField(source='id')
    system_name = serializers.CharField(source='name')
    serial_number = serializers.CharField(source='serial')

    class Meta:
        model = System
        list_serializer_class = FilteredListSerializer
        fields = (
            'mac_id', 'serial_number', 'system_name', 'system_active', 
        )

En vista:

class SystemView(viewsets.GenericViewSet, viewsets.ViewSet):
    def retrieve(self, request, email=None):
        data = get_object_or_404(UserProfile.objects.all(), email=email)
        serializer = UserSerializer(data)
        return Response(serializer.data)
Vinay Kumar
fuente
4

Me resulta más fácil, y más sencillo, usar un SerializerMethodFielden el campo del serializador que desea filtrar.

Entonces harías algo como esto.

class CarTypesSerializer(serializers.ModelSerializer):

    class Meta:
        model = CarType
        fields = '__all__'


class CarSerializer(serializers.ModelSerializer):

    car_types = serializers.SerializerMethodField()

    class Meta:
        model = Car
        fields = '__all__'

    def get_car_types(self, instance):
        # Filter using the Car model instance and the CarType's related_name
        # (which in this case defaults to car_types_set)
        car_types_instances = instance.car_types_set.filter(brand="Toyota")
        return CarTypesSerializer(car_types_instances, many=True).data

Esto le evita tener que crear muchas modificaciones serializers.ListSerializersi necesita diferentes criterios de filtrado para diferentes serializadores.

También tiene el beneficio adicional de ver exactamente lo que hace el filtro dentro del serializador en lugar de sumergirse en una definición de subclase.

Por supuesto, la desventaja es que si tiene un serializador con muchos objetos anidados, todos deben filtrarse de alguna manera. Podría hacer que el código del serializador aumente considerablemente. Depende de usted cómo le gustaría filtrar.

¡Espero que esto ayude!

Rob B
fuente