mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-01 10:53:13 +03:00
Merge branch 'main' into support-async
This commit is contained in:
commit
2ae927f06a
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
pip install wheel
|
pip install wheel
|
||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
- name: Publish a Python distribution to PyPI
|
- name: Publish a Python distribution to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
password: ${{ secrets.pypi_password }}
|
password: ${{ secrets.pypi_password }}
|
||||||
|
|
5
.github/workflows/lint.yml
vendored
5
.github/workflows/lint.yml
vendored
|
@ -1,6 +1,9 @@
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
|
@ -1,6 +1,9 @@
|
||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -8,13 +11,13 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
django: ["3.2", "4.0", "4.1"]
|
django: ["3.2", "4.1", "4.2"]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
include:
|
include:
|
||||||
- django: "3.2"
|
|
||||||
python-version: "3.7"
|
|
||||||
- django: "4.1"
|
- django: "4.1"
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
- django: "4.2"
|
||||||
|
python-version: "3.11"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
@ -29,4 +32,3 @@ jobs:
|
||||||
run: tox
|
run: tox
|
||||||
env:
|
env:
|
||||||
DJANGO: ${{ matrix.django }}
|
DJANGO: ${{ matrix.django }}
|
||||||
TOXENV: ${{ matrix.toxenv }}
|
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -11,6 +11,9 @@ __pycache__/
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
env/
|
env/
|
||||||
|
.env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
|
@ -80,3 +83,8 @@ Session.vim
|
||||||
tags
|
tags
|
||||||
.tox/
|
.tox/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
.python-version
|
||||||
|
|
|
@ -15,16 +15,12 @@ repos:
|
||||||
- --autofix
|
- --autofix
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude: README.md
|
exclude: README.md
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v3.3.2
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args: [--py37-plus]
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 6.0.0
|
rev: v0.0.283
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
|
||||||
|
|
33
.ruff.toml
Normal file
33
.ruff.toml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle
|
||||||
|
"W", # pycodestyle
|
||||||
|
"F", # pyflake
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
"E501", # line-too-long
|
||||||
|
"B017", # pytest.raises(Exception) should be considered evil
|
||||||
|
"B028", # warnings.warn called without an explicit stacklevel keyword argument
|
||||||
|
"B904", # check for raise statements in exception handlers that lack a from clause
|
||||||
|
]
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
"**/docs",
|
||||||
|
]
|
||||||
|
|
||||||
|
target-version = "py38"
|
||||||
|
|
||||||
|
[per-file-ignores]
|
||||||
|
# Ignore unused imports (F401) in these files
|
||||||
|
"__init__.py" = ["F401"]
|
||||||
|
"graphene_django/compat.py" = ["F401"]
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
known-first-party = ["graphene", "graphene-django"]
|
||||||
|
known-local-folder = ["cookbook"]
|
||||||
|
force-wrap-aliases = true
|
||||||
|
combine-as-imports = true
|
4
Makefile
4
Makefile
|
@ -10,7 +10,7 @@ dev-setup:
|
||||||
|
|
||||||
.PHONY: tests ## Run unit tests
|
.PHONY: tests ## Run unit tests
|
||||||
tests:
|
tests:
|
||||||
py.test graphene_django --cov=graphene_django -vv
|
PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv
|
||||||
|
|
||||||
.PHONY: format ## Format code
|
.PHONY: format ## Format code
|
||||||
format:
|
format:
|
||||||
|
@ -18,7 +18,7 @@ format:
|
||||||
|
|
||||||
.PHONY: lint ## Lint code
|
.PHONY: lint ## Lint code
|
||||||
lint:
|
lint:
|
||||||
flake8 graphene_django examples
|
ruff graphene_django examples
|
||||||
|
|
||||||
.PHONY: docs ## Generate docs
|
.PHONY: docs ## Generate docs
|
||||||
docs: dev-setup
|
docs: dev-setup
|
||||||
|
|
|
@ -144,6 +144,21 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
|
||||||
return queryset.filter(published=True)
|
return queryset.filter(published=True)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Defining a custom ``get_queryset`` gives the guaranteed it will be called
|
||||||
|
when resolving the ``DjangoObjectType``, even through related objects.
|
||||||
|
Note that because of this, benefits from using ``select_related``
|
||||||
|
in objects that define a relation to this ``DjangoObjectType`` will be canceled out.
|
||||||
|
In the case of ``prefetch_related``, the benefits of the optimization will be lost only
|
||||||
|
if the custom ``get_queryset`` modifies the queryset. For more information about this, refers
|
||||||
|
to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related.
|
||||||
|
|
||||||
|
|
||||||
|
If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving,
|
||||||
|
you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this
|
||||||
|
can lead to authorization leaks if you are performing authorization checks in the custom
|
||||||
|
``get_queryset``.
|
||||||
|
|
||||||
Filtering ID-based Node Access
|
Filtering ID-based Node Access
|
||||||
------------------------------
|
------------------------------
|
||||||
|
@ -197,8 +212,8 @@ For Django 2.2 and above:
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# some other urls
|
# some other urls
|
||||||
path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
||||||
]
|
]
|
||||||
|
|
||||||
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin
|
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin
|
||||||
|
|
|
@ -78,7 +78,7 @@ release = "1.0.dev"
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
# language = None
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
# non-false value, then it is used:
|
# non-false value, then it is used:
|
||||||
|
@ -445,4 +445,7 @@ epub_exclude_files = ["search.html"]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
intersphinx_mapping = {
|
||||||
|
# "https://docs.python.org/": None,
|
||||||
|
"python": ("https://docs.python.org/", None),
|
||||||
|
}
|
||||||
|
|
|
@ -57,9 +57,9 @@ specify the parameters in your settings.py:
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'tutorial.quickstart.schema',
|
'SCHEMA': 'tutorial.quickstart.schema',
|
||||||
'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json,
|
'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json,
|
||||||
'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line)
|
'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -125,6 +125,55 @@ to change how the form is saved or to return a different Graphene object type.
|
||||||
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
|
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
|
||||||
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
|
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
|
||||||
|
|
||||||
|
DjangoFormInputObjectType
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django.forms.types import DjangoFormInputObjectType
|
||||||
|
|
||||||
|
|
||||||
|
class PetFormInput(DjangoFormInputObjectType):
|
||||||
|
# any other fields can be placed here as well as
|
||||||
|
# other djangoforminputobjects and intputobjects
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
object_type = PetType
|
||||||
|
|
||||||
|
class QuestionFormInput(DjangoFormInputObjectType)
|
||||||
|
class Meta:
|
||||||
|
form_class = QuestionForm
|
||||||
|
object_type = QuestionType
|
||||||
|
|
||||||
|
class SeveralFormsInputData(graphene.InputObjectType):
|
||||||
|
pet = PetFormInput(required=True)
|
||||||
|
question = QuestionFormInput(required=True)
|
||||||
|
|
||||||
|
class SomeSophisticatedMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
data = SeveralFormsInputData(required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mutate(_root, _info, data):
|
||||||
|
pet_form_inst = PetForm(data=data.pet)
|
||||||
|
question_form_inst = QuestionForm(data=data.question)
|
||||||
|
|
||||||
|
if pet_form_inst.is_valid():
|
||||||
|
pet_model_instance = pet_form_inst.save(commit=False)
|
||||||
|
|
||||||
|
if question_form_inst.is_valid():
|
||||||
|
question_model_instance = question_form_inst.save(commit=False)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Additional to **InputObjectType** ``Meta`` class attributes:
|
||||||
|
|
||||||
|
* ``form_class`` is required and should be equal to django form class.
|
||||||
|
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
|
||||||
|
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
|
||||||
|
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
|
||||||
|
|
||||||
Django REST Framework
|
Django REST Framework
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
Sphinx==1.5.3
|
Sphinx==7.0.0
|
||||||
sphinx-autobuild==0.7.1
|
sphinx-autobuild==2021.3.14
|
||||||
|
pygments-graphql-lexer==0.1.0
|
||||||
# Docs template
|
# Docs template
|
||||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||||
|
|
|
@ -224,7 +224,7 @@ Default: ``/graphql``
|
||||||
|
|
||||||
|
|
||||||
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
||||||
---------------------
|
-----------------------------------
|
||||||
|
|
||||||
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ app <https://github.com/graphql-python/graphene-django/tree/master/examples/cook
|
||||||
A good idea is to check the following things first:
|
A good idea is to check the following things first:
|
||||||
|
|
||||||
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
|
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
|
||||||
* `GraphQL Relay Specification <https://facebook.github.io/relay/docs/en/graphql-server-specification.html>`__
|
* `GraphQL Relay Specification <https://relay.dev/docs/guides/graphql-server-specification/>`__
|
||||||
|
|
||||||
Setup the Django project
|
Setup the Django project
|
||||||
------------------------
|
------------------------
|
||||||
|
|
|
@ -62,3 +62,12 @@ Now head on over to
|
||||||
and run some queries!
|
and run some queries!
|
||||||
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
||||||
for some example queries)
|
for some example queries)
|
||||||
|
|
||||||
|
Testing local graphene-django changes
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
In `requirements.txt`, replace the entire `graphene-django=...` line with the following (so that we install the local version instead of the one from PyPI):
|
||||||
|
|
||||||
|
```
|
||||||
|
../../ # graphene-django
|
||||||
|
```
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
import cookbook.ingredients.schema
|
import cookbook.ingredients.schema
|
||||||
import cookbook.recipes.schema
|
import cookbook.recipes.schema
|
||||||
import graphene
|
|
||||||
|
|
||||||
from graphene_django.debug import DjangoDebug
|
|
||||||
|
|
||||||
|
|
||||||
class Query(
|
class Query(
|
||||||
|
|
|
@ -5,10 +5,10 @@ Django settings for cookbook project.
|
||||||
Generated by 'django-admin startproject' using Django 1.9.
|
Generated by 'django-admin startproject' using Django 1.9.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/1.9/topics/settings/
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/1.9/ref/settings/
|
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
||||||
|
@ -81,7 +81,7 @@ WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
@ -90,9 +90,11 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
|
@ -105,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
@ -119,6 +121,6 @@ USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from django.urls import path
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
graphene>=2.1,<3
|
django~=3.2
|
||||||
graphene-django>=2.1,<3
|
graphene
|
||||||
graphql-core>=2.1,<3
|
graphene-django>=3.1
|
||||||
django==3.1.14
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
|
||||||
from graphene import Node
|
from graphene import Node
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
# Graphene will automatically map the Category model's fields onto the CategoryNode.
|
# Graphene will automatically map the Category model's fields onto the CategoryNode.
|
||||||
# This is configured in the CategoryNode's Meta class (as you can see below)
|
# This is configured in the CategoryNode's Meta class (as you can see below)
|
||||||
|
|
|
@ -6,7 +6,9 @@ from cookbook.ingredients.models import Ingredient
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model):
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
instructions = models.TextField()
|
instructions = models.TextField()
|
||||||
__unicode__ = lambda self: self.title
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(models.Model):
|
class RecipeIngredient(models.Model):
|
||||||
|
|
|
@ -7,6 +7,8 @@ from graphene import Node, String, Field
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
class RecipeNode(DjangoObjectType):
|
class RecipeNode(DjangoObjectType):
|
||||||
async_field = String()
|
async_field = String()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
import cookbook.ingredients.schema
|
import cookbook.ingredients.schema
|
||||||
import cookbook.recipes.schema
|
import cookbook.recipes.schema
|
||||||
import graphene
|
|
||||||
|
|
||||||
from graphene_django.debug import DjangoDebug
|
|
||||||
|
|
||||||
|
|
||||||
class Query(
|
class Query(
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.urls import re_path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from graphene_django.views import AsyncGraphQLView
|
from graphene_django.views import AsyncGraphQLView
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r"^admin/", admin.site.urls),
|
re_path(r"^admin/", admin.site.urls),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
|
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, ROOT_PATH + "/examples/")
|
sys.path.insert(0, ROOT_PATH + "/examples/")
|
||||||
|
|
|
@ -3,9 +3,11 @@ from graphene import Schema, relay, resolve_only_args
|
||||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
|
|
||||||
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
|
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
|
||||||
from .models import Character as CharacterModel
|
from .models import (
|
||||||
from .models import Faction as FactionModel
|
Character as CharacterModel,
|
||||||
from .models import Ship as ShipModel
|
Faction as FactionModel,
|
||||||
|
Ship as ShipModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Ship(DjangoObjectType):
|
class Ship(DjangoObjectType):
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
from .fields import DjangoConnectionField, DjangoListField
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
|
from .utils import bypass_get_queryset
|
||||||
|
|
||||||
__version__ = "3.0.2"
|
__version__ = "3.1.5"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
"DjangoObjectType",
|
"DjangoObjectType",
|
||||||
"DjangoListField",
|
"DjangoListField",
|
||||||
"DjangoConnectionField",
|
"DjangoConnectionField",
|
||||||
|
"bypass_get_queryset",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
# For backwards compatibility, we import JSONField to have it available for import via
|
||||||
|
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
|
||||||
|
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
|
||||||
|
from django.db.models import JSONField
|
||||||
|
|
||||||
|
|
||||||
class MissingType:
|
class MissingType:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
@ -7,19 +13,10 @@ try:
|
||||||
# Postgres fields are only available in Django with psycopg2 installed
|
# Postgres fields are only available in Django with psycopg2 installed
|
||||||
# and we cannot have psycopg2 on PyPy
|
# and we cannot have psycopg2 on PyPy
|
||||||
from django.contrib.postgres.fields import (
|
from django.contrib.postgres.fields import (
|
||||||
IntegerRangeField,
|
|
||||||
ArrayField,
|
ArrayField,
|
||||||
HStoreField,
|
HStoreField,
|
||||||
JSONField as PGJSONField,
|
IntegerRangeField,
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
IntegerRangeField, ArrayField, HStoreField, PGJSONField, RangeField = (
|
IntegerRangeField, ArrayField, HStoreField, RangeField = (MissingType,) * 4
|
||||||
MissingType,
|
|
||||||
) * 5
|
|
||||||
|
|
||||||
try:
|
|
||||||
# JSONField is only available from Django 3.1
|
|
||||||
from django.db.models import JSONField
|
|
||||||
except ImportError:
|
|
||||||
JSONField = MissingType
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
import inspect
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from functools import singledispatch, wraps
|
from functools import partial, singledispatch, wraps
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
ID,
|
ID,
|
||||||
|
@ -12,6 +14,7 @@ from graphene import (
|
||||||
Boolean,
|
Boolean,
|
||||||
Date,
|
Date,
|
||||||
DateTime,
|
DateTime,
|
||||||
|
Decimal,
|
||||||
Dynamic,
|
Dynamic,
|
||||||
Enum,
|
Enum,
|
||||||
Field,
|
Field,
|
||||||
|
@ -21,12 +24,11 @@ from graphene import (
|
||||||
NonNull,
|
NonNull,
|
||||||
String,
|
String,
|
||||||
Time,
|
Time,
|
||||||
Decimal,
|
|
||||||
)
|
)
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
|
from graphene.types.resolver import get_default_resolver
|
||||||
from graphene.types.scalars import BigInt
|
from graphene.types.scalars import BigInt
|
||||||
from graphene.utils.str_converters import to_camel_case
|
from graphene.utils.str_converters import to_camel_case
|
||||||
from graphql import GraphQLError
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from graphql import assert_name
|
from graphql import assert_name
|
||||||
|
@ -35,8 +37,8 @@ except ImportError:
|
||||||
from graphql import assert_valid_name as assert_name
|
from graphql import assert_valid_name as assert_name
|
||||||
from graphql.pyutils import register_description
|
from graphql.pyutils import register_description
|
||||||
|
|
||||||
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
|
from .compat import ArrayField, HStoreField, RangeField
|
||||||
from .fields import DjangoListField, DjangoConnectionField
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
from .utils.str_converters import to_const
|
from .utils.str_converters import to_const
|
||||||
|
|
||||||
|
@ -159,9 +161,7 @@ def get_django_field_description(field):
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def convert_django_field(field, registry=None):
|
def convert_django_field(field, registry=None):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Don't know how to convert the Django field {} ({})".format(
|
f"Don't know how to convert the Django field {field} ({field.__class__})"
|
||||||
field, field.__class__
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -258,6 +258,10 @@ def convert_time_to_string(field, registry=None):
|
||||||
|
|
||||||
@convert_django_field.register(models.OneToOneRel)
|
@convert_django_field.register(models.OneToOneRel)
|
||||||
def convert_onetoone_field_to_djangomodel(field, registry=None):
|
def convert_onetoone_field_to_djangomodel(field, registry=None):
|
||||||
|
from graphene.utils.str_converters import to_snake_case
|
||||||
|
|
||||||
|
from .types import DjangoObjectType
|
||||||
|
|
||||||
model = field.related_model
|
model = field.related_model
|
||||||
|
|
||||||
def dynamic_type():
|
def dynamic_type():
|
||||||
|
@ -265,7 +269,55 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
|
||||||
if not _type:
|
if not _type:
|
||||||
return
|
return
|
||||||
|
|
||||||
return Field(_type, required=not field.null)
|
class CustomField(Field):
|
||||||
|
def wrap_resolve(self, parent_resolver):
|
||||||
|
"""
|
||||||
|
Implements a custom resolver which goes through the `get_node` method to ensure that
|
||||||
|
it goes through the `get_queryset` method of the DjangoObjectType.
|
||||||
|
"""
|
||||||
|
resolver = super().wrap_resolve(parent_resolver)
|
||||||
|
|
||||||
|
# If `get_queryset` was not overridden in the DjangoObjectType
|
||||||
|
# or if we explicitly bypass the `get_queryset` method,
|
||||||
|
# we can just return the default resolver.
|
||||||
|
if (
|
||||||
|
_type.get_queryset.__func__
|
||||||
|
is DjangoObjectType.get_queryset.__func__
|
||||||
|
or getattr(resolver, "_bypass_get_queryset", False)
|
||||||
|
):
|
||||||
|
return resolver
|
||||||
|
|
||||||
|
def custom_resolver(root, info, **args):
|
||||||
|
# Note: this function is used to resolve 1:1 relation fields
|
||||||
|
|
||||||
|
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
|
||||||
|
|
||||||
|
if is_resolver_awaitable:
|
||||||
|
fk_obj = resolver(root, info, **args)
|
||||||
|
# In case the resolver is a custom awaitable resolver that overwrites
|
||||||
|
# the default Django resolver
|
||||||
|
return fk_obj
|
||||||
|
|
||||||
|
field_name = to_snake_case(info.field_name)
|
||||||
|
reversed_field_name = root.__class__._meta.get_field(
|
||||||
|
field_name
|
||||||
|
).remote_field.name
|
||||||
|
try:
|
||||||
|
return _type.get_queryset(
|
||||||
|
_type._meta.model.objects.filter(
|
||||||
|
**{reversed_field_name: root.pk}
|
||||||
|
),
|
||||||
|
info,
|
||||||
|
).get()
|
||||||
|
except _type._meta.model.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return custom_resolver
|
||||||
|
|
||||||
|
return CustomField(
|
||||||
|
_type,
|
||||||
|
required=not field.null,
|
||||||
|
)
|
||||||
|
|
||||||
return Dynamic(dynamic_type)
|
return Dynamic(dynamic_type)
|
||||||
|
|
||||||
|
@ -313,6 +365,10 @@ def convert_field_to_list_or_connection(field, registry=None):
|
||||||
@convert_django_field.register(models.OneToOneField)
|
@convert_django_field.register(models.OneToOneField)
|
||||||
@convert_django_field.register(models.ForeignKey)
|
@convert_django_field.register(models.ForeignKey)
|
||||||
def convert_field_to_djangomodel(field, registry=None):
|
def convert_field_to_djangomodel(field, registry=None):
|
||||||
|
from graphene.utils.str_converters import to_snake_case
|
||||||
|
|
||||||
|
from .types import DjangoObjectType
|
||||||
|
|
||||||
model = field.related_model
|
model = field.related_model
|
||||||
|
|
||||||
def dynamic_type():
|
def dynamic_type():
|
||||||
|
@ -320,7 +376,79 @@ def convert_field_to_djangomodel(field, registry=None):
|
||||||
if not _type:
|
if not _type:
|
||||||
return
|
return
|
||||||
|
|
||||||
return Field(
|
class CustomField(Field):
|
||||||
|
def wrap_resolve(self, parent_resolver):
|
||||||
|
"""
|
||||||
|
Implements a custom resolver which goes through the `get_node` method to ensure that
|
||||||
|
it goes through the `get_queryset` method of the DjangoObjectType.
|
||||||
|
"""
|
||||||
|
resolver = super().wrap_resolve(parent_resolver)
|
||||||
|
|
||||||
|
# If `get_queryset` was not overridden in the DjangoObjectType
|
||||||
|
# or if we explicitly bypass the `get_queryset` method,
|
||||||
|
# we can just return the default resolver.
|
||||||
|
if (
|
||||||
|
_type.get_queryset.__func__
|
||||||
|
is DjangoObjectType.get_queryset.__func__
|
||||||
|
or getattr(resolver, "_bypass_get_queryset", False)
|
||||||
|
):
|
||||||
|
return resolver
|
||||||
|
|
||||||
|
def custom_resolver(root, info, **args):
|
||||||
|
# Note: this function is used to resolve FK or 1:1 fields
|
||||||
|
# it does not differentiate between custom-resolved fields
|
||||||
|
# and default resolved fields.
|
||||||
|
|
||||||
|
# because this is a django foreign key or one-to-one field, the primary-key for
|
||||||
|
# this node can be accessed from the root node.
|
||||||
|
# ex: article.reporter_id
|
||||||
|
|
||||||
|
# get the name of the id field from the root's model
|
||||||
|
field_name = to_snake_case(info.field_name)
|
||||||
|
db_field_key = root.__class__._meta.get_field(field_name).attname
|
||||||
|
if hasattr(root, db_field_key):
|
||||||
|
# get the object's primary-key from root
|
||||||
|
object_pk = getattr(root, db_field_key)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
|
||||||
|
|
||||||
|
if is_resolver_awaitable:
|
||||||
|
fk_obj = resolver(root, info, **args)
|
||||||
|
# In case the resolver is a custom awaitable resolver that overwrites
|
||||||
|
# the default Django resolver
|
||||||
|
return fk_obj
|
||||||
|
|
||||||
|
instance_from_get_node = _type.get_node(info, object_pk)
|
||||||
|
|
||||||
|
if instance_from_get_node is None:
|
||||||
|
# no instance to return
|
||||||
|
return
|
||||||
|
elif (
|
||||||
|
isinstance(resolver, partial)
|
||||||
|
and resolver.func is get_default_resolver()
|
||||||
|
):
|
||||||
|
return instance_from_get_node
|
||||||
|
elif resolver is not get_default_resolver():
|
||||||
|
# Default resolver is overridden
|
||||||
|
# For optimization, add the instance to the resolver
|
||||||
|
setattr(root, field_name, instance_from_get_node)
|
||||||
|
# Explanation:
|
||||||
|
# previously, _type.get_node` is called which results in at least one hit to the database.
|
||||||
|
# But, if we did not pass the instance to the root, calling the resolver will result in
|
||||||
|
# another call to get the instance which results in at least two database queries in total
|
||||||
|
# to resolve this node only.
|
||||||
|
# That's why the value of the object is set in the root so when the object is accessed
|
||||||
|
# in the resolver (root.field_name) it does not access the database unless queried explicitly.
|
||||||
|
fk_obj = resolver(root, info, **args)
|
||||||
|
return fk_obj
|
||||||
|
else:
|
||||||
|
return instance_from_get_node
|
||||||
|
|
||||||
|
return custom_resolver
|
||||||
|
|
||||||
|
return CustomField(
|
||||||
_type,
|
_type,
|
||||||
description=get_django_field_description(field),
|
description=get_django_field_description(field),
|
||||||
required=not field.null,
|
required=not field.null,
|
||||||
|
@ -346,9 +474,8 @@ def convert_postgres_array_to_list(field, registry=None):
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(HStoreField)
|
@convert_django_field.register(HStoreField)
|
||||||
@convert_django_field.register(PGJSONField)
|
@convert_django_field.register(models.JSONField)
|
||||||
@convert_django_field.register(JSONField)
|
def convert_json_field_to_string(field, registry=None):
|
||||||
def convert_pg_and_json_field_to_string(field, registry=None):
|
|
||||||
return JSONString(
|
return JSONString(
|
||||||
description=get_django_field_description(field), required=not field.null
|
description=get_django_field_description(field), required=not field.null
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import graphene
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from graphene import List, ObjectType
|
from graphene import List, ObjectType
|
||||||
|
|
||||||
from .sql.types import DjangoDebugSQL
|
|
||||||
from .exception.types import DjangoDebugException
|
from .exception.types import DjangoDebugException
|
||||||
|
from .sql.types import DjangoDebugSQL
|
||||||
|
|
||||||
|
|
||||||
class DjangoDebug(ObjectType):
|
class DjangoDebug(ObjectType):
|
||||||
|
|
|
@ -2,7 +2,6 @@ import inspect
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from graphql_relay import (
|
from graphql_relay import (
|
||||||
connection_from_array_slice,
|
connection_from_array_slice,
|
||||||
cursor_to_offset,
|
cursor_to_offset,
|
||||||
|
@ -11,6 +10,7 @@ from graphql_relay import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
from promise import Promise
|
||||||
|
|
||||||
from graphene import Int, NonNull
|
from graphene import Int, NonNull
|
||||||
from graphene.relay import ConnectionField
|
from graphene.relay import ConnectionField
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from ..utils import DJANGO_FILTER_INSTALLED
|
from ..utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
if not DJANGO_FILTER_INSTALLED:
|
if not DJANGO_FILTER_INSTALLED:
|
||||||
|
|
|
@ -3,8 +3,8 @@ from functools import partial
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from graphene.types.enum import EnumType
|
|
||||||
from graphene.types.argument import to_arguments
|
from graphene.types.argument import to_arguments
|
||||||
|
from graphene.types.enum import EnumType
|
||||||
from graphene.utils.str_converters import to_snake_case
|
from graphene.utils.str_converters import to_snake_case
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
@ -60,7 +60,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
def filterset_class(self):
|
def filterset_class(self):
|
||||||
if not self._filterset_class:
|
if not self._filterset_class:
|
||||||
fields = self._fields or self.node_type._meta.filter_fields
|
fields = self._fields or self.node_type._meta.filter_fields
|
||||||
meta = dict(model=self.model, fields=fields)
|
meta = {"model": self.model, "fields": fields}
|
||||||
if self._extra_filter_meta:
|
if self._extra_filter_meta:
|
||||||
meta.update(self._extra_filter_meta)
|
meta.update(self._extra_filter_meta)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from ...utils import DJANGO_FILTER_INSTALLED
|
from ...utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
if not DJANGO_FILTER_INSTALLED:
|
if not DJANGO_FILTER_INSTALLED:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django_filters import Filter, MultipleChoiceFilter
|
from django_filters import Filter, MultipleChoiceFilter
|
||||||
|
|
||||||
from graphql_relay.node.node import from_global_id
|
from graphql_relay.node.node import from_global_id
|
||||||
|
|
||||||
from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_filters.filterset import BaseFilterSet, FilterSet
|
from django_filters.filterset import (
|
||||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
FILTER_FOR_DBFIELD_DEFAULTS,
|
||||||
|
BaseFilterSet,
|
||||||
|
FilterSet,
|
||||||
|
)
|
||||||
|
|
||||||
from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter
|
from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter
|
||||||
|
|
||||||
|
|
||||||
GRAPHENE_FILTER_SET_OVERRIDES = {
|
GRAPHENE_FILTER_SET_OVERRIDES = {
|
||||||
models.AutoField: {"filter_class": GlobalIDFilter},
|
models.AutoField: {"filter_class": GlobalIDFilter},
|
||||||
models.OneToOneField: {"filter_class": GlobalIDFilter},
|
models.OneToOneField: {"filter_class": GlobalIDFilter},
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django_filters import filters
|
|
||||||
from django_filters import FilterSet
|
from django_filters import FilterSet
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
from graphene_django.filter import ArrayFilter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
from graphene_django.filter import ArrayFilter, ListFilter
|
|
||||||
|
|
||||||
from ...compat import ArrayField
|
from ...compat import ArrayField
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,7 @@ import pytest
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
from graphene_django import DjangoObjectType, DjangoConnectionField
|
|
||||||
from graphene_django.tests.models import Article, Reporter
|
from graphene_django.tests.models import Article, Reporter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,8 @@ if DJANGO_FILTER_INSTALLED:
|
||||||
from django_filters import FilterSet, NumberFilter, OrderingFilter
|
from django_filters import FilterSet, NumberFilter, OrderingFilter
|
||||||
|
|
||||||
from graphene_django.filter import (
|
from graphene_django.filter import (
|
||||||
GlobalIDFilter,
|
|
||||||
DjangoFilterConnectionField,
|
DjangoFilterConnectionField,
|
||||||
|
GlobalIDFilter,
|
||||||
GlobalIDMultipleChoiceFilter,
|
GlobalIDMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from graphene_django.filter.tests.filters import (
|
from graphene_django.filter.tests.filters import (
|
||||||
|
@ -222,7 +222,7 @@ def test_filter_filterset_information_on_meta_related():
|
||||||
reporter = Field(ReporterFilterNode)
|
reporter = Field(ReporterFilterNode)
|
||||||
article = Field(ArticleFilterNode)
|
article = Field(ArticleFilterNode)
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
Schema(query=Query)
|
||||||
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
||||||
assert_arguments(articles_field, "headline", "reporter")
|
assert_arguments(articles_field, "headline", "reporter")
|
||||||
assert_not_orderable(articles_field)
|
assert_not_orderable(articles_field)
|
||||||
|
@ -294,7 +294,7 @@ def test_filter_filterset_class_information_on_meta_related():
|
||||||
reporter = Field(ReporterFilterNode)
|
reporter = Field(ReporterFilterNode)
|
||||||
article = Field(ArticleFilterNode)
|
article = Field(ArticleFilterNode)
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
Schema(query=Query)
|
||||||
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
||||||
assert_arguments(articles_field, "headline", "reporter")
|
assert_arguments(articles_field, "headline", "reporter")
|
||||||
assert_not_orderable(articles_field)
|
assert_not_orderable(articles_field)
|
||||||
|
@ -1186,7 +1186,7 @@ def test_filter_filterset_based_on_mixin():
|
||||||
first_name="Adam", last_name="Doe", email="adam@doe.com"
|
first_name="Adam", last_name="Doe", email="adam@doe.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
article_2 = Article.objects.create(
|
Article.objects.create(
|
||||||
headline="Good Bye",
|
headline="Good Bye",
|
||||||
reporter=reporter_2,
|
reporter=reporter_2,
|
||||||
editor=reporter_2,
|
editor=reporter_2,
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django_filters import (
|
||||||
|
FilterSet,
|
||||||
|
rest_framework as filters,
|
||||||
|
)
|
||||||
|
|
||||||
from django_filters import FilterSet
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
from graphene import ObjectType, Schema
|
from graphene import ObjectType, Schema
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.tests.models import Pet, Person, Reporter, Article, Film
|
|
||||||
from graphene_django.filter.tests.filters import ArticleFilter
|
from graphene_django.filter.tests.filters import ArticleFilter
|
||||||
|
from graphene_django.tests.models import Article, Film, Person, Pet, Reporter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
pytestmark = []
|
pytestmark = []
|
||||||
|
@ -348,9 +350,9 @@ def test_fk_id_in_filter(query):
|
||||||
|
|
||||||
schema = Schema(query=query)
|
schema = Schema(query=query)
|
||||||
|
|
||||||
query = """
|
query = f"""
|
||||||
query {{
|
query {{
|
||||||
articles (reporter_In: [{}, {}]) {{
|
articles (reporter_In: [{john_doe.id}, {jean_bon.id}]) {{
|
||||||
edges {{
|
edges {{
|
||||||
node {{
|
node {{
|
||||||
headline
|
headline
|
||||||
|
@ -361,10 +363,7 @@ def test_fk_id_in_filter(query):
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
""".format(
|
"""
|
||||||
john_doe.id,
|
|
||||||
jean_bon.id,
|
|
||||||
)
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["articles"]["edges"] == [
|
assert result.data["articles"]["edges"] == [
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django_filters import FilterSet
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
from graphene import ObjectType, Schema
|
from graphene import ObjectType, Schema
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django_filters import FilterSet
|
from django_filters import FilterSet
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.tests.models import Article, Reporter
|
from graphene_django.tests.models import Article, Reporter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
@ -14,8 +12,8 @@ pytestmark = []
|
||||||
if DJANGO_FILTER_INSTALLED:
|
if DJANGO_FILTER_INSTALLED:
|
||||||
from graphene_django.filter import (
|
from graphene_django.filter import (
|
||||||
DjangoFilterConnectionField,
|
DjangoFilterConnectionField,
|
||||||
TypedFilter,
|
|
||||||
ListFilter,
|
ListFilter,
|
||||||
|
TypedFilter,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pytestmark.append(
|
pytestmark.append(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import graphene
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django_filters.utils import get_model_field, get_field_parts
|
from django_filters.utils import get_model_field
|
||||||
from django_filters.filters import Filter, BaseCSVFilter
|
|
||||||
from .filters import ArrayFilter, ListFilter, RangeFilter, TypedFilter
|
import graphene
|
||||||
from .filterset import custom_filterset_factory, setup_filterset
|
|
||||||
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
|
from .filters import ListFilter, RangeFilter, TypedFilter
|
||||||
|
from .filterset import custom_filterset_factory, setup_filterset
|
||||||
|
|
||||||
|
|
||||||
def get_field_type(registry, model, field_name):
|
def get_field_type(registry, model, field_name):
|
||||||
|
@ -50,7 +51,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
||||||
):
|
):
|
||||||
# Get the filter field for filters that are no explicitly declared.
|
# Get the filter field for filters that are no explicitly declared.
|
||||||
if filter_type == "isnull":
|
if filter_type == "isnull":
|
||||||
field = graphene.Boolean(required=required)
|
field_type = graphene.Boolean
|
||||||
else:
|
else:
|
||||||
model_field = get_model_field(model, filter_field.field_name)
|
model_field = get_model_field(model, filter_field.field_name)
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,15 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
ID,
|
ID,
|
||||||
|
UUID,
|
||||||
Boolean,
|
Boolean,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
Decimal,
|
Decimal,
|
||||||
Float,
|
Float,
|
||||||
Int,
|
Int,
|
||||||
List,
|
List,
|
||||||
String,
|
String,
|
||||||
UUID,
|
|
||||||
Date,
|
|
||||||
DateTime,
|
|
||||||
Time,
|
Time,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ def get_form_field_description(field):
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def convert_form_field(field):
|
def convert_form_field(field):
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Don't know how to convert the Django form field %s (%s) "
|
f"Don't know how to convert the Django form field {field} ({field.__class__}) "
|
||||||
"to Graphene type" % (field, field.__class__)
|
"to Graphene type"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import binascii
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import CharField, Field, MultipleChoiceField
|
from django.forms import CharField, Field, MultipleChoiceField
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from graphql_relay import from_global_id
|
from graphql_relay import from_global_id
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
import graphene
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
String,
|
|
||||||
Int,
|
|
||||||
Boolean,
|
|
||||||
Decimal,
|
|
||||||
Float,
|
|
||||||
ID,
|
ID,
|
||||||
UUID,
|
UUID,
|
||||||
|
Boolean,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Decimal,
|
||||||
|
Float,
|
||||||
|
Int,
|
||||||
List,
|
List,
|
||||||
NonNull,
|
NonNull,
|
||||||
DateTime,
|
String,
|
||||||
Date,
|
|
||||||
Time,
|
Time,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
333
graphene_django/forms/tests/test_djangoinputobject.py
Normal file
333
graphene_django/forms/tests/test_djangoinputobject.py
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
from django import forms
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from ...tests.models import CHOICES, Film, Reporter
|
||||||
|
from ..types import DjangoFormInputObjectType
|
||||||
|
|
||||||
|
# Reporter a_choice CHOICES = ((1, "this"), (2, _("that")))
|
||||||
|
THIS = CHOICES[0][0]
|
||||||
|
THIS_ON_CLIENT_CONVERTED = "A_1"
|
||||||
|
|
||||||
|
# Film genre choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")],
|
||||||
|
DOCUMENTARY = "do"
|
||||||
|
DOCUMENTARY_ON_CLIENT_CONVERTED = "DO"
|
||||||
|
|
||||||
|
|
||||||
|
class FilmForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
exclude = ()
|
||||||
|
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class ReporterForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
exclude = ("pets", "email", "fans")
|
||||||
|
|
||||||
|
|
||||||
|
class MyForm(forms.Form):
|
||||||
|
text_field = forms.CharField()
|
||||||
|
int_field = forms.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_form_class():
|
||||||
|
with raises(Exception) as exc:
|
||||||
|
|
||||||
|
class MyInputType(DjangoFormInputObjectType):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert exc.value.args[0] == "form_class is required for DjangoFormInputObjectType"
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_from_modelform_has_input_fields():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
only_fields = ("first_name", "last_name", "a_choice")
|
||||||
|
|
||||||
|
fields = ["first_name", "last_name", "a_choice", "id"]
|
||||||
|
assert all(f in ReporterInputType._meta.fields for f in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_from_form_has_input_fields():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "id"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_custom_id_field():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
add_id_field_name = "pk"
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "pk"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
assert MyFormInputType._meta.fields["pk"].type is graphene.ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_custom_id_field_type():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
add_id_field_name = "pk"
|
||||||
|
add_id_field_type = graphene.String(required=False)
|
||||||
|
|
||||||
|
fields = ["text_field", "int_field", "pk"]
|
||||||
|
assert all(f in MyFormInputType._meta.fields for f in fields)
|
||||||
|
assert MyFormInputType._meta.fields["pk"].type is graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
class MockQuery(graphene.ObjectType):
|
||||||
|
a = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_with_form_djangoforminputtype():
|
||||||
|
class MyFormInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
class MyFormMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
form_data = MyFormInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.Boolean()
|
||||||
|
|
||||||
|
def mutate(_root, _info, form_data):
|
||||||
|
form = MyForm(data=form_data)
|
||||||
|
if form.is_valid():
|
||||||
|
result = form.cleaned_data == {
|
||||||
|
"text_field": "text",
|
||||||
|
"int_field": 777,
|
||||||
|
}
|
||||||
|
return MyFormMutation(result=result)
|
||||||
|
return MyFormMutation(result=False)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
myForm_mutation = MyFormMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyFormMutation($formData: MyFormInputType!) {
|
||||||
|
myFormMutation(formData: $formData) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"formData": {"textField": "text", "intField": 777}},
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data == {"myFormMutation": {"result": True}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_with_modelform_djangoforminputtype():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterType
|
||||||
|
only_fields = ("first_name", "last_name", "a_choice")
|
||||||
|
|
||||||
|
class ReporterMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
reporter_data = ReporterInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def mutate(_root, _info, reporter_data):
|
||||||
|
reporter = Reporter.objects.get(pk=reporter_data.id)
|
||||||
|
form = ReporterForm(data=reporter_data, instance=reporter)
|
||||||
|
if form.is_valid():
|
||||||
|
reporter = form.save()
|
||||||
|
return ReporterMutation(result=reporter)
|
||||||
|
|
||||||
|
return ReporterMutation(result=None)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
report_mutation = ReporterMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
reporter = Reporter.objects.create(
|
||||||
|
first_name="Bob", last_name="Roberts", a_choice=THIS
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation ReportMutation($reporterData: ReporterInputType!) {
|
||||||
|
reportMutation(reporterData: $reporterData) {
|
||||||
|
result {
|
||||||
|
id,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
aChoice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={
|
||||||
|
"reporterData": {
|
||||||
|
"id": reporter.pk,
|
||||||
|
"firstName": "Dave",
|
||||||
|
"lastName": "Smith",
|
||||||
|
"aChoice": THIS_ON_CLIENT_CONVERTED,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["result"] == {
|
||||||
|
"id": "1",
|
||||||
|
"firstName": "Dave",
|
||||||
|
"lastName": "Smith",
|
||||||
|
"aChoice": THIS_ON_CLIENT_CONVERTED,
|
||||||
|
}
|
||||||
|
assert Reporter.objects.count() == 1
|
||||||
|
reporter.refresh_from_db()
|
||||||
|
assert reporter.first_name == "Dave"
|
||||||
|
|
||||||
|
|
||||||
|
def reporter_enum_convert_mutation_result(
|
||||||
|
ReporterInputType, choice_val_on_client=THIS_ON_CLIENT_CONVERTED
|
||||||
|
):
|
||||||
|
class ReporterMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
reporter = ReporterInputType(required=True)
|
||||||
|
|
||||||
|
result_str = graphene.String()
|
||||||
|
result_int = graphene.Int()
|
||||||
|
|
||||||
|
def mutate(_root, _info, reporter):
|
||||||
|
if isinstance(reporter.a_choice, int) or reporter.a_choice.isdigit():
|
||||||
|
return ReporterMutation(result_int=reporter.a_choice, result_str=None)
|
||||||
|
return ReporterMutation(result_int=None, result_str=reporter.a_choice)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
report_mutation = ReporterMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
return schema.execute(
|
||||||
|
""" mutation ReportMutation($reporter: ReporterInputType!) {
|
||||||
|
reportMutation(reporter: $reporter) {
|
||||||
|
resultStr,
|
||||||
|
resultInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"reporter": {"aChoice": choice_val_on_client}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_not_converted():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultStr"] == THIS_ON_CLIENT_CONVERTED
|
||||||
|
assert result.data["reportMutation"]["resultInt"] is None
|
||||||
|
assert ReporterInputType._meta.fields["a_choice"].type is graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_is_converted_to_original():
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterType
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultInt"] == THIS
|
||||||
|
assert result.data["reportMutation"]["resultStr"] is None
|
||||||
|
assert (
|
||||||
|
ReporterInputType._meta.fields["a_choice"].type.__name__
|
||||||
|
== "AChoiceEnumBackConvString"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_choices_to_enum_is_false_and_field_type_as_in_model():
|
||||||
|
class ReporterTypeNotConvertChoices(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
convert_choices_to_enum = False
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class ReporterInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = ReporterForm
|
||||||
|
object_type = ReporterTypeNotConvertChoices
|
||||||
|
only_fields = ("a_choice",)
|
||||||
|
|
||||||
|
result = reporter_enum_convert_mutation_result(ReporterInputType, THIS)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["reportMutation"]["resultInt"] == THIS
|
||||||
|
assert result.data["reportMutation"]["resultStr"] is None
|
||||||
|
assert ReporterInputType._meta.fields["a_choice"].type is graphene.Int
|
||||||
|
|
||||||
|
|
||||||
|
def enum_convert_mutation_result_film(FilmInputType):
|
||||||
|
class FilmMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
film = FilmInputType(required=True)
|
||||||
|
|
||||||
|
result = graphene.String()
|
||||||
|
|
||||||
|
def mutate(_root, _info, film):
|
||||||
|
return FilmMutation(result=film.genre)
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
film_mutation = FilmMutation.Field()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
return schema.execute(
|
||||||
|
""" mutation FilmMutation($film: FilmInputType!) {
|
||||||
|
filmMutation(film: $film) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"film": {"genre": DOCUMENTARY_ON_CLIENT_CONVERTED}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_not_converted_required_non_number():
|
||||||
|
class FilmInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = FilmForm
|
||||||
|
only_fields = ("genre",)
|
||||||
|
|
||||||
|
result = enum_convert_mutation_result_film(FilmInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["filmMutation"]["result"] == DOCUMENTARY_ON_CLIENT_CONVERTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_is_converted_to_original_required_non_number():
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class FilmInputType(DjangoFormInputObjectType):
|
||||||
|
class Meta:
|
||||||
|
form_class = FilmForm
|
||||||
|
object_type = FilmType
|
||||||
|
only_fields = ("genre",)
|
||||||
|
|
||||||
|
result = enum_convert_mutation_result_film(FilmInputType)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["filmMutation"]["result"] == DOCUMENTARY
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
@ -280,7 +279,7 @@ def test_model_form_mutation_mutate_invalid_form():
|
||||||
result = PetMutation.mutate_and_get_payload(None, None)
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
|
|
||||||
# A pet was not created
|
# A pet was not created
|
||||||
Pet.objects.count() == 0
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
fields_w_error = [e.field for e in result.errors]
|
fields_w_error = [e.field for e in result.errors]
|
||||||
assert len(result.errors) == 2
|
assert len(result.errors) == 2
|
||||||
|
|
|
@ -1 +1,117 @@
|
||||||
|
import graphene
|
||||||
|
from graphene import ID
|
||||||
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
|
from graphene.utils.str_converters import to_camel_case
|
||||||
|
|
||||||
|
from ..converter import BlankValueField
|
||||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
||||||
|
from .mutation import fields_for_form
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoFormInputObjectType(InputObjectType):
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass_with_meta__(
|
||||||
|
cls,
|
||||||
|
container=None,
|
||||||
|
_meta=None,
|
||||||
|
only_fields=(),
|
||||||
|
exclude_fields=(),
|
||||||
|
form_class=None,
|
||||||
|
object_type=None,
|
||||||
|
add_id_field_name=None,
|
||||||
|
add_id_field_type=None,
|
||||||
|
**options,
|
||||||
|
):
|
||||||
|
"""Retrieve fields from django form (Meta.form_class). Received
|
||||||
|
fields are set to cls (they will be converted to input fields
|
||||||
|
by InputObjectType). Type of fields with choices (converted
|
||||||
|
to enum) is set to custom scalar type (using Meta.object_type)
|
||||||
|
to dynamically convert enum values back.
|
||||||
|
|
||||||
|
class MyDjangoFormInput(DjangoFormInputObjectType):
|
||||||
|
# any other fields can be placed here and other inputobjectforms as well
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
form_class = MyDjangoModelForm
|
||||||
|
object_type = MyModelType
|
||||||
|
|
||||||
|
class SomeMutation(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
data = MyDjangoFormInput(required=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mutate(_root, _info, data):
|
||||||
|
form_inst = MyDjangoModelForm(data=data)
|
||||||
|
if form_inst.is_valid():
|
||||||
|
django_model_instance = form_inst.save(commit=False)
|
||||||
|
# ... etc ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not form_class:
|
||||||
|
raise Exception("form_class is required for DjangoFormInputObjectType")
|
||||||
|
|
||||||
|
form = form_class()
|
||||||
|
form_fields = fields_for_form(form, only_fields, exclude_fields)
|
||||||
|
|
||||||
|
for name, field in form_fields.items():
|
||||||
|
if (
|
||||||
|
object_type
|
||||||
|
and name in object_type._meta.fields
|
||||||
|
and isinstance(object_type._meta.fields[name], BlankValueField)
|
||||||
|
):
|
||||||
|
# Field type BlankValueField here means that field
|
||||||
|
# with choises have been converted to enum
|
||||||
|
# (BlankValueField is using only for that task ?)
|
||||||
|
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
|
||||||
|
elif (
|
||||||
|
object_type
|
||||||
|
and name in object_type._meta.fields
|
||||||
|
and object_type._meta.convert_choices_to_enum is False
|
||||||
|
and form.fields[name].__class__.__name__ == "TypedChoiceField"
|
||||||
|
):
|
||||||
|
# FIXME
|
||||||
|
# in case if convert_choices_to_enum is False
|
||||||
|
# form field class is converted to String but original
|
||||||
|
# model field type is needed here... (.converter.py bug?)
|
||||||
|
# This is temp workaround to get field type from ObjectType field
|
||||||
|
# TEST: test_enum_not_converted_and_field_type_as_in_model
|
||||||
|
setattr(cls, name, object_type._meta.fields[name].type())
|
||||||
|
else:
|
||||||
|
# set input field according to django form field
|
||||||
|
setattr(cls, name, field)
|
||||||
|
|
||||||
|
# explicitly adding id field (absent in django form fields)
|
||||||
|
# with name and type from Meta or 'id' with graphene.ID by default
|
||||||
|
if add_id_field_name:
|
||||||
|
setattr(cls, add_id_field_name, add_id_field_type or ID(required=False))
|
||||||
|
elif "id" not in exclude_fields:
|
||||||
|
cls.id = ID(required=False)
|
||||||
|
|
||||||
|
super().__init_subclass_with_meta__(container=container, _meta=_meta, **options)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_enum_cnv_cls_instance(field_name, object_type):
|
||||||
|
"""Saves args in context to convert enum values in
|
||||||
|
Dynamically created Scalar derived class
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_value(value):
|
||||||
|
# field_name & object_type have been saved in context (closure)
|
||||||
|
field = object_type._meta.fields[field_name]
|
||||||
|
if isinstance(field.type, graphene.NonNull):
|
||||||
|
val_before_convert = field.type._of_type[value].value
|
||||||
|
else:
|
||||||
|
val_before_convert = field.type[value].value
|
||||||
|
return graphene.String.parse_value(val_before_convert)
|
||||||
|
|
||||||
|
cls_doc = "String scalar to convert choice value back from enum to original"
|
||||||
|
scalar_type = type(
|
||||||
|
(
|
||||||
|
f"{field_name[0].upper()}{to_camel_case(field_name[1:])}"
|
||||||
|
"EnumBackConvString"
|
||||||
|
),
|
||||||
|
(graphene.String,),
|
||||||
|
{"parse_value": parse_value, "__doc__": cls_doc},
|
||||||
|
)
|
||||||
|
return scalar_type()
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import os
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
import functools
|
import os
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils import autoreload
|
from django.utils import autoreload
|
||||||
|
|
||||||
from graphql import print_schema
|
from graphql import print_schema
|
||||||
|
|
||||||
from graphene_django.settings import graphene_settings
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class Command(CommandArguments):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
options_schema = options.get("schema")
|
options_schema = options.get("schema")
|
||||||
|
|
||||||
if options_schema and type(options_schema) is str:
|
if options_schema and isinstance(options_schema, str):
|
||||||
module_str, schema_name = options_schema.rsplit(".", 1)
|
module_str, schema_name = options_schema.rsplit(".", 1)
|
||||||
mod = importlib.import_module(module_str)
|
mod = importlib.import_module(module_str)
|
||||||
schema = getattr(mod, schema_name)
|
schema = getattr(mod, schema_name)
|
||||||
|
|
|
@ -8,9 +8,7 @@ class Registry:
|
||||||
|
|
||||||
assert issubclass(
|
assert issubclass(
|
||||||
cls, DjangoObjectType
|
cls, DjangoObjectType
|
||||||
), 'Only DjangoObjectTypes can be registered, received "{}"'.format(
|
), f'Only DjangoObjectTypes can be registered, received "{cls.__name__}"'
|
||||||
cls.__name__
|
|
||||||
)
|
|
||||||
assert cls._meta.registry == self, "Registry for a Model have to match."
|
assert cls._meta.registry == self, "Registry for a Model have to match."
|
||||||
# assert self.get_type_for_model(cls._meta.model) == cls, (
|
# assert self.get_type_for_model(cls._meta.model) == cls, (
|
||||||
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
|
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)
|
||||||
|
|
|
@ -14,3 +14,14 @@ class MyFakeModelWithPassword(models.Model):
|
||||||
class MyFakeModelWithDate(models.Model):
|
class MyFakeModelWithDate(models.Model):
|
||||||
cool_name = models.CharField(max_length=50)
|
cool_name = models.CharField(max_length=50)
|
||||||
last_edited = models.DateField()
|
last_edited = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModelWithChoiceField(models.Model):
|
||||||
|
class ChoiceType(models.Choices):
|
||||||
|
ASDF = "asdf"
|
||||||
|
HI = "hi"
|
||||||
|
|
||||||
|
choice_type = models.CharField(
|
||||||
|
max_length=4,
|
||||||
|
default=ChoiceType.HI.name,
|
||||||
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -41,6 +42,9 @@ def fields_for_serializer(
|
||||||
field.read_only
|
field.read_only
|
||||||
and is_input
|
and is_input
|
||||||
and lookup_field != name, # don't show read_only fields in Input
|
and lookup_field != name, # don't show read_only fields in Input
|
||||||
|
isinstance(
|
||||||
|
field, serializers.HiddenField
|
||||||
|
), # don't show hidden fields in Input
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,8 +127,10 @@ class SerializerMutation(ClientIDMutation):
|
||||||
def get_serializer_kwargs(cls, root, info, **input):
|
def get_serializer_kwargs(cls, root, info, **input):
|
||||||
lookup_field = cls._meta.lookup_field
|
lookup_field = cls._meta.lookup_field
|
||||||
model_class = cls._meta.model_class
|
model_class = cls._meta.model_class
|
||||||
|
|
||||||
if model_class:
|
if model_class:
|
||||||
|
for input_dict_key, maybe_enum in input.items():
|
||||||
|
if isinstance(maybe_enum, Enum):
|
||||||
|
input[input_dict_key] = maybe_enum.value
|
||||||
if "update" in cls._meta.model_operations and lookup_field in input:
|
if "update" in cls._meta.model_operations and lookup_field in input:
|
||||||
instance = get_object_or_404(
|
instance = get_object_or_404(
|
||||||
model_class, **{lookup_field: input[lookup_field]}
|
model_class, **{lookup_field: input[lookup_field]}
|
||||||
|
|
|
@ -5,16 +5,16 @@ from rest_framework import serializers
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from ..registry import get_global_registry
|
|
||||||
from ..converter import convert_choices_to_named_enum_with_descriptions
|
from ..converter import convert_choices_to_named_enum_with_descriptions
|
||||||
|
from ..registry import get_global_registry
|
||||||
from .types import DictType
|
from .types import DictType
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def get_graphene_type_from_serializer_field(field):
|
def get_graphene_type_from_serializer_field(field):
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Don't know how to convert the serializer field %s (%s) "
|
f"Don't know how to convert the serializer field {field} ({field.__class__}) "
|
||||||
"to Graphene type" % (field, field.__class__)
|
"to Graphene type"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
import graphene
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from graphene import InputObjectType
|
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
|
||||||
from ..serializer_converter import convert_serializer_field
|
from ..serializer_converter import convert_serializer_field
|
||||||
from ..types import DictType
|
from ..types import DictType
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,12 @@ from graphene import Field, ResolveInfo
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
|
|
||||||
from ...types import DjangoObjectType
|
from ...types import DjangoObjectType
|
||||||
from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
|
from ..models import (
|
||||||
|
MyFakeModel,
|
||||||
|
MyFakeModelWithChoiceField,
|
||||||
|
MyFakeModelWithDate,
|
||||||
|
MyFakeModelWithPassword,
|
||||||
|
)
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,6 +169,21 @@ def test_read_only_fields():
|
||||||
), "'cool_name' is read_only field and shouldn't be on arguments"
|
), "'cool_name' is read_only field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
|
def test_hidden_fields():
|
||||||
|
class SerializerWithHiddenField(serializers.Serializer):
|
||||||
|
cool_name = serializers.CharField()
|
||||||
|
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = SerializerWithHiddenField
|
||||||
|
|
||||||
|
assert "cool_name" in MyMutation.Input._meta.fields
|
||||||
|
assert (
|
||||||
|
"user" not in MyMutation.Input._meta.fields
|
||||||
|
), "'user' is hidden field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -230,7 +250,7 @@ def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
model_operations = ["update"]
|
model_operations = ["update"]
|
||||||
|
|
||||||
with raises(Exception) as exc:
|
with raises(Exception) as exc:
|
||||||
result = InvalidModelMutation.mutate_and_get_payload(
|
InvalidModelMutation.mutate_and_get_payload(
|
||||||
None, mock_info(), **{"cool_name": "Narf"}
|
None, mock_info(), **{"cool_name": "Narf"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -253,6 +273,39 @@ def test_perform_mutate_success():
|
||||||
assert result.days_since_last_edit == 4
|
assert result.days_since_last_edit == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_mutate_success_with_enum_choice_field():
|
||||||
|
class ListViewChoiceFieldSerializer(serializers.ModelSerializer):
|
||||||
|
choice_type = serializers.ChoiceField(
|
||||||
|
choices=[(x.name, x.value) for x in MyFakeModelWithChoiceField.ChoiceType],
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithChoiceField
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class SomeCreateSerializerMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = ListViewChoiceFieldSerializer
|
||||||
|
|
||||||
|
choice_type = {
|
||||||
|
"choice_type": SomeCreateSerializerMutation.Input.choice_type.type.get("ASDF")
|
||||||
|
}
|
||||||
|
name = MyFakeModelWithChoiceField.ChoiceType.ASDF.name
|
||||||
|
result = SomeCreateSerializerMutation.mutate_and_get_payload(
|
||||||
|
None, mock_info(), **choice_type
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.choice_type == name
|
||||||
|
kwargs = SomeCreateSerializerMutation.get_serializer_kwargs(
|
||||||
|
None, mock_info(), **choice_type
|
||||||
|
)
|
||||||
|
assert kwargs["data"]["choice_type"] == name
|
||||||
|
assert 1 == MyFakeModelWithChoiceField.objects.count()
|
||||||
|
item = MyFakeModelWithChoiceField.objects.first()
|
||||||
|
assert item.choice_type == name
|
||||||
|
|
||||||
|
|
||||||
def test_mutate_and_get_payload_error():
|
def test_mutate_and_get_payload_error():
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -12,11 +12,10 @@ Graphene settings, checking for user settings first, then falling
|
||||||
back to the defaults.
|
back to the defaults.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test.signals import setting_changed
|
|
||||||
|
|
||||||
import importlib # Available in Python 3.1+
|
import importlib # Available in Python 3.1+
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test.signals import setting_changed
|
||||||
|
|
||||||
# Copied shamelessly from Django REST Framework
|
# Copied shamelessly from Django REST Framework
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ add "&raw" to the end of the URL within a browser.
|
||||||
integrity="{{graphiql_css_sri}}"
|
integrity="{{graphiql_css_sri}}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
crossorigin="anonymous" />
|
crossorigin="anonymous" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/style.css"
|
||||||
|
integrity="{{graphiql_plugin_explorer_css_sri}}"
|
||||||
|
rel="stylesheet"
|
||||||
|
crossorigin="anonymous" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
||||||
integrity="{{whatwg_fetch_sri}}"
|
integrity="{{whatwg_fetch_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
# https://github.com/graphql-python/graphene-django/issues/520
|
# https://github.com/graphql-python/graphene-django/issues/520
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from graphene import Field, ResolveInfo
|
from ...forms.mutation import DjangoFormMutation
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
|
||||||
from pytest import raises
|
|
||||||
from pytest import mark
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from ...types import DjangoObjectType
|
|
||||||
from ...rest_framework.models import MyFakeModel
|
from ...rest_framework.models import MyFakeModel
|
||||||
from ...rest_framework.mutation import SerializerMutation
|
from ...rest_framework.mutation import SerializerMutation
|
||||||
from ...forms.mutation import DjangoFormMutation
|
|
||||||
|
|
||||||
|
|
||||||
class MyModelSerializer(serializers.ModelSerializer):
|
class MyModelSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -19,7 +19,11 @@ class Pet(models.Model):
|
||||||
class FilmDetails(models.Model):
|
class FilmDetails(models.Model):
|
||||||
location = models.CharField(max_length=30)
|
location = models.CharField(max_length=30)
|
||||||
film = models.OneToOneField(
|
film = models.OneToOneField(
|
||||||
"Film", on_delete=models.CASCADE, related_name="details"
|
"Film",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="details",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,9 +47,10 @@ class Reporter(models.Model):
|
||||||
last_name = models.CharField(max_length=30)
|
last_name = models.CharField(max_length=30)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
pets = models.ManyToManyField("self")
|
pets = models.ManyToManyField("self")
|
||||||
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
|
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True)
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
doe_objects = DoeReporterManager()
|
doe_objects = DoeReporterManager()
|
||||||
|
fans = models.ManyToManyField(Person)
|
||||||
|
|
||||||
reporter_type = models.IntegerField(
|
reporter_type = models.IntegerField(
|
||||||
"Reporter Type",
|
"Reporter Type",
|
||||||
|
@ -90,6 +95,16 @@ class CNNReporter(Reporter):
|
||||||
objects = CNNReporterManager()
|
objects = CNNReporterManager()
|
||||||
|
|
||||||
|
|
||||||
|
class APNewsReporter(Reporter):
|
||||||
|
"""
|
||||||
|
This class only inherits from Reporter for testing multi table inheritence
|
||||||
|
similar to what you'd see in django-polymorphic
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias = models.CharField(max_length=30)
|
||||||
|
objects = models.Manager()
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
headline = models.CharField(max_length=100)
|
headline = models.CharField(max_length=100)
|
||||||
pub_date = models.DateField(auto_now_add=True)
|
pub_date = models.DateField(auto_now_add=True)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from graphene import Field
|
from graphene import Field
|
||||||
|
|
||||||
from graphene_django.forms.mutation import DjangoFormMutation, DjangoModelFormMutation
|
from graphene_django.forms.mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||||
|
|
||||||
from .forms import PetForm
|
from .forms import PetForm
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
from io import StringIO
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from unittest.mock import mock_open, patch
|
||||||
|
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from io import StringIO
|
|
||||||
from unittest.mock import mock_open, patch
|
|
||||||
|
|
||||||
from graphene import ObjectType, Schema, String
|
from graphene import ObjectType, Schema, String
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def test_generate_graphql_file_on_call_graphql_schema():
|
||||||
open_mock.assert_called_once()
|
open_mock.assert_called_once()
|
||||||
|
|
||||||
handle = open_mock()
|
handle = open_mock()
|
||||||
assert handle.write.called_once()
|
handle.write.assert_called_once()
|
||||||
|
|
||||||
schema_output = handle.write.call_args[0][0]
|
schema_output = handle.write.call_args[0][0]
|
||||||
assert schema_output == dedent(
|
assert schema_output == dedent(
|
||||||
|
|
|
@ -15,8 +15,6 @@ from graphene.types.scalars import BigInt
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
ArrayField,
|
ArrayField,
|
||||||
HStoreField,
|
HStoreField,
|
||||||
JSONField,
|
|
||||||
PGJSONField,
|
|
||||||
MissingType,
|
MissingType,
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
|
@ -33,10 +31,10 @@ from .models import Article, Film, FilmDetails, Reporter
|
||||||
|
|
||||||
|
|
||||||
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
||||||
_kwargs = kwargs.copy()
|
_kwargs = {**kwargs, "help_text": "Custom Help Text"}
|
||||||
if "null" not in kwargs:
|
if "null" not in kwargs:
|
||||||
_kwargs["null"] = True
|
_kwargs["null"] = True
|
||||||
field = django_field(help_text="Custom Help Text", *args, **_kwargs)
|
field = django_field(*args, **_kwargs)
|
||||||
graphene_type = convert_django_field(field)
|
graphene_type = convert_django_field(field)
|
||||||
assert isinstance(graphene_type, graphene_field)
|
assert isinstance(graphene_type, graphene_field)
|
||||||
field = graphene_type.Field()
|
field = graphene_type.Field()
|
||||||
|
@ -372,16 +370,6 @@ def test_should_postgres_hstore_convert_string():
|
||||||
assert_conversion(HStoreField, JSONString)
|
assert_conversion(HStoreField, JSONString)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(PGJSONField is MissingType, reason="PGJSONField should exist")
|
|
||||||
def test_should_postgres_json_convert_string():
|
|
||||||
assert_conversion(PGJSONField, JSONString)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist")
|
|
||||||
def test_should_json_convert_string():
|
|
||||||
assert_conversion(JSONField, JSONString)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(RangeField is MissingType, reason="RangeField should exist")
|
@pytest.mark.skipif(RangeField is MissingType, reason="RangeField should exist")
|
||||||
def test_should_postgres_range_convert_list():
|
def test_should_postgres_range_convert_list():
|
||||||
from django.contrib.postgres.fields import IntegerRangeField
|
from django.contrib.postgres.fields import IntegerRangeField
|
||||||
|
|
|
@ -2,8 +2,8 @@ import datetime
|
||||||
import re
|
import re
|
||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count, Prefetch
|
||||||
from asgiref.sync import sync_to_async, async_to_sync
|
from asgiref.sync import sync_to_async, async_to_sync
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.db.models import Count, Prefetch
|
||||||
|
|
||||||
from graphene import List, NonNull, ObjectType, Schema, String
|
from graphene import List, NonNull, ObjectType, Schema, String
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class TestDjangoListField:
|
||||||
foo = String()
|
foo = String()
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
list_field = DjangoListField(TestType)
|
DjangoListField(TestType)
|
||||||
|
|
||||||
def test_only_import_paths(self):
|
def test_only_import_paths(self):
|
||||||
list_field = DjangoListField("graphene_django.tests.schema.Human")
|
list_field = DjangoListField("graphene_django.tests.schema.Human")
|
||||||
|
|
|
@ -3,7 +3,6 @@ from pytest import raises
|
||||||
|
|
||||||
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
|
|
||||||
|
|
||||||
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
|
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
from graphql_relay import to_global_id
|
|
||||||
|
|
||||||
from ..fields import DjangoConnectionField
|
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
|
from .models import Article, Film, FilmDetails, Reporter
|
||||||
from .models import Article, Reporter
|
|
||||||
|
|
||||||
|
|
||||||
class TestShouldCallGetQuerySetOnForeignKey:
|
class TestShouldCallGetQuerySetOnForeignKey:
|
||||||
|
@ -127,6 +124,69 @@ class TestShouldCallGetQuerySetOnForeignKey:
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"reporter": {"firstName": "Jane"}}
|
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:
|
class TestShouldCallGetQuerySetOnForeignKeyNode:
|
||||||
"""
|
"""
|
||||||
|
@ -233,3 +293,272 @@ class TestShouldCallGetQuerySetOnForeignKeyNode:
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {"reporter": {"firstName": "Jane"}}
|
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"}}]},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestShouldCallGetQuerySetOnOneToOne:
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_schema(self):
|
||||||
|
class FilmDetailsType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = FilmDetails
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
if info.context and info.context.get("permission_get_film_details"):
|
||||||
|
return queryset
|
||||||
|
raise Exception("Not authorized to access film details.")
|
||||||
|
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
if info.context and info.context.get("permission_get_film"):
|
||||||
|
return queryset
|
||||||
|
raise Exception("Not authorized to access film.")
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
film_details = graphene.Field(
|
||||||
|
FilmDetailsType, id=graphene.ID(required=True)
|
||||||
|
)
|
||||||
|
film = graphene.Field(FilmType, id=graphene.ID(required=True))
|
||||||
|
|
||||||
|
def resolve_film_details(self, info, id):
|
||||||
|
return (
|
||||||
|
FilmDetailsType.get_queryset(FilmDetails.objects, info)
|
||||||
|
.filter(id=id)
|
||||||
|
.last()
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_film(self, info, id):
|
||||||
|
return FilmType.get_queryset(Film.objects, info).filter(id=id).last()
|
||||||
|
|
||||||
|
self.schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
self.films = [
|
||||||
|
Film.objects.create(
|
||||||
|
genre="do",
|
||||||
|
),
|
||||||
|
Film.objects.create(
|
||||||
|
genre="ac",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.film_details = [
|
||||||
|
FilmDetails.objects.create(
|
||||||
|
film=self.films[0],
|
||||||
|
),
|
||||||
|
FilmDetails.objects.create(
|
||||||
|
film=self.films[1],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_get_queryset_called_on_field(self):
|
||||||
|
# A user tries to access a film
|
||||||
|
query = """
|
||||||
|
query getFilm($id: ID!) {
|
||||||
|
film(id: $id) {
|
||||||
|
genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# With `permission_get_film`
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.films[0].id},
|
||||||
|
context_value={"permission_get_film": True},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["film"] == {
|
||||||
|
"genre": "DO",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without `permission_get_film`
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.films[1].id},
|
||||||
|
context_value={"permission_get_film": False},
|
||||||
|
)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Not authorized to access film."
|
||||||
|
|
||||||
|
# A user tries to access a film details
|
||||||
|
query = """
|
||||||
|
query getFilmDetails($id: ID!) {
|
||||||
|
filmDetails(id: $id) {
|
||||||
|
location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# With `permission_get_film`
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.film_details[0].id},
|
||||||
|
context_value={"permission_get_film_details": True},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"filmDetails": {"location": ""}}
|
||||||
|
|
||||||
|
# Without `permission_get_film`
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.film_details[0].id},
|
||||||
|
context_value={"permission_get_film_details": False},
|
||||||
|
)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Not authorized to access film details."
|
||||||
|
|
||||||
|
def test_get_queryset_called_on_foreignkey(self, django_assert_num_queries):
|
||||||
|
# A user tries to access a film details through a film
|
||||||
|
query = """
|
||||||
|
query getFilm($id: ID!) {
|
||||||
|
film(id: $id) {
|
||||||
|
genre
|
||||||
|
details {
|
||||||
|
location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# With `permission_get_film_details`
|
||||||
|
with django_assert_num_queries(2):
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.films[0].id},
|
||||||
|
context_value={
|
||||||
|
"permission_get_film": True,
|
||||||
|
"permission_get_film_details": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["film"] == {
|
||||||
|
"genre": "DO",
|
||||||
|
"details": {"location": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without `permission_get_film_details`
|
||||||
|
with django_assert_num_queries(1):
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.films[0].id},
|
||||||
|
context_value={
|
||||||
|
"permission_get_film": True,
|
||||||
|
"permission_get_film_details": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Not authorized to access film details."
|
||||||
|
|
||||||
|
# A user tries to access a film through a film details
|
||||||
|
query = """
|
||||||
|
query getFilmDetails($id: ID!) {
|
||||||
|
filmDetails(id: $id) {
|
||||||
|
location
|
||||||
|
film {
|
||||||
|
genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# With `permission_get_film`
|
||||||
|
with django_assert_num_queries(2):
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.film_details[0].id},
|
||||||
|
context_value={
|
||||||
|
"permission_get_film": True,
|
||||||
|
"permission_get_film_details": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["filmDetails"] == {
|
||||||
|
"location": "",
|
||||||
|
"film": {"genre": "DO"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without `permission_get_film`
|
||||||
|
with django_assert_num_queries(1):
|
||||||
|
result = self.schema.execute(
|
||||||
|
query,
|
||||||
|
variables={"id": self.film_details[1].id},
|
||||||
|
context_value={
|
||||||
|
"permission_get_film": False,
|
||||||
|
"permission_get_film_details": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == "Not authorized to access film."
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import datetime
|
|
||||||
import base64
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -16,9 +16,17 @@ from ..compat import IntegerRangeField, MissingType
|
||||||
from ..fields import DjangoConnectionField
|
from ..fields import DjangoConnectionField
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
from ..utils import DJANGO_FILTER_INSTALLED
|
from ..utils import DJANGO_FILTER_INSTALLED
|
||||||
from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter
|
|
||||||
from .async_test_helper import assert_async_result_equal
|
from .async_test_helper import assert_async_result_equal
|
||||||
|
from .models import (
|
||||||
|
APNewsReporter,
|
||||||
|
Article,
|
||||||
|
CNNReporter,
|
||||||
|
Film,
|
||||||
|
FilmDetails,
|
||||||
|
Person,
|
||||||
|
Pet,
|
||||||
|
Reporter,
|
||||||
|
)
|
||||||
|
|
||||||
def test_should_query_only_fields():
|
def test_should_query_only_fields():
|
||||||
with raises(Exception):
|
with raises(Exception):
|
||||||
|
@ -123,15 +131,14 @@ def test_should_query_well():
|
||||||
@pytest.mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist")
|
@pytest.mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist")
|
||||||
def test_should_query_postgres_fields():
|
def test_should_query_postgres_fields():
|
||||||
from django.contrib.postgres.fields import (
|
from django.contrib.postgres.fields import (
|
||||||
IntegerRangeField,
|
|
||||||
ArrayField,
|
ArrayField,
|
||||||
JSONField,
|
|
||||||
HStoreField,
|
HStoreField,
|
||||||
|
IntegerRangeField,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
ages = IntegerRangeField(help_text="The age ranges")
|
ages = IntegerRangeField(help_text="The age ranges")
|
||||||
data = JSONField(help_text="Data")
|
data = models.JSONField(help_text="Data")
|
||||||
store = HStoreField()
|
store = HStoreField()
|
||||||
tags = ArrayField(models.CharField(max_length=50))
|
tags = ArrayField(models.CharField(max_length=50))
|
||||||
|
|
||||||
|
@ -357,7 +364,7 @@ def test_should_query_connectionfields():
|
||||||
|
|
||||||
|
|
||||||
def test_should_keep_annotations():
|
def test_should_keep_annotations():
|
||||||
from django.db.models import Count, Avg
|
from django.db.models import Avg, Count
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -521,7 +528,7 @@ def test_should_query_node_filtering_with_distinct_queryset():
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
f = Film.objects.create()
|
f = Film.objects.create()
|
||||||
fd = FilmDetails.objects.create(location="Berlin", film=f)
|
FilmDetails.objects.create(location="Berlin", film=f)
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
query = """
|
query = """
|
||||||
|
@ -646,7 +653,7 @@ def test_should_enforce_first_or_last(graphene_settings):
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -689,7 +696,7 @@ def test_should_error_if_first_is_greater_than_max(graphene_settings):
|
||||||
|
|
||||||
assert Query.all_reporters.max_limit == 100
|
assert Query.all_reporters.max_limit == 100
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -732,7 +739,7 @@ def test_should_error_if_last_is_greater_than_max(graphene_settings):
|
||||||
|
|
||||||
assert Query.all_reporters.max_limit == 100
|
assert Query.all_reporters.max_limit == 100
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -798,7 +805,7 @@ def test_should_query_promise_connectionfields():
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_connectionfields_with_last():
|
def test_should_query_connectionfields_with_last():
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -836,11 +843,11 @@ def test_should_query_connectionfields_with_last():
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_connectionfields_with_manager():
|
def test_should_query_connectionfields_with_manager():
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="NotDoe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="NotDoe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1186,11 +1193,306 @@ def test_proxy_model_support():
|
||||||
assert_async_result_equal(schema, query, result)
|
assert_async_result_equal(schema, query, result)
|
||||||
|
|
||||||
|
|
||||||
def test_should_resolve_get_queryset_connectionfields():
|
def test_model_inheritance_support_reverse_relationships():
|
||||||
reporter_1 = Reporter.objects.create(
|
"""
|
||||||
|
This test asserts that we can query reverse relationships for all Reporters and proxied Reporters and multi table Reporters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class CNNReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = CNNReporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class APNewsReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = APNewsReporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
film = Film.objects.create(genre="do")
|
||||||
|
|
||||||
|
reporter = Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
reporter_2 = CNNReporter.objects.create(
|
|
||||||
|
cnn_reporter = CNNReporter.objects.create(
|
||||||
|
first_name="Some",
|
||||||
|
last_name="Guy",
|
||||||
|
email="someguy@cnn.com",
|
||||||
|
a_choice=1,
|
||||||
|
reporter_type=2, # set this guy to be CNN
|
||||||
|
)
|
||||||
|
|
||||||
|
ap_news_reporter = APNewsReporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
film.reporters.add(cnn_reporter, ap_news_reporter)
|
||||||
|
film.save()
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
cnn_reporters = DjangoConnectionField(CNNReporterType)
|
||||||
|
ap_news_reporters = DjangoConnectionField(APNewsReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ProxyModelQuery {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
films {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cnnReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
films {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apNewsReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
films {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"allReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", reporter.id),
|
||||||
|
"films": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", cnn_reporter.id),
|
||||||
|
"films": [{"id": f"{film.id}"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", ap_news_reporter.id),
|
||||||
|
"films": [{"id": f"{film.id}"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cnnReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("CNNReporterType", cnn_reporter.id),
|
||||||
|
"films": [{"id": f"{film.id}"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"apNewsReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("APNewsReporterType", ap_news_reporter.id),
|
||||||
|
"films": [{"id": f"{film.id}"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_inheritance_support_local_relationships():
|
||||||
|
"""
|
||||||
|
This test asserts that we can query local relationships for all Reporters and proxied Reporters and multi table Reporters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class PersonType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class CNNReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = CNNReporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
class APNewsReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = APNewsReporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
film = Film.objects.create(genre="do")
|
||||||
|
|
||||||
|
reporter = Reporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
reporter_fan = Person.objects.create(name="Reporter Fan")
|
||||||
|
|
||||||
|
reporter.fans.add(reporter_fan)
|
||||||
|
reporter.save()
|
||||||
|
|
||||||
|
cnn_reporter = CNNReporter.objects.create(
|
||||||
|
first_name="Some",
|
||||||
|
last_name="Guy",
|
||||||
|
email="someguy@cnn.com",
|
||||||
|
a_choice=1,
|
||||||
|
reporter_type=2, # set this guy to be CNN
|
||||||
|
)
|
||||||
|
cnn_fan = Person.objects.create(name="CNN Fan")
|
||||||
|
cnn_reporter.fans.add(cnn_fan)
|
||||||
|
cnn_reporter.save()
|
||||||
|
|
||||||
|
ap_news_reporter = APNewsReporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
ap_news_fan = Person.objects.create(name="AP News Fan")
|
||||||
|
ap_news_reporter.fans.add(ap_news_fan)
|
||||||
|
ap_news_reporter.save()
|
||||||
|
|
||||||
|
film.reporters.add(cnn_reporter, ap_news_reporter)
|
||||||
|
film.save()
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
cnn_reporters = DjangoConnectionField(CNNReporterType)
|
||||||
|
ap_news_reporters = DjangoConnectionField(APNewsReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ProxyModelQuery {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fans {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cnnReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fans {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apNewsReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fans {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"allReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", reporter.id),
|
||||||
|
"fans": [{"name": f"{reporter_fan.name}"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", cnn_reporter.id),
|
||||||
|
"fans": [{"name": f"{cnn_fan.name}"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("ReporterType", ap_news_reporter.id),
|
||||||
|
"fans": [{"name": f"{ap_news_fan.name}"}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cnnReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("CNNReporterType", cnn_reporter.id),
|
||||||
|
"fans": [{"name": f"{cnn_fan.name}"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"apNewsReporters": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"id": to_global_id("APNewsReporterType", ap_news_reporter.id),
|
||||||
|
"fans": [{"name": f"{ap_news_fan.name}"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_resolve_get_queryset_connectionfields():
|
||||||
|
Reporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
CNNReporter.objects.create(
|
||||||
first_name="Some",
|
first_name="Some",
|
||||||
last_name="Guy",
|
last_name="Guy",
|
||||||
email="someguy@cnn.com",
|
email="someguy@cnn.com",
|
||||||
|
@ -1233,10 +1535,10 @@ def test_should_resolve_get_queryset_connectionfields():
|
||||||
|
|
||||||
|
|
||||||
def test_connection_should_limit_after_to_list_length():
|
def test_connection_should_limit_after_to_list_length():
|
||||||
reporter_1 = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
reporter_2 = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1
|
first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1263,7 +1565,7 @@ def test_connection_should_limit_after_to_list_length():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
after = base64.b64encode(b"arrayconnection:10").decode()
|
after = base64.b64encode(b"arrayconnection:10").decode()
|
||||||
result = schema.execute(query, variable_values=dict(after=after))
|
result = schema.execute(query, variable_values={"after": after})
|
||||||
expected = {"allReporters": {"edges": []}}
|
expected = {"allReporters": {"edges": []}}
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
@ -1271,12 +1573,12 @@ def test_connection_should_limit_after_to_list_length():
|
||||||
|
|
||||||
|
|
||||||
REPORTERS = [
|
REPORTERS = [
|
||||||
dict(
|
{
|
||||||
first_name=f"First {i}",
|
"first_name": f"First {i}",
|
||||||
last_name=f"Last {i}",
|
"last_name": f"Last {i}",
|
||||||
email=f"johndoe+{i}@example.com",
|
"email": f"johndoe+{i}@example.com",
|
||||||
a_choice=1,
|
"a_choice": 1,
|
||||||
)
|
}
|
||||||
for i in range(6)
|
for i in range(6)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1353,7 +1655,7 @@ def test_should_have_next_page(graphene_settings):
|
||||||
assert_async_result_equal(schema, query, result, variable_values={})
|
assert_async_result_equal(schema, query, result, variable_values={})
|
||||||
|
|
||||||
last_result = result.data["allReporters"]["pageInfo"]["endCursor"]
|
last_result = result.data["allReporters"]["pageInfo"]["endCursor"]
|
||||||
result2 = schema.execute(query, variable_values=dict(first=4, after=last_result))
|
result2 = schema.execute(query, variable_values={"first": 4, "after": last_result})
|
||||||
assert not result2.errors
|
assert not result2.errors
|
||||||
assert len(result2.data["allReporters"]["edges"]) == 2
|
assert len(result2.data["allReporters"]["edges"]) == 2
|
||||||
assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"]
|
assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"]
|
||||||
|
@ -1448,8 +1750,8 @@ class TestBackwardPagination:
|
||||||
|
|
||||||
after = base64.b64encode(b"arrayconnection:0").decode()
|
after = base64.b64encode(b"arrayconnection:0").decode()
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
query,
|
query_first_last_and_after,
|
||||||
variable_values=dict(after=after),
|
variable_values={"after": after},
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert len(result.data["allReporters"]["edges"]) == 3
|
assert len(result.data["allReporters"]["edges"]) == 3
|
||||||
|
@ -1484,8 +1786,8 @@ class TestBackwardPagination:
|
||||||
|
|
||||||
before = base64.b64encode(b"arrayconnection:5").decode()
|
before = base64.b64encode(b"arrayconnection:5").decode()
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
query,
|
query_first_last_and_after,
|
||||||
variable_values=dict(before=before),
|
variable_values={"before": before},
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert len(result.data["allReporters"]["edges"]) == 1
|
assert len(result.data["allReporters"]["edges"]) == 1
|
||||||
|
@ -1544,7 +1846,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||||
"""
|
"""
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
with django_assert_num_queries(3) as captured:
|
with django_assert_num_queries(3):
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert_async_result_equal(schema, query, result)
|
assert_async_result_equal(schema, query, result)
|
||||||
|
@ -1715,7 +2017,7 @@ def test_connection_should_forbid_offset_filtering_with_before():
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
before = base64.b64encode(b"arrayconnection:2").decode()
|
before = base64.b64encode(b"arrayconnection:2").decode()
|
||||||
result = schema.execute(query, variable_values=dict(before=before))
|
result = schema.execute(query, variable_values={"before": before})
|
||||||
expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
|
expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert result.errors[0].message == expected_error
|
assert result.errors[0].message == expected_error
|
||||||
|
@ -1754,7 +2056,7 @@ def test_connection_should_allow_offset_filtering_with_after():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
after = base64.b64encode(b"arrayconnection:0").decode()
|
after = base64.b64encode(b"arrayconnection:0").decode()
|
||||||
result = schema.execute(query, variable_values=dict(after=after))
|
result = schema.execute(query, variable_values={"after": after})
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": {
|
"allReporters": {
|
||||||
|
@ -1791,7 +2093,7 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects():
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = schema.execute(query, variable_values=dict(last=2))
|
result = schema.execute(query, variable_values={"last": 2})
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
expected = {"allReporters": {"edges": []}}
|
expected = {"allReporters": {"edges": []}}
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
@ -1802,7 +2104,7 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects():
|
||||||
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
||||||
Reporter.objects.create(first_name="Some", last_name="Lady")
|
Reporter.objects.create(first_name="Some", last_name="Lady")
|
||||||
|
|
||||||
result = schema.execute(query, variable_values=dict(last=2))
|
result = schema.execute(query, variable_values={"last": 2})
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": {
|
"allReporters": {
|
||||||
|
@ -1815,7 +2117,7 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
assert_async_result_equal(schema, query, result, variable_values=dict(last=2))
|
assert_async_result_equal(schema, query, result, variable_values=dict(last=2))
|
||||||
|
|
||||||
result = schema.execute(query, variable_values=dict(last=4))
|
result = schema.execute(query, variable_values={"last": 4})
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": {
|
"allReporters": {
|
||||||
|
@ -1830,7 +2132,7 @@ def test_connection_should_succeed_if_last_higher_than_number_of_objects():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
assert_async_result_equal(schema, query, result, variable_values=dict(last=4))
|
assert_async_result_equal(schema, query, result, variable_values=dict(last=4))
|
||||||
|
|
||||||
result = schema.execute(query, variable_values=dict(last=20))
|
result = schema.execute(query, variable_values={"last": 20})
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": {
|
"allReporters": {
|
||||||
|
@ -1868,7 +2170,7 @@ def test_should_query_nullable_foreign_key():
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
person = Person.objects.create(name="Jane")
|
person = Person.objects.create(name="Jane")
|
||||||
pets = [
|
[
|
||||||
Pet.objects.create(name="Stray dog", age=1),
|
Pet.objects.create(name="Stray dog", age=1),
|
||||||
Pet.objects.create(name="Jane's dog", owner=person, age=1),
|
Pet.objects.create(name="Jane's dog", owner=person, age=1),
|
||||||
]
|
]
|
||||||
|
@ -1908,3 +2210,74 @@ def test_should_query_nullable_foreign_key():
|
||||||
assert result.data["person"] == {
|
assert result.data["person"] == {
|
||||||
"pets": [{"name": "Jane's dog"}],
|
"pets": [{"name": "Jane's dog"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_nullable_one_to_one_relation_with_custom_resolver():
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
class FilmDetailsType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = FilmDetails
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
film = graphene.Field(FilmType, genre=graphene.String(required=True))
|
||||||
|
film_details = graphene.Field(
|
||||||
|
FilmDetailsType, location=graphene.String(required=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_film(self, info, genre):
|
||||||
|
return Film.objects.filter(genre=genre).first()
|
||||||
|
|
||||||
|
def resolve_film_details(self, info, location):
|
||||||
|
return FilmDetails.objects.filter(location=location).first()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
Film.objects.create(genre="do")
|
||||||
|
FilmDetails.objects.create(location="London")
|
||||||
|
|
||||||
|
query_film = """
|
||||||
|
query getFilm($genre: String!) {
|
||||||
|
film(genre: $genre) {
|
||||||
|
genre
|
||||||
|
details {
|
||||||
|
location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
query_film_details = """
|
||||||
|
query getFilmDetails($location: String!) {
|
||||||
|
filmDetails(location: $location) {
|
||||||
|
location
|
||||||
|
film {
|
||||||
|
genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query_film, variables={"genre": "do"})
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["film"] == {
|
||||||
|
"genre": "DO",
|
||||||
|
"details": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query_film_details, variables={"location": "London"})
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data["filmDetails"] == {
|
||||||
|
"location": "London",
|
||||||
|
"film": None,
|
||||||
|
}
|
||||||
|
|
|
@ -33,17 +33,18 @@ def test_should_map_fields_correctly():
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
fields = list(ReporterType2._meta.fields.keys())
|
fields = list(ReporterType2._meta.fields.keys())
|
||||||
assert fields[:-2] == [
|
assert fields[:-3] == [
|
||||||
"id",
|
"id",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
"pets",
|
"pets",
|
||||||
"a_choice",
|
"a_choice",
|
||||||
|
"fans",
|
||||||
"reporter_type",
|
"reporter_type",
|
||||||
]
|
]
|
||||||
|
|
||||||
assert sorted(fields[-2:]) == ["articles", "films"]
|
assert sorted(fields[-3:]) == ["apnewsreporter", "articles", "films"]
|
||||||
|
|
||||||
|
|
||||||
def test_should_map_only_few_fields():
|
def test_should_map_only_few_fields():
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
@ -11,8 +11,10 @@ from graphene.relay import Node
|
||||||
from .. import registry
|
from .. import registry
|
||||||
from ..filter import DjangoFilterConnectionField
|
from ..filter import DjangoFilterConnectionField
|
||||||
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
||||||
from .models import Article as ArticleModel
|
from .models import (
|
||||||
from .models import Reporter as ReporterModel
|
Article as ArticleModel,
|
||||||
|
Reporter as ReporterModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Reporter(DjangoObjectType):
|
class Reporter(DjangoObjectType):
|
||||||
|
@ -67,16 +69,17 @@ def test_django_get_node(get):
|
||||||
def test_django_objecttype_map_correct_fields():
|
def test_django_objecttype_map_correct_fields():
|
||||||
fields = Reporter._meta.fields
|
fields = Reporter._meta.fields
|
||||||
fields = list(fields.keys())
|
fields = list(fields.keys())
|
||||||
assert fields[:-2] == [
|
assert fields[:-3] == [
|
||||||
"id",
|
"id",
|
||||||
"first_name",
|
"first_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
"pets",
|
"pets",
|
||||||
"a_choice",
|
"a_choice",
|
||||||
|
"fans",
|
||||||
"reporter_type",
|
"reporter_type",
|
||||||
]
|
]
|
||||||
assert sorted(fields[-2:]) == ["articles", "films"]
|
assert sorted(fields[-3:]) == ["apnewsreporter", "articles", "films"]
|
||||||
|
|
||||||
|
|
||||||
def test_django_objecttype_with_node_have_correct_fields():
|
def test_django_objecttype_with_node_have_correct_fields():
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.utils.translation import gettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from ..utils import camelize, get_model_fields, GraphQLTestCase
|
from ..utils import GraphQLTestCase, camelize, get_model_fields, get_reverse_fields
|
||||||
from .models import Film, Reporter
|
|
||||||
from ..utils.testing import graphql_query
|
from ..utils.testing import graphql_query
|
||||||
|
from .models import APNewsReporter, CNNReporter, Film, Reporter
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_fields_no_duplication():
|
def test_get_model_fields_no_duplication():
|
||||||
|
@ -19,6 +19,18 @@ def test_get_model_fields_no_duplication():
|
||||||
assert len(film_fields) == len(film_name_set)
|
assert len(film_fields) == len(film_name_set)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reverse_fields_includes_proxied_models():
|
||||||
|
reporter_fields = get_reverse_fields(Reporter, [])
|
||||||
|
cnn_reporter_fields = get_reverse_fields(CNNReporter, [])
|
||||||
|
ap_news_reporter_fields = get_reverse_fields(APNewsReporter, [])
|
||||||
|
|
||||||
|
assert (
|
||||||
|
len(list(reporter_fields))
|
||||||
|
== len(list(cnn_reporter_fields))
|
||||||
|
== len(list(ap_news_reporter_fields))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_camelize():
|
def test_camelize():
|
||||||
assert camelize({}) == {}
|
assert camelize({}) == {}
|
||||||
assert camelize("value_a") == "value_a"
|
assert camelize("value_a") == "value_a"
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
from graphene_django.settings import graphene_settings
|
|
||||||
|
|
||||||
from .models import Pet
|
from .models import Pet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -31,8 +27,12 @@ def response_json(response):
|
||||||
return json.loads(response.content.decode())
|
return json.loads(response.content.decode())
|
||||||
|
|
||||||
|
|
||||||
j = lambda **kwargs: json.dumps(kwargs)
|
def j(**kwargs):
|
||||||
jl = lambda **kwargs: json.dumps([kwargs])
|
return json.dumps(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def jl(**kwargs):
|
||||||
|
return json.dumps([kwargs])
|
||||||
|
|
||||||
|
|
||||||
def test_graphiql_is_enabled(client):
|
def test_graphiql_is_enabled(client):
|
||||||
|
@ -229,7 +229,7 @@ def test_allows_sending_a_mutation_via_post(client):
|
||||||
def test_allows_post_with_url_encoding(client):
|
def test_allows_post_with_url_encoding(client):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_string(),
|
url_string(),
|
||||||
urlencode(dict(query="{test}")),
|
urlencode({"query": "{test}"}),
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -303,10 +303,10 @@ def test_supports_post_url_encoded_query_with_string_variables(client):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_string(),
|
url_string(),
|
||||||
urlencode(
|
urlencode(
|
||||||
dict(
|
{
|
||||||
query="query helloWho($who: String){ test(who: $who) }",
|
"query": "query helloWho($who: String){ test(who: $who) }",
|
||||||
variables=json.dumps({"who": "Dolly"}),
|
"variables": json.dumps({"who": "Dolly"}),
|
||||||
)
|
}
|
||||||
),
|
),
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
)
|
)
|
||||||
|
@ -329,7 +329,7 @@ def test_supports_post_json_quey_with_get_variable_values(client):
|
||||||
def test_post_url_encoded_query_with_get_variable_values(client):
|
def test_post_url_encoded_query_with_get_variable_values(client):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_string(variables=json.dumps({"who": "Dolly"})),
|
url_string(variables=json.dumps({"who": "Dolly"})),
|
||||||
urlencode(dict(query="query helloWho($who: String){ test(who: $who) }")),
|
urlencode({"query": "query helloWho($who: String){ test(who: $who) }"}),
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -511,7 +511,7 @@ def test_handles_django_request_error(client, monkeypatch):
|
||||||
|
|
||||||
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read)
|
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read)
|
||||||
|
|
||||||
valid_json = json.dumps(dict(foo="bar"))
|
valid_json = json.dumps({"foo": "bar"})
|
||||||
response = client.post(url_string(), valid_json, "application/json")
|
response = client.post(url_string(), valid_json, "application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import warnings
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Type
|
from typing import Type # noqa: F401
|
||||||
|
|
||||||
|
from django.db.models import Model # noqa: F401
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from django.db.models import Model
|
|
||||||
from graphene.relay import Connection, Node
|
from graphene.relay import Connection, Node
|
||||||
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
|
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
|
||||||
from graphene.types.utils import yank_fields_from_attrs
|
from graphene.types.utils import yank_fields_from_attrs
|
||||||
|
@ -150,7 +151,7 @@ class DjangoObjectType(ObjectType):
|
||||||
interfaces=(),
|
interfaces=(),
|
||||||
convert_choices_to_enum=True,
|
convert_choices_to_enum=True,
|
||||||
_meta=None,
|
_meta=None,
|
||||||
**options
|
**options,
|
||||||
):
|
):
|
||||||
assert is_valid_django_model(model), (
|
assert is_valid_django_model(model), (
|
||||||
'You need to pass a valid Django Model in {}.Meta, received "{}".'
|
'You need to pass a valid Django Model in {}.Meta, received "{}".'
|
||||||
|
@ -160,9 +161,9 @@ class DjangoObjectType(ObjectType):
|
||||||
registry = get_global_registry()
|
registry = get_global_registry()
|
||||||
|
|
||||||
assert isinstance(registry, Registry), (
|
assert isinstance(registry, Registry), (
|
||||||
"The attribute registry in {} needs to be an instance of "
|
f"The attribute registry in {cls.__name__} needs to be an instance of "
|
||||||
'Registry, received "{}".'
|
f'Registry, received "{registry}".'
|
||||||
).format(cls.__name__, registry)
|
)
|
||||||
|
|
||||||
if filter_fields and filterset_class:
|
if filter_fields and filterset_class:
|
||||||
raise Exception("Can't set both filter_fields and filterset_class")
|
raise Exception("Can't set both filter_fields and filterset_class")
|
||||||
|
@ -175,7 +176,7 @@ class DjangoObjectType(ObjectType):
|
||||||
|
|
||||||
assert not (fields and exclude), (
|
assert not (fields and exclude), (
|
||||||
"Cannot set both 'fields' and 'exclude' options on "
|
"Cannot set both 'fields' and 'exclude' options on "
|
||||||
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
|
f"DjangoObjectType {cls.__name__}."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Alias only_fields -> fields
|
# Alias only_fields -> fields
|
||||||
|
@ -214,8 +215,8 @@ class DjangoObjectType(ObjectType):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Creating a DjangoObjectType without either the `fields` "
|
"Creating a DjangoObjectType without either the `fields` "
|
||||||
"or the `exclude` option is deprecated. Add an explicit `fields "
|
"or the `exclude` option is deprecated. Add an explicit `fields "
|
||||||
"= '__all__'` option on DjangoObjectType {class_name} to use all "
|
f"= '__all__'` option on DjangoObjectType {cls.__name__} to use all "
|
||||||
"fields".format(class_name=cls.__name__),
|
"fields",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
@ -240,9 +241,9 @@ class DjangoObjectType(ObjectType):
|
||||||
)
|
)
|
||||||
|
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
assert issubclass(connection, Connection), (
|
assert issubclass(
|
||||||
"The connection must be a Connection. Received {}"
|
connection, Connection
|
||||||
).format(connection.__name__)
|
), f"The connection must be a Connection. Received {connection.__name__}"
|
||||||
|
|
||||||
if not _meta:
|
if not _meta:
|
||||||
_meta = DjangoObjectTypeOptions(cls)
|
_meta = DjangoObjectTypeOptions(cls)
|
||||||
|
@ -253,6 +254,7 @@ class DjangoObjectType(ObjectType):
|
||||||
_meta.filterset_class = filterset_class
|
_meta.filterset_class = filterset_class
|
||||||
_meta.fields = django_fields
|
_meta.fields = django_fields
|
||||||
_meta.connection = connection
|
_meta.connection = connection
|
||||||
|
_meta.convert_choices_to_enum = convert_choices_to_enum
|
||||||
|
|
||||||
super().__init_subclass_with_meta__(
|
super().__init_subclass_with_meta__(
|
||||||
_meta=_meta, interfaces=interfaces, **options
|
_meta=_meta, interfaces=interfaces, **options
|
||||||
|
@ -272,7 +274,7 @@ class DjangoObjectType(ObjectType):
|
||||||
if isinstance(root, cls):
|
if isinstance(root, cls):
|
||||||
return True
|
return True
|
||||||
if not is_valid_django_model(root.__class__):
|
if not is_valid_django_model(root.__class__):
|
||||||
raise Exception(('Received incompatible instance "{}".').format(root))
|
raise Exception(f'Received incompatible instance "{root}".')
|
||||||
|
|
||||||
if cls._meta.model._meta.proxy:
|
if cls._meta.model._meta.proxy:
|
||||||
model = root._meta.model
|
model = root._meta.model
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from .testing import GraphQLTestCase
|
from .testing import GraphQLTestCase
|
||||||
from .utils import (
|
from .utils import (
|
||||||
DJANGO_FILTER_INSTALLED,
|
DJANGO_FILTER_INSTALLED,
|
||||||
|
bypass_get_queryset,
|
||||||
camelize,
|
camelize,
|
||||||
get_model_fields,
|
get_model_fields,
|
||||||
get_reverse_fields,
|
get_reverse_fields,
|
||||||
|
@ -20,4 +21,5 @@ __all__ = [
|
||||||
"GraphQLTestCase",
|
"GraphQLTestCase",
|
||||||
"is_sync_function",
|
"is_sync_function",
|
||||||
"is_running_async",
|
"is_running_async",
|
||||||
|
"bypass_get_queryset",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from text_unidecode import unidecode
|
from text_unidecode import unidecode
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .. import GraphQLTestCase
|
|
||||||
from ...tests.test_types import with_local_registry
|
|
||||||
from ...settings import graphene_settings
|
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
||||||
|
from ...settings import graphene_settings
|
||||||
|
from ...tests.test_types import with_local_registry
|
||||||
|
from .. import GraphQLTestCase
|
||||||
|
|
||||||
|
|
||||||
@with_local_registry
|
@with_local_registry
|
||||||
def test_graphql_test_case_deprecated_client_getter():
|
def test_graphql_test_case_deprecated_client_getter():
|
||||||
|
@ -23,7 +23,7 @@ def test_graphql_test_case_deprecated_client_getter():
|
||||||
tc.setUpClass()
|
tc.setUpClass()
|
||||||
|
|
||||||
with pytest.warns(PendingDeprecationWarning):
|
with pytest.warns(PendingDeprecationWarning):
|
||||||
tc._client
|
tc._client # noqa: B018
|
||||||
|
|
||||||
|
|
||||||
@with_local_registry
|
@with_local_registry
|
||||||
|
|
|
@ -38,18 +38,52 @@ def camelize(data):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_reverse_fields(model, local_field_names):
|
def _get_model_ancestry(model):
|
||||||
for name, attr in model.__dict__.items():
|
model_ancestry = [model]
|
||||||
# Don't duplicate any local fields
|
|
||||||
if name in local_field_names:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# "rel" for FK and M2M relations and "related" for O2O Relations
|
for base in model.__bases__:
|
||||||
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
|
if is_valid_django_model(base) and getattr(base, "_meta", False):
|
||||||
if isinstance(related, models.ManyToOneRel):
|
model_ancestry.append(base)
|
||||||
yield (name, related)
|
return model_ancestry
|
||||||
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
|
||||||
yield (name, related)
|
|
||||||
|
def get_reverse_fields(model, local_field_names):
|
||||||
|
"""
|
||||||
|
Searches through the model's ancestry and gets reverse relationships the models
|
||||||
|
Yields a tuple of (field.name, field)
|
||||||
|
"""
|
||||||
|
model_ancestry = _get_model_ancestry(model)
|
||||||
|
|
||||||
|
for _model in model_ancestry:
|
||||||
|
for name, attr in _model.__dict__.items():
|
||||||
|
# Don't duplicate any local fields
|
||||||
|
if name in local_field_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# "rel" for FK and M2M relations and "related" for O2O Relations
|
||||||
|
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
|
||||||
|
if isinstance(related, models.ManyToOneRel):
|
||||||
|
yield (name, related)
|
||||||
|
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
||||||
|
yield (name, related)
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_fields(model):
|
||||||
|
"""
|
||||||
|
Searches through the model's ancestry and gets the fields on the models
|
||||||
|
Returns a dict of {field.name: field}
|
||||||
|
"""
|
||||||
|
model_ancestry = _get_model_ancestry(model)
|
||||||
|
|
||||||
|
local_fields_dict = {}
|
||||||
|
for _model in model_ancestry:
|
||||||
|
for field in sorted(
|
||||||
|
list(_model._meta.fields) + list(_model._meta.local_many_to_many)
|
||||||
|
):
|
||||||
|
if field.name not in local_fields_dict:
|
||||||
|
local_fields_dict[field.name] = field
|
||||||
|
|
||||||
|
return list(local_fields_dict.items())
|
||||||
|
|
||||||
|
|
||||||
def maybe_queryset(value):
|
def maybe_queryset(value):
|
||||||
|
@ -59,17 +93,14 @@ def maybe_queryset(value):
|
||||||
|
|
||||||
|
|
||||||
def get_model_fields(model):
|
def get_model_fields(model):
|
||||||
local_fields = [
|
"""
|
||||||
(field.name, field)
|
Gets all the fields and relationships on the Django model and its ancestry.
|
||||||
for field in sorted(
|
Prioritizes local fields and relationships over the reverse relationships of the same name
|
||||||
list(model._meta.fields) + list(model._meta.local_many_to_many)
|
Returns a tuple of (field.name, field)
|
||||||
)
|
"""
|
||||||
]
|
local_fields = get_local_fields(model)
|
||||||
|
local_field_names = {field[0] for field in local_fields}
|
||||||
# Make sure we don't duplicate local fields with "reverse" version
|
|
||||||
local_field_names = [field[0] for field in local_fields]
|
|
||||||
reverse_fields = get_reverse_fields(model, local_field_names)
|
reverse_fields = get_reverse_fields(model, local_field_names)
|
||||||
|
|
||||||
all_fields = local_fields + list(reverse_fields)
|
all_fields = local_fields + list(reverse_fields)
|
||||||
|
|
||||||
return all_fields
|
return all_fields
|
||||||
|
@ -121,3 +152,11 @@ def is_sync_function(func):
|
||||||
return not inspect.iscoroutinefunction(func) and not inspect.isasyncgenfunction(
|
return not inspect.iscoroutinefunction(func) and not inspect.isasyncgenfunction(
|
||||||
func
|
func
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def bypass_get_queryset(resolver):
|
||||||
|
"""
|
||||||
|
Adds a bypass_get_queryset attribute to the resolver, which is used to
|
||||||
|
bypass any custom get_queryset method of the DjangoObjectType.
|
||||||
|
"""
|
||||||
|
resolver._bypass_get_queryset = True
|
||||||
|
return resolver
|
||||||
|
|
|
@ -16,10 +16,9 @@ from django.views.generic import View
|
||||||
from graphql import OperationType, get_operation_ast, parse
|
from graphql import OperationType, get_operation_ast, parse
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
from graphql.execution import ExecutionResult
|
from graphql.execution import ExecutionResult
|
||||||
|
|
||||||
from graphene import Schema
|
|
||||||
from graphql.execution.middleware import MiddlewareManager
|
from graphql.execution.middleware import MiddlewareManager
|
||||||
|
|
||||||
|
from graphene import Schema
|
||||||
from graphene_django.constants import MUTATION_ERRORS_FLAG
|
from graphene_django.constants import MUTATION_ERRORS_FLAG
|
||||||
from graphene_django.utils.utils import set_rollback
|
from graphene_django.utils.utils import set_rollback
|
||||||
|
|
||||||
|
@ -44,9 +43,9 @@ def get_accepted_content_types(request):
|
||||||
|
|
||||||
raw_content_types = request.META.get("HTTP_ACCEPT", "*/*").split(",")
|
raw_content_types = request.META.get("HTTP_ACCEPT", "*/*").split(",")
|
||||||
qualified_content_types = map(qualify, raw_content_types)
|
qualified_content_types = map(qualify, raw_content_types)
|
||||||
return list(
|
return [
|
||||||
x[0] for x in sorted(qualified_content_types, key=lambda x: x[1], reverse=True)
|
x[0] for x in sorted(qualified_content_types, key=lambda x: x[1], reverse=True)
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
def instantiate_middleware(middlewares):
|
def instantiate_middleware(middlewares):
|
||||||
|
@ -70,18 +69,21 @@ class GraphQLView(View):
|
||||||
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
|
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
|
||||||
|
|
||||||
# The GraphiQL React app.
|
# The GraphiQL React app.
|
||||||
graphiql_version = "2.4.1" # "1.0.3"
|
graphiql_version = "2.4.7"
|
||||||
graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
graphiql_sri = "sha256-n/LKaELupC1H/PU6joz+ybeRJHT2xCdekEt6OYMOOZU="
|
||||||
graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
graphiql_css_sri = "sha256-OsbM+LQHcnFHi0iH7AUKueZvDcEBoy/z4hJ7jx1cpsM="
|
||||||
|
|
||||||
# The websocket transport library for subscriptions.
|
# The websocket transport library for subscriptions.
|
||||||
subscriptions_transport_ws_version = "5.12.1"
|
subscriptions_transport_ws_version = "5.13.1"
|
||||||
subscriptions_transport_ws_sri = (
|
subscriptions_transport_ws_sri = (
|
||||||
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
|
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
|
||||||
)
|
)
|
||||||
|
|
||||||
graphiql_plugin_explorer_version = "0.1.15"
|
graphiql_plugin_explorer_version = "0.1.15"
|
||||||
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
|
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
|
||||||
|
graphiql_plugin_explorer_css_sri = (
|
||||||
|
"sha256-fA0LPUlukMNR6L4SPSeFqDTYav8QdWjQ2nr559Zln1U="
|
||||||
|
)
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
|
@ -109,17 +111,19 @@ class GraphQLView(View):
|
||||||
if middleware is None:
|
if middleware is None:
|
||||||
middleware = graphene_settings.MIDDLEWARE
|
middleware = graphene_settings.MIDDLEWARE
|
||||||
|
|
||||||
self.schema = self.schema or schema
|
self.schema = schema or self.schema
|
||||||
if middleware is not None:
|
if middleware is not None:
|
||||||
if isinstance(middleware, MiddlewareManager):
|
if isinstance(middleware, MiddlewareManager):
|
||||||
self.middleware = middleware
|
self.middleware = middleware
|
||||||
else:
|
else:
|
||||||
self.middleware = list(instantiate_middleware(middleware))
|
self.middleware = list(instantiate_middleware(middleware))
|
||||||
self.root_value = root_value
|
self.root_value = root_value
|
||||||
self.pretty = self.pretty or pretty
|
self.pretty = pretty or self.pretty
|
||||||
self.graphiql = self.graphiql or graphiql
|
self.graphiql = graphiql or self.graphiql
|
||||||
self.batch = self.batch or batch
|
self.batch = batch or self.batch
|
||||||
self.execution_context_class = execution_context_class
|
self.execution_context_class = (
|
||||||
|
execution_context_class or self.execution_context_class
|
||||||
|
)
|
||||||
if subscription_path is None:
|
if subscription_path is None:
|
||||||
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
||||||
|
|
||||||
|
|
37
setup.cfg
37
setup.cfg
|
@ -4,46 +4,9 @@ test=pytest
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal=1
|
universal=1
|
||||||
|
|
||||||
[flake8]
|
|
||||||
exclude = docs,graphene_django/debug/sql/*
|
|
||||||
max-line-length = 120
|
|
||||||
select =
|
|
||||||
# Dictionary key repeated
|
|
||||||
F601,
|
|
||||||
# Ensure use of ==/!= to compare with str, bytes and int literals
|
|
||||||
F632,
|
|
||||||
# Redefinition of unused name
|
|
||||||
F811,
|
|
||||||
# Using an undefined variable
|
|
||||||
F821,
|
|
||||||
# Defining an undefined variable in __all__
|
|
||||||
F822,
|
|
||||||
# Using a variable before it is assigned
|
|
||||||
F823,
|
|
||||||
# Duplicate argument in function declaration
|
|
||||||
F831,
|
|
||||||
# Black would format this line
|
|
||||||
BLK,
|
|
||||||
# Do not use bare except
|
|
||||||
B001,
|
|
||||||
# Don't allow ++n. You probably meant n += 1
|
|
||||||
B002,
|
|
||||||
# Do not use mutable structures for argument defaults
|
|
||||||
B006,
|
|
||||||
# Do not perform calls in argument defaults
|
|
||||||
B008
|
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
omit = */tests/*
|
omit = */tests/*
|
||||||
|
|
||||||
[isort]
|
|
||||||
known_first_party=graphene,graphene_django
|
|
||||||
multi_line_output=3
|
|
||||||
include_trailing_comma=True
|
|
||||||
force_grid_wrap=0
|
|
||||||
use_parentheses=True
|
|
||||||
line_length=88
|
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
DJANGO_SETTINGS_MODULE = examples.django_test_settings
|
DJANGO_SETTINGS_MODULE = examples.django_test_settings
|
||||||
addopts = --random-order
|
addopts = --random-order
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -27,10 +27,8 @@ tests_require = [
|
||||||
|
|
||||||
|
|
||||||
dev_requires = [
|
dev_requires = [
|
||||||
"black==23.3.0",
|
"black==23.7.0",
|
||||||
"flake8==6.0.0",
|
"ruff==0.0.283",
|
||||||
"flake8-black==0.3.6",
|
|
||||||
"flake8-bugbear==23.3.23",
|
|
||||||
"pre-commit",
|
"pre-commit",
|
||||||
] + tests_require
|
] + tests_require
|
||||||
|
|
||||||
|
@ -39,6 +37,7 @@ setup(
|
||||||
version=version,
|
version=version,
|
||||||
description="Graphene Django integration",
|
description="Graphene Django integration",
|
||||||
long_description=open("README.md").read(),
|
long_description=open("README.md").read(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
url="https://github.com/graphql-python/graphene-django",
|
url="https://github.com/graphql-python/graphene-django",
|
||||||
author="Syrus Akbary",
|
author="Syrus Akbary",
|
||||||
author_email="me@syrusakbary.com",
|
author_email="me@syrusakbary.com",
|
||||||
|
@ -48,7 +47,6 @@ setup(
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
@ -56,8 +54,8 @@ setup(
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Framework :: Django :: 3.2",
|
"Framework :: Django :: 3.2",
|
||||||
"Framework :: Django :: 4.0",
|
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.1",
|
||||||
|
"Framework :: Django :: 4.2",
|
||||||
],
|
],
|
||||||
keywords="api graphql protocol rest relay graphene",
|
keywords="api graphql protocol rest relay graphene",
|
||||||
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
||||||
|
|
15
tox.ini
15
tox.ini
|
@ -1,13 +1,12 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{37,38,39,310}-django32,
|
py{38,39,310}-django32
|
||||||
py{38,39,310}-django{40,41,main},
|
py{38,39}-django{41,42}
|
||||||
py311-django{41,main}
|
py{310,311}-django{41,42,main}
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
python =
|
python =
|
||||||
3.7: py37
|
|
||||||
3.8: py38
|
3.8: py38
|
||||||
3.9: py39
|
3.9: py39
|
||||||
3.10: py310
|
3.10: py310
|
||||||
|
@ -16,8 +15,8 @@ python =
|
||||||
[gh-actions:env]
|
[gh-actions:env]
|
||||||
DJANGO =
|
DJANGO =
|
||||||
3.2: django32
|
3.2: django32
|
||||||
4.0: django40
|
|
||||||
4.1: django41
|
4.1: django41
|
||||||
|
4.2: django42
|
||||||
main: djangomain
|
main: djangomain
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -30,13 +29,13 @@ deps =
|
||||||
-e.[test]
|
-e.[test]
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
django32: Django>=3.2,<4.0
|
django32: Django>=3.2,<4.0
|
||||||
django40: Django>=4.0,<4.1
|
|
||||||
django41: Django>=4.1,<4.2
|
django41: Django>=4.1,<4.2
|
||||||
|
django42: Django>=4.2,<4.3
|
||||||
djangomain: https://github.com/django/django/archive/main.zip
|
djangomain: https://github.com/django/django/archive/main.zip
|
||||||
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
commands = {posargs:pytest --cov=graphene_django graphene_django examples}
|
||||||
|
|
||||||
[testenv:pre-commit]
|
[testenv:pre-commit]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps = pre-commit
|
deps = pre-commit
|
||||||
commands =
|
commands =
|
||||||
pre-commit run --all-files --show-diff-on-failure
|
pre-commit run {posargs:--all-files --show-diff-on-failure}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user