Django-rest-framework: Soporte para ordenar por campos relacionados anidados en OrderingFilter

Creado en 25 jul. 2013  ·  28Comentarios  ·  Fuente: encode/django-rest-framework

Un ejemplo de uso sería ordenar por un objeto relacionado anidado.

Representación anidada:

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

Una opción es admitir la notación de subrayado doble de django orm __ para modelos relacionados.

Ex. ?ordering=stats__facebook_friends ordenaría por facebook_friends.

Actualmente, ordenar solo funciona para campos del modelo particular especificado en queryset.

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

Comentario más útil

Gracias por el consejo: +1:. El enlace deja la solución. Esto 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 comentarios

Consideraría solicitudes de extracción para esto, pero no es algo para lo que tenga tiempo de hacer yo mismo.

Cualquiera que desee implementar esto debe garantizar un comportamiento sensato cuando se utilizan nombres de campo incorrectos en el filtrado. Como mínimo, los resultados deberían volver bien. (Lo ideal es que se ignoren los nombres de campo incorrectos, pero se deben dejar los demás campos).

Considerándolo, voy a cerrar esto. Si alguien emite una solicitud de extracción y prueba que se ocupa de ello, entonces podría reconsiderarlo, pero las implementaciones más completas de cada una de las clases de filtrado básicas son realmente algo que alguien podría abordar muy bien en un paquete de terceros, que luego podría mantenerse por separado. y vinculado a desde los documentos principales.

Sin solicitud de extracción porque no he tenido tiempo de escribir pruebas o documentos, pero en caso de que esto ayude a otras personas a encontrar este problema, estoy 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 su versión no permite la clasificación de relación inversa, solo la versión directa de fk aquí está parcheada

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

El fragmento de pySilver funcionó bien para mí, ¿podemos poner esto en DRF por casualidad?

¿De dónde importas RelatedObject ? No parece estar disponible para mi

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 se ha eliminado de 1.8, consulte https://code.djangoproject.com/ticket/21414

Gracias por el consejo: +1:. El enlace deja la solución. Esto 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('-'))]

No tuve el problema con la última versión de django rest framework :)

No olvide especificar correctamente su ordering_fields

Solo una nota: en Django> 1.10, un par de métodos han cambiado

Específicamente, tendrás que cambiar lo siguiente:

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

y la firma de remove_invalid_fields ha cambiado a:

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

Lo que da como resultado la versión de trabajo final 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('-'))]

El código anterior tendrá problemas con OneToOneField, agregando una solución para eso:

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 y field.rel.to generarán una advertencia de obsolescencia en Django> = 1.10. Ahora son respectivamente:

  • field.remote_field
  • field.model

@tomchristie teniendo en cuenta que este parche se ha mantenido (y se ha actualizado constantemente) en este número durante 4 años (lo hemos estado usando en producción desde el año pasado), ¿podría ser adecuado para un PR y se fusionó con DRF (sí, respaldado por un conjunto adecuado de pruebas unitarias)?

Solo mis .2 centavos

Este parche me funciona, ¿podemos fusionarlo con las versiones de lanzamiento?

Como se dijo anteriormente, conviértalo en un tercero (preferido) o necesita un PR adecuado con pruebas y documentación.

@filiperinaldi Creo que en Django> = 1.10 field.rel.to convierte en field.related_model . Aquí está la última versión del parche que pasa nuestras pruebas unitarias.

nb Estamos usando Django 1.11 así que mmm

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

Bien, gente, para el cuarto cumpleaños de este número, decidí intentarlo y hackear un paquete externo para instalarlo junto con DRF para admitir este pedido útil:

https://github.com/apiraino/djangorestframework_custom_filters_ordering

Trabajaré en él en los próximos días para terminar el trabajo (es decir, necesito empaquetar y factorizar correctamente el código, refinar las pruebas y garantizar el soporte para las versiones de Django con soporte activo).

¡Las contribuciones son bienvenidas, por supuesto!

Salud

ref # 5533.

Estoy muy confundido. ¿Parece que esto funciona de forma predeterminada?

Llegué a este problema, leí todos los comentarios y procedí a implementar la solución de @apiraino , pero luego descubrí que simplemente había escrito mal el nombre de mi campo relacionado.

Sin embargo, ahora estoy usando ?ordering=job__customer__company para una relación anidada doble para ordenar los resultados de la API y está funcionando bien.

@halfnibble : creo que esto se solucionó en el n. ° 5533.

cc @carltongibson

@halfnibble , ¿qué versión

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

Lo siento, ahora lo entiendo. De forma predeterminada, solo funcionará si el parámetro de pedido relacionado se incluye en ordering_fields . Pero para una solución más general, todavía se requiere el parche.

Hola, soy relativamente nuevo en DRF y estaba buscando una manera de agregar pedidos a los campos de mi pedido sin exponer la estructura de nuestra base de datos. Por ejemplo, si quisiera buscar z, necesitaría escribir x__y__z, y pasar eso al punto final como parámetro expone la estructura. ¿Es esto lo que estoy buscando si solo quisiera decir zy que la función verifique si es un campo relacionado en el ORM?

Podría intentar anular 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 ¡ Muchas gracias! Esto es exactamente lo que estaba buscando. Agradezco la ayuda

Aquí hay una solución que armé:

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__'
¿Fue útil esta página
0 / 5 - 0 calificaciones