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: