Repository: graphql-python/graphene-django

Issue:
DjangoListField returning stale data by default
https://repl.it/talk/share/DjangoListField-bug/79205
Idk if this is the correct way of sharing the repl, if not, lmk

Repro:
1. Create superuser
2. In one tab, run query
```py
{
  allRecipes {
    id
    title
    instructions
  }
}
```
3. In another tab, go to admin panel, add new record to recipes
4. Rerun the above query

You'll see that the data returned does not include the new recipe that was added

Discussion:
zbyte64: Not sure if this is how we want to fix the issue, seems to me default_queryset should be a callable to be called during view execution time.

Patch:
diff --git a/graphene_django/fields.py b/graphene_django/fields.py
index 78efceb..fdf95aa 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -43,16 +43,16 @@ class DjangoListField(Field):
     def model(self):
         return self._underlying_type._meta.model
 
-    def get_default_queryset(self):
-        return self.model._default_manager.get_queryset()
+    def get_manager(self):
+        return self.model._default_manager
 
     @staticmethod
     def list_resolver(
-        django_object_type, resolver, default_queryset, root, info, **args
+        django_object_type, resolver, default_manager, root, info, **args
     ):
         queryset = maybe_queryset(resolver(root, info, **args))
         if queryset is None:
-            queryset = default_queryset
+            queryset = maybe_queryset(default_manager)
 
         if isinstance(queryset, QuerySet):
             # Pass queryset to the DjangoObjectType get_queryset method
@@ -66,10 +66,7 @@ class DjangoListField(Field):
             _type = _type.of_type
         django_object_type = _type.of_type.of_type
         return partial(
-            self.list_resolver,
-            django_object_type,
-            parent_resolver,
-            self.get_default_queryset(),
+            self.list_resolver, django_object_type, parent_resolver, self.get_manager(),
         )
 
 
@@ -147,14 +144,11 @@ class DjangoConnectionField(ConnectionField):
 
         if isinstance(iterable, QuerySet):
             list_length = iterable.count()
-            list_slice_length = (
-                min(max_limit, list_length) if max_limit is not None else list_length
-            )
         else:
             list_length = len(iterable)
-            list_slice_length = (
-                min(max_limit, list_length) if max_limit is not None else list_length
-            )
+        list_slice_length = (
+            min(max_limit, list_length) if max_limit is not None else list_length
+        )
 
         # If after is higher than list_length, connection_from_list_slice
         # would try to do a negative slicing which makes django throw an
@@ -162,7 +156,11 @@ class DjangoConnectionField(ConnectionField):
         after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length)
 
         if max_limit is not None and "first" not in args:
-            args["first"] = max_limit
+            if "last" in args:
+                args["first"] = list_length
+                list_slice_length = list_length
+            else:
+                args["first"] = max_limit
 
         connection = connection_from_list_slice(
             iterable[after:],
diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py
index 8916456..5d17680 100644
--- a/graphene_django/forms/converter.py
+++ b/graphene_django/forms/converter.py
@@ -63,6 +63,11 @@ def convert_form_field_to_list(field):
     return List(ID, required=field.required)
 
 
+@convert_form_field.register(forms.MultipleChoiceField)
+def convert_form_field_to_string_list(field):
+    return List(String, required=field.required)
+
+
 @convert_form_field.register(forms.DateField)
 def convert_form_field_to_date(field):
     return Date(description=field.help_text, required=field.required)


Test patch:
diff --git a/graphene_django/forms/tests/test_converter.py b/graphene_django/forms/tests/test_converter.py
index 955b952..ccf630f 100644
--- a/graphene_django/forms/tests/test_converter.py
+++ b/graphene_django/forms/tests/test_converter.py
@@ -101,7 +101,14 @@ def test_should_decimal_convert_float():
     assert_conversion(forms.DecimalField, Float)
 
 
-def test_should_multiple_choice_convert_connectionorlist():
+def test_should_multiple_choice_convert_list():
+    field = forms.MultipleChoiceField()
+    graphene_type = convert_form_field(field)
+    assert isinstance(graphene_type, List)
+    assert graphene_type.of_type == String
+
+
+def test_should_model_multiple_choice_convert_connectionorlist():
     field = forms.ModelMultipleChoiceField(queryset=None)
     graphene_type = convert_form_field(field)
     assert isinstance(graphene_type, List)
diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py
index 57f913e..f68470e 100644
--- a/graphene_django/tests/test_fields.py
+++ b/graphene_django/tests/test_fields.py
@@ -75,6 +75,39 @@ class TestDjangoListField:
             "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}]
         }
 
