Graphene-django: DjangoFilterConnectionField and resolve support

Created on 21 Oct 2016  路  18Comments  路  Source: graphql-python/graphene-django

I found that the following example from the docs doesn't work correctly.

The thing is that if there is a resolver for a DjangoFilterConnectionField like in the example, the filtering and ordering created by DjangoFilterConnectionField will be ignored. Details below.

class Query(ObjectType):
    my_posts = DjangoFilterConnectionField(CategoryNode)

    def resolve_my_posts(self, args, context, info):
        # context will reference to the Django request
        if not context.user.is_authenticated():
            return Post.objects.none()
        else:
            return Post.objects.filter(owner=context.user)

The issues seems to be that DjangoFilterConnectionField ignores what the resolver returns.

@staticmethod
def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args,
                        root, args, context, info):
    filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
    order = args.get('order_by', None)
    qs = default_manager.get_queryset()
    if order:
        qs = qs.order_by(order)
    qs = filterset_class(data=filter_kwargs, queryset=qs)

    return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info)

Here the QuerySet is built just fine but the resolver does not have an impact on the QuerySet (which is fine if no resolver is used anyway).

The problem arrises when the DjangoConnectionField resolves the query.

@staticmethod
def connection_resolver(resolver, connection, default_manager, root, args, context, info):
    iterable = resolver(root, args, context, info)
    if iterable is None:
        iterable = default_manager
    iterable = maybe_queryset(iterable)
    if isinstance(iterable, QuerySet):
        _len = iterable.count()
    else:
        _len = len(iterable)
    connection = connection_from_list_slice(
        iterable,
        args,
        slice_start=0,
        list_length=_len,
        list_slice_length=_len,
        connection_type=connection,
        edge_type=connection.Edge,
        pageinfo_type=PageInfo,
    )
    connection.iterable = iterable
    connection.length = _len
    return connection

Here we consider the default_manager (which is the QuerySet that was built by DjangoFilterConnectionField) only if there resolver is None.

So you either can have the DjangoFilterConnection field or a resolver. But it should be possible to use both (to limit an initial QuerySet and check of authorisation, etc.).

Solution

The solution I use now locally by changing DjangoFilterConnectionField is the following:

@staticmethod
def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args,
                        root, args, context, info):
    filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
    order = args.get('order_by', None)

    def new_resolver(root, args, context, info):
        qs = resolver(root, args, context, info)
        if qs is None:
            qs = default_manager.get_queryset()
        if order:
            qs = qs.order_by(order)
        qs = filterset_class(data=filter_kwargs, queryset=qs)
        return qs

    return DjangoConnectionField.connection_resolver(new_resolver, connection, None, root, args, context, info)

Basically it wraps the resolver and uses its QuerySet if available or create one with the default_manager. It also guarantees that the resolver returns something and therefore we can set the default_manager to None just to make sure it there is no other way to get to the QuerySet.

I would have created a PR but I couldn't get the tests of the repo running locally. :(

PS: I'd also really like to get the tests running so I can contribute to the code directly. Any pointers how to get it running are really appreciated as I didn't have luck by following the instructions in "Contributing".

Most helpful comment

The above solution also fixes #21

All 18 comments

The above solution also fixes #21

@syrusakbary I was also unable to run tests for this project immediately after checking out.

However, I was able to run them after adapting setup.py file from graphql-python/graphene project. Unfortunately tests fail with some import errors, see this gist for pip list and setup.py test output.

@drakon one way to run tests is to install this package (e.g. into virtualenv), then going to the directory it was installed and running pytest command directly. I hope you can do it, write some tests and make a PR for your solution, since it works perfectly in the project I tested it in.

@michalochman Thanks for the input but it doesn't work that way either.

I tried this (Win7):

git clone https://github.com/graphql-python/graphene-django.git
cd graphene-django
virtualenv venv
venvScriptsactivate.bat
python setup.py install
pytest

And I get 13 errors:
"E ImproperlyConfigured: Requested setting DEFAULT_INDEX_TABLESPACE, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings."

Did you get this working by changing the setup.py file?

When I do: "python setup.py test" instead I get a linker error.

@drakon What you can do is to install graphene-django in the "develop mode" (I suggest forking this repo first and using appropriate repo URL below):

$ virtualenv venv
$ venv\Scripts\activate.bat
(venv)$ pip install -e git+https://github.com/graphql-python/graphene-django.git#egg=master  # this will put code in venv\src\master directory
(venv)$ pip install django-filter pytest pytest-django==2.9.1 mock psycopg2  # which are `tests_require` from setup.py
(venv)$ cd venv\src\master
(venv)$ pytest  # run tests

I am not sure if you would also need this, but I also had to pip install pbr funcsigs as these modules were missing when I ran pytest.

Then you can edit files in src\master and run tests.

Hi, any update on the issue?

Trying to get this working to allow for filtering of nested objects... so far unsuccessful.

@drakon 's fix works... I had a stale library and needed to install from my hash commit. But now I can filter on nested objects woohoo!

-e git://github.com/timothyjlaurent/graphene-django.git@94ab3d6d593eb514fad97132870fc99bbdca245d#egg=graphene-django

