Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-graphene-django for 
openSUSE:Factory checked in at 2023-01-26 13:58:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-graphene-django (Old)
 and      /work/SRC/openSUSE:Factory/.python-graphene-django.new.32243 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-graphene-django"

Thu Jan 26 13:58:39 2023 rev:8 rq:1061085 version:3.0.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-graphene-django/python-graphene-django.changes
    2022-09-25 15:36:12.391753490 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-graphene-django.new.32243/python-graphene-django.changes
 2023-01-26 14:08:02.923480949 +0100
@@ -1,0 +2,17 @@
+Thu Jan 26 01:05:11 UTC 2023 - John Vandenberg <jay...@gmail.com>
+
+- Update to v3.0.0
+  * See https://github.com/graphql-python/graphene-django/releases/tag/v3.0.0
+- from v3.0.0b9
+  * fix: unit test for https://github.com/graphql-python/graphene/pull/1412
+  * Make errors in form mutation non nullable
+  * Fixes related to https://github.com/graphql-python/graphene/pull/1412
+  * Delay assignment of csrftoken in Graphiql
+  * Fix type hint for DjangoObjectTypeOptions.model
+  * Fix code examples in queries.rst
+  * Fixed graphql_relay deprecation warning
+  * Make instructions runnable without tweaking
+  * Update tutorial-relay.rst
+  * Add support to persist GraphQL headers in GraphiQL
+
+-------------------------------------------------------------------

Old:
----
  graphene-django-3.0.0b8.tar.gz

New:
----
  graphene-django-3.0.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-graphene-django.spec ++++++
--- /var/tmp/diff_new_pack.BGVAjc/_old  2023-01-26 14:08:03.351483470 +0100
+++ /var/tmp/diff_new_pack.BGVAjc/_new  2023-01-26 14:08:03.355483493 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-graphene-django
 #
-# Copyright (c) 2022 SUSE LLC
+# Copyright (c) 2023 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-graphene-django
-Version:        3.0.0b8
+Version:        3.0.0
 Release:        0
 Summary:        Graphene Django integration
 License:        MIT
@@ -51,6 +51,7 @@
 BuildRequires:  %{python_module graphql-relay}
 BuildRequires:  %{python_module promise >= 2.1}
 BuildRequires:  %{python_module psycopg2}
+BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module pytest-django >= 3.3.2}
 BuildRequires:  %{python_module pytz}
 BuildRequires:  %{python_module text-unidecode}
@@ -65,6 +66,8 @@
 %patch0 -p1
 sed -i 's/from mock import MagicMock/from unittest.mock import MagicMock/' 
graphene_django/filter/tests/conftest.py
 
+sed -i 's/py\.test/pytest/g' graphene_django/tests/*.py 
graphene_django/tests/issues/*.py graphene_django/*/tests/*.py
+
 rm setup.cfg
 sed -i '/pytest-runner/d' setup.py
 
@@ -90,6 +93,6 @@
 %files %{python_files}
 %doc README.rst README.md
 %license LICENSE
-%{python_sitelib}/graphene[_-]django*
+%{python_sitelib}/graphene[_-]django*/
 
 %changelog

++++++ graphene-django-3.0.0b8.tar.gz -> graphene-django-3.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/docs/queries.rst 
new/graphene-django-3.0.0/docs/queries.rst
--- old/graphene-django-3.0.0b8/docs/queries.rst        2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/docs/queries.rst  2022-09-26 14:08:32.000000000 
+0200
@@ -151,7 +151,7 @@
 
 Results in the following GraphQL schema definition:
 
