Jak odfiltrować zagnieżdżony serializator w Django Rest Framework?

84

W Django Rest Framework, jak filtrować serializator, gdy jest on zagnieżdżony w innym serializatorze?

Moje filtry są nakładane w zestawach widoków DRF, ale kiedy wywołujesz serializator z innego serializatora, zestaw widoków zagnieżdżonego serializatora nigdy nie jest wywoływany, więc zagnieżdżone wyniki są wyświetlane jako niefiltrowane.

Próbowałem dodać filtr na źródłowym zestawie widoków, ale nie wydaje się, aby filtrował zagnieżdżone wyniki, ponieważ zagnieżdżone wyniki są wywoływane jako oddzielne wstępnie przygotowane zapytanie. (Widzisz, zagnieżdżony serializator jest wyszukiwaniem wstecznym).

Czy można dodać przesłonięcie get_queryset () w samym zagnieżdżonym serializatorze (przenosząc go z zestawu widoków), aby dodać tam filtr? Też próbowałem, ale bez powodzenia.

To jest to, czego próbowałem, ale nawet nie wydaje się, żebym go wywołał:

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
Jan
źródło
6
get_querysetto klasa włączona ModelViewSet, a nie w Serializatorze, dlatego nie jest wywoływana
NotSimon

Odpowiedzi:

99

Możesz podklasować ListSerializer i nadpisać to_representationmetodę.

Domyślnie to_representationmetoda wywołuje data.all()zagnieżdżony zestaw zapytań. Więc musisz skutecznie zrobić, data = data.filter(**your_filters)zanim metoda zostanie wywołana. Następnie musisz dodać podklasę ListSerializer jako list_serializer_class w meta zagnieżdżonego serializatora.

  1. podklasa ListSerializer, nadpisując, to_representationa następnie wywołując super
  2. dodaj podklasę ListSerializer jako meta list_serializer_classw zagnieżdżonym Serializatorze

Oto odpowiedni kod dla Twojej próbki.

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
w perspektywie
źródło
1
To załatwiło sprawę! Chociaż w końcu zdecydowałem, że moje serializatory stają się zbyt złożone i ponownie je wszystkie, zmuszając klienta do uruchomienia kilku dodatkowych wywołań API, ale znacznie upraszczając moją aplikację.
Jan
3
Próbując wykorzystać to jako podstawę rozwiązania podobnego problemu; nie jestem pewien, czy naprawdę zasługuje na własne pytanie. Jak mogę przekazać var ​​z QuestionnaireSerializerdo ListSerializer? Dla przybliżenia muszę filtrować według ID wydania oraz ID kwestionariusza.
Brendan,
3
Powinno to znaleźć się w dokumentacji DRF. Bardzo przydatne, dziękuję!
Daniel van Flymen
7
W mojej implementacji otrzymuję 'FilteredListSerializer' object has no attribute 'request'Ktoś inny dostaje to samo?
Dominooch
11
Aby odpowiedzieć na @Dominooch, musisz użyć self.context ['request'] zamiast self.request
rojoca Kwietnia
25

Przetestowałem wiele rozwiązań z SO i nie tylko.

Znalazłem tylko jedno działające rozwiązanie dla Django 2.0 + DRF 3.7.7.

Zdefiniuj metodę w modelu, który ma zagnieżdżoną klasę. Stwórz filtr, który będzie pasował do Twoich potrzeb.

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',)

Zwróć uwagę, source="current_epg"a zrozumiesz.

duddits
źródło
Tak! Ten komentarz wykorzystuje zdolność źródła do bycia funkcją, którą definiujesz w modelu, a następnie możesz skorzystać z filtrowania tam! Fajne!
possumkeys
czy możliwe jest przekazanie ciągu do funkcji pod klasą?
AlexW
Potrzebowałem tylko zamówienia pola związanego z wieloma tomami. Próbowałem też wielu różnych rozwiązań (gra słów zamierzona). Ale to było jedyne rozwiązanie, które zadziałało! Dzięki!
gabn88
Wygląda na to, że jest to bardziej poprawne rozwiązanie pod względem filozofii kodu django niż akceptowana odpowiedź. Django proponuje podejście ActiveModel („grube modele”), więc filtrowanie powinno odbywać się na poziomie modelu (lub na poziomie zestawu widoków), a serializacja nie powinna wiedzieć nic o logice biznesowej.
oxfn
14

Chociaż wszystkie powyższe odpowiedzi działają, uważam, że użycie obiektu Django jest Prefetchnajłatwiejsze ze wszystkich.

Powiedzmy, że obiekt Restaurantma wiele MenuItems, z których niektóre są is_remove == True, a chcesz tylko te, które nie są usuwane.

W RestaurantViewSet, zrób coś takiego

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')
)

W RestaurantSerializer, zrób coś takiego

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

Dennis Lau
źródło
2
Świetne rozwiązanie, zgadzam się, że to najlepszy sposób na rozwiązanie tego problemu.
Jordan
To powinno być u góry. Bieżące najlepsze rozwiązanie filtruje dane za pomocą to_representation po tym, jak zostały już pobrane z bazy danych. To rozwiązanie filtruje dane w zapytaniu i pobiera je w żądaniu zbiorczym. O wiele lepiej w większości przypadków.
Alex
To bardzo pomogło, dziękuję!
jepsolutnie
7

Po utworzeniu wystąpienia serializatora i przekazaniu wielu = True zostanie utworzone wystąpienie ListSerializer. Klasa serializatora staje się następnie elementem podrzędnym ListSerializer nadrzędnego

Ta metoda przyjmuje cel pola jako argument wartości i powinna zwrócić reprezentację, która powinna być używana do serializacji celu. Argument wartość będzie zwykle wystąpieniem modelu.

Poniżej znajduje się przykład zagnieżdżonego serializatora

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', 
        )

Z uwagi:

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
źródło
5

Uważam, że łatwiejsze i prostsze jest użycie SerializerMethodFieldznaku w polu serializatora, które chcesz filtrować.

Więc zrobiłbyś coś takiego.

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

Dzięki temu nie musisz tworzyć wielu przesłonięć, serializers.ListSerializerjeśli potrzebujesz różnych kryteriów filtrowania dla różnych serializatorów.

Ma również dodatkową zaletę polegającą na tym, że widzi dokładnie, co robi filtr w serializatorze, zamiast nurkować w definicji podklasy.

Oczywiście wadą jest to, że masz serializator z wieloma zagnieżdżonymi obiektami, które muszą być w jakiś sposób filtrowane. Może to spowodować znaczny wzrost kodu serializatora. To od Ciebie zależy, jak chcesz filtrować.

Mam nadzieję że to pomoże!

Rob B.
źródło