djangorestframework-queryfields icon indicating copy to clipboard operation
djangorestframework-queryfields copied to clipboard

New Feature - Nested Fields

Open stunaz opened this issue 7 years ago • 14 comments

Hi, Thank you for the lib. Would it be possible to add a mechanism for nested fields? i.e. some how : Given GET http://127.0.0.1:8000/things/

[
  {
    "id": 1,
    "key1": "val1",
    "key2": "val2",
    "key3":{
      "id": 2,
      "key1": "valA",
      "key2": "valB",
      "key3": "valC",
    },
  }
]

then we could do :

GET http://127.0.0.1:8000/things/?fields=id,key3__id, key3__key2

[
  {
    "id": 1,
    "key3":{
      "id": 2,
      "key2": "valB"
    },
  }
]

stunaz avatar Mar 15 '17 13:03 stunaz

That's a good idea. I assume you didn't intend to include "key1": "val1", in the example response here? I'll have a look and see what I can do. Handling exclusion queries will need some design decisions - could you add examples for what you think makes most sense for those?

wimglenn avatar Mar 16 '17 03:03 wimglenn

@wimglenn yes indeed, meant to exclude "key1": "val1", removed it. exclusions would work the same way: i.e.: with initial example from my first post

GET http://127.0.0.1:8000/things/?fields!=id,key3__id, key3__key2

[
  {
    "key1": "val1",
    "key2": "val2",
    "key3":{
      "id": 2,
      "key3": "valC",
    },
  }
]

stunaz avatar Mar 16 '17 22:03 stunaz

Why not like this?

[
  {
    "key1": "val1",
    "key2": "val2",
    "key3":{
      "key1": "valA",
      "key3": "valC",
    },
  }
]

wimglenn avatar Mar 17 '17 15:03 wimglenn

@wimglenn exactly

stunaz avatar Mar 17 '17 15:03 stunaz

FWIW, the other project claims to have this functionality: https://github.com/dbrgn/drf-dynamic-fields/blob/master/drf_dynamic_fields/init.py

mlissner avatar Apr 20 '17 22:04 mlissner

@mlissner Could you clarify? I don't see that claim anywhere there, nor code to support that feature. And there is no test coverage showing nested filtering working.

wimglenn avatar Apr 20 '17 22:04 wimglenn

Yeah, I didn't see the code either, but I also didn't understand it all. Here's where they claim it supports nested fields:

https://github.com/dbrgn/drf-dynamic-fields/blob/master/CHANGELOG.md#020---2017-04-07

And this seems to (maybe) be where it was implemented (judging by commit msg):

https://github.com/dbrgn/drf-dynamic-fields/pull/14

Not sure. Maybe I'm misunderstanding something?

mlissner avatar Apr 20 '17 22:04 mlissner

@wimglenn Any plans to implement this feature?

adinvadim avatar Jun 21 '18 19:06 adinvadim

Hey this would be so amazing!

jsmedmar avatar Aug 20 '18 16:08 jsmedmar

Yeah, I didn't see the code either, but I also didn't understand it all. Here's where they claim it supports nested fields:

https://github.com/dbrgn/drf-dynamic-fields/blob/master/CHANGELOG.md#020---2017-04-07

And this seems to (maybe) be where it was implemented (judging by commit msg):

dbrgn/drf-dynamic-fields#14

Not sure. Maybe I'm misunderstanding something?

From my understanding, they are fixing a mistake that the filter ALSO apply to subserializer. For example,

{
    "id": 1,
    "key1": "val1",
    "key2": {
        "id": 2,
        "key1": "valA"
    }
}

If you supply ?omit=key1, the expect result should be:

{
    "id": 1,
    "key2": {
        "id": 2,
        "key1": "valA"
    }
}

But actual result was:

{
    "id": 1,
    "key2": {
        "id": 2,
    }
}

The OP here, however, want to able to filter the nested serializer, and (I believe) should be explicit. So, ?fields!=key1 will only apply to first level but not sub-levels, and ?fields!=key2__key1 will only apply to second level but not root level.

johncpang avatar Sep 13 '18 06:09 johncpang

There is another project that said to have this functionality: drf_tweaks. Just have a brief look in their source and it's much bigger than djangorestframework-queryfields. Some of us prefer smaller better.

Here is a short summary after I tried both of them.

djangorestframework-queryfields works on first level only. drf_tweaks works on multiple levels (nested-serializer).

djangorestframework-queryfields allows you to define the list of fields you want. drf_tweaks allows you to define the list of fields you want.

djangorestframework-queryfields allows you to define the list of fields you don't want (and you get all the remainings). If you want all fields, do nothing. drf_tweaks can do similar but the other way: first excluding them using 'on_demand_fields', and bring them back using '?include_fields='. If you want all fields, you need to use '?include_fields'.

  • use "?include_fields=" when you want those fields back.
  • do nothing when you want those fields excluded.

