Django-rest-framework: Prise en charge de la commande par champs connexes imbriqués dans OrderingFilter

Créé le 25 juil. 2013  ·  28Commentaires  ·  Source: encode/django-rest-framework

Un exemple d'utilisation serait le tri par un objet lié imbriqué.

Représentation imbriquée :

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

Une option consiste à prendre en charge la notation de soulignement double django orm __ pour les modèles associés.

Ex. ?ordering=stats__facebook_friends serait trié par facebook_friends.

Actuellement, la commande ne fonctionne que pour les champs du modèle particulier spécifié dans le jeu de requêtes.

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

Commentaire le plus utile

Merci pour le conseil :+1: . Le lien vers le correctif. Cela marche

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

Tous les 28 commentaires

J'envisagerais des demandes d'extraction pour cela, mais ce n'est pas quelque chose que j'aurais le temps de faire moi-même.

Quiconque cherche à implémenter cela doit s'assurer d'un comportement raisonnable lorsque des noms de champ incorrects sont utilisés dans le filtrage. Au minimum, les résultats devraient toujours être corrects. (Idéalement, tout nom de champ incorrect doit être ignoré, mais les autres champs doivent être laissés.)

Après réflexion, je vais clore ça. Si quelqu'un émet une pull request et des tests qui la traitent, je pourrais reconsidérer ma décision, mais des implémentations plus complètes de chacune des classes de filtrage de base sont vraiment quelque chose que quelqu'un pourrait très bien aborder dans un package tiers, qui pourrait alors être maintenu séparément, et lié à partir de la doc principale.

Pas de pull request car je n'ai pas eu le temps d'écrire des tests ou des docs, mais au cas où cela aiderait d'autres personnes à trouver ce problème, j'utilise :

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 votre version n'autorise pas le tri par relation inverse, seuls les fk directs ici sont une version corrigée

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

L'extrait de pySilver a bien fonctionné pour moi, pouvons-nous l'intégrer dans DRF par hasard ?

D'où importez-vous RelatedObject ? ne semble pas être disponible pour moi

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, il semble qu'il ait été supprimé de la version 1.8, voir https://code.djangoproject.com/ticket/21414

Merci pour le conseil :+1: . Le lien vers le correctif. Cela marche

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

Je n'ai pas eu de problème avec la dernière version de Django rest framework :)

N'oubliez pas de spécifier correctement votre ordering_fields

Juste une note - dans Django > 1.10, quelques méthodes ont changé

Plus précisément, vous devrez modifier les éléments suivants :

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

et la signature pour remove_invalid_fields est devenue :

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

Ce qui donne la version de travail finale pour 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('-'))]

Le code ci-dessus aura un problème avec OneToOneField, ajoutant un correctif pour cela :

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 et field.rel.to lèveront un avertissement de dépréciation sur Django >= 1.10. Ils sont désormais respectivement :

  • field.remote_field
  • modèle.de.champ

@tomchristie étant donné que ce correctif persiste (et est constamment mis à jour) dans ce numéro depuis 4 ans (nous l'utilisons en production depuis l'année dernière), pourrait-il convenir à un PR et fusionner dans DRF (oui, soutenu par un ensemble approprié de tests unitaires) ?

Juste mon .2 cents

Ce correctif fonctionne pour moi, pouvons-nous le fusionner avec les versions finales ?

Comme dit précédemment, soit en faire un tiers (préféré), soit il a besoin d'un PR approprié avec des tests et de la documentation.

@filiperinaldi je pense que dans Django >= 1.10 field.rel.to devient field.related_model . Voici la dernière version du patch qui passe nos tests unitaires.

nb Nous utilisons Django 1.11 donc 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 les gens, pour le 4e anniversaire de ce numéro, j'ai décidé de l'essayer et de pirater ensemble un package externe à installer aux côtés de DRF afin de prendre en charge cette commande utile :

https://github.com/apiraino/djangorestframework_custom_filters_ordering

Je travaillerai dessus dans les jours suivants pour terminer le travail (à savoir que je dois correctement empaqueter et factoriser le code, affiner les tests et assurer la prise en charge des versions Django activement prises en charge).

Les contributions sont les bienvenues, bien sûr !

Acclamations

réf #5533.

Je suis très confus. Cela semble fonctionner par défaut?

Je suis arrivé à ce problème, j'ai lu tous les commentaires et j'ai mis en œuvre la solution par @apiraino , mais j'ai ensuite découvert que j'avais simplement mal tapé le nom de mon champ connexe.

Cependant, j'utilise maintenant ?ordering=job__customer__company pour une relation double imbriquée pour ordonner les résultats de l'API et cela fonctionne bien.

@halfnibble - Je crois que cela a été corrigé dans #5533.

cc @carltongibson

@halfnibble , quelle version

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

Désolé, maintenant je comprends. Par défaut, cela ne fonctionnera que si le paramètre de commande associé est inclus dans ordering_fields . Mais pour une solution plus générale, le patch est toujours nécessaire.

Bonjour, je suis relativement nouveau sur DRF et je cherchais un moyen d'ajouter des commandes à mes champs de commande sans exposer la structure de notre base de données. Par exemple, si je voulais rechercher z, j'aurais besoin d'écrire x__y__z, et le passer dans le point de terminaison en tant que paramètre expose la structure. Est-ce ce que je recherche si je voulais seulement dire z et faire vérifier la fonction pour voir s'il s'agit d'un domaine connexe dans l'ORM ?

Vous pouvez essayer de remplacer OrderingFilter.get_ordering . Quelque chose comme...

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 Merci beaucoup ! Ceci est exactement ce que je cherchais. J'apprécie l'aide.

Voici une solution que j'ai mis en place:

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__'
Cette page vous a été utile?
0 / 5 - 0 notes