^^ if anyone else wants to try

I also tried to get the testing set up but running

python setup.py test

results in lots of errors:

E           ImproperlyConfigured: Requested setting LOGGING_CONFIG, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

I got the tests working by running this:

DJANGO_SETTINGS_MODULE=django_test_settings python setup.py test

But there are a couple of broken tests

Thanks for all the comments! Too bad we don't get the code running, otherwise I could create a PR (would love to since I've never done it one before here ^^). I tried it yesterday again for some time but wasn't able to not get a lot of errors.

Hello!

Coming from #21 ... Any updates? This is important :-(

Thanks for your time!

I am also locked in this error

Had problems using this commit because got "len" error.
Solved, adding: .qs to filterset_class.

@staticmethod
def connection_resolver(resolver, connection, default_manager,
                        filterset_class, filtering_args,
                        root, args, context, info):
    filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
    order = args.get('order_by', None)

    def new_resolver(root, args, context, info):
        qs = resolver(root, args, context, info)
        if qs is None:
            qs = default_manager.get_queryset()
        if order:
            qs = qs.order_by(order)
        qs = filterset_class(data=filter_kwargs, queryset=qs).qs  <<-- HERE
        return qs

    return DjangoConnectionField.connection_resolver(new_resolver,
                                                     connection, None, root,
                                                     args, context, info)

Update:
Just realized that the code was updated. I think you probably are right. Haven't updated my project yet to the latest version.

I'm not 100% sure but I think this might be wrong like that @krailler. Are you sure you are using the django.FilterSet class the right way?

class XYZFilter(django_filters.FilterSet):
...
xyz = MyDjangoFilterConnectionField(XYZ, filterset_class=XYZFilter)

I'm using it like that (and this seems to be the recommended way of using it).

@ayozemr I haven't got the tests running (haven't tried it since November though.).

My recommended solution would be to just use a custom class for this and override the function like I did above.

class MyDjangoFilterConnectionField(DjangoFilterConnectionField):
    """
    Workaround to make resolver work with DjangoFilterConnectionField

    See also: https://github.com/graphql-python/graphene-django/issues/30
    """
    @staticmethod
    def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args,
                            root, args, context, info):
        filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
        order = args.get('order_by', None)

        def new_resolver(root, args, context, info):
            qs = resolver(root, args, context, info)
            if qs is None:
                qs = default_manager.get_queryset()
            if order:
                qs = qs.order_by(order)
            qs = filterset_class(data=filter_kwargs, queryset=qs)
            return qs

        return DjangoConnectionField.connection_resolver(new_resolver, connection, None, root, args, context, info)

I now use this class instead of the DjangoFilterConnectionField. Not the prettiest solution but until the issue is fixed this works.

If someone gets the tests running, I'm happy to create a PR with the fix.

I now created a PR and pushed the changes mentioned here. I also included the fix from @krailler. Now the tests seem to fail and it's a bit hard to debug if they don't fail on my local machine.

If anyone finds out why the tests fail, please let me know. The changes work for my project and I couldn't reproduce the failing tests in it.

I realized this solution fails in tests because resolver can return a list instead of a queryset so this break tests. I found anther solution calculating intersection between the resolved relationship and the filtered manager, and this passed the tests.

Please check my PR #82 to verify this works in all cases.

@syrusakbary did you close this thread because you have a solution, or perhaps it is working as it should be? It would be beneficial to provide a reason when closing an issue that people are wrestling with.

sorry for my bad english y found the problem.

this is my code:
Query
class Query(graphene.ObjectType): all_properties = DjangoFilterConnectionField(PropertyNode, filterset_class=PropertyFilter, order=graphene.List(of_type=graphene.String), exclude=graphene.List(of_type=graphene.Int), )

-I need to set the order from the query in a list. then my resolver try this.

Resolver
def resolve_all_properties(self, info, **kwargs): exclude = kwargs.get('exclude') order = kwargs.get('order') qs = PropertyPage.objects.live() if order is not None: qs = qs.order_by(*order) if exclude is not None: qs = qs.exclude(id__in=exclude) return qs

the problem when the
graphene_django.fields.djangoConnectionField

resolve connection in this method
def resolve_connection(cls, connection, default_manager, args, iterable):

they run a simple IF.

if iterable is not default_manager: default_queryset = maybe_queryset(default_manager) iterable = cls.merge_querysets(default_queryset, iterable) _len = iterable.count()

thats meaning if the resolver return a queryset in this case. The default_manager merge default_query_set instead the generated in the resolver.

The solution is return a list instead a queryset in the resolver.

def resolve_all_properties(self, info, **kwargs): exclude = kwargs.get('exclude') order = kwargs.get('order') qs = PropertyPage.objects.live() if order is not None: qs = qs.order_by(*order) if exclude is not None: qs = qs.exclude(id__in=exclude) return list(qs)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

licx picture licx  路  3Comments

khankuan picture khankuan  路  4Comments

ZuluPro picture ZuluPro  路  3Comments

x9sheikh picture x9sheikh  路  4Comments

Northshoot picture Northshoot  路  4Comments