-.. code::
+.. code:: graphql
 
    type Pet {
      id: ID!
@@ -178,7 +178,7 @@
             fields = ("id", "kind",)
             convert_choices_to_enum = False
 
-.. code::
+.. code:: graphql
 
   type Pet {
     id: ID!
@@ -313,7 +313,7 @@
             bar=graphene.Int()
         )
 
-        def resolve_question(root, info, foo, bar):
+        def resolve_question(root, info, foo=None, bar=None):
             # If `foo` or `bar` are declared in the GraphQL query they will be 
here, else None.
             return Question.objects.filter(foo=foo, bar=bar).first()
 
@@ -336,12 +336,12 @@
     class Query(graphene.ObjectType):
         questions = graphene.List(QuestionType)
 
-    def resolve_questions(root, info):
-        # See if a user is authenticated
-        if info.context.user.is_authenticated():
-            return Question.objects.all()
-        else:
-            return Question.objects.none()
+        def resolve_questions(root, info):
+            # See if a user is authenticated
+            if info.context.user.is_authenticated():
+                return Question.objects.all()
+            else:
+                return Question.objects.none()
 
 
 DjangoObjectTypes
@@ -418,29 +418,29 @@
 You can now execute queries like:
 
 
-.. code:: python
+.. code:: graphql
 
     {
         questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
             pageInfo {
-            startCursor
-            endCursor
-            hasNextPage
-            hasPreviousPage
+                startCursor
+                endCursor
+                hasNextPage
+                hasPreviousPage
             }
             edges {
-            cursor
-            node {
-                id
-                question_text
-            }
+                cursor
+                node {
+                    id
+                    question_text
+                }
             }
         }
     }
 
 Which returns:
 
-.. code:: python
+.. code:: json
 
     {
         "data": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/docs/settings.rst 
new/graphene-django-3.0.0/docs/settings.rst
--- old/graphene-django-3.0.0b8/docs/settings.rst       2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/docs/settings.rst 2022-09-26 14:08:32.000000000 
+0200
@@ -189,7 +189,7 @@
 
 
 ``GRAPHIQL_HEADER_EDITOR_ENABLED``
----------------------
+----------------------------------
 
 GraphiQL starting from version 1.0.0 allows setting custom headers in similar 
fashion to query variables.
 
@@ -207,3 +207,36 @@
    GRAPHENE = {
       'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
    }
+
+
+``TESTING_ENDPOINT``
+--------------------
+
+Define the graphql endpoint url used for the `GraphQLTestCase` class.
+
+Default: ``/graphql``
+
+.. code:: python
+
+   GRAPHENE = {
+      'TESTING_ENDPOINT': '/customEndpoint'
+   }
+
+
+``GRAPHIQL_SHOULD_PERSIST_HEADERS``
+---------------------
+
+Set to ``True`` if you want to persist GraphiQL headers after refreshing the 
page.
+
+This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for 
details refer to GraphiQLDocs_.
+
+.. _GraphiQLDocs: 
https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
+
+
+Default: ``False``
+
+.. code:: python
+
+   GRAPHENE = {
+      'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
+   }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/docs/testing.rst 
new/graphene-django-3.0.0/docs/testing.rst
--- old/graphene-django-3.0.0b8/docs/testing.rst        2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/docs/testing.rst  2022-09-26 14:08:32.000000000 
+0200
@@ -6,7 +6,8 @@
 
 If you want to unittest your API calls derive your test case from the class 
`GraphQLTestCase`.
 
-Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. 
The default endpoint is `GRAPHQL_URL = "/graphql/"`.
+The default endpoint for testing is `/graphql`. You can override this in the 
`settings 
<https://docs.graphene-python.org/projects/django/en/latest/settings/#testing-endpoint>`__.
+
 
 Usage:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/docs/tutorial-plain.rst 
new/graphene-django-3.0.0/docs/tutorial-plain.rst
--- old/graphene-django-3.0.0b8/docs/tutorial-plain.rst 2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/docs/tutorial-plain.rst   2022-09-26 
14:08:32.000000000 +0200
@@ -35,6 +35,7 @@
 
 .. code:: bash
 
+    cd ..
     python manage.py migrate
 
 Let's create a few simple models...
@@ -77,6 +78,18 @@
         "cookbook.ingredients",
     ]
 
+Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is 
set to ``cookbook.ingredients``.
+
+.. code:: python
+
+    # cookbook/ingredients/apps.py
+
+    from django.apps import AppConfig
+    
+    
+    class IngredientsConfig(AppConfig):
+        default_auto_field = 'django.db.models.BigAutoField'
+        name = 'cookbook.ingredients'
 
 Don't forget to create & run migrations:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/__init__.py 
