Repository: graphql-python/graphene-django

Issue:
custom @classmethod get_queryset of DjangoObjectType not works
in official docs: https://docs.graphene-python.org/projects/django/en/latest/queries/#default-queryset. In practice nothing happens. Please help

Discussion:
plus500s: When can we expect this PR to be merged?

Patch:
diff --git a/docs/fields.rst b/docs/fields.rst
new file mode 100644
index 0000000..1a8afc3
--- /dev/null
+++ b/docs/fields.rst
@@ -0,0 +1,83 @@
+Fields
+======
+
+Graphene-Django provides some useful fields to help integrate Django with your GraphQL
+Schema.
+
+DjangoListField
+---------------
+
+``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType<queries-objecttypes>`'s. By default it will resolve the default queryset of the Django model.
+
+.. code:: python
+
+   from graphene import ObjectType, Schema
+   from graphene_django import DjangoListField
+
+   class RecipeType(DjangoObjectType):
+      class Meta:
+         model = Recipe
+         fields = ("title", "instructions")
+
+   class Query(ObjectType):
+      recipes = DjangoListField(RecipeType)
+
+   schema = Schema(query=Query)
+
+The above code results in the following schema definition:
+
+.. code::
+
+   schema {
+     query: Query
+   }
+
+   type Query {
+     recipes: [RecipeType!]
+   }
+
+   type RecipeType {
+     title: String!
+     instructions: String!
+   }
+
+Custom resolvers
+****************
+
+If your ``DjangoObjectType`` has defined a custom
+:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
+``DjangoListField`` it will be called with either the return of the field
+resolver (if one is defined) or the default queryeset from the Django model.
+
+For example the following schema will only resolve recipes which have been
+published and have a title:
+
+.. code:: python
+
+   from graphene import ObjectType, Schema
+   from graphene_django import DjangoListField
+
+   class RecipeType(DjangoObjectType):
+      class Meta:
+         model = Recipe
+         fields = ("title", "instructions")
+
+      @classmethod
+      def get_queryset(cls, queryset, info):
+         # Filter out recipes that have no title
+         return queryset.exclude(title__exact="")
+
+   class Query(ObjectType):
+      recipes = DjangoListField(RecipeType)
+
+      def resolve_recipes(parent, info):
+         # Only get recipes that have been published
+         return Recipe.objects.filter(published=True)
+
+   schema = Schema(query=Query)
+
+
+DjangoConnectionField
+---------------------
+
+*TODO*
diff --git a/docs/filtering.rst b/docs/filtering.rst
index 0d37f46..dbbab9d 100644
--- a/docs/filtering.rst
+++ b/docs/filtering.rst
@@ -1,7 +1,7 @@
 Filtering
 =========
 
-Graphene integrates with
+Graphene-Django integrates with
 `django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
 Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
 documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
diff --git a/docs/index.rst b/docs/index.rst
index 93f37db..f4f718c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -25,6 +25,7 @@ For more advanced use, check out the Relay tutorial.
    tutorial-relay
    schema
    queries
+   fields
    extra-types
    mutations
    filtering
diff --git a/docs/queries.rst b/docs/queries.rst
index 36cdab1..4b3f718 100644
--- a/docs/queries.rst
+++ b/docs/queries.rst
@@ -1,3 +1,5 @@
+.. _queries-objecttypes:
+
 Queries & ObjectTypes
 =====================
 
@@ -205,6 +207,8 @@ need to create the most basic class for this to work:
         class Meta:
             model = Category
 
+.. _django-objecttype-get-queryset:
+
 Default QuerySet
 -----------------
 
diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py
index 38f8d8a..62318e9 100644
--- a/graphene_django/__init__.py
+++ b/graphene_django/__init__.py
@@ -1,6 +1,11 @@
+from .fields import DjangoConnectionField, DjangoListField
 from .types import DjangoObjectType
-from .fields import DjangoConnectionField
 
 __version__ = "2.9.1"
 
-__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
+__all__ = [
+    "__version__",
+    "DjangoObjectType",
+    "DjangoListField",
+    "DjangoConnectionField",
+]
diff --git a/graphene_django/fields.py b/graphene_django/fields.py
index fb6b98a..7539cf2 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -38,16 +38,21 @@ class DjangoListField(Field):
     def model(self):
         return self._underlying_type._meta.model
 
+    def get_default_queryset(self):
+        return self.model._default_manager.get_queryset()
+
     @staticmethod
-    def list_resolver(django_object_type, resolver, root, info, **args):
+    def list_resolver(
+        django_object_type, resolver, default_queryset, root, info, **args
+    ):
         queryset = maybe_queryset(resolver(root, info, **args))
         if queryset is None:
-            # Default to Django Model queryset
-            # N.B. This happens if DjangoListField is used in the top level Query object
-            model_manager = django_object_type._meta.model.objects
-            queryset = maybe_queryset(
-                django_object_type.get_queryset(model_manager, info)
-            )
+            queryset = default_queryset
+
+        if isinstance(queryset, QuerySet):
+            # Pass queryset to the DjangoObjectType get_queryset method
+            queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
+
         return queryset
 
     def get_resolver(self, parent_resolver):
@@ -55,7 +60,12 @@ class DjangoListField(Field):
         if isinstance(_type, NonNull):
             _type = _type.of_type
         django_object_type = _type.of_type.of_type
