Як фільтрувати вкладений серіалізатор у Django Rest Framework?


84

У Django Rest Framework, як ви фільтруєте серіалізатор, коли він вкладений в інший серіалізатор?

Мої фільтри вводяться у набори перегляду DRF, але коли ви викликаєте серіалізатор зсередини іншого серіалізатора, набір переглядів вкладеного серіалізатора ніколи не викликається, тому вкладені результати здаються нефільтрованими.

Я спробував додати фільтр до вихідного набору переглядів, але, схоже, він не фільтрує вкладені результати, оскільки вкладені результати викликаються як окремий попередньо розібраний запит. (Як ви розумієте, вкладений серіалізатор - це зворотний пошук).

Чи можна додати перевизначення get_queryset () у сам вкладений серіалізатор (перемістивши його з набору переглядів), щоб додати туди фільтр? Я теж пробував це, не маючи удачі.

Це те, що я спробував, але, схоже, навіть не викликають:

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

6
get_querysetце клас на ModelViewSet, а не на Серіалізаторі, саме тому його не викликають
NotSimon

Відповіді:


99

Ви можете підкласувати ListSerializer і перезаписати to_representationметод.

За замовчуванням to_representationметод викликає data.all()вкладений набір запитів. Отже, вам потрібно ефективно зробити, data = data.filter(**your_filters)перш ніж метод буде викликаний. Потім вам потрібно додати свій підклас ListSerializer як list_serializer_class у мета вкладеного серіалізатора.

  1. підклас ListSerializer, перезапис, to_representationа потім виклик супер
  2. додати підкласний ListSerializer як мета list_serializer_classна вкладеному серіалізаторі

Ось відповідний код для вашого зразка.

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

1
Це зробило фокус! Хоча врешті-решт я вирішив, що мої серіалізатори стають занадто складними, і я переробив їх усі, змусивши клієнта запустити ще кілька дзвінків API, але значно спростивши мій додаток.
Джон

3
Намагаючись використати це як основу для вирішення подібної проблеми; не впевнений, чи справді це заслуговує власного питання. Як я можу передати var QuestionnaireSerializerу ListSerializer? Для наближення мені потрібно відфільтрувати за ідентифікатором видання, а також ідентифікатором анкети.
Brendan

3
Це має бути в документації DRF. Супер корисно дякую!
Даніель ван Флаймен

7
У своїй реалізації я отримую, щоб 'FilteredListSerializer' object has no attribute 'request'хтось інший отримував те саме?
Доміноч

11
Щоб відповісти @Dominooch, потрібно використовувати self.context ['запит'] замість self.request
rojoca

25

Перевірив багато рішень з SO та інших місць.

Знайдено лише одне робоче рішення для Django 2.0 + DRF 3.7.7.

Визначте метод у моделі, яка має вкладений клас. Створіть фільтр, який відповідатиме вашим потребам.

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

Зверніть увагу, source="current_epg"і ви зрозумієте суть.


Так! Цей коментар використовує здатність джерела бути функцією, яку ви визначаєте в моделі, тоді ви можете скористатися фільтруванням там! Класно!
posssumkeys

чи можна передати рядок функції у класі?
AlexW

Мені потрібне було лише замовлення багатьох пов'язаних з ними полів. Також спробував багато-багато різних рішень (задумано каламбур). Але це було єдине рішення, яке працювало для мене! Дякую!
gabn88

Схоже, це більш правильне рішення з точки зору філософії коду django, ніж прийнята відповідь. Django пропонує підхід ActiveModel ("жирні моделі"), тому фільтрація повинна здійснюватися на рівні моделі (або на рівні перегляду), а серіалізація не повинна нічого знати про бізнес-логіку.
oxfn

14

Хоча всі вищезазначені відповіді працюють, я вважаю використання об'єкта Django Prefetchнайпростішим способом із усіх.

Скажімо, у Restaurantobj є багато MenuItems, деякі з яких є is_remove == True, і ви хочете лише ті, які не видаляються.

В RestaurantViewSet, зробіть щось подібне

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

В RestaurantSerializer, зробіть щось подібне

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


2
Чудове рішення, я згоден, що це найкращий спосіб його вирішити.
Йорданія

Це повинно бути вгорі. Поточне найкраще рішення фільтрує дані за допомогою to_representation після того, як їх уже було вилучено з бази даних. Це рішення фільтрує дані запиту та отримує їх у масовому запиті. Набагато краще для більшості випадків.
Олексій

Це дуже допомогло, дякую!
абсолютно

7

Коли інстанціюється серіалізатор і передається багато = True, буде створений екземпляр ListSerializer. Клас серіалізатора потім стає нащадком батьківського ListSerializer

Цей метод приймає ціль поля як аргумент значення і повинен повертати подання, яке слід використовувати для серіалізації цілі. Аргумент значення зазвичай є екземпляром моделі.

Нижче наведено приклад вкладеного серіалізатора

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

З огляду на:

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)

5

Я вважаю, що простіше і прямолінійніше використовувати a SerializerMethodFieldу полі серіалізатора, яке потрібно відфільтрувати.

Тож ти зробив би щось подібне.

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

Це позбавляє вас від необхідності створювати багато перевизначень, serializers.ListSerializerякщо вам потрібні різні критерії фільтрації для різних серіалізаторів.

Він також має додаткову перевагу, бачачи, що саме робить фільтр у серіалізаторі, замість того, щоб занурюватися у визначення підкласу.

Звичайно, мінус полягає у тому, що у вас є серіалізатор із великою кількістю вкладених об’єктів, які всі потрібно якимось чином відфільтрувати. Це може призвести до значного збільшення коду серіалізатора. Це залежить від вас, як ви хочете фільтрувати.

Сподіваюся, це допомагає!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.