new/graphene-django-3.0.0/graphene_django/__init__.py
--- old/graphene-django-3.0.0b8/graphene_django/__init__.py     2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/__init__.py       2022-09-26 
14:08:32.000000000 +0200
@@ -1,7 +1,7 @@
 from .fields import DjangoConnectionField, DjangoListField
 from .types import DjangoObjectType
 
-__version__ = "3.0.0b8"
+__version__ = "3.0.0"
 
 __all__ = [
     "__version__",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/converter.py 
new/graphene-django-3.0.0/graphene_django/converter.py
--- old/graphene-django-3.0.0b8/graphene_django/converter.py    2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/converter.py      2022-09-26 
14:08:32.000000000 +0200
@@ -308,7 +308,24 @@
         if not _type:
             return
 
-        return Field(
+        class CustomField(Field):
+            def wrap_resolve(self, parent_resolver):
+                """
+                Implements a custom resolver which go through the `get_node` 
method to insure that
+                it goes through the `get_queryset` method of the 
DjangoObjectType.
+                """
+                resolver = super().wrap_resolve(parent_resolver)
+
+                def custom_resolver(root, info, **args):
+                    fk_obj = resolver(root, info, **args)
+                    if fk_obj is None:
+                        return None
+                    else:
+                        return _type.get_node(info, fk_obj.pk)
+
+                return custom_resolver
+
+        return CustomField(
             _type,
             description=get_django_field_description(field),
             required=not field.null,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/fields.py 
new/graphene-django-3.0.0/graphene_django/fields.py
--- old/graphene-django-3.0.0b8/graphene_django/fields.py       2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/fields.py 2022-09-26 
14:08:32.000000000 +0200
@@ -2,7 +2,7 @@
 
 from django.db.models.query import QuerySet
 
-from graphql_relay.connection.array_connection import (
+from graphql_relay import (
     connection_from_array_slice,
     cursor_to_offset,
     get_offset_with_default,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/forms/mutation.py 
new/graphene-django-3.0.0/graphene_django/forms/mutation.py
--- old/graphene-django-3.0.0b8/graphene_django/forms/mutation.py       
2022-09-23 10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/forms/mutation.py 2022-09-26 
14:08:32.000000000 +0200
@@ -117,7 +117,7 @@
     class Meta:
         abstract = True
 
-    errors = graphene.List(ErrorType)
+    errors = graphene.List(graphene.NonNull(ErrorType), required=True)
 
     @classmethod
     def __init_subclass_with_meta__(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/settings.py 
new/graphene-django-3.0.0/graphene_django/settings.py
--- old/graphene-django-3.0.0b8/graphene_django/settings.py     2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/settings.py       2022-09-26 
14:08:32.000000000 +0200
@@ -41,7 +41,9 @@
     # This sets headerEditorEnabled GraphiQL option, for details go to
     # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
     "GRAPHIQL_HEADER_EDITOR_ENABLED": True,
+    "GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
     "ATOMIC_MUTATIONS": False,
+    "TESTING_ENDPOINT": "/graphql",
 }
 
 if settings.DEBUG:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/static/graphene_django/graphiql.js 
new/graphene-django-3.0.0/graphene_django/static/graphene_django/graphiql.js
--- 
old/graphene-django-3.0.0b8/graphene_django/static/graphene_django/graphiql.js  
    2022-09-23 10:38:11.000000000 +0200
+++ 
new/graphene-django-3.0.0/graphene_django/static/graphene_django/graphiql.js    
    2022-09-26 14:08:32.000000000 +0200
@@ -10,14 +10,6 @@
   history,
   location,
 ) {
-  // Parse the cookie value for a CSRF token
-  var csrftoken;
-  var cookies = ("; " + document.cookie).split("; csrftoken=");
-  if (cookies.length == 2) {
-    csrftoken = cookies.pop().split(";").shift();
-  } else {
-    csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
-  }
 
   // Collect the URL parameters
   var parameters = {};
@@ -68,9 +60,19 @@
     var headers = opts.headers || {};
     headers['Accept'] = headers['Accept'] || 'application/json';
     headers['Content-Type'] = headers['Content-Type'] || 'application/json';
+
+    // Parse the cookie value for a CSRF token
+    var csrftoken;
+    var cookies = ("; " + document.cookie).split("; csrftoken=");
+    if (cookies.length == 2) {
+      csrftoken = cookies.pop().split(";").shift();
+    } else {
+      csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
+    }
     if (csrftoken) {
       headers['X-CSRFToken'] = csrftoken
     }
+
     return fetch(fetchURL, {
       method: "post",
       headers: headers,
@@ -176,6 +178,7 @@
     onEditVariables: onEditVariables,
     onEditOperationName: onEditOperationName,
     headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
+    shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
     query: parameters.query,
   };
   if (parameters.variables) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/templates/graphene/graphiql.html 
new/graphene-django-3.0.0/graphene_django/templates/graphene/graphiql.html
--- 
old/graphene-django-3.0.0b8/graphene_django/templates/graphene/graphiql.html    
    2022-09-23 10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/templates/graphene/graphiql.html  
2022-09-26 14:08:32.000000000 +0200
@@ -46,6 +46,7 @@
       subscriptionPath: "{{subscription_path}}",
     {% endif %}
       graphiqlHeaderEditorEnabled: {{ 
graphiql_header_editor_enabled|yesno:"true,false" }},
+      graphiqlShouldPersistHeaders: {{ 
graphiql_should_persist_headers|yesno:"true,false" }},
     };
   </script>
   <script src="{% static 'graphene_django/graphiql.js' %}"></script>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/tests/models.py 
new/graphene-django-3.0.0/graphene_django/tests/models.py
--- old/graphene-django-3.0.0b8/graphene_django/tests/models.py 2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/tests/models.py   2022-09-26 
14:08:32.000000000 +0200
@@ -13,6 +13,9 @@
 class Pet(models.Model):
     name = models.CharField(max_length=30)
     age = models.PositiveIntegerField()
+    owner = models.ForeignKey(
+        "Person", on_delete=models.CASCADE, null=True, blank=True, 
related_name="pets"
+    )
 
 
 class FilmDetails(models.Model):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/tests/test_get_queryset.py 
new/graphene-django-3.0.0/graphene_django/tests/test_get_queryset.py
--- old/graphene-django-3.0.0b8/graphene_django/tests/test_get_queryset.py      
1970-01-01 01:00:00.000000000 +0100
+++ new/graphene-django-3.0.0/graphene_django/tests/test_get_queryset.py        
2022-09-26 14:08:32.000000000 +0200
@@ -0,0 +1,361 @@
+import pytest
+
+import graphene
+from graphene.relay import Node
+
+from graphql_relay import to_global_id
+
+from ..fields import DjangoConnectionField
+from ..types import DjangoObjectType
+
+from .models import Article, Reporter
+
+
+class TestShouldCallGetQuerySetOnForeignKey:
+    """
+    Check that the get_queryset method is called in both forward and reversed 
direction
+    of a foreignkey on types.
+    (see issue #1111)
+    """
+
+    @pytest.fixture(autouse=True)
+    def setup_schema(self):
+        class ReporterType(DjangoObjectType):
+            class Meta:
+                model = Reporter
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                if info.context and info.context.get("admin"):
+                    return queryset
+                raise Exception("Not authorized to access reporters.")
+
+        class ArticleType(DjangoObjectType):
+            class Meta:
+                model = Article
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                return queryset.exclude(headline__startswith="Draft")
+
+        class Query(graphene.ObjectType):
+            reporter = graphene.Field(ReporterType, 
id=graphene.ID(required=True))
+            article = graphene.Field(ArticleType, 
id=graphene.ID(required=True))
+
+            def resolve_reporter(self, info, id):
+                return (
+                    ReporterType.get_queryset(Reporter.objects, info)
+                    .filter(id=id)
+                    .last()
+                )
+
+            def resolve_article(self, info, id):
+                return (
+                    ArticleType.get_queryset(Article.objects, 
info).filter(id=id).last()
+                )
+
+        self.schema = graphene.Schema(query=Query)
+
+        self.reporter = Reporter.objects.create(first_name="Jane", 
last_name="Doe")
+
+        self.articles = [
+            Article.objects.create(
+                headline="A fantastic article",
+                reporter=self.reporter,
+                editor=self.reporter,
+            ),
+            Article.objects.create(
+                headline="Draft: My next best seller",
+                reporter=self.reporter,
+                editor=self.reporter,
+            ),
+        ]
+
+    def test_get_queryset_called_on_field(self):
+        # If a user tries to access an article it is fine as long as it's not 
a draft one
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                }
+            }
+        """
+        # Non-draft
+        result = self.schema.execute(query, variables={"id": 
self.articles[0].id})
+        assert not result.errors
+        assert result.data["article"] == {
+            "headline": "A fantastic article",
+        }
+        # Draft
+        result = self.schema.execute(query, variables={"id": 
self.articles[1].id})
+        assert not result.errors
+        assert result.data["article"] is None
+
+        # If a non admin user tries to access a reporter they should get our 
authorization error
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                }
+            }
+        """
+
+        result = self.schema.execute(query, variables={"id": self.reporter.id})
+        assert len(result.errors) == 1
+        assert result.errors[0].message == "Not authorized to access 
reporters."
+
+        # An admin user should be able to get reporters
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": self.reporter.id},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data == {"reporter": {"firstName": "Jane"}}
+
+    def test_get_queryset_called_on_foreignkey(self):
+        # If a user tries to access a reporter through an article they should 
get our authorization error
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                    reporter {
+                        firstName
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(query, variables={"id": 
self.articles[0].id})
+        assert len(result.errors) == 1
+        assert result.errors[0].message == "Not authorized to access 
reporters."
+
+        # An admin user should be able to get reporters through an article
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                    reporter {
+                        firstName
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": self.articles[0].id},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data["article"] == {
+            "headline": "A fantastic article",
+            "reporter": {"firstName": "Jane"},
+        }
+
+        # An admin user should not be able to access draft article through a 
reporter
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                    articles {
+                        headline
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": self.reporter.id},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data["reporter"] == {
+            "firstName": "Jane",
+            "articles": [{"headline": "A fantastic article"}],
+        }
+
+
+class TestShouldCallGetQuerySetOnForeignKeyNode:
+    """
+    Check that the get_queryset method is called in both forward and reversed 
direction
+    of a foreignkey on types using a node interface.
+    (see issue #1111)
+    """
+
+    @pytest.fixture(autouse=True)
+    def setup_schema(self):
+        class ReporterType(DjangoObjectType):
+            class Meta:
+                model = Reporter
+                interfaces = (Node,)
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                if info.context and info.context.get("admin"):
+                    return queryset
+                raise Exception("Not authorized to access reporters.")
+
+        class ArticleType(DjangoObjectType):
+            class Meta:
+                model = Article
+                interfaces = (Node,)
+
+            @classmethod
+            def get_queryset(cls, queryset, info):
+                return queryset.exclude(headline__startswith="Draft")
+
+        class Query(graphene.ObjectType):
+            reporter = Node.Field(ReporterType)
+            article = Node.Field(ArticleType)
+
+        self.schema = graphene.Schema(query=Query)
+
+        self.reporter = Reporter.objects.create(first_name="Jane", 
last_name="Doe")
+
+        self.articles = [
+            Article.objects.create(
+                headline="A fantastic article",
+                reporter=self.reporter,
+                editor=self.reporter,
+            ),
+            Article.objects.create(
+                headline="Draft: My next best seller",
+                reporter=self.reporter,
+                editor=self.reporter,
+            ),
+        ]
+
+    def test_get_queryset_called_on_node(self):
+        # If a user tries to access an article it is fine as long as it's not 
a draft one
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                }
+            }
+        """
+        # Non-draft
+        result = self.schema.execute(
+            query, variables={"id": to_global_id("ArticleType", 
self.articles[0].id)}
+        )
+        assert not result.errors
+        assert result.data["article"] == {
+            "headline": "A fantastic article",
+        }
+        # Draft
+        result = self.schema.execute(
+            query, variables={"id": to_global_id("ArticleType", 
self.articles[1].id)}
+        )
+        assert not result.errors
+        assert result.data["article"] is None
+
+        # If a non admin user tries to access a reporter they should get our 
authorization error
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query, variables={"id": to_global_id("ReporterType", 
self.reporter.id)}
+        )
+        assert len(result.errors) == 1
+        assert result.errors[0].message == "Not authorized to access 
reporters."
+
+        # An admin user should be able to get reporters
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": to_global_id("ReporterType", self.reporter.id)},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data == {"reporter": {"firstName": "Jane"}}
+
+    def test_get_queryset_called_on_foreignkey(self):
+        # If a user tries to access a reporter through an article they should 
get our authorization error
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                    reporter {
+                        firstName
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query, variables={"id": to_global_id("ArticleType", 
self.articles[0].id)}
+        )
+        assert len(result.errors) == 1
+        assert result.errors[0].message == "Not authorized to access 
reporters."
+
+        # An admin user should be able to get reporters through an article
+        query = """
+            query getArticle($id: ID!) {
+                article(id: $id) {
+                    headline
+                    reporter {
+                        firstName
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": to_global_id("ArticleType", self.articles[0].id)},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data["article"] == {
+            "headline": "A fantastic article",
+            "reporter": {"firstName": "Jane"},
+        }
+
+        # An admin user should not be able to access draft article through a 
reporter
+        query = """
+            query getReporter($id: ID!) {
+                reporter(id: $id) {
+                    firstName
+                    articles {
+                        edges {
+                            node {
+                                headline
+                            }
+                        }
+                    }
+                }
+            }
+        """
+
+        result = self.schema.execute(
+            query,
+            variables={"id": to_global_id("ReporterType", self.reporter.id)},
+            context_value={"admin": True},
+        )
+        assert not result.errors
+        assert result.data["reporter"] == {
+            "firstName": "Jane",
+            "articles": {"edges": [{"node": {"headline": "A fantastic 
article"}}]},
+        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/tests/test_query.py 
new/graphene-django-3.0.0/graphene_django/tests/test_query.py
--- old/graphene-django-3.0.0b8/graphene_django/tests/test_query.py     
2022-09-23 10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/tests/test_query.py       
2022-09-26 14:08:32.000000000 +0200
@@ -15,7 +15,7 @@
 from ..fields import DjangoConnectionField
 from ..types import DjangoObjectType
 from ..utils import DJANGO_FILTER_INSTALLED
-from .models import Article, CNNReporter, Film, FilmDetails, Reporter
+from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, 
Reporter
 
 
 def test_should_query_only_fields():
@@ -251,8 +251,8 @@
 
 
 def test_should_query_onetoone_fields():
-    film = Film(id=1)
-    film_details = FilmDetails(id=1, film=film)
+    film = Film.objects.create(id=1)
+    film_details = FilmDetails.objects.create(id=1, film=film)
 
     class FilmNode(DjangoObjectType):
         class Meta:
@@ -1697,3 +1697,67 @@
         }
     }
     assert result.data == expected
+
+
+def test_should_query_nullable_foreign_key():
+    class PetType(DjangoObjectType):
+        class Meta:
+            model = Pet
+
+    class PersonType(DjangoObjectType):
+        class Meta:
+            model = Person
+
+    class Query(graphene.ObjectType):
+        pet = graphene.Field(PetType, name=graphene.String(required=True))
+        person = graphene.Field(PersonType, 
name=graphene.String(required=True))
+
+        def resolve_pet(self, info, name):
+            return Pet.objects.filter(name=name).first()
+
+        def resolve_person(self, info, name):
+            return Person.objects.filter(name=name).first()
+
+    schema = graphene.Schema(query=Query)
+
+    person = Person.objects.create(name="Jane")
+    pets = [
+        Pet.objects.create(name="Stray dog", age=1),
+        Pet.objects.create(name="Jane's dog", owner=person, age=1),
+    ]
+
+    query_pet = """
+        query getPet($name: String!) {
+            pet(name: $name) {
+                owner {
+                    name
+                }
+            }
+        }
+    """
+    result = schema.execute(query_pet, variables={"name": "Stray dog"})
+    assert not result.errors
+    assert result.data["pet"] == {
+        "owner": None,
+    }
+
+    result = schema.execute(query_pet, variables={"name": "Jane's dog"})
+    assert not result.errors
+    assert result.data["pet"] == {
+        "owner": {"name": "Jane"},
+    }
+
+    query_owner = """
+        query getOwner($name: String!) {
+            person(name: $name) {
+                pets {
+                    name
+                }
+            }
+        }
+    """
+    result = schema.execute(query_owner, variables={"name": "Jane"})
+    assert not result.errors
+    assert result.data["person"] == {
+        "pets": [{"name": "Jane's dog"}],
+    }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/types.py 
new/graphene-django-3.0.0/graphene_django/types.py
--- old/graphene-django-3.0.0b8/graphene_django/types.py        2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/types.py  2022-09-26 
14:08:32.000000000 +0200
@@ -122,7 +122,7 @@
 
 
 class DjangoObjectTypeOptions(ObjectTypeOptions):
-    model = None  # type: Model
+    model = None  # type: Type[Model]
     registry = None  # type: Registry
     connection = None  # type: Type[Connection]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/utils/testing.py 
new/graphene-django-3.0.0/graphene_django/utils/testing.py
--- old/graphene-django-3.0.0b8/graphene_django/utils/testing.py        
2022-09-23 10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/utils/testing.py  2022-09-26 
14:08:32.000000000 +0200
@@ -3,6 +3,8 @@
 
 from django.test import Client, TestCase, TransactionTestCase
 
+from graphene_django.settings import graphene_settings
+
 DEFAULT_GRAPHQL_URL = "/graphql"
 
 
@@ -40,7 +42,7 @@
     if client is None:
         client = Client()
     if not graphql_url:
-        graphql_url = DEFAULT_GRAPHQL_URL
+        graphql_url = graphene_settings.TESTING_ENDPOINT
 
     body = {"query": query}
     if operation_name:
@@ -69,7 +71,7 @@
     """
 
     # URL to graphql endpoint
-    GRAPHQL_URL = DEFAULT_GRAPHQL_URL
+    GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT
 
     def query(
         self, query, operation_name=None, input_data=None, variables=None, 
headers=None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/graphene-django-3.0.0b8/graphene_django/utils/tests/test_testing.py 
new/graphene-django-3.0.0/graphene_django/utils/tests/test_testing.py
--- old/graphene-django-3.0.0b8/graphene_django/utils/tests/test_testing.py     
2022-09-23 10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/utils/tests/test_testing.py       
2022-09-26 14:08:32.000000000 +0200
@@ -2,6 +2,7 @@
 
 from .. import GraphQLTestCase
 from ...tests.test_types import with_local_registry
+from ...settings import graphene_settings
 from django.test import Client
 
 
@@ -43,3 +44,11 @@
 
     with pytest.warns(PendingDeprecationWarning):
         tc._client = Client()
+
+
+def test_graphql_test_case_imports_endpoint():
+    """
+    GraphQLTestCase class should import the default endpoint from settings file
+    """
+
+    assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/graphene-django-3.0.0b8/graphene_django/views.py 
new/graphene-django-3.0.0/graphene_django/views.py
--- old/graphene-django-3.0.0b8/graphene_django/views.py        2022-09-23 
10:38:11.000000000 +0200
+++ new/graphene-django-3.0.0/graphene_django/views.py  2022-09-26 
14:08:32.000000000 +0200
@@ -162,6 +162,7 @@
                     subscription_path=self.subscription_path,
                     # GraphiQL headers tab,
                     
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
+                    
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
                 )
 
             if self.batch:

Reply via email to