-        return partial(self.list_resolver, django_object_type, parent_resolver)
+        return partial(
+            self.list_resolver,
+            django_object_type,
+            parent_resolver,
+            self.get_default_queryset(),
+        )
 
 
 class DjangoConnectionField(ConnectionField):


Test patch:
diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py
index 67b3a35..cd5bd1b 100644
--- a/graphene_django/tests/test_fields.py
+++ b/graphene_django/tests/test_fields.py
@@ -1,4 +1,5 @@
 import datetime
+from django.db.models import Count
 
 import pytest
 
@@ -141,13 +142,26 @@ class TestDjangoListField:
             pub_date_time=datetime.datetime.now(),
             editor=r1,
         )
+        ArticleModel.objects.create(
+            headline="Not so good news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
 
         result = schema.execute(query)
 
         assert not result.errors
         assert result.data == {
             "reporters": [
-                {"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
+                {
+                    "firstName": "Tara",
+                    "articles": [
+                        {"headline": "Amazing news"},
+                        {"headline": "Not so good news"},
+                    ],
+                },
                 {"firstName": "Debra", "articles": []},
             ]
         }
@@ -163,8 +177,8 @@ class TestDjangoListField:
                 model = ReporterModel
                 fields = ("first_name", "articles")
 
-            def resolve_reporters(reporter, info):
-                return reporter.articles.all()
+            def resolve_articles(reporter, info):
+                return reporter.articles.filter(headline__contains="Amazing")
 
         class Query(ObjectType):
             reporters = DjangoListField(Reporter)
@@ -192,6 +206,13 @@ class TestDjangoListField:
             pub_date_time=datetime.datetime.now(),
             editor=r1,
         )
+        ArticleModel.objects.create(
+            headline="Not so good news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
 
         result = schema.execute(query)
 
@@ -202,3 +223,155 @@ class TestDjangoListField:
                 {"firstName": "Debra", "articles": []},
             ]
         }
+
+    def test_get_queryset_filter(self):
+        class Reporter(DjangoObjectType):
+            class Meta:
+                model = ReporterModel
+                fields = ("first_name", "articles")
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                # Only get reporters with at least 1 article
+                return queryset.annotate(article_count=Count("articles")).filter(
+                    article_count__gt=0
+                )
+
+        class Query(ObjectType):
+            reporters = DjangoListField(Reporter)
+
+            def resolve_reporters(_, info):
+                return ReporterModel.objects.all()
+
+        schema = Schema(query=Query)
+
+        query = """
+            query {
+                reporters {
+                    firstName
+                }
+            }
+        """
+
+        r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
+        ReporterModel.objects.create(first_name="Debra", last_name="Payne")
+
+        ArticleModel.objects.create(
+            headline="Amazing news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
+
+        result = schema.execute(query)
+
+        assert not result.errors
+        assert result.data == {"reporters": [{"firstName": "Tara"},]}
+
+    def test_resolve_list(self):
+        """Resolving a plain list should work (and not call get_queryset)"""
+
+        class Reporter(DjangoObjectType):
+            class Meta:
+                model = ReporterModel
+                fields = ("first_name", "articles")
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                # Only get reporters with at least 1 article
+                return queryset.annotate(article_count=Count("articles")).filter(
+                    article_count__gt=0
+                )
+
+        class Query(ObjectType):
+            reporters = DjangoListField(Reporter)
+
+            def resolve_reporters(_, info):
+                return [ReporterModel.objects.get(first_name="Debra")]
+
+        schema = Schema(query=Query)
+
+        query = """
+            query {
+                reporters {
+                    firstName
+                }
+            }
+        """
+
+        r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
+        ReporterModel.objects.create(first_name="Debra", last_name="Payne")
+
+        ArticleModel.objects.create(
+            headline="Amazing news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
+
+        result = schema.execute(query)
+
+        assert not result.errors
+        assert result.data == {"reporters": [{"firstName": "Debra"},]}
+
+    def test_get_queryset_foreign_key(self):
+        class Article(DjangoObjectType):
+            class Meta:
+                model = ArticleModel
+                fields = ("headline",)
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                # Rose tinted glasses
+                return queryset.exclude(headline__contains="Not so good")
+
+        class Reporter(DjangoObjectType):
+            class Meta:
+                model = ReporterModel
+                fields = ("first_name", "articles")
+
+        class Query(ObjectType):
+            reporters = DjangoListField(Reporter)
+
+        schema = Schema(query=Query)
+
+        query = """
+            query {
+                reporters {
+                    firstName
+                    articles {
+                        headline
+                    }
+                }
+            }
+        """
+
+        r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
+        ReporterModel.objects.create(first_name="Debra", last_name="Payne")
+
+        ArticleModel.objects.create(
+            headline="Amazing news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
+        ArticleModel.objects.create(
+            headline="Not so good news",
+            reporter=r1,
+            pub_date=datetime.date.today(),
+            pub_date_time=datetime.datetime.now(),
+            editor=r1,
+        )
+
+        result = schema.execute(query)
+
+        assert not result.errors
+        assert result.data == {
+            "reporters": [
+                {"firstName": "Tara", "articles": [{"headline": "Amazing news"},],},
+                {"firstName": "Debra", "articles": []},
+            ]
+        }