+    def test_list_field_queryset_is_not_cached(self):
+        class Reporter(DjangoObjectType):
+            class Meta:
+                model = ReporterModel
+                fields = ("first_name",)
+
+        class Query(ObjectType):
+            reporters = DjangoListField(Reporter)
+
+        schema = Schema(query=Query)
+
+        query = """
+            query {
+                reporters {
+                    firstName
+                }
+            }
+        """
+
+        result = schema.execute(query)
+        assert not result.errors
+        assert result.data == {"reporters": []}
+
+        ReporterModel.objects.create(first_name="Tara", last_name="West")
+        ReporterModel.objects.create(first_name="Debra", last_name="Payne")
+
+        result = schema.execute(query)
+
+        assert not result.errors
+        assert result.data == {
+            "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}]
+        }
+
     def test_override_resolver(self):
         class Reporter(DjangoObjectType):
             class Meta:
diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py
index 6add0b8..a2d8373 100644
--- a/graphene_django/tests/test_query.py
+++ b/graphene_django/tests/test_query.py
@@ -1213,6 +1213,103 @@ def test_should_have_next_page(graphene_settings):
     }
 
 
+class TestBackwardPagination:
+    def setup_schema(self, graphene_settings, max_limit):
+        graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
+        reporters = [Reporter(**kwargs) for kwargs in REPORTERS]
+        Reporter.objects.bulk_create(reporters)
+
+        class ReporterType(DjangoObjectType):
+            class Meta:
+                model = Reporter
+                interfaces = (Node,)
+
+        class Query(graphene.ObjectType):
+            all_reporters = DjangoConnectionField(ReporterType)
+
+        schema = graphene.Schema(query=Query)
+        return schema
+
+    def do_queries(self, schema):
+        # Simply last 3
+        query_last = """
+            query {
+                allReporters(last: 3) {
+                    edges {
+                        node {
+                            firstName
+                        }
+                    }
+                }
+            }
+        """
+
+        result = schema.execute(query_last)
+        assert not result.errors
+        assert len(result.data["allReporters"]["edges"]) == 3
+        assert [
+            e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
+        ] == ["First 3", "First 4", "First 5"]
+
+        # Use a combination of first and last
+        query_first_and_last = """
+            query {
+                allReporters(first: 4, last: 3) {
+                    edges {
+                        node {
+                            firstName
+                        }
+                    }
+                }
+            }
+        """
+
+        result = schema.execute(query_first_and_last)
+        assert not result.errors
+        assert len(result.data["allReporters"]["edges"]) == 3
+        assert [
+            e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
+        ] == ["First 1", "First 2", "First 3"]
+
+        # Use a combination of first and last and after
+        query_first_last_and_after = """
+            query queryAfter($after: String) {
+                allReporters(first: 4, last: 3, after: $after) {
+                    edges {
+                        node {
+                            firstName
+                        }
+                    }
+                }
+            }
+        """
+
+        after = base64.b64encode(b"arrayconnection:0").decode()
+        result = schema.execute(
+            query_first_last_and_after, variable_values=dict(after=after)
+        )
+        assert not result.errors
+        assert len(result.data["allReporters"]["edges"]) == 3
+        assert [
+            e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
+        ] == ["First 2", "First 3", "First 4"]
+
+    def test_should_query(self, graphene_settings):
+        """
+        Backward pagination should work as expected
+        """
+        schema = self.setup_schema(graphene_settings, max_limit=100)
+        self.do_queries(schema)
+
+    def test_should_query_with_low_max_limit(self, graphene_settings):
+        """
+        When doing backward pagination (using last) in combination with a max limit higher than the number of objects
+        we should really retrieve the last ones.
+        """
+        schema = self.setup_schema(graphene_settings, max_limit=4)
+        self.do_queries(schema)
+
+
 def test_should_preserve_prefetch_related(django_assert_num_queries):
     class ReporterType(DjangoObjectType):
         class Meta:
diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py
index 7c9b152..871c440 100644
--- a/graphene_django/utils/testing.py
+++ b/graphene_django/utils/testing.py
@@ -109,9 +109,9 @@ class GraphQLTestCase(TestCase):
         the call was fine.
         :resp HttpResponse: Response
         """
-        self.assertEqual(resp.status_code, 200)
         content = json.loads(resp.content)
-        self.assertNotIn("errors", list(content.keys()), msg)
+        self.assertEqual(resp.status_code, 200, msg or content)
+        self.assertNotIn("errors", list(content.keys()), msg or content)
 
     def assertResponseHasErrors(self, resp, msg=None):
         """
@@ -119,4 +119,4 @@ class GraphQLTestCase(TestCase):
         :resp HttpResponse: Response
         """
         content = json.loads(resp.content)
-        self.assertIn("errors", list(content.keys()), msg)
+        self.assertIn("errors", list(content.keys()), msg or content)