Django-rest-framework: Поддержка упорядочивания по вложенным связанным полям в OrderingFilter

Созданный на 25 июл. 2013  ·  28Комментарии  ·  Источник: encode/django-rest-framework

Примером использования может быть сортировка по вложенному связанному объекту.

Вложенное представление:

{
    'username': 'george',
    'email': '[email protected]'
    'stats': {
        'facebook_friends': 560,
        'twitter_followers': 4043,
        ...
    },
},
{
    'username': 'michael',
    'email': '[email protected]'
    'stats': {
        'facebook_friends': 256,
        'twitter_followers': 120,
        ...
    },
},
...

Один из вариантов - поддержка нотации двойного подчеркивания __ в django orm для связанных моделей.

Бывший. ?ordering=stats__facebook_friends будет сортировать по facebook_friends.

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

https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/filters.py#L125

Самый полезный комментарий

Спасибо за подсказку: +1:. Ссылка пусть на исправление. Это работает

from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from rest_framework.filters import OrderingFilter


class RelatedOrderingFilter(OrderingFilter):
    """
    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:
            field, parent_model, direct, m2m = \
                model._meta.get_field_by_name(components[0])

            # reverse relation
            if isinstance(field, ForeignObjectRel):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, ordering, view):
        return [term for term in ordering
                if self.is_valid_field(queryset.model, term.lstrip('-'))]

Все 28 Комментарий

Я бы рассмотрел запросы на вытягивание для этого, но у меня не было бы времени делать это сам.

Любой, кто хочет реализовать это, должен обеспечить разумное поведение при использовании неправильных имен полей при фильтрации. По крайней мере, результаты все равно должны быть нормальными. (В идеале любые неправильные имена полей следует игнорировать, но другие поля следует оставить.)

Обдумывая, я закрою это. Если кто-то выдаст пул-реквест и тесты, которые имеют дело с этим, я мог бы пересмотреть, но более полные реализации каждого из основных классов фильтрации - это действительно то, что кто-то может действительно хорошо решить в стороннем пакете, который затем может поддерживаться отдельно, и связаны с основными документами.

Нет запроса на перенос, потому что у меня не было времени писать тесты или документы, но если это помогает другим людям найти эту проблему, я использую:

from django.db.models.fields import FieldDoesNotExist 
from rest_framework.filters import OrderingFilter

class RelatedOrderingFilter(OrderingFilter):
    """ 
    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related 
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:
            field, parent_model, direct, m2m = model._meta.get_field_by_name(components[0])
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, ordering):
        return [term for term in ordering if self.is_valid_field(queryset.model, term.lstrip('-'))]

@rhunwicks ваша версия не позволяет сортировку обратных отношений, только прямые fk - это исправленная версия

class RelatedOrderingFilter(OrderingFilter):
    """
    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:
            field, parent_model, direct, m2m = \
                model._meta.get_field_by_name(components[0])

            # reverse relation
            if isinstance(field, RelatedObject):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, ordering, view):
        return [term for term in ordering
                if self.is_valid_field(queryset.model, term.lstrip('-'))]

Фрагмент pySilver сработал для меня, можем ли мы случайно добавить его в DRF?

Откуда вы импортируете RelatedObject ? Мне кажется, что это недоступно

Python 3.4.3
Django==1.8.5
djangorestframework==3.2.3

@eldamir django.db.models.related.RelatedObject

ImportError: No module named 'django.db.models.related'

ах, кажется, он был удален из 1.8, см. https://code.djangoproject.com/ticket/21414

Спасибо за подсказку: +1:. Ссылка пусть на исправление. Это работает

from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from rest_framework.filters import OrderingFilter


class RelatedOrderingFilter(OrderingFilter):
    """
    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:
            field, parent_model, direct, m2m = \
                model._meta.get_field_by_name(components[0])

            # reverse relation
            if isinstance(field, ForeignObjectRel):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, ordering, view):
        return [term for term in ordering
                if self.is_valid_field(queryset.model, term.lstrip('-'))]

Не было проблем с последней версией django rest framework :)

Не забудьте правильно указать свои ordering_fields