Maybe we can include both mixins on serializer (umm the serializer getting fat). Since both of them are available in mixins, we selectively apply one or both per serializer.

johncpang avatar Sep 13 '18 06:09 johncpang

I think django-restql is more advanced and simpler, it works much like GraphQL.

yezyilomo avatar May 10 '19 20:05 yezyilomo

@mlissner I have checked 'drf-dynamic-fields' package but still it is also not doing fields filtering on nested fields

shafiyal avatar Jun 27 '19 13:06 shafiyal

I found a good intersection between djangorestframework-queryfields and django-restql that the owner of this repo may want to look at (or any other person). Here I propose a QueryFieldsMixin that supports both functionalities, using both projects, and motivated by these two ideas:

  • I love the fact that djangorestframework-queryfields optimize queries to avoid requesting more data than required from database
  • However, it's a bummer that nested filtering is not yet supported

This solution supports queries in this format:

GET http://127.0.0.1:8000/things/?fields=id,key3__id, key3__key2

Query Syntax

First we need to be able to convert between djangorestframework-queryfields query syntax to django-restql syntax.

from collections import defaultdict


def fields2restql(include_str):
    """Convert include string to restql query format."""
    fields_list = [i for i in (include_str or "").split(",") if i]

    def _f2q(fields_list):
        query, fields_dict = "", defaultdict(list)

        for i in sorted(set(fields_list)):
            split = i.split("__", 1)

            if len(split) == 2:
                fields_dict[split[0].strip()].append(split[1].strip())
            else:
                fields_dict[split[0].strip()] = []

        for field, nested in sorted(fields_dict.items()):
            new_query = (field + " " + _f2q(nested)) if nested else field
            query = ", ".join(i for i in [query, new_query] if i)

        return "{{{0}}}".format(query) if query else query

    return _f2q(fields_list)

A new QueryFieldsMixin

This mixin can be used interchangeably with that from djangorestframework-queryfields.

from django_restql.exceptions import FieldNotFound
from django_restql.exceptions import FormatError
from django_restql.exceptions import InvalidField
from django_restql.mixins import get_formatted_query
from django_restql.mixins import parse_query
from drf_queryfields import QueryFieldsMixin
from rest_framework.exceptions import NotFound
from rest_framework.exceptions import ParseError
import dictfier


class CombinedQueryFieldsMixin(QueryFieldsMixin):
    def __init__(self, *args, **kwargs):
        """Add suport for restql filtering."""
        super(QueryFieldsMixin, self).__init__(  # pylint: disable=bad-super-call
            *args, **kwargs
        )

        try:
            request = self.context["request"]
        except (AttributeError, TypeError, KeyError):
            # The serializer was not initialized with request context.
            return

        query_params = getattr(request, "query_params", {})
        include_str = query_params.get(self.include_arg_name, "")

        if "__" in include_str:
            self.context["restql_query"] = self.get_dictfier_query(include_str)
            request._request.GET = request._request.GET.copy()
            request.query_params[self.include_arg_name] = ",".join(
                [i.split("__")[0] for i in include_str.split(",")]
            )

        super().__init__(*args, **kwargs)

    @property
    def data(self):
        ret = super().data
        query = self.context.get("restql_query")
        return dictfier.filter(ret, query) if query else ret

    def get_dictfier_query(self, include_str):
        try:
            raw_query = parse_query(fields2restql(include_str))
            schema = self.get_fields()
            return get_formatted_query(raw_query, schema)
        except FormatError as error:
            raise ParseError({"error": error})
        except FieldNotFound as error:
            raise NotFound({"error": error})
        except InvalidField as error:
            raise ParseError({"error": error})

    @classmethod
    def many_init(cls, *args, **kwargs):
        meta = getattr(cls, "Meta", None)

        if meta:
            list_serializer_class = getattr(
                meta, "list_serializer_class", serializers.ListSerializer
            )

            class ListSerializer(  # pylint:disable=abstract-method
                list_serializer_class
            ):
                @property
                def data(self):
                    ret = super().data
                    query = self.context.get("restql_query")
                    return dictfier.filter(ret, [query]) if query else ret

            meta.list_serializer_class = ListSerializer

        return super().many_init(*args, **kwargs)

Testing

For those interested I wrote some tests here https://github.com/yezyilomo/django-restql/issues/26.

Conclusions

Wanted to share this solution here in case other people may benefit from it. I think it also provides some hints into how djangorestframework-queryfields could be extended to support nested fields by using something like dictfier as in django-restql.

jsmedmar avatar Jun 29 '19 20:06 jsmedmar