Django-rest-framework: Unterstützung für die Sortierung nach verschachtelten verwandten Feldern in OrderingFilter

Erstellt am 25. Juli 2013  ·  28Kommentare  ·  Quelle: encode/django-rest-framework

Ein Anwendungsbeispiel wäre das Sortieren nach einem verschachtelten verwandten Objekt.

Verschachtelte Darstellung:

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

Eine Möglichkeit besteht darin, die django orm-Doppelunterstrich-Notation __ für verwandte Modelle zu unterstützen.

Ex. ?ordering=stats__facebook_friends würde nach facebook_friends sortieren.

Derzeit funktioniert die Sortierung nur für Felder des bestimmten Modells, das im Abfragesatz angegeben ist.

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

Hilfreichster Kommentar

Danke für den Tipp :+1: . Der Link führte zum Fix. Das funktioniert

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

Alle 28 Kommentare

Ich würde dafür Pull-Requests in Betracht ziehen, aber dafür hätte ich selbst keine Zeit.

Wer dies umsetzen möchte, muss auf ein vernünftiges Verhalten achten, wenn bei der Filterung falsche Feldnamen verwendet werden. Zumindest sollten die Ergebnisse immer noch in Ordnung sein. (Idealerweise sollten alle falschen Feldnamen ignoriert werden, andere Felder sollten jedoch beibehalten werden.)

Nach Überlegung schließe ich dies ab. Wenn jemand einen Pull-Request ausstellt und testet, der damit umgeht, überlege ich es mir vielleicht noch einmal, aber vollständigere Implementierungen jeder der grundlegenden Filterklassen sind wirklich etwas, das jemand wirklich gut in einem Drittanbieterpaket angehen könnte, das dann separat gepflegt werden könnte. und von den Hauptdokumenten verlinkt.

Keine Pull-Anfrage, da ich keine Zeit hatte, Tests oder Dokumente zu schreiben, aber falls dies anderen Leuten hilft, dieses Problem zu finden, verwende ich:

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 deine Version erlaubt keine umgekehrte Sortierung der Relationen, nur direkte fk's hier sind gepatchte Version

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

Das Snippet von pySilver hat bei mir gut funktioniert, können wir das zufällig in DRF bekommen?

Woher importieren Sie RelatedObject ? Scheint bei mir nicht verfügbar zu sein

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, anscheinend wurde es aus 1.8 entfernt, siehe https://code.djangoproject.com/ticket/21414

Danke für den Tipp :+1: . Der Link führte zum Fix. Das funktioniert

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

Hatte das Problem mit der letzten Version von Django Rest Framework nicht :)

Vergessen Sie nicht, Ihre ordering_fields korrekt anzugeben

Nur eine Anmerkung - in Django > 1.10 haben sich einige Methoden geändert

Im Einzelnen müssen Sie Folgendes ändern:

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

und die Signatur für remove_invalid_fields hat sich geändert zu:

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

Was in der endgültigen Arbeitsversion für Django > 1.10 resultiert:

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

Der obige Code hat ein Problem mit OneToOneField und fügt einen Fix dafür hinzu:

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 und field.rel.to geben eine veraltete Warnung auf Django >= 1.10 aus. Sie sind jetzt jeweils:

  • field.remote_field
  • field.model

@tomchristie in Anbetracht dessen, dass dieser Patch seit 4 Jahren in dieser Ausgabe verweilt (und ständig aktualisiert wird) (wir verwenden ihn seit letztem Jahr in der Produktion), könnte er für eine PR geeignet und in DRF zusammengeführt werden (ja, unterstützt von ein richtiger Satz von Unittests)?

Nur meine 0,2 Cent

Dieser Patch funktioniert bei mir, können wir ihn mit den Release-Versionen zusammenführen?

Wie bereits erwähnt, machen Sie es entweder zu einem Drittanbieter (bevorzugt) oder es benötigt eine angemessene PR mit Tests und Dokumentation.

@filiperinaldi Ich denke, dass in Django >= 1.10 field.rel.to zu field.related_model . Hier ist die neueste Version des Patches, die unsere Unittests besteht.

nb Wir verwenden Django 1.11, also 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 Leute, zum 4. Geburtstag dieser Ausgabe habe ich beschlossen, es auszuprobieren und ein externes Paket zusammenzustellen, das neben DRF installiert werden soll, um diese nützliche Reihenfolge zu unterstützen:

https://github.com/apiraino/djangorestframework_custom_filters_ordering

Ich werde in den nächsten Tagen daran arbeiten, um den Job abzuschließen (nämlich muss ich den Code richtig verpacken und faktorisieren, das Testen verfeinern und den Support für aktiv unterstützte Django-Versionen sicherstellen).

Beiträge sind natürlich willkommen!

Beifall

Ref #5533.

Ich bin sehr verwirrt. Dies scheint standardmäßig zu funktionieren?

Ich kam zu diesem Problem, las alle Kommentare und fuhr fort, die Lösung von @apiraino zu implementieren, aber dann stellte ich fest, dass ich lediglich den Namen meines verwandten Felds falsch eingegeben hatte.

Allerdings verwende ich jetzt ?ordering=job__customer__company für eine doppelt verschachtelte Beziehung, um die API-Ergebnisse zu sortieren, und es funktioniert gut.

@halfnibble - Ich glaube, das wurde in #5533 behoben.

cc @carltongibson

@halfnibble , welche Version hast du verwendet? Ich bin auf 3.8.2 und es funktioniert nicht für mich. Das sind meine Versionen:

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

Entschuldigung, jetzt verstehe ich. Standardmäßig funktioniert es nur, wenn der zugehörige Sortierparameter in ordering_fields . Für eine allgemeinere Lösung ist der Patch jedoch weiterhin erforderlich.

Hallo, ich bin relativ neu bei DRF und suchte nach einer Möglichkeit, meinen Bestellfeldern Bestellungen hinzuzufügen, ohne die Struktur unserer Datenbank offenzulegen. Wenn ich beispielsweise nach z suchen wollte, müsste ich x__y__z schreiben, und die Übergabe an den Endpunkt als Parameter macht die Struktur verfügbar. Ist dies das, was ich suche, wenn ich nur z sagen und die Funktion prüfen möchte, ob es sich um ein verwandtes Feld im ORM handelt?

Sie könnten versuchen, OrderingFilter.get_ordering überschreiben. Etwas wie...

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 Vielen Dank! Genau das habe ich gesucht. Ich schätze die Hilfe.

Hier ist eine Lösung, die ich zusammengestellt habe:

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__'
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen