Django-rest-framework: Suporte para ordenação por campos relacionados aninhados em OrderingFilter

Criado em 25 jul. 2013  ·  28Comentários  ·  Fonte: encode/django-rest-framework

Um exemplo de uso seria a classificação por um objeto relacionado aninhado.

Representação aninhada:

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

Uma opção é suportar a notação de sublinhado duplo django orm __ para modelos relacionados.

Ex. ?ordering=stats__facebook_friends classificaria por facebook_friends.

Atualmente, o pedido funciona apenas para campos do modelo específico especificado no queryset.

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

Comentários muito úteis

Obrigado pela dica: +1:. O link permite a correção. Isso funciona

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

Todos 28 comentários

Eu consideraria puxar solicitações para isso, mas não é algo que eu teria tempo de fazer sozinho.

Qualquer pessoa que queira implementar isso precisa garantir um comportamento sensato quando nomes de campo incorretos são usados ​​na filtragem. No mínimo, os resultados ainda devem retornar bem. (Idealmente, quaisquer nomes de campo incorretos devem ser ignorados, mas os outros campos devem ser deixados.)

Pensando bem, vou encerrar isso. Se alguém emitir uma solicitação pull e testar que lida com isso, então eu devo reconsiderar, mas implementações mais completas de cada uma das classes básicas de filtragem é realmente algo que alguém poderia resolver muito bem em um pacote de terceiros, que poderia então ser mantido separadamente, e vinculado a partir dos documentos principais.

Sem solicitação pull porque não tive tempo de escrever testes ou documentos, mas caso isso ajude outras pessoas a encontrar esse problema, estou usando:

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 sua versão não permite ordenação reversa de relação, apenas fk's diretos aqui são a versão corrigida

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

O snippet de pySilver funcionou bem para mim, podemos colocar isso no DRF por acaso?

De onde você importa RelatedObject ? Não parece estar disponível para mim

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'

ah, parece que foi removido do 1.8, consulte https://code.djangoproject.com/ticket/21414

Obrigado pela dica: +1:. O link permite a correção. Isso funciona

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

Não teve problemas com a última versão do django rest framework :)

Não se esqueça de especificar corretamente o seu ordering_fields

Apenas uma nota - no Django> 1.10 alguns métodos mudaram

Especificamente, você terá que alterar o seguinte:

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

e a assinatura de remove_invalid_fields mudou para:

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

O que resulta na versão final de trabalho para 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('-'))]

O código acima terá problemas com OneToOneField, adicionando uma correção para isso:

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 e field.rel.to irão gerar um aviso de depreciação no Django> = 1.10. Eles são agora respectivamente:

  • field.remote_field
  • field.model

@tomchristie, considerando que este patch foi persistente (e constantemente atualizado) nesta edição por 4 anos (temos usado isso em produção desde o ano passado), poderia ser adequado para um PR e incorporado em DRF (sim, apoiado por um conjunto adequado de testes de unidade)?

Apenas meus 0,2 centavos

Este patch funciona para mim, podemos mesclá-lo com as versões de lançamento?

Como dito anteriormente, torne-o um terceiro (preferencial) ou ele precisa de um RP adequado com testes e documentação.

@filiperinaldi Acho que em Django> = 1,10 field.rel.to se torna field.related_model . Esta é a versão mais recente do patch que passou em nossos testes de unidade.

nb Estamos usando Django 1.11 então 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('-'))]

Ok pessoal, para o 4º aniversário desta edição, decidi tentar e hackear juntos um pacote externo para ser instalado junto com o DRF, a fim de oferecer suporte a esta ordem útil:

https://github.com/apiraino/djangorestframework_custom_filters_ordering

Estarei trabalhando nisso nos próximos dias para terminar o trabalho (ou seja, preciso empacotar e fatorar o código corretamente, refinar os testes e garantir o suporte para versões do Django com suporte ativo).

As contribuições são bem-vindas, é claro!

Felicidades

ref # 5533.

Estou muito confuso. Isso parece funcionar por padrão?

Eu cheguei a esse problema, li todos os comentários e continuei a implementar a solução por @apiraino , mas então descobri que simplesmente havia digitado errado o nome do meu campo relacionado.

No entanto, agora estou usando ?ordering=job__customer__company para um relacionamento aninhado duplo para ordenar os resultados da API e está funcionando bem.

@halfnibble - acredito que isso foi corrigido no # 5533.

cc @carltongibson

@halfnibble , qual versão você usou? Estou no 3.8.2 e não está funcionando para mim. Estas são minhas versões:

    "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"
    ]

Desculpe, agora eu entendo. Por padrão, ele só funcionará se o parâmetro de pedido relacionado estiver incluído em ordering_fields . Mas para uma solução mais geral, o patch ainda é necessário.

Olá, sou relativamente novo no DRF e estou procurando uma maneira de adicionar pedidos aos meus campos de pedidos sem expor a estrutura do nosso banco de dados. Por exemplo, se eu quisesse procurar z, precisaria escrever x__y__z e passar isso para o ponto de extremidade como um parâmetro expõe a estrutura. É isso que estou procurando se eu só quiser dizer z e fazer com que a função verifique se é um campo relacionado no ORM?

Você pode tentar substituir OrderingFilter.get_ordering . Algo como...

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 Muito obrigado! Isso é exatamente o que eu estava procurando. Agradeço a ajuda.

Aqui está uma solução que criei:

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__'
Esta página foi útil?
0 / 5 - 0 avaliações