Замечание - в Django> 1.10 несколько методов изменились.

В частности, вам нужно будет изменить следующее:

field, parent_model, direct, m2m = model._meta.get_field_by_name(components[0])
к
field = model._meta.get_field(components[0])

и подпись для remove_invalid_fields изменилась на:

def remove_invalid_fields(self, queryset, ordering, view, request):

В результате получается финальная рабочая версия для Django> 1.10:

class RelatedOrderingFilter(OrderingFilter):
    """
    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:

            field = model._meta.get_field(components[0])

            # reverse relation
            if isinstance(field, ForeignObjectRel):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, fields, view, request):
        return [term for term in fields if self.is_valid_field(queryset.model, term.lstrip('-'))]

В приведенном выше коде будет проблема с OneToOneField, добавив исправление для этого:

from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.reverse_related import ForeignObjectRel, OneToOneRel

from rest_framework.filters import OrderingFilter


class RelatedOrderingFilter(OrderingFilter):
    """
    Extends OrderingFilter to support ordering by fields in related models.
    """    

    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:

            field = model._meta.get_field(components[0])

            if isinstance(field, OneToOneRel):
                return self.is_valid_field(field.related_model, components[1])

            # reverse relation
            if isinstance(field, ForeignObjectRel):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.rel and len(components) == 2:
                return self.is_valid_field(field.rel.to, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, fields, view):
        return [term for term in fields
                if self.is_valid_field(queryset.model, term.lstrip('-'))]        

field.rel и field.rel.to вызовут предупреждение об устаревании Django> = 1.10. Теперь они соответственно:

  • field.remote_field
  • field.model

@tomchristie, учитывая, что этот патч задерживается (и постоянно обновляется) в этом выпуске в течение 4 лет (мы используем его в производстве с прошлого года), может ли он быть подходящим для PR и объединен с DRF (да, при поддержке правильный набор юнит-тестов)?

Только мои 0,2 цента

Этот патч мне подходит, можем ли мы объединить его с релизными версиями?

Как было сказано ранее, либо сделайте его сторонним (предпочтительным), либо ему потребуется надлежащий PR с тестами и документацией.

@filiperinaldi Я думаю, что в Django> = 1.10 field.rel.to становится field.related_model . Вот последняя версия патча, прошедшая наши модульные тесты.

nb Мы используем Django 1.11, поэтому ymmw

class RelatedOrderingFilter(filters.OrderingFilter):
    """

    See: https://github.com/tomchristie/django-rest-framework/issues/1005

    Extends OrderingFilter to support ordering by fields in related models
    using the Django ORM __ notation
    """
    def is_valid_field(self, model, field):
        """
        Return true if the field exists within the model (or in the related
        model specified using the Django ORM __ notation)
        """
        components = field.split('__', 1)
        try:
            field = model._meta.get_field(components[0])

            if isinstance(field, OneToOneRel):
                return self.is_valid_field(field.related_model, components[1])

            # reverse relation
            if isinstance(field, ForeignObjectRel):
                return self.is_valid_field(field.model, components[1])

            # foreign key
            if field.remote_field and len(components) == 2:
                return self.is_valid_field(field.related_model, components[1])
            return True
        except FieldDoesNotExist:
            return False

    def remove_invalid_fields(self, queryset, fields, ordering, view):
        return [term for term in fields
                if self.is_valid_field(queryset.model, term.lstrip('-'))]

Хорошо, люди, к 4-му дню рождения этого вопроса я решил попробовать и собрать внешний пакет, который будет установлен вместе с DRF, чтобы поддержать этот полезный порядок:

https://github.com/apiraino/djangorestframework_custom_filters_ordering

Я буду работать над этим в следующие дни, чтобы завершить работу (а именно, мне нужно правильно упаковать и разложить код, уточнить тестирование и обеспечить поддержку активно поддерживаемых версий Django).

Пожертвования, конечно, приветствуются!

Ваше здоровье

исх № 5533.

Я очень смущен. Кажется, это работает по умолчанию?

Я подошел к этой проблеме, прочитал все комментарии и приступил к реализации решения с помощью @apiraino , но затем обнаружил, что просто неправильно набрал имя связанного поля.

Однако теперь я использую ?ordering=job__customer__company для двойного вложенного отношения, чтобы упорядочить результаты API, и он работает нормально.

@halfnibble - я считаю, что это было исправлено в # 5533.

cc @carltongibson

@halfnibble , какой версией

    "install_requires": [
        "django==2.0.3",
        "coreapi==2.3.3",
        "django-filter==1.1.0",
        "djangorestframework-filters==0.10.2.post0",
        "djangorestframework-queryfields==1.0.0",
        "djangorestframework==3.8.2",
        "django-bulk-update==2.2.0",
        "django-cors-headers==2.4.0",
        "django-rest-auth[with_social]==0.9.2",
        "drf-yasg==1.6.0",
        "django-taggit==0.22.2",
        "google-api-python-client==1.6.2",
        "markdown==2.6.11",
        "pygments==2.2.0",
        "xlrd==1.1.0",
        "xlsxwriter==0.9.8",
        "factory-boy==2.10.0",
        "psycopg2-binary==2.7.4",
        "django-admin-tools==0.8.1"
    ]

Извините, теперь я понял. По умолчанию он будет работать только в том случае, если соответствующий параметр порядка включен в ordering_fields . Но для более общего решения все же требуется патч.

Привет, я относительно новичок в DRF и искал способ добавить порядок в мои поля заказов, не раскрывая структуру нашей базы данных. Например, если бы я хотел найти z, мне нужно было бы написать x__y__z и передать его в конечную точку в качестве параметра, раскрывающего структуру. Это то, что я ищу, если бы я хотел только сказать z и проверить функцию, чтобы увидеть, является ли это связанным полем в ORM?

Вы можете попробовать переопределить OrderingFilter.get_ordering . Что-то типа...

class CustomOrderingFilter(OrderingFilter):
    def get_ordering(self, request, queryset, view):
        ordering = super().get_ordering(request, queryset, view)
        field_map = {
            'z': 'x__y__z',
        }
        return [field_map.get(o, o) for o in ordering]

@rpkilby Большое спасибо! Это именно то, что я искал. Я ценю помощь.

Вот решение, которое я собрал:

class RelatedOrderingFilter(filters.OrderingFilter):
    _max_related_depth = 3

    <strong i="6">@staticmethod</strong>
    def _get_verbose_name(field: models.Field, non_verbose_name: str) -> str:
        return field.verbose_name if hasattr(field, 'verbose_name') else non_verbose_name.replace('_', ' ')

    def _retrieve_all_related_fields(
            self,
            fields: Tuple[models.Field],
            model: models.Model,
            depth: int = 0
    ) -> List[tuple]:
        valid_fields = []
        if depth > self._max_related_depth:
            return valid_fields
        for field in fields:
            if field.related_model and field.related_model != model:
                rel_fields = self._retrieve_all_related_fields(
                    field.related_model._meta.get_fields(),
                    field.related_model,
                    depth + 1
                )
                for rel_field in rel_fields:
                    valid_fields.append((
                        f'{field.name}__{rel_field[0]}',
                        self._get_verbose_name(field, rel_field[1])
                    ))
            else:
                valid_fields.append((
                    field.name,
                    self._get_verbose_name(field, field.name),
                ))
        return valid_fields

    def get_valid_fields(self, queryset: models.QuerySet, view, context: dict = None) -> List[tuple]:
        valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
        if not valid_fields == '__all_related__':
            if not context:
                context = {}
            valid_fields = super().get_valid_fields(queryset, view, context)
        else:
            valid_fields = [
                *self._retrieve_all_related_fields(queryset.model._meta.get_fields(), queryset.model),
                *[(key, key.title().split('__')) for key in queryset.query.annotations]
            ]
        return valid_fields
````

Then I add this to wherever I want to be able to order by all related fields:
```python
filter_backends = (RelatedOrderingFilter,)
ordering_fields = '__all_related__'
Была ли эта страница полезной?
0 / 5 - 0 рейтинги