mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-16 07:02:04 +03:00
Merge branch 'v3' into test-utils-rename-op-name
This commit is contained in:
commit
270108d472
25
.github/workflows/deploy.yml
vendored
Normal file
25
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: 🚀 Deploy to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Build wheel and source tarball
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.1.0
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_password }}
|
22
.github/workflows/lint.yml
vendored
Normal file
22
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- name: Run lint 💅
|
||||
run: tox
|
||||
env:
|
||||
TOXENV: flake8
|
28
.github/workflows/tests.yml
vendored
Normal file
28
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
django: ["2.2", "3.0"]
|
||||
python-version: ["3.6", "3.7", "3.8"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
env:
|
||||
DJANGO: ${{ matrix.django }}
|
||||
TOXENV: ${{ matrix.toxenv }}
|
68
.travis.yml
68
.travis.yml
|
@ -1,68 +0,0 @@
|
|||
language: python
|
||||
cache: pip
|
||||
dist: xenial
|
||||
|
||||
install:
|
||||
- pip install tox tox-travis
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- pip install coveralls
|
||||
- coveralls
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: deploy
|
||||
if: tag IS present
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
|
||||
allow_failures:
|
||||
- env: DJANGO=master
|
||||
|
||||
include:
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2
|
||||
- python: 3.6
|
||||
env: DJANGO=3.0
|
||||
- python: 3.6
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.7
|
||||
env: DJANGO=1.11
|
||||
- python: 3.7
|
||||
env: DJANGO=2.2
|
||||
- python: 3.7
|
||||
env: DJANGO=3.0
|
||||
- python: 3.7
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.8
|
||||
env: DJANGO=1.11
|
||||
- python: 3.8
|
||||
env: DJANGO=2.2
|
||||
- python: 3.8
|
||||
env: DJANGO=3.0
|
||||
- python: 3.8
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.8
|
||||
env: TOXENV=black,flake8
|
||||
|
||||
- stage: deploy
|
||||
script: skip
|
||||
python: 3.8
|
||||
after_success: true
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: syrusakbary
|
||||
on:
|
||||
tags: true
|
||||
password:
|
||||
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
|
||||
distributions: "sdist bdist_wheel"
|
|
@ -28,7 +28,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
|
|||
For installing graphene, just run this command in your shell
|
||||
|
||||
```bash
|
||||
pip install "graphene-django>=2.0"
|
||||
pip install "graphene-django>=3"
|
||||
```
|
||||
|
||||
### Settings
|
||||
|
|
|
@ -23,7 +23,7 @@ For installing graphene, just run this command in your shell
|
|||
|
||||
.. code:: bash
|
||||
|
||||
pip install "graphene-django>=2.0"
|
||||
pip install "graphene-django>=3"
|
||||
|
||||
Settings
|
||||
~~~~~~~~
|
||||
|
|
|
@ -166,16 +166,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
|
|||
|
||||
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
||||
|
||||
For Django 1.11:
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns = [
|
||||
# some other urls
|
||||
url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
]
|
||||
|
||||
For Django 2.0 and above:
|
||||
For Django 2.2 and above:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ Filtering
|
|||
=========
|
||||
|
||||
Graphene integrates with
|
||||
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. See the `usage
|
||||
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
|
||||
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
|
||||
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
|
||||
for details on the format for ``filter_fields``.
|
||||
|
||||
This filtering is automatically available when implementing a ``relay.Node``.
|
||||
|
@ -123,6 +123,15 @@ create your own ``FilterSet``. You can pass it directly as follows:
|
|||
class AnimalFilter(django_filters.FilterSet):
|
||||
# Do case-insensitive lookups on 'name'
|
||||
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
||||
# Allow multiple genera to be selected at once
|
||||
genera = django_filters.MultipleChoiceFilter(
|
||||
field_name='genus',
|
||||
choices=(
|
||||
('Canis', 'Canis'),
|
||||
('Panthera', 'Panthera'),
|
||||
('Seahorse', 'Seahorse')
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
|
@ -135,6 +144,22 @@ create your own ``FilterSet``. You can pass it directly as follows:
|
|||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||
filterset_class=AnimalFilter)
|
||||
|
||||
|
||||
If you were interested in selecting all dogs and cats, you might query as follows:
|
||||
|
||||
.. code::
|
||||
|
||||
query {
|
||||
allAnimals(genera: ["Canis", "Panthera"]) {
|
||||
edges {
|
||||
node {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
You can also specify the ``FilterSet`` class using the ``filterset_class``
|
||||
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
||||
in unison with the ``filter_fields`` parameter:
|
||||
|
@ -162,6 +187,7 @@ in unison with the ``filter_fields`` parameter:
|
|||
animal = relay.Node.Field(AnimalNode)
|
||||
all_animals = DjangoFilterConnectionField(AnimalNode)
|
||||
|
||||
|
||||
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
||||
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
||||
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||
|
|
|
@ -8,7 +8,7 @@ Requirements
|
|||
|
||||
Graphene-Django currently supports the following versions of Django:
|
||||
|
||||
* >= Django 1.11
|
||||
* >= Django 2.2
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -32,19 +32,7 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
|
|||
|
||||
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
|
||||
|
||||
For Django 1.11:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.conf.urls import url
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r"graphql", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
||||
For Django 2.0 and above:
|
||||
For Django 2.2 and above:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
|
18
graphene_django/conftest.py
Normal file
18
graphene_django/conftest.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from graphene_django.settings import graphene_settings as gsettings
|
||||
|
||||
from .registry import reset_global_registry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_registry_fixture(db):
|
||||
yield None
|
||||
reset_global_registry()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def graphene_settings():
|
||||
settings = dict(gsettings.__dict__)
|
||||
yield gsettings
|
||||
gsettings.__dict__ = settings
|
|
@ -3,11 +3,14 @@ from functools import singledispatch
|
|||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from graphene import (
|
||||
ID,
|
||||
UUID,
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
Dynamic,
|
||||
Enum,
|
||||
Field,
|
||||
|
@ -16,25 +19,23 @@ from graphene import (
|
|||
List,
|
||||
NonNull,
|
||||
String,
|
||||
UUID,
|
||||
DateTime,
|
||||
Date,
|
||||
Time,
|
||||
)
|
||||
from graphene.types.json import JSONString
|
||||
from graphene.utils.str_converters import to_camel_case, to_const
|
||||
from graphql import assert_valid_name
|
||||
from graphql import GraphQLError, assert_valid_name
|
||||
from graphql.pyutils import register_description
|
||||
|
||||
from .settings import graphene_settings
|
||||
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
||||
from .fields import DjangoListField, DjangoConnectionField
|
||||
from .fields import DjangoConnectionField, DjangoListField
|
||||
from .settings import graphene_settings
|
||||
|
||||
|
||||
def convert_choice_name(name):
|
||||
name = to_const(force_str(name))
|
||||
try:
|
||||
assert_valid_name(name)
|
||||
except AssertionError:
|
||||
except GraphQLError:
|
||||
name = "A_%s" % name
|
||||
return name
|
||||
|
||||
|
@ -52,7 +53,9 @@ def get_choices(choices):
|
|||
while name in converted_names:
|
||||
name += "_" + str(len(converted_names))
|
||||
converted_names.append(name)
|
||||
description = help_text
|
||||
description = str(
|
||||
help_text
|
||||
) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58
|
||||
yield name, value, description
|
||||
|
||||
|
||||
|
@ -64,7 +67,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
|
|||
class EnumWithDescriptionsType(object):
|
||||
@property
|
||||
def description(self):
|
||||
return named_choices_descriptions[self.name]
|
||||
return str(named_choices_descriptions[self.name])
|
||||
|
||||
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||
|
||||
|
@ -276,3 +279,8 @@ def convert_postgres_range_to_string(field, registry=None):
|
|||
if not isinstance(inner_type, (List, NonNull)):
|
||||
inner_type = type(inner_type)
|
||||
return List(inner_type, description=field.help_text, required=not field.null)
|
||||
|
||||
|
||||
# Register Django lazy()-wrapped values as GraphQL description/help_text.
|
||||
# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58.
|
||||
register_description(Promise)
|
||||
|
|
|
@ -17,7 +17,7 @@ class DjangoDebugContext(object):
|
|||
if not self.debug_promise:
|
||||
self.debug_promise = Promise.all(self.promises)
|
||||
self.promises = []
|
||||
return self.debug_promise.then(self.on_resolve_all_promises)
|
||||
return self.debug_promise.then(self.on_resolve_all_promises).get()
|
||||
|
||||
def on_resolve_all_promises(self, values):
|
||||
if self.promises:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import pytest
|
||||
|
||||
import graphene
|
||||
from graphene.relay import Node
|
||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||
|
@ -13,11 +11,6 @@ class context(object):
|
|||
pass
|
||||
|
||||
|
||||
# from examples.starwars_django.models import Character
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_should_query_field():
|
||||
r1 = Reporter(last_name="ABA")
|
||||
r1.save()
|
||||
|
@ -75,7 +68,7 @@ def test_should_query_nested_field():
|
|||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||
|
||||
def resolve_reporter(self, info, **args):
|
||||
return Reporter.objects.first()
|
||||
|
@ -89,7 +82,7 @@ def test_should_query_nested_field():
|
|||
pets { edges { node { lastName } } }
|
||||
} } }
|
||||
}
|
||||
__debug {
|
||||
_debug {
|
||||
sql {
|
||||
rawSql
|
||||
}
|
||||
|
@ -117,12 +110,12 @@ def test_should_query_nested_field():
|
|||
)
|
||||
assert not result.errors
|
||||
query = str(Reporter.objects.order_by("pk")[:1].query)
|
||||
assert result.data["__debug"]["sql"][0]["rawSql"] == query
|
||||
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
|
||||
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
|
||||
assert len(result.data["__debug"]["sql"]) == 5
|
||||
assert result.data["_debug"]["sql"][0]["rawSql"] == query
|
||||
assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"]
|
||||
assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"]
|
||||
assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"]
|
||||
assert len(result.data["_debug"]["sql"]) == 5
|
||||
|
||||
assert result.data["reporter"] == expected["reporter"]
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from functools import partial
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from graphql_relay.connection.arrayconnection import connection_from_list_slice
|
||||
from graphql_relay.connection.arrayconnection import connection_from_array_slice
|
||||
from promise import Promise
|
||||
|
||||
from graphene import NonNull
|
||||
from graphene.relay import ConnectionField, PageInfo
|
||||
from graphene.relay import ConnectionField
|
||||
from graphene.relay.connection import connection_adapter, page_info_adapter
|
||||
from graphene.types import Field, List
|
||||
|
||||
from .settings import graphene_settings
|
||||
|
@ -122,15 +123,15 @@ class DjangoConnectionField(ConnectionField):
|
|||
_len = iterable.count()
|
||||
else:
|
||||
_len = len(iterable)
|
||||
connection = connection_from_list_slice(
|
||||
connection = connection_from_array_slice(
|
||||
iterable,
|
||||
args,
|
||||
slice_start=0,
|
||||
list_length=_len,
|
||||
list_slice_length=_len,
|
||||
connection_type=connection,
|
||||
array_length=_len,
|
||||
array_slice_length=_len,
|
||||
connection_type=partial(connection_adapter, connection),
|
||||
edge_type=connection.Edge,
|
||||
pageinfo_type=PageInfo,
|
||||
page_info_type=page_info_adapter,
|
||||
)
|
||||
connection.iterable = iterable
|
||||
connection.length = _len
|
||||
|
|
|
@ -35,9 +35,6 @@ else:
|
|||
)
|
||||
)
|
||||
|
||||
pytestmark.append(pytest.mark.django_db)
|
||||
|
||||
|
||||
if DJANGO_FILTER_INSTALLED:
|
||||
|
||||
class ArticleNode(DjangoObjectType):
|
||||
|
@ -809,38 +806,56 @@ def test_integer_field_filter_type():
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type PetType implements Node {
|
||||
age: Int!
|
||||
id: ID!
|
||||
type Query {
|
||||
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
|
||||
}
|
||||
|
||||
type PetTypeConnection {
|
||||
\"""Pagination data for this connection.\"""
|
||||
pageInfo: PageInfo!
|
||||
|
||||
\"""Contains the nodes in this connection.\"""
|
||||
edges: [PetTypeEdge]!
|
||||
}
|
||||
|
||||
\"""
|
||||
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||
\"""
|
||||
type PageInfo {
|
||||
\"""When paginating forwards, are there more items?\"""
|
||||
hasNextPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, are there more items?\"""
|
||||
hasPreviousPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, the cursor to continue.\"""
|
||||
startCursor: String
|
||||
|
||||
\"""When paginating forwards, the cursor to continue.\"""
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
\"""A Relay edge containing a `PetType` and its cursor.\"""
|
||||
type PetTypeEdge {
|
||||
\"""The item at the end of the edge\"""
|
||||
node: PetType
|
||||
|
||||
\"""A cursor for use in pagination\"""
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||
|
||||
type PetType implements Node {
|
||||
\"""\"""
|
||||
age: Int!
|
||||
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
|
||||
\"""An object with an ID\"""
|
||||
interface Node {
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
@ -861,40 +876,58 @@ def test_other_filter_types():
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type PetType implements Node {
|
||||
age: Int!
|
||||
id: ID!
|
||||
type Query {
|
||||
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection
|
||||
}
|
||||
|
||||
type PetTypeConnection {
|
||||
\"""Pagination data for this connection.\"""
|
||||
pageInfo: PageInfo!
|
||||
|
||||
\"""Contains the nodes in this connection.\"""
|
||||
edges: [PetTypeEdge]!
|
||||
}
|
||||
|
||||
\"""
|
||||
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||
\"""
|
||||
type PageInfo {
|
||||
\"""When paginating forwards, are there more items?\"""
|
||||
hasNextPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, are there more items?\"""
|
||||
hasPreviousPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, the cursor to continue.\"""
|
||||
startCursor: String
|
||||
|
||||
\"""When paginating forwards, the cursor to continue.\"""
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
\"""A Relay edge containing a `PetType` and its cursor.\"""
|
||||
type PetTypeEdge {
|
||||
\"""The item at the end of the edge\"""
|
||||
node: PetType
|
||||
|
||||
\"""A cursor for use in pagination\"""
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
||||
type PetType implements Node {
|
||||
\"""\"""
|
||||
age: Int!
|
||||
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
"""
|
||||
|
||||
\"""An object with an ID\"""
|
||||
interface Node {
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -55,9 +55,14 @@ def convert_form_field_to_float(field):
|
|||
return Float(description=field.help_text, required=field.required)
|
||||
|
||||
|
||||
@convert_form_field.register(forms.MultipleChoiceField)
|
||||
def convert_form_field_to_string_list(field):
|
||||
return List(String, description=field.help_text, required=field.required)
|
||||
|
||||
|
||||
@convert_form_field.register(forms.ModelMultipleChoiceField)
|
||||
@convert_form_field.register(GlobalIDMultipleChoiceField)
|
||||
def convert_form_field_to_list(field):
|
||||
def convert_form_field_to_id_list(field):
|
||||
return List(ID, required=field.required)
|
||||
|
||||
|
||||
|
|
|
@ -66,6 +66,10 @@ def test_should_choice_convert_string():
|
|||
assert_conversion(forms.ChoiceField, String)
|
||||
|
||||
|
||||
def test_should_multiple_choice_convert_list():
|
||||
assert_conversion(forms.MultipleChoiceField, List)
|
||||
|
||||
|
||||
def test_should_base_field_convert_string():
|
||||
assert_conversion(forms.Field, String)
|
||||
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import pytest
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from py.test import raises
|
||||
|
||||
from graphene import ObjectType, Schema, String, Field
|
||||
from graphene import Field, ObjectType, Schema, String
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.tests.models import Film, Pet
|
||||
from graphene_django.tests.models import Pet
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def pet_type():
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Pet
|
||||
fields = "__all__"
|
||||
|
||||
return PetType
|
||||
|
||||
|
||||
class MyForm(forms.Form):
|
||||
text = forms.CharField()
|
||||
|
||||
|
@ -36,18 +45,6 @@ class PetForm(forms.ModelForm):
|
|||
return age
|
||||
|
||||
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Pet
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class FilmType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Film
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
def test_needs_form_class():
|
||||
with raises(Exception) as exc:
|
||||
|
||||
|
@ -73,7 +70,7 @@ def test_has_input_fields():
|
|||
assert "text" in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
def test_mutation_error_camelcased(pet_type, graphene_settings):
|
||||
class ExtraPetForm(PetForm):
|
||||
test_field = forms.CharField(required=True)
|
||||
|
||||
|
@ -86,234 +83,237 @@ def test_mutation_error_camelcased():
|
|||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
class MockQuery(ObjectType):
|
||||
a = String()
|
||||
|
||||
|
||||
class FormMutationTests(TestCase):
|
||||
def test_form_invalid_form(self):
|
||||
class MyMutation(DjangoFormMutation):
|
||||
class Meta:
|
||||
form_class = MyForm
|
||||
def test_form_invalid_form():
|
||||
class MyMutation(DjangoFormMutation):
|
||||
class Meta:
|
||||
form_class = MyForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
my_mutation = MyMutation.Field()
|
||||
class Mutation(ObjectType):
|
||||
my_mutation = MyMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation MyMutation {
|
||||
myMutation(input: { text: "INVALID_INPUT" }) {
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
text
|
||||
result = schema.execute(
|
||||
""" mutation MyMutation {
|
||||
myMutation(input: { text: "INVALID_INPUT" }) {
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert result.errors is None
|
||||
assert result.data["myMutation"]["errors"] == [
|
||||
{"field": "text", "messages": ["Invalid input"]}
|
||||
]
|
||||
|
||||
|
||||
def test_form_valid_input():
|
||||
class MyMutation(DjangoFormMutation):
|
||||
class Meta:
|
||||
form_class = MyForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
my_mutation = MyMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation MyMutation {
|
||||
myMutation(input: { text: "VALID_INPUT" }) {
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
assert result.errors is None
|
||||
assert result.data["myMutation"]["errors"] == []
|
||||
assert result.data["myMutation"]["text"] == "VALID_INPUT"
|
||||
|
||||
|
||||
def test_default_meta_fields(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
assert PetMutation._meta.model is Pet
|
||||
assert PetMutation._meta.return_field_name == "pet"
|
||||
assert "pet" in PetMutation._meta.fields
|
||||
|
||||
|
||||
def test_default_input_meta_fields(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
assert PetMutation._meta.model is Pet
|
||||
assert PetMutation._meta.return_field_name == "pet"
|
||||
assert "name" in PetMutation.Input._meta.fields
|
||||
assert "client_mutation_id" in PetMutation.Input._meta.fields
|
||||
assert "id" in PetMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_exclude_fields_input_meta_fields(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
exclude_fields = ["id"]
|
||||
|
||||
assert PetMutation._meta.model is Pet
|
||||
assert PetMutation._meta.return_field_name == "pet"
|
||||
assert "name" in PetMutation.Input._meta.fields
|
||||
assert "age" in PetMutation.Input._meta.fields
|
||||
assert "client_mutation_id" in PetMutation.Input._meta.fields
|
||||
assert "id" not in PetMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_custom_return_field_name(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
model = Pet
|
||||
return_field_name = "animal"
|
||||
|
||||
assert PetMutation._meta.model is Pet
|
||||
assert PetMutation._meta.return_field_name == "animal"
|
||||
assert "animal" in PetMutation._meta.fields
|
||||
|
||||
|
||||
def test_model_form_mutation_mutate_existing(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(pet_type)
|
||||
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
pet = Pet.objects.create(name="Axel", age=10)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation($pk: ID!) {
|
||||
petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
}
|
||||
""",
|
||||
variable_values={"pk": pet.pk},
|
||||
)
|
||||
|
||||
self.assertIs(result.errors, None)
|
||||
self.assertEqual(
|
||||
result.data["myMutation"]["errors"],
|
||||
[{"field": "text", "messages": ["Invalid input"]}],
|
||||
)
|
||||
assert result.errors is None
|
||||
assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
|
||||
|
||||
def test_form_valid_input(self):
|
||||
class MyMutation(DjangoFormMutation):
|
||||
class Meta:
|
||||
form_class = MyForm
|
||||
assert Pet.objects.count() == 1
|
||||
pet.refresh_from_db()
|
||||
assert pet.name == "Mia"
|
||||
|
||||
class Mutation(ObjectType):
|
||||
my_mutation = MyMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
def test_model_form_mutation_creates_new(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(pet_type)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation MyMutation {
|
||||
myMutation(input: { text: "VALID_INPUT" }) {
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
text
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation {
|
||||
petMutation(input: { name: "Mia", age: 10 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert result.errors is None
|
||||
assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
|
||||
|
||||
self.assertIs(result.errors, None)
|
||||
self.assertEqual(result.data["myMutation"]["errors"], [])
|
||||
self.assertEqual(result.data["myMutation"]["text"], "VALID_INPUT")
|
||||
assert Pet.objects.count() == 1
|
||||
pet = Pet.objects.get()
|
||||
assert pet.name == "Mia"
|
||||
assert pet.age == 10
|
||||
|
||||
|
||||
class ModelFormMutationTests(TestCase):
|
||||
def test_default_meta_fields(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
def test_model_form_mutation_invalid_input(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(pet_type)
|
||||
|
||||
self.assertEqual(PetMutation._meta.model, Pet)
|
||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
||||
self.assertIn("pet", PetMutation._meta.fields)
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
def test_default_input_meta_fields(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
|
||||
self.assertEqual(PetMutation._meta.model, Pet)
|
||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
||||
self.assertIn("name", PetMutation.Input._meta.fields)
|
||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
||||
self.assertIn("id", PetMutation.Input._meta.fields)
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
def test_exclude_fields_input_meta_fields(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
exclude_fields = ["id"]
|
||||
|
||||
self.assertEqual(PetMutation._meta.model, Pet)
|
||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
||||
self.assertIn("name", PetMutation.Input._meta.fields)
|
||||
self.assertIn("age", PetMutation.Input._meta.fields)
|
||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
||||
self.assertNotIn("id", PetMutation.Input._meta.fields)
|
||||
|
||||
def test_custom_return_field_name(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
model = Pet
|
||||
return_field_name = "animal"
|
||||
|
||||
self.assertEqual(PetMutation._meta.model, Pet)
|
||||
self.assertEqual(PetMutation._meta.return_field_name, "animal")
|
||||
self.assertIn("animal", PetMutation._meta.fields)
|
||||
|
||||
def test_model_form_mutation_mutate_existing(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(PetType)
|
||||
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
pet = Pet.objects.create(name="Axel", age=10)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation($pk: ID!) {
|
||||
petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation {
|
||||
petMutation(input: { name: "Mia", age: 99 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
""",
|
||||
variable_values={"pk": pet.pk},
|
||||
)
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert result.errors is None
|
||||
assert result.data["petMutation"]["pet"] is None
|
||||
assert result.data["petMutation"]["errors"] == [
|
||||
{"field": "age", "messages": ["Too old"]}
|
||||
]
|
||||
|
||||
self.assertIs(result.errors, None)
|
||||
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
||||
assert Pet.objects.count() == 0
|
||||
|
||||
self.assertEqual(Pet.objects.count(), 1)
|
||||
pet.refresh_from_db()
|
||||
self.assertEqual(pet.name, "Mia")
|
||||
|
||||
def test_model_form_mutation_creates_new(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(PetType)
|
||||
def test_model_form_mutation_mutate_invalid_form(pet_type):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
# A pet was not created
|
||||
Pet.objects.count() == 0
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation {
|
||||
petMutation(input: { name: "Mia", age: 10 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.assertIs(result.errors, None)
|
||||
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
||||
|
||||
self.assertEqual(Pet.objects.count(), 1)
|
||||
pet = Pet.objects.get()
|
||||
self.assertEqual(pet.name, "Mia")
|
||||
self.assertEqual(pet.age, 10)
|
||||
|
||||
def test_model_form_mutation_invalid_input(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(PetType)
|
||||
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
class Mutation(ObjectType):
|
||||
pet_mutation = PetMutation.Field()
|
||||
|
||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||
|
||||
result = schema.execute(
|
||||
""" mutation PetMutation {
|
||||
petMutation(input: { name: "Mia", age: 99 }) {
|
||||
pet {
|
||||
name
|
||||
age
|
||||
}
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.assertIs(result.errors, None)
|
||||
self.assertEqual(result.data["petMutation"]["pet"], None)
|
||||
self.assertEqual(
|
||||
result.data["petMutation"]["errors"],
|
||||
[{"field": "age", "messages": ["Too old"],}],
|
||||
)
|
||||
|
||||
self.assertEqual(Pet.objects.count(), 0)
|
||||
|
||||
def test_model_form_mutation_mutate_invalid_form(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
|
||||
# A pet was not created
|
||||
self.assertEqual(Pet.objects.count(), 0)
|
||||
|
||||
fields_w_error = [e.field for e in result.errors]
|
||||
self.assertEqual(len(result.errors), 2)
|
||||
self.assertIn("name", fields_w_error)
|
||||
self.assertEqual(result.errors[0].messages, ["This field is required."])
|
||||
self.assertIn("age", fields_w_error)
|
||||
self.assertEqual(result.errors[1].messages, ["This field is required."])
|
||||
fields_w_error = [e.field for e in result.errors]
|
||||
assert len(result.errors) == 2
|
||||
assert result.errors[0].messages == ["This field is required."]
|
||||
assert result.errors[1].messages == ["This field is required."]
|
||||
assert "age" in fields_w_error
|
||||
assert "name" in fields_w_error
|
||||
|
|
|
@ -48,6 +48,7 @@ class CommandArguments(BaseCommand):
|
|||
class Command(CommandArguments):
|
||||
help = "Dump Graphene schema as a JSON or GraphQL file"
|
||||
can_import_settings = True
|
||||
requires_system_checks = False
|
||||
|
||||
def save_json_file(self, out, schema_dict, indent):
|
||||
with open(out, "w") as outfile:
|
||||
|
@ -55,7 +56,7 @@ class Command(CommandArguments):
|
|||
|
||||
def save_graphql_file(self, out, schema):
|
||||
with open(out, "w") as outfile:
|
||||
outfile.write(print_schema(schema))
|
||||
outfile.write(print_schema(schema.graphql_schema))
|
||||
|
||||
def get_schema(self, schema, out, indent):
|
||||
schema_dict = {"data": schema.introspect()}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import graphene
|
||||
import pytest
|
||||
from django.db import models
|
||||
from graphene import Schema
|
||||
from rest_framework import serializers
|
||||
|
||||
import graphene
|
||||
from graphene import Schema
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class MyFakeChildModel(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import datetime
|
||||
|
||||
from py.test import mark, raises
|
||||
from py.test import raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from graphene import Field, ResolveInfo
|
||||
from graphene.types.inputobjecttype import InputObjectType
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ...types import DjangoObjectType
|
||||
from ..models import MyFakeModel, MyFakeModelWithPassword, MyFakeModelWithDate
|
||||
from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
|
||||
from ..mutation import SerializerMutation
|
||||
|
||||
|
||||
|
@ -18,12 +17,14 @@ def mock_info():
|
|||
None,
|
||||
None,
|
||||
None,
|
||||
path=None,
|
||||
schema=None,
|
||||
fragments=None,
|
||||
root_value=None,
|
||||
operation=None,
|
||||
variable_values=None,
|
||||
context=None,
|
||||
is_awaitable=None,
|
||||
)
|
||||
|
||||
|
||||
|
@ -99,7 +100,6 @@ def test_exclude_fields():
|
|||
assert "created" not in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_write_only_field():
|
||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
@ -122,7 +122,6 @@ def test_write_only_field():
|
|||
), "'password' is write_only field and shouldn't be visible"
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_write_only_field_using_extra_kwargs():
|
||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -144,7 +143,6 @@ def test_write_only_field_using_extra_kwargs():
|
|||
), "'password' is write_only field and shouldn't be visible"
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_read_only_fields():
|
||||
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||
cool_name = serializers.CharField(read_only=True)
|
||||
|
@ -194,7 +192,6 @@ def test_mutate_and_get_payload_success():
|
|||
assert result.errors is None
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_model_add_mutate_and_get_payload_success():
|
||||
result = MyModelMutation.mutate_and_get_payload(
|
||||
None, mock_info(), **{"cool_name": "Narf"}
|
||||
|
@ -204,7 +201,6 @@ def test_model_add_mutate_and_get_payload_success():
|
|||
assert isinstance(result.created, datetime.datetime)
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_model_update_mutate_and_get_payload_success():
|
||||
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||
result = MyModelMutation.mutate_and_get_payload(
|
||||
|
@ -214,7 +210,6 @@ def test_model_update_mutate_and_get_payload_success():
|
|||
assert result.cool_name == "New Narf"
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_model_partial_update_mutate_and_get_payload_success():
|
||||
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||
result = MyModelMutation.mutate_and_get_payload(
|
||||
|
@ -224,7 +219,6 @@ def test_model_partial_update_mutate_and_get_payload_success():
|
|||
assert result.cool_name == "Narf"
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_model_invalid_update_mutate_and_get_payload_success():
|
||||
class InvalidModelMutation(SerializerMutation):
|
||||
class Meta:
|
||||
|
@ -239,7 +233,6 @@ def test_model_invalid_update_mutate_and_get_payload_success():
|
|||
assert '"id" required' in str(exc.value)
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_perform_mutate_success():
|
||||
class MyMethodMutation(SerializerMutation):
|
||||
class Meta:
|
||||
|
@ -272,11 +265,10 @@ def test_model_mutate_and_get_payload_error():
|
|||
assert len(result.errors) > 0
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
def test_mutation_error_camelcased(graphene_settings):
|
||||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||
assert result.errors[0].field == "coolName"
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
def test_invalid_serializer_operations():
|
||||
|
|
|
@ -8,7 +8,7 @@ from graphene import ObjectType, Schema, String
|
|||
|
||||
|
||||
@patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
|
||||
def test_generate_json_file_on_call_graphql_schema(savefile_mock, settings):
|
||||
def test_generate_json_file_on_call_graphql_schema(savefile_mock):
|
||||
out = StringIO()
|
||||
management.call_command("graphql_schema", schema="", stdout=out)
|
||||
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
|
||||
|
@ -51,10 +51,6 @@ def test_generate_graphql_file_on_call_graphql_schema():
|
|||
schema_output = handle.write.call_args[0][0]
|
||||
assert schema_output == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Query {
|
||||
hi: String
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import pytest
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from graphene import NonNull
|
||||
from py.test import raises
|
||||
|
||||
import graphene
|
||||
from graphene import NonNull
|
||||
from graphene.relay import ConnectionField, Node
|
||||
from graphene.types.datetime import DateTime, Date, Time
|
||||
from graphene.types.datetime import Date, DateTime, Time
|
||||
from graphene.types.json import JSONString
|
||||
|
||||
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
|
||||
from ..compat import ArrayField, HStoreField, JSONField, MissingType, RangeField
|
||||
from ..converter import (
|
||||
convert_django_field,
|
||||
convert_django_field_with_choices,
|
||||
|
@ -18,10 +19,8 @@ from ..converter import (
|
|||
)
|
||||
from ..registry import Registry
|
||||
from ..types import DjangoObjectType
|
||||
from ..settings import graphene_settings
|
||||
from .models import Article, Film, FilmDetails, Reporter
|
||||
|
||||
|
||||
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
||||
|
||||
|
||||
|
@ -333,7 +332,7 @@ def test_should_postgres_range_convert_list():
|
|||
assert field.type.of_type.of_type == graphene.Int
|
||||
|
||||
|
||||
def test_generate_enum_name():
|
||||
def test_generate_enum_name(graphene_settings):
|
||||
MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
|
||||
|
||||
|
@ -351,5 +350,3 @@ def test_generate_enum_name():
|
|||
generate_enum_name(model_meta, field)
|
||||
== "SomeLongAppNameSomeObjectFizzBuzzChoices"
|
||||
)
|
||||
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
|
||||
|
|
|
@ -10,7 +10,6 @@ from .models import Article as ArticleModel
|
|||
from .models import Reporter as ReporterModel
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestDjangoListField:
|
||||
def test_only_django_object_types(self):
|
||||
class TestType(ObjectType):
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
import base64
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from graphql_relay import to_global_id
|
||||
from py.test import raises
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from graphql_relay import to_global_id
|
||||
import graphene
|
||||
from graphene.relay import Node
|
||||
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from ..compat import MissingType, JSONField
|
||||
from ..compat import JSONField, MissingType
|
||||
from ..fields import DjangoConnectionField
|
||||
from ..types import DjangoObjectType
|
||||
from ..settings import graphene_settings
|
||||
from .models import Article, CNNReporter, Reporter, Film, FilmDetails
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
|
||||
|
||||
|
||||
def test_should_query_only_fields():
|
||||
|
@ -147,9 +142,6 @@ def test_should_query_postgres_fields():
|
|||
|
||||
|
||||
def test_should_node():
|
||||
# reset_global_registry()
|
||||
# Node._meta.registry = get_global_registry()
|
||||
|
||||
class ReporterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
|
@ -588,7 +580,7 @@ def test_should_query_node_multiple_filtering():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_enforce_first_or_last():
|
||||
def test_should_enforce_first_or_last(graphene_settings):
|
||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
|
@ -620,14 +612,14 @@ def test_should_enforce_first_or_last():
|
|||
|
||||
result = schema.execute(query)
|
||||
assert len(result.errors) == 1
|
||||
assert str(result.errors[0]) == (
|
||||
assert str(result.errors[0]).startswith(
|
||||
"You must provide a `first` or `last` value to properly "
|
||||
"paginate the `allReporters` connection."
|
||||
"paginate the `allReporters` connection.\n"
|
||||
)
|
||||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_error_if_first_is_greater_than_max():
|
||||
def test_should_error_if_first_is_greater_than_max(graphene_settings):
|
||||
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
|
@ -661,16 +653,14 @@ def test_should_error_if_first_is_greater_than_max():
|
|||
|
||||
result = schema.execute(query)
|
||||
assert len(result.errors) == 1
|
||||
assert str(result.errors[0]) == (
|
||||
assert str(result.errors[0]).startswith(
|
||||
"Requesting 101 records on the `allReporters` connection "
|
||||
"exceeds the `first` limit of 100 records."
|
||||
"exceeds the `first` limit of 100 records.\n"
|
||||
)
|
||||
assert result.data == expected
|
||||
|
||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
||||
|
||||
|
||||
def test_should_error_if_last_is_greater_than_max():
|
||||
def test_should_error_if_last_is_greater_than_max(graphene_settings):
|
||||
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
|
@ -704,14 +694,12 @@ def test_should_error_if_last_is_greater_than_max():
|
|||
|
||||
result = schema.execute(query)
|
||||
assert len(result.errors) == 1
|
||||
assert str(result.errors[0]) == (
|
||||
assert str(result.errors[0]).startswith(
|
||||
"Requesting 101 records on the `allReporters` connection "
|
||||
"exceeds the `last` limit of 100 records."
|
||||
"exceeds the `last` limit of 100 records.\n"
|
||||
)
|
||||
assert result.data == expected
|
||||
|
||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
||||
|
||||
|
||||
def test_should_query_promise_connectionfields():
|
||||
from promise import Promise
|
||||
|
@ -725,7 +713,7 @@ def test_should_query_promise_connectionfields():
|
|||
all_reporters = DjangoConnectionField(ReporterType)
|
||||
|
||||
def resolve_all_reporters(self, info, **args):
|
||||
return Promise.resolve([Reporter(id=1)])
|
||||
return Promise.resolve([Reporter(id=1)]).get()
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
query = """
|
||||
|
@ -854,7 +842,7 @@ def test_should_query_dataloader_fields():
|
|||
articles = DjangoConnectionField(ArticleType)
|
||||
|
||||
def resolve_articles(self, info, **args):
|
||||
return article_loader.load(self.id)
|
||||
return article_loader.load(self.id).get()
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
all_reporters = DjangoConnectionField(ReporterType)
|
||||
|
@ -1087,7 +1075,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
|
|||
class Query(graphene.ObjectType):
|
||||
films = DjangoConnectionField(FilmType)
|
||||
|
||||
def resolve_films(root, info):
|
||||
def resolve_films(root, info, **kwargs):
|
||||
qs = Film.objects.prefetch_related("reporters")
|
||||
return qs
|
||||
|
||||
|
@ -1117,9 +1105,10 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
|
|||
}
|
||||
"""
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
with django_assert_num_queries(3) as captured:
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert not result.errors
|
||||
|
||||
|
||||
def test_should_preserve_annotations():
|
||||
|
@ -1139,7 +1128,7 @@ def test_should_preserve_annotations():
|
|||
class Query(graphene.ObjectType):
|
||||
films = DjangoConnectionField(FilmType)
|
||||
|
||||
def resolve_films(root, info):
|
||||
def resolve_films(root, info, **kwargs):
|
||||
qs = Film.objects.prefetch_related("reporters")
|
||||
return qs.annotate(reporters_count=models.Count("reporters"))
|
||||
|
||||
|
@ -1172,3 +1161,4 @@ def test_should_preserve_annotations():
|
|||
}
|
||||
}
|
||||
assert result.data == expected, str(result.data)
|
||||
assert not result.errors
|
||||
|
|
|
@ -9,14 +9,10 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
|||
from graphene.relay import Node
|
||||
|
||||
from .. import registry
|
||||
from ..settings import graphene_settings
|
||||
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
||||
from ..converter import convert_choice_field_to_enum
|
||||
from .models import Article as ArticleModel
|
||||
from .models import Reporter as ReporterModel
|
||||
|
||||
registry.reset_global_registry()
|
||||
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
"""Reporter description"""
|
||||
|
@ -115,90 +111,171 @@ def test_django_objecttype_with_custom_meta():
|
|||
|
||||
|
||||
def test_schema_representation():
|
||||
expected = """
|
||||
schema {
|
||||
query: RootQuery
|
||||
}
|
||||
expected = dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: RootQuery
|
||||
}
|
||||
|
||||
type Article implements Node {
|
||||
id: ID!
|
||||
headline: String!
|
||||
pubDate: Date!
|
||||
pubDateTime: DateTime!
|
||||
reporter: Reporter!
|
||||
editor: Reporter!
|
||||
lang: ArticleLang!
|
||||
importance: ArticleImportance
|
||||
}
|
||||
\"""Article description\"""
|
||||
type Article implements Node {
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
|
||||
type ArticleConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [ArticleEdge]!
|
||||
test: String
|
||||
}
|
||||
\"""\"""
|
||||
headline: String!
|
||||
|
||||
type ArticleEdge {
|
||||
node: Article
|
||||
cursor: String!
|
||||
}
|
||||
\"""\"""
|
||||
pubDate: Date!
|
||||
|
||||
enum ArticleImportance {
|
||||
A_1
|
||||
A_2
|
||||
}
|
||||
\"""\"""
|
||||
pubDateTime: DateTime!
|
||||
|
||||
enum ArticleLang {
|
||||
ES
|
||||
EN
|
||||
}
|
||||
\"""\"""
|
||||
reporter: Reporter!
|
||||
|
||||
scalar Date
|
||||
\"""\"""
|
||||
editor: Reporter!
|
||||
|
||||
scalar DateTime
|
||||
\"""Language\"""
|
||||
lang: ArticleLang!
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
\"""\"""
|
||||
importance: ArticleImportance
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
\"""An object with an ID\"""
|
||||
interface Node {
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Reporter {
|
||||
id: ID!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
pets: [Reporter!]!
|
||||
aChoice: ReporterAChoice
|
||||
reporterType: ReporterReporterType
|
||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||
}
|
||||
\"""
|
||||
The `Date` scalar type represents a Date
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
\"""
|
||||
scalar Date
|
||||
|
||||
enum ReporterAChoice {
|
||||
A_1
|
||||
A_2
|
||||
}
|
||||
\"""
|
||||
The `DateTime` scalar type represents a DateTime
|
||||
value as specified by
|
||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
\"""
|
||||
scalar DateTime
|
||||
|
||||
enum ReporterReporterType {
|
||||
A_1
|
||||
A_2
|
||||
}
|
||||
\"""An enumeration.\"""
|
||||
enum ArticleLang {
|
||||
\"""Spanish\"""
|
||||
ES
|
||||
|
||||
type RootQuery {
|
||||
node(id: ID!): Node
|
||||
}
|
||||
""".lstrip()
|
||||
\"""English\"""
|
||||
EN
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum ArticleImportance {
|
||||
\"""Very important\"""
|
||||
A_1
|
||||
|
||||
\"""Not as important\"""
|
||||
A_2
|
||||
}
|
||||
|
||||
\"""Reporter description\"""
|
||||
type Reporter {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
\"""\"""
|
||||
firstName: String!
|
||||
|
||||
\"""\"""
|
||||
lastName: String!
|
||||
|
||||
\"""\"""
|
||||
email: String!
|
||||
|
||||
\"""\"""
|
||||
pets: [Reporter!]!
|
||||
|
||||
\"""\"""
|
||||
aChoice: ReporterAChoice
|
||||
|
||||
\"""\"""
|
||||
reporterType: ReporterReporterType
|
||||
|
||||
\"""\"""
|
||||
articles(before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection!
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum ReporterAChoice {
|
||||
\"""this\"""
|
||||
A_1
|
||||
|
||||
\"""that\"""
|
||||
A_2
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum ReporterReporterType {
|
||||
\"""Regular\"""
|
||||
A_1
|
||||
|
||||
\"""CNN Reporter\"""
|
||||
A_2
|
||||
}
|
||||
|
||||
type ArticleConnection {
|
||||
\"""Pagination data for this connection.\"""
|
||||
pageInfo: PageInfo!
|
||||
|
||||
\"""Contains the nodes in this connection.\"""
|
||||
edges: [ArticleEdge]!
|
||||
test: String
|
||||
}
|
||||
|
||||
\"""
|
||||
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||
\"""
|
||||
type PageInfo {
|
||||
\"""When paginating forwards, are there more items?\"""
|
||||
hasNextPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, are there more items?\"""
|
||||
hasPreviousPage: Boolean!
|
||||
|
||||
\"""When paginating backwards, the cursor to continue.\"""
|
||||
startCursor: String
|
||||
|
||||
\"""When paginating forwards, the cursor to continue.\"""
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
\"""A Relay edge containing a `Article` and its cursor.\"""
|
||||
type ArticleEdge {
|
||||
\"""The item at the end of the edge\"""
|
||||
node: Article
|
||||
|
||||
\"""A cursor for use in pagination\"""
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
node(
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
): Node
|
||||
}
|
||||
"""
|
||||
)
|
||||
assert str(schema) == expected
|
||||
|
||||
|
||||
def with_local_registry(func):
|
||||
def inner(*args, **kwargs):
|
||||
old = registry.get_global_registry()
|
||||
registry.reset_global_registry()
|
||||
try:
|
||||
retval = func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
|
@ -420,20 +497,21 @@ class TestDjangoObjectType:
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
type Pet {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
\"""\"""
|
||||
kind: String!
|
||||
|
||||
\"""\"""
|
||||
cuteness: Int!
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
||||
|
@ -449,25 +527,30 @@ class TestDjangoObjectType:
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
cuteness: Int!
|
||||
}
|
||||
type Pet {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
\"""\"""
|
||||
kind: PetModelKind!
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
\"""\"""
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
\"""An enumeration.\"""
|
||||
enum PetModelKind {
|
||||
\"""Cat\"""
|
||||
CAT
|
||||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
||||
|
@ -483,23 +566,26 @@ class TestDjangoObjectType:
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
type Pet {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
\"""\"""
|
||||
kind: String!
|
||||
|
||||
\"""\"""
|
||||
cuteness: Int!
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_naming_collisions(self, PetModel):
|
||||
def test_django_objecttype_convert_choices_enum_naming_collisions(
|
||||
self, PetModel, graphene_settings
|
||||
):
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
|
||||
|
||||
class PetModelKind(DjangoObjectType):
|
||||
|
@ -514,28 +600,32 @@ class TestDjangoObjectType:
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
type Query {
|
||||
pet: PetModelKind
|
||||
}
|
||||
|
||||
type PetModelKind {
|
||||
id: ID!
|
||||
kind: TestsPetModelKindChoices!
|
||||
}
|
||||
type PetModelKind {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
type Query {
|
||||
pet: PetModelKind
|
||||
}
|
||||
\"""\"""
|
||||
kind: TestsPetModelKindChoices!
|
||||
}
|
||||
|
||||
enum TestsPetModelKindChoices {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
\"""An enumeration.\"""
|
||||
enum TestsPetModelKindChoices {
|
||||
\"""Cat\"""
|
||||
CAT
|
||||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
)
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = False
|
||||
|
||||
def test_django_objecttype_choices_custom_enum_name(self, PetModel):
|
||||
def test_django_objecttype_choices_custom_enum_name(
|
||||
self, PetModel, graphene_settings
|
||||
):
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
|
||||
"graphene_django.tests.test_types.custom_enum_name"
|
||||
)
|
||||
|
@ -552,24 +642,25 @@ class TestDjangoObjectType:
|
|||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
type Query {
|
||||
pet: PetModelKind
|
||||
}
|
||||
|
||||
enum CustomEnumKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
type PetModelKind {
|
||||
\"""\"""
|
||||
id: ID!
|
||||
|
||||
type PetModelKind {
|
||||
id: ID!
|
||||
kind: CustomEnumKind!
|
||||
}
|
||||
\"""\"""
|
||||
kind: CustomEnumKind!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: PetModelKind
|
||||
}
|
||||
"""
|
||||
\"""An enumeration.\"""
|
||||
enum CustomEnumKind {
|
||||
\"""Cat\"""
|
||||
CAT
|
||||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = None
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
from django.utils.translation import gettext_lazy
|
||||
import json
|
||||
|
||||
from ..utils import camelize, get_model_fields
|
||||
import pytest
|
||||
from django.utils.translation import gettext_lazy
|
||||
from mock import patch
|
||||
|
||||
from ..utils import camelize, get_model_fields, GraphQLTestCase
|
||||
from .models import Film, Reporter
|
||||
|
||||
|
||||
|
@ -30,3 +34,27 @@ def test_camelize():
|
|||
"valueA": "value_b"
|
||||
}
|
||||
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("graphene_django.utils.testing.Client.post")
|
||||
def test_graphql_test_case_op_name(post_mock):
|
||||
"""
|
||||
Test that `GraphQLTestCase.query()`'s `op_name` argument produces an `operationName` field.
|
||||
"""
|
||||
|
||||
class TestClass(GraphQLTestCase):
|
||||
GRAPHQL_SCHEMA = True
|
||||
|
||||
def runTest(self):
|
||||
pass
|
||||
|
||||
tc = TestClass()
|
||||
tc.setUpClass()
|
||||
tc.query("query { }", op_name="QueryName")
|
||||
body = json.loads(post_mock.call_args.args[1])
|
||||
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
|
||||
assert (
|
||||
"operationName",
|
||||
"QueryName",
|
||||
) in body.items(), "Field 'operationName' is not present in the final request."
|
||||
|
|
|
@ -99,12 +99,14 @@ def test_reports_validation_errors(client):
|
|||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{
|
||||
"message": 'Cannot query field "unknownOne" on type "QueryRoot".',
|
||||
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
|
||||
"locations": [{"line": 1, "column": 9}],
|
||||
"path": None,
|
||||
},
|
||||
{
|
||||
"message": 'Cannot query field "unknownTwo" on type "QueryRoot".',
|
||||
"message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
|
||||
"locations": [{"line": 1, "column": 21}],
|
||||
"path": None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -124,7 +126,9 @@ def test_errors_when_missing_operation_name(client):
|
|||
assert response_json(response) == {
|
||||
"errors": [
|
||||
{
|
||||
"message": "Must provide operation name if query contains multiple operations."
|
||||
"message": "Must provide operation name if query contains multiple operations.",
|
||||
"locations": None,
|
||||
"path": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -464,8 +468,8 @@ def test_handles_syntax_errors_caught_by_graphql(client):
|
|||
"errors": [
|
||||
{
|
||||
"locations": [{"column": 1, "line": 1}],
|
||||
"message": "Syntax Error GraphQL (1:1) "
|
||||
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n',
|
||||
"message": "Syntax Error: Unexpected Name 'syntaxerror'.",
|
||||
"path": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -34,20 +34,20 @@ class GraphQLTestCase(TestCase):
|
|||
supply the op_name. For annon queries ("{ ... }"),
|
||||
should be None (default).
|
||||
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||
to this value. If both ``input_data`` and ``variables``,
|
||||
to this value. If both ``input_data`` and ``variables``,
|
||||
are provided, the ``input`` field in the ``variables``
|
||||
dict will be overwritten with this value.
|
||||
variables (dict) - If provided, the "variables" field in GraphQL will be
|
||||
set to this value.
|
||||
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
||||
will be set to this value.
|
||||
will be set to this value.
|
||||
|
||||
Returns:
|
||||
Response object from client
|
||||
"""
|
||||
body = {"query": query}
|
||||
if operation_name:
|
||||
body["operationName"] = operation_name
|
||||
if op_name:
|
||||
body["operationName"] = op_name
|
||||
if variables:
|
||||
body["variables"] = variables
|
||||
if input_data:
|
||||
|
|
|
@ -6,14 +6,14 @@ from django.http import HttpResponse, HttpResponseNotAllowed
|
|||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from graphql import get_default_backend
|
||||
from graphql.error import format_error as format_graphql_error
|
||||
from django.views.generic import View
|
||||
from graphql import OperationType, get_operation_ast, parse, validate
|
||||
from graphql.error import GraphQLError
|
||||
from graphql.error import format_error as format_graphql_error
|
||||
from graphql.execution import ExecutionResult
|
||||
from graphql.type.schema import GraphQLSchema
|
||||
|
||||
from graphene import Schema
|
||||
|
||||
from .settings import graphene_settings
|
||||
|
||||
|
@ -56,8 +56,6 @@ class GraphQLView(View):
|
|||
|
||||
schema = None
|
||||
graphiql = False
|
||||
executor = None
|
||||
backend = None
|
||||
middleware = None
|
||||
root_value = None
|
||||
pretty = False
|
||||
|
@ -66,35 +64,28 @@ class GraphQLView(View):
|
|||
def __init__(
|
||||
self,
|
||||
schema=None,
|
||||
executor=None,
|
||||
middleware=None,
|
||||
root_value=None,
|
||||
graphiql=False,
|
||||
pretty=False,
|
||||
batch=False,
|
||||
backend=None,
|
||||
):
|
||||
if not schema:
|
||||
schema = graphene_settings.SCHEMA
|
||||
|
||||
if backend is None:
|
||||
backend = get_default_backend()
|
||||
|
||||
if middleware is None:
|
||||
middleware = graphene_settings.MIDDLEWARE
|
||||
|
||||
self.schema = self.schema or schema
|
||||
if middleware is not None:
|
||||
self.middleware = list(instantiate_middleware(middleware))
|
||||
self.executor = executor
|
||||
self.root_value = root_value
|
||||
self.pretty = self.pretty or pretty
|
||||
self.graphiql = self.graphiql or graphiql
|
||||
self.batch = self.batch or batch
|
||||
self.backend = backend
|
||||
|
||||
assert isinstance(
|
||||
self.schema, GraphQLSchema
|
||||
self.schema, Schema
|
||||
), "A Schema is required to be provided to GraphQLView."
|
||||
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
||||
|
||||
|
@ -108,9 +99,6 @@ class GraphQLView(View):
|
|||
def get_context(self, request):
|
||||
return request
|
||||
|
||||
def get_backend(self, request):
|
||||
return self.backend
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
|
@ -172,7 +160,9 @@ class GraphQLView(View):
|
|||
self.format_error(e) for e in execution_result.errors
|
||||
]
|
||||
|
||||
if execution_result.invalid:
|
||||
if execution_result.errors and any(
|
||||
not e.path for e in execution_result.errors
|
||||
):
|
||||
status_code = 400
|
||||
else:
|
||||
response["data"] = execution_result.data
|
||||
|
@ -245,14 +235,13 @@ class GraphQLView(View):
|
|||
raise HttpError(HttpResponseBadRequest("Must provide query string."))
|
||||
|
||||
try:
|
||||
backend = self.get_backend(request)
|
||||
document = backend.document_from_string(self.schema, query)
|
||||
document = parse(query)
|
||||
except Exception as e:
|
||||
return ExecutionResult(errors=[e], invalid=True)
|
||||
return ExecutionResult(errors=[e])
|
||||
|
||||
if request.method.lower() == "get":
|
||||
operation_type = document.get_operation_type(operation_name)
|
||||
if operation_type and operation_type != "query":
|
||||
operation_ast = get_operation_ast(document, operation_name)
|
||||
if operation_ast and operation_ast.operation != OperationType.QUERY:
|
||||
if show_graphiql:
|
||||
return None
|
||||
|
||||
|
@ -260,28 +249,23 @@ class GraphQLView(View):
|
|||
HttpResponseNotAllowed(
|
||||
["POST"],
|
||||
"Can only perform a {} operation from a POST request.".format(
|
||||
operation_type
|
||||
operation_ast.operation.value
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
extra_options = {}
|
||||
if self.executor:
|
||||
# We only include it optionally since
|
||||
# executor is not a valid argument in all backends
|
||||
extra_options["executor"] = self.executor
|
||||
validation_errors = validate(self.schema.graphql_schema, document)
|
||||
if validation_errors:
|
||||
return ExecutionResult(data=None, errors=validation_errors)
|
||||
|
||||
return document.execute(
|
||||
root_value=self.get_root_value(request),
|
||||
variable_values=variables,
|
||||
operation_name=operation_name,
|
||||
context_value=self.get_context(request),
|
||||
middleware=self.get_middleware(request),
|
||||
**extra_options
|
||||
)
|
||||
except Exception as e:
|
||||
return ExecutionResult(errors=[e], invalid=True)
|
||||
return self.schema.execute(
|
||||
source=query,
|
||||
root_value=self.get_root_value(request),
|
||||
variable_values=variables,
|
||||
operation_name=operation_name,
|
||||
context_value=self.get_context(request),
|
||||
middleware=self.get_middleware(request),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def can_display_graphiql(cls, request, data):
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
|
@ -43,3 +43,7 @@ include_trailing_comma=True
|
|||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
|
||||
[tool:pytest]
|
||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
||||
addopts = --random-order
|
||||
|
|
10
setup.py
10
setup.py
|
@ -1,7 +1,8 @@
|
|||
from setuptools import find_packages, setup
|
||||
import ast
|
||||
import re
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
_version_re = re.compile(r"__version__\s+=\s+(.*)")
|
||||
|
||||
with open("graphene_django/__init__.py", "rb") as f:
|
||||
|
@ -15,6 +16,7 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
|
|||
tests_require = [
|
||||
"pytest>=3.6.3",
|
||||
"pytest-cov",
|
||||
"pytest-random-order",
|
||||
"coveralls",
|
||||
"mock",
|
||||
"pytz",
|
||||
|
@ -52,9 +54,9 @@ setup(
|
|||
keywords="api graphql protocol rest relay graphene",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=[
|
||||
"graphene>=2.1.7,<3",
|
||||
"graphql-core>=2.1.0,<3",
|
||||
"Django>=1.11,!=2.0.*,!=2.1.*",
|
||||
"graphene>=3.0.0b1,<4",
|
||||
"graphql-core>=3.1.0,<4",
|
||||
"Django>=2.2",
|
||||
"promise>=2.1",
|
||||
],
|
||||
setup_requires=["pytest-runner"],
|
||||
|
|
14
tox.ini
14
tox.ini
|
@ -1,11 +1,16 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py{36,37,38}-django{111,22,30,master},
|
||||
py{36,37,38}-django{22,30,master},
|
||||
black,flake8
|
||||
|
||||
[travis:env]
|
||||
[gh-actions]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
|
||||
[gh-actions:env]
|
||||
DJANGO =
|
||||
1.11: django111
|
||||
2.2: django22
|
||||
3.0: django30
|
||||
master: djangomaster
|
||||
|
@ -18,7 +23,6 @@ setenv =
|
|||
deps =
|
||||
-e.[test]
|
||||
psycopg2-binary
|
||||
django111: Django>=1.11,<2.0
|
||||
django22: Django>=2.2,<3.0
|
||||
django30: Django>=3.0a1,<3.1
|
||||
djangomaster: https://github.com/django/django/archive/master.zip
|
||||
|
@ -34,4 +38,4 @@ commands =
|
|||
basepython = python3.8
|
||||
deps = -e.[dev]
|
||||
commands =
|
||||
flake8 graphene_django examples
|
||||
flake8 graphene_django examples setup.py
|
||||
|
|
Loading…
Reference in New Issue
Block a user