Merge branch 'main' into fix-m2m-filter-type

This commit is contained in:
Firas K 2023-01-09 12:00:06 +03:00 committed by GitHub
commit e1b111d3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1664 additions and 480 deletions

View File

@ -16,7 +16,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox pip install tox
- name: Run lint 💅 - name: Run pre-commit 💅
run: tox run: tox
env: env:
TOXENV: flake8 TOXENV: pre-commit

View File

@ -8,9 +8,11 @@ jobs:
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
django: ["2.2", "3.0", "3.1", "3.2"] django: ["3.2", "4.0", "4.1"]
python-version: ["3.6", "3.7", "3.8", "3.9"] python-version: ["3.8", "3.9", "3.10"]
include:
- django: "3.2"
python-version: "3.7"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}

30
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,30 @@
default_language_version:
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ^docs/.*$
- id: pretty-format-json
args:
- --autofix
- id: trailing-whitespace
exclude: README.md
- repo: https://github.com/asottile/pyupgrade
rev: v3.2.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8

View File

@ -1,22 +1,21 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: dev-setup ## Install development dependencies .PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
.PHONY: install-dev .PHONY: tests ## Run unit tests
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests: tests:
py.test graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
.PHONY: test .PHONY: format ## Format code
test: tests # Alias test -> tests
.PHONY: format
format: format:
black --exclude "/migrations/" graphene_django examples setup.py black graphene_django examples setup.py
.PHONY: lint .PHONY: lint ## Lint code
lint: lint:
flake8 graphene_django examples flake8 graphene_django examples

View File

@ -55,7 +55,7 @@ from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
# ... # ...
path('graphql', GraphQLView.as_view(graphiql=True)), path('graphql/', GraphQLView.as_view(graphiql=True)),
] ]
``` ```

View File

@ -198,7 +198,7 @@ For Django 2.2 and above:
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

View File

@ -2,8 +2,8 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results. `django-filter <https://django-filter.readthedocs.io/en/main/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__ See the `usage documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. This filtering is automatically available when implementing a ``relay.Node``.
@ -26,7 +26,7 @@ After installing ``django-filter`` you'll need to add the application in the ``s
] ]
Note: The techniques below are demoed in the `cookbook example Note: The techniques below are demoed in the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__. app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.
Filterable fields Filterable fields
----------------- -----------------
@ -34,7 +34,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering ``django-filter``, so see the `filtering
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__ documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__
for full details on the range of options available. for full details on the range of options available.
For example: For example:
@ -192,7 +192,7 @@ in unison with the ``filter_fields`` parameter:
all_animals = DjangoFilterConnectionField(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>`__ The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/main/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your 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 filters to be context-dependent. We could modify the ``AnimalFilter`` above to
pre-filter animals owned by the authenticated user (set in ``context.user``). pre-filter animals owned by the authenticated user (set in ``context.user``).

View File

@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``:
Results in the following GraphQL schema definition: Results in the following GraphQL schema definition:
.. code:: .. code:: graphql
type Pet { type Pet {
id: ID! id: ID!
@ -178,7 +178,7 @@ You can disable this automatic conversion by setting
fields = ("id", "kind",) fields = ("id", "kind",)
convert_choices_to_enum = False convert_choices_to_enum = False
.. code:: .. code:: graphql
type Pet { type Pet {
id: ID! id: ID!
@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
bar=graphene.Int() bar=graphene.Int()
) )
def resolve_question(root, info, foo, bar): def resolve_question(root, info, foo=None, bar=None):
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None. # If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
return Question.objects.filter(foo=foo, bar=bar).first() return Question.objects.filter(foo=foo, bar=bar).first()
@ -336,12 +336,12 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
questions = graphene.List(QuestionType) questions = graphene.List(QuestionType)
def resolve_questions(root, info): def resolve_questions(root, info):
# See if a user is authenticated # See if a user is authenticated
if info.context.user.is_authenticated(): if info.context.user.is_authenticated():
return Question.objects.all() return Question.objects.all()
else: else:
return Question.objects.none() return Question.objects.none()
DjangoObjectTypes DjangoObjectTypes
@ -418,29 +418,29 @@ the core graphene pages for more information on customizing the Relay experience
You can now execute queries like: You can now execute queries like:
.. code:: python .. code:: graphql
{ {
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
pageInfo { pageInfo {
startCursor startCursor
endCursor endCursor
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
} }
edges { edges {
cursor cursor
node { node {
id id
question_text question_text
} }
} }
} }
} }
Which returns: Which returns:
.. code:: python .. code:: json
{ {
"data": { "data": {

View File

@ -1,60 +1,57 @@
import graphene import graphene
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = '__all__' fields = "__all__"
class IngredientType(DjangoObjectType): class IngredientType(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = '__all__' fields = "__all__"
class Query(object): class Query:
category = graphene.Field(CategoryType, category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
id=graphene.Int(), all_categories = graphene.List(CategoryType)
name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
ingredient = graphene.Field(IngredientType, def resolve_all_categories(self, info, **kwargs):
id=graphene.Int(), return Category.objects.all()
name=graphene.String())
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs): def resolve_all_ingredients(self, info, **kwargs):
return Category.objects.all() return Ingredient.objects.all()
def resolve_all_ingredients(self, info, **kwargs): def resolve_category(self, info, **kwargs):
return Ingredient.objects.all() id = kwargs.get("id")
name = kwargs.get("name")
def resolve_category(self, info, **kwargs): if id is not None:
id = kwargs.get('id') return Category.objects.get(pk=id)
name = kwargs.get('name')
if id is not None: if name is not None:
return Category.objects.get(pk=id) return Category.objects.get(name=name)
if name is not None: return None
return Category.objects.get(name=name)
return None def resolve_ingredient(self, info, **kwargs):
id = kwargs.get("id")
name = kwargs.get("name")
def resolve_ingredient(self, info, **kwargs): if id is not None:
id = kwargs.get('id') return Ingredient.objects.get(pk=id)
name = kwargs.get('name')
if id is not None: if name is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(name=name)
if name is not None: return None
return Ingredient.objects.get(name=name)
return None

View File

@ -189,7 +189,7 @@ Default: ``None``
``GRAPHIQL_HEADER_EDITOR_ENABLED`` ``GRAPHIQL_HEADER_EDITOR_ENABLED``
--------------------- ----------------------------------
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables. GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
@ -207,3 +207,36 @@ Default: ``True``
GRAPHENE = { GRAPHENE = {
'GRAPHIQL_HEADER_EDITOR_ENABLED': True, 'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
} }
``TESTING_ENDPOINT``
--------------------
Define the graphql endpoint url used for the `GraphQLTestCase` class.
Default: ``/graphql``
.. code:: python
GRAPHENE = {
'TESTING_ENDPOINT': '/customEndpoint'
}
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
---------------------
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
}

View File

@ -6,7 +6,8 @@ Using unittest
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`. The default endpoint for testing is `/graphql`. You can override this in the `settings <https://docs.graphene-python.org/projects/django/en/latest/settings/#testing-endpoint>`__.
Usage: Usage:
@ -27,7 +28,7 @@ Usage:
} }
} }
''', ''',
op_name='myModel' operation_name='myModel'
) )
content = json.loads(response.content) content = json.loads(response.content)
@ -48,7 +49,7 @@ Usage:
} }
} }
''', ''',
op_name='myModel', operation_name='myModel',
variables={'id': 1} variables={'id': 1}
) )
@ -72,7 +73,7 @@ Usage:
} }
} }
''', ''',
op_name='myMutation', operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'} input_data={'my_field': 'foo', 'other_field': 'bar'}
) )
@ -107,7 +108,7 @@ Usage:
} }
} }
''', ''',
op_name='myMutation', operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'} input_data={'my_field': 'foo', 'other_field': 'bar'}
) )
@ -147,7 +148,7 @@ To use pytest define a simple fixture using the query helper below
} }
} }
''', ''',
op_name='myModel' operation_name='myModel'
) )
content = json.loads(response.content) content = json.loads(response.content)

View File

@ -35,6 +35,7 @@ Now sync your database for the first time:
.. code:: bash .. code:: bash
cd ..
python manage.py migrate python manage.py migrate
Let's create a few simple models... Let's create a few simple models...
@ -77,6 +78,18 @@ Add ingredients as INSTALLED_APPS:
"cookbook.ingredients", "cookbook.ingredients",
] ]
Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``.
.. code:: python
# cookbook/ingredients/apps.py
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cookbook.ingredients'
Don't forget to create & run migrations: Don't forget to create & run migrations:

View File

@ -151,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, ) interfaces = (relay.Node, )
class Query(graphene.ObjectType): class Query(ObjectType):
category = relay.Node.Field(CategoryNode) category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -14,7 +14,7 @@ whole Graphene repository:
```bash ```bash
# Get the example project code # Get the example project code
git clone https://github.com/graphql-python/graphene-django.git git clone https://github.com/graphql-python/graphene-django.git
cd graphene-django/examples/cookbook cd graphene-django/examples/cookbook-plain
``` ```
It is good idea (but not required) to create a virtual environment It is good idea (but not required) to create a virtual environment

View File

@ -1 +1,52 @@
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] [
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -10,24 +8,46 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Ingredient', name="Ingredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
('notes', models.TextField()), models.AutoField(
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
], ],
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -8,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='ingredient', model_name="ingredient",
name='notes', name="notes",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ingredients', '0002_auto_20161104_0050'), ("ingredients", "0002_auto_20161104_0050"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='category', name="category",
options={'verbose_name_plural': 'Categories'}, options={"verbose_name_plural": "Categories"},
), ),
] ]

View File

@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType):
fields = "__all__" fields = "__all__"
class Query(object): class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType) all_categories = graphene.List(CategoryType)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -11,26 +9,62 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recipe', name="Recipe",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=100)), "id",
('instructions', models.TextField()), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='RecipeIngredient', name="RecipeIngredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('amount', models.FloatField()), "id",
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), models.AutoField(
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), auto_created=True,
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
], ],
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -8,18 +6,26 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0001_initial'), ("recipes", "0001_initial"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='recipeingredient', model_name="recipeingredient",
old_name='recipes', old_name="recipes",
new_name='recipe', new_name="recipe",
), ),
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', name="unit",
field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), field=models.CharField(
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -6,13 +6,21 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0002_auto_20161104_0106'), ("recipes", "0002_auto_20161104_0106"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', name="unit",
field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), field=models.CharField(
choices=[
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType):
fields = "__all__" fields = "__all__"
class Query(object): class Query:
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType) all_recipes = graphene.List(RecipeType)

View File

@ -1,4 +1,4 @@
Cookbook Example Django Project Cookbook Example (Relay) Django Project
=============================== ===============================
This example project demos integration between Graphene and Django. This example project demos integration between Graphene and Django.
@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
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-relay/#testing-our-graphql-schema)
for some example queries) for some example queries)

View File

@ -1 +1,52 @@
[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] [
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -10,24 +8,46 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Ingredient', name="Ingredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=100)), "id",
('notes', models.TextField()), models.AutoField(
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
], ],
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -8,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='ingredient', model_name="ingredient",
name='notes', name="notes",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType):
} }
class Query(object): class Query:
category = Node.Field(CategoryNode) category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -11,26 +9,62 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('ingredients', '0001_initial'), ("ingredients", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Recipe', name="Recipe",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=100)), "id",
('instructions', models.TextField()), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='RecipeIngredient', name="RecipeIngredient",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('amount', models.FloatField()), "id",
('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), models.AutoField(
('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), auto_created=True,
('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
], ],
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -8,18 +6,26 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('recipes', '0001_initial'), ("recipes", "0001_initial"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='recipeingredient', model_name="recipeingredient",
old_name='recipes', old_name="recipes",
new_name='recipe', new_name="recipe",
), ),
migrations.AlterField( migrations.AlterField(
model_name='recipeingredient', model_name="recipeingredient",
name='unit', name="unit",
field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), field=models.CharField(
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -25,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType):
} }
class Query(object): class Query:
recipe = Node.Field(RecipeNode) recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

@ -1 +1,302 @@
[{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "last_login": "2016-11-04T00:46:58Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "asdf@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-11-03T18:24:40Z", "groups": [], "user_permissions": []}}, {"model": "recipes.recipe", "pk": 1, "fields": {"title": "Cheerios With a Shot of Vermouth", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 2, "fields": {"title": "Quail Eggs in Whipped Cream and MSG", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 3, "fields": {"title": "Deep Fried Skittles", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 4, "fields": {"title": "Newt ala Doritos", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 5, "fields": {"title": "Fruit Salad", "instructions": "Chop up and add together"}}, {"model": "recipes.recipeingredient", "pk": 1, "fields": {"recipes": 5, "ingredient": 9, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 2, "fields": {"recipes": 5, "ingredient": 10, "amount": 2.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 3, "fields": {"recipes": 5, "ingredient": 7, "amount": 3.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 4, "fields": {"recipes": 5, "ingredient": 8, "amount": 4.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 5, "fields": {"recipes": 4, "ingredient": 5, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 6, "fields": {"recipes": 4, "ingredient": 6, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 7, "fields": {"recipes": 3, "ingredient": 4, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 8, "fields": {"recipes": 2, "ingredient": 2, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 9, "fields": {"recipes": 2, "ingredient": 11, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 10, "fields": {"recipes": 2, "ingredient": 12, "amount": 3.0, "unit": "st"}}, {"model": "recipes.recipeingredient", "pk": 11, "fields": {"recipes": 1, "ingredient": 1, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 12, "fields": {"recipes": 1, "ingredient": 3, "amount": 1.0, "unit": "st"}}, {"model": "ingredients.category", "pk": 1, "fields": {"name": "fruit"}}, {"model": "ingredients.category", "pk": 3, "fields": {"name": "xkcd"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Cheerios", "notes": "this is a note", "category": 3}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Quail Eggs", "notes": "has more notes", "category": 3}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Vermouth", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Skittles", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 5, "fields": {"name": "Newt", "notes": "Braised and Confuesd", "category": 3}}, {"model": "ingredients.ingredient", "pk": 6, "fields": {"name": "Doritos", "notes": "Crushed", "category": 3}}, {"model": "ingredients.ingredient", "pk": 7, "fields": {"name": "Apple", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 8, "fields": {"name": "Orange", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 9, "fields": {"name": "Banana", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 10, "fields": {"name": "Grapes", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 11, "fields": {"name": "Whipped Cream", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 12, "fields": {"name": "MSG", "notes": "", "category": 3}}] [
{
"fields": {
"date_joined": "2016-11-03T18:24:40Z",
"email": "asdf@example.com",
"first_name": "",
"groups": [],
"is_active": true,
"is_staff": true,
"is_superuser": true,
"last_login": "2016-11-04T00:46:58Z",
"last_name": "",
"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=",
"user_permissions": [],
"username": "admin"
},
"model": "auth.user",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Cheerios With a Shot of Vermouth"
},
"model": "recipes.recipe",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Quail Eggs in Whipped Cream and MSG"
},
"model": "recipes.recipe",
"pk": 2
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Deep Fried Skittles"
},
"model": "recipes.recipe",
"pk": 3
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Newt ala Doritos"
},
"model": "recipes.recipe",
"pk": 4
},
{
"fields": {
"instructions": "Chop up and add together",
"title": "Fruit Salad"
},
"model": "recipes.recipe",
"pk": 5
},
{
"fields": {
"amount": 1.0,
"ingredient": 9,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 1
},
{
"fields": {
"amount": 2.0,
"ingredient": 10,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 2
},
{
"fields": {
"amount": 3.0,
"ingredient": 7,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 3
},
{
"fields": {
"amount": 4.0,
"ingredient": 8,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 4
},
{
"fields": {
"amount": 1.0,
"ingredient": 5,
"recipes": 4,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 5
},
{
"fields": {
"amount": 2.0,
"ingredient": 6,
"recipes": 4,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 6
},
{
"fields": {
"amount": 1.0,
"ingredient": 4,
"recipes": 3,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 7
},
{
"fields": {
"amount": 1.0,
"ingredient": 2,
"recipes": 2,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 8
},
{
"fields": {
"amount": 2.0,
"ingredient": 11,
"recipes": 2,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 9
},
{
"fields": {
"amount": 3.0,
"ingredient": 12,
"recipes": 2,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 10
},
{
"fields": {
"amount": 1.0,
"ingredient": 1,
"recipes": 1,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 11
},
{
"fields": {
"amount": 1.0,
"ingredient": 3,
"recipes": 1,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 12
},
{
"fields": {
"name": "fruit"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "xkcd"
},
"model": "ingredients.category",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Cheerios",
"notes": "this is a note"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 3,
"name": "Quail Eggs",
"notes": "has more notes"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 3,
"name": "Vermouth",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Skittles",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 4
},
{
"fields": {
"category": 3,
"name": "Newt",
"notes": "Braised and Confuesd"
},
"model": "ingredients.ingredient",
"pk": 5
},
{
"fields": {
"category": 3,
"name": "Doritos",
"notes": "Crushed"
},
"model": "ingredients.ingredient",
"pk": 6
},
{
"fields": {
"category": 1,
"name": "Apple",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 7
},
{
"fields": {
"category": 1,
"name": "Orange",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 8
},
{
"fields": {
"category": 1,
"name": "Banana",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 9
},
{
"fields": {
"category": 1,
"name": "Grapes",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 10
},
{
"fields": {
"category": 3,
"name": "Whipped Cream",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 11
},
{
"fields": {
"category": 3,
"name": "MSG",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 12
}
]

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django.db import models from django.db import models

View File

@ -1,7 +1,7 @@
from .fields import DjangoConnectionField, DjangoListField from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType from .types import DjangoObjectType
__version__ = "3.0.0b7" __version__ = "3.0.0"
__all__ = [ __all__ = [
"__version__", "__version__",

View File

@ -1,4 +1,4 @@
class MissingType(object): class MissingType:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pass pass

View File

@ -24,8 +24,15 @@ from graphene import (
Decimal, Decimal,
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
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, assert_valid_name from graphql import GraphQLError
try:
from graphql import assert_name
except ImportError:
# Support for older versions of graphql
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, JSONField, PGJSONField, RangeField
@ -55,7 +62,7 @@ class BlankValueField(Field):
def convert_choice_name(name): def convert_choice_name(name):
name = to_const(force_str(name)) name = to_const(force_str(name))
try: try:
assert_valid_name(name) assert_name(name)
except GraphQLError: except GraphQLError:
name = "A_%s" % name name = "A_%s" % name
return name return name
@ -67,8 +74,7 @@ def get_choices(choices):
choices = choices.items() choices = choices.items()
for value, help_text in choices: for value, help_text in choices:
if isinstance(help_text, (tuple, list)): if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text): yield from get_choices(help_text)
yield choice
else: else:
name = convert_choice_name(value) name = convert_choice_name(value)
while name in converted_names: while name in converted_names:
@ -85,7 +91,7 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
named_choices = [(c[0], c[1]) for c in choices] named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices} named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object): class EnumWithDescriptionsType:
@property @property
def description(self): def description(self):
return str(named_choices_descriptions[self.name]) return str(named_choices_descriptions[self.name])
@ -102,7 +108,7 @@ def generate_enum_name(django_model_meta, field):
) )
name = custom_func(field) name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True: elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True:
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name)) name = to_camel_case(f"{django_model_meta.object_name}_{field.name}")
else: else:
name = "{app_label}{object_name}{field_name}Choices".format( name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()), app_label=to_camel_case(django_model_meta.app_label.title()),
@ -148,7 +154,9 @@ 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 %s (%s)" % (field, field.__class__) "Don't know how to convert the Django field {} ({})".format(
field, field.__class__
)
) )
@ -186,10 +194,14 @@ def convert_field_to_uuid(field, registry=None):
) )
@convert_django_field.register(models.BigIntegerField)
def convert_big_int_field(field, registry=None):
return BigInt(description=field.help_text, required=not field.null)
@convert_django_field.register(models.PositiveIntegerField) @convert_django_field.register(models.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField) @convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None): def convert_field_to_int(field, registry=None):
return Int(description=get_django_field_description(field), required=not field.null) return Int(description=get_django_field_description(field), required=not field.null)
@ -205,7 +217,9 @@ def convert_field_to_boolean(field, registry=None):
@convert_django_field.register(models.DecimalField) @convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None): def convert_field_to_decimal(field, registry=None):
return Decimal(description=field.help_text, required=not field.null) return Decimal(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.FloatField) @convert_django_field.register(models.FloatField)
@ -301,7 +315,26 @@ 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 go through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
def custom_resolver(root, info, **args):
fk_obj = resolver(root, info, **args)
if not isinstance(fk_obj, model):
# In case the resolver is a custom one that overwrites
# the default Django resolver
# This happens, for example, when using custom awaitable resolvers.
return fk_obj
return _type.get_node(info, fk_obj.pk)
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,

View File

@ -11,7 +11,7 @@ def wrap_exception(exception):
exc_type=force_str(type(exception)), exc_type=force_str(type(exception)),
stack="".join( stack="".join(
traceback.format_exception( traceback.format_exception(
etype=type(exception), value=exception, tb=exception.__traceback__ exception, value=exception, tb=exception.__traceback__
) )
), ),
) )

View File

@ -7,7 +7,7 @@ from .exception.formating import wrap_exception
from .types import DjangoDebug from .types import DjangoDebug
class DjangoDebugContext(object): class DjangoDebugContext:
def __init__(self): def __init__(self):
self.debug_promise = None self.debug_promise = None
self.promises = [] self.promises = []
@ -46,7 +46,7 @@ class DjangoDebugContext(object):
unwrap_cursor(connection) unwrap_cursor(connection)
class DjangoDebugMiddleware(object): class DjangoDebugMiddleware:
def resolve(self, next, root, info, **args): def resolve(self, next, root, info, **args):
context = info.context context = info.context
django_debug = getattr(context, "django_debug", None) django_debug = getattr(context, "django_debug", None)

View File

@ -1,5 +1,4 @@
# Code obtained from django-debug-toolbar sql panel tracking # Code obtained from django-debug-toolbar sql panel tracking
from __future__ import absolute_import, unicode_literals
import json import json
from threading import local from threading import local
@ -50,7 +49,7 @@ def unwrap_cursor(connection):
del connection._graphene_cursor del connection._graphene_cursor
class ExceptionCursorWrapper(object): class ExceptionCursorWrapper:
""" """
Wraps a cursor and raises an exception on any operation. Wraps a cursor and raises an exception on any operation.
Used in Templates panel. Used in Templates panel.
@ -63,7 +62,7 @@ class ExceptionCursorWrapper(object):
raise SQLQueryTriggered() raise SQLQueryTriggered()
class NormalCursorWrapper(object): class NormalCursorWrapper:
""" """
Wraps a cursor and logs queries. Wraps a cursor and logs queries.
""" """
@ -85,7 +84,7 @@ class NormalCursorWrapper(object):
if not params: if not params:
return params return params
if isinstance(params, dict): if isinstance(params, dict):
return dict((key, self._quote_expr(value)) for key, value in params.items()) return {key: self._quote_expr(value) for key, value in params.items()}
return list(map(self._quote_expr, params)) return list(map(self._quote_expr, params))
def _decode(self, param): def _decode(self, param):

View File

@ -8,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug from ..types import DjangoDebug
class context(object): class context:
pass pass

View File

@ -1,12 +1,14 @@
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.connection.arrayconnection import (
from graphql_relay import (
connection_from_array_slice, connection_from_array_slice,
cursor_to_offset, cursor_to_offset,
get_offset_with_default, get_offset_with_default,
offset_to_cursor, offset_to_cursor,
) )
from promise import Promise from promise import Promise
from graphene import Int, NonNull from graphene import Int, NonNull
@ -26,7 +28,7 @@ class DjangoListField(Field):
_type = _type.of_type _type = _type.of_type
# Django would never return a Set of None vvvvvvv # Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs) super().__init__(List(NonNull(_type)), *args, **kwargs)
assert issubclass( assert issubclass(
self._underlying_type, DjangoObjectType self._underlying_type, DjangoObjectType
@ -61,13 +63,16 @@ class DjangoListField(Field):
return queryset return queryset
def wrap_resolve(self, parent_resolver): def wrap_resolve(self, parent_resolver):
resolver = super(DjangoListField, self).wrap_resolve(parent_resolver) resolver = super().wrap_resolve(parent_resolver)
_type = self.type _type = self.type
if isinstance(_type, NonNull): if isinstance(_type, NonNull):
_type = _type.of_type _type = _type.of_type
django_object_type = _type.of_type.of_type django_object_type = _type.of_type.of_type
return partial( return partial(
self.list_resolver, django_object_type, resolver, self.get_manager(), self.list_resolver,
django_object_type,
resolver,
self.get_manager(),
) )
@ -82,7 +87,7 @@ class DjangoConnectionField(ConnectionField):
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
) )
kwargs.setdefault("offset", Int()) kwargs.setdefault("offset", Int())
super(DjangoConnectionField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property @property
def type(self): def type(self):
@ -144,36 +149,40 @@ class DjangoConnectionField(ConnectionField):
iterable = maybe_queryset(iterable) iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet): if isinstance(iterable, QuerySet):
list_length = iterable.count() array_length = iterable.count()
else: else:
list_length = len(iterable) array_length = len(iterable)
list_slice_length = (
min(max_limit, list_length) if max_limit is not None else list_length
)
# If after is higher than list_length, connection_from_list_slice # If after is higher than array_length, connection_from_array_slice
# would try to do a negative slicing which makes django throw an # would try to do a negative slicing which makes django throw an
# AssertionError # AssertionError
after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length) slice_start = min(
get_offset_with_default(args.get("after"), -1) + 1,
array_length,
)
array_slice_length = array_length - slice_start
if max_limit is not None and args.get("first", None) is None: # Impose the maximum limit via the `first` field if neither first or last are already provided
if args.get("last", None) is not None: # (note that if any of them is provided they must be under max_limit otherwise an error is raised).
after = list_length - args["last"] if (
else: max_limit is not None
args["first"] = max_limit and args.get("first", None) is None
and args.get("last", None) is None
):
args["first"] = max_limit
connection = connection_from_array_slice( connection = connection_from_array_slice(
iterable[after:], iterable[slice_start:],
args, args,
slice_start=after, slice_start=slice_start,
array_length=list_length, array_length=array_length,
array_slice_length=list_slice_length, array_slice_length=array_slice_length,
connection_type=partial(connection_adapter, connection), connection_type=partial(connection_adapter, connection),
edge_type=connection.Edge, edge_type=connection.Edge,
page_info_type=page_info_adapter, page_info_type=page_info_adapter,
) )
connection.iterable = iterable connection.iterable = iterable
connection.length = list_length connection.length = array_length
return connection return connection
@classmethod @classmethod

View File

@ -30,7 +30,7 @@ def convert_enum(data):
class DjangoFilterConnectionField(DjangoConnectionField): class DjangoFilterConnectionField(DjangoConnectionField):
def __init__( def __init__(
self, self,
type, type_,
fields=None, fields=None,
order_by=None, order_by=None,
extra_filter_meta=None, extra_filter_meta=None,
@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
self._filtering_args = None self._filtering_args = None
self._extra_filter_meta = extra_filter_meta self._extra_filter_meta = extra_filter_meta
self._base_args = None self._base_args = None
super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) super().__init__(type_, *args, **kwargs)
@property @property
def args(self): def args(self):
@ -90,9 +90,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
kwargs[k] = convert_enum(v) kwargs[k] = convert_enum(v)
return kwargs return kwargs
qs = super(DjangoFilterConnectionField, cls).resolve_queryset( qs = super().resolve_queryset(connection, iterable, info, args)
connection, iterable, info, args
)
filterset = filterset_class( filterset = filterset_class(
data=filter_kwargs(), queryset=qs, request=info.context data=filter_kwargs(), queryset=qs, request=info.context

View File

@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter):
return qs return qs
if self.distinct: if self.distinct:
qs = qs.distinct() qs = qs.distinct()
lookup = "%s__%s" % (self.field_name, self.lookup_expr) lookup = f"{self.field_name}__{self.lookup_expr}"
qs = self.get_method(qs)(**{lookup: value}) qs = self.get_method(qs)(**{lookup: value})
return qs return qs

View File

@ -13,11 +13,11 @@ class GlobalIDFilter(Filter):
field_class = GlobalIDFormField field_class = GlobalIDFormField
def filter(self, qs, value): def filter(self, qs, value):
""" Convert the filter value to a primary key before filtering """ """Convert the filter value to a primary key before filtering"""
_id = None _id = None
if value is not None: if value is not None:
_, _id = from_global_id(value) _, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id) return super().filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
@ -25,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
def filter(self, qs, value): def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value] gids = [from_global_id(v)[1] for v in value]
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) return super().filter(qs, gids)

View File

@ -23,4 +23,4 @@ class ListFilter(TypedFilter):
else: else:
return qs.none() return qs.none()
else: else:
return super(ListFilter, self).filter(qs, value) return super().filter(qs, value)

View File

@ -12,7 +12,7 @@ class TypedFilter(Filter):
def __init__(self, input_type=None, *args, **kwargs): def __init__(self, input_type=None, *args, **kwargs):
self._input_type = input_type self._input_type = input_type
super(TypedFilter, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property @property
def input_type(self): def input_type(self):

View File

@ -18,8 +18,8 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
class GrapheneFilterSetMixin(BaseFilterSet): class GrapheneFilterSetMixin(BaseFilterSet):
""" A django_filters.filterset.BaseFilterSet with default filter overrides """A django_filters.filterset.BaseFilterSet with default filter overrides
to handle global IDs """ to handle global IDs"""
FILTER_DEFAULTS = dict( FILTER_DEFAULTS = dict(
itertools.chain( itertools.chain(
@ -29,20 +29,18 @@ class GrapheneFilterSetMixin(BaseFilterSet):
def setup_filterset(filterset_class): def setup_filterset(filterset_class):
""" Wrap a provided filterset in Graphene-specific functionality """Wrap a provided filterset in Graphene-specific functionality"""
"""
return type( return type(
"Graphene{}".format(filterset_class.__name__), f"Graphene{filterset_class.__name__}",
(filterset_class, GrapheneFilterSetMixin), (filterset_class, GrapheneFilterSetMixin),
{}, {},
) )
def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta):
""" Create a filterset for the given model using the provided meta data """Create a filterset for the given model using the provided meta data"""
"""
meta.update({"model": model}) meta.update({"model": model})
meta_class = type(str("Meta"), (object,), meta) meta_class = type("Meta", (object,), meta)
filterset = type( filterset = type(
str("%sFilterSet" % model._meta.object_name), str("%sFilterSet" % model._meta.object_name),
(filterset_base_class, GrapheneFilterSetMixin), (filterset_base_class, GrapheneFilterSetMixin),

View File

@ -1,4 +1,4 @@
from mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from django.db import models from django.db import models
@ -89,10 +89,10 @@ def Query(EventType):
def resolve_events(self, info, **kwargs): def resolve_events(self, info, **kwargs):
events = [ events = [
Event(name="Live Show", tags=["concert", "music", "rock"],), Event(name="Live Show", tags=["concert", "music", "rock"]),
Event(name="Musical", tags=["movie", "music"],), Event(name="Musical", tags=["movie", "music"]),
Event(name="Ballet", tags=["concert", "dance"],), Event(name="Ballet", tags=["concert", "dance"]),
Event(name="Speech", tags=[],), Event(name="Speech", tags=[]),
] ]
STORE["events"] = events STORE["events"] = events

View File

@ -120,10 +120,7 @@ def test_array_field_filter_schema_type(Query):
"randomField": "[Boolean!]", "randomField": "[Boolean!]",
} }
filters_str = ", ".join( filters_str = ", ".join(
[ [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
) )
assert ( assert (
f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str

View File

@ -54,13 +54,13 @@ def reporter_article_data():
first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2 first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2
) )
Article.objects.create( Article.objects.create(
headline="Article Node 1", reporter=john, editor=john, lang="es", headline="Article Node 1", reporter=john, editor=john, lang="es"
) )
Article.objects.create( Article.objects.create(
headline="Article Node 2", reporter=john, editor=john, lang="en", headline="Article Node 2", reporter=john, editor=john, lang="en"
) )
Article.objects.create( Article.objects.create(
headline="Article Node 3", reporter=jane, editor=jane, lang="en", headline="Article Node 3", reporter=jane, editor=jane, lang="en"
) )
@ -80,7 +80,13 @@ def test_filter_enum_on_connection(schema, reporter_article_data):
} }
""" """
expected = {"allArticles": {"edges": [{"node": {"headline": "Article Node 1"}},]}} expected = {
"allArticles": {
"edges": [
{"node": {"headline": "Article Node 1"}},
]
}
}
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
@ -152,9 +158,6 @@ def test_filter_enum_field_schema_type(schema):
"reporter_AChoice_In": "[TestsReporterAChoiceChoices]", "reporter_AChoice_In": "[TestsReporterAChoiceChoices]",
} }
filters_str = ", ".join( filters_str = ", ".join(
[ [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
f"{filter_field}: {gql_type} = null"
for filter_field, gql_type in filters.items()
]
) )
assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str

View File

@ -5,7 +5,7 @@ import pytest
from django.db.models import TextField, Value from django.db.models import TextField, Value
from django.db.models.functions import Concat from django.db.models.functions import Concat
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String from graphene import Argument, Boolean, Decimal, Field, ObjectType, Schema, String
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -67,7 +67,7 @@ def assert_arguments(field, *arguments):
actual = [name for name in args if name not in ignore and not name.startswith("_")] actual = [name for name in args if name not in ignore and not name.startswith("_")]
assert set(arguments) == set( assert set(arguments) == set(
actual actual
), "Expected arguments ({}) did not match actual ({})".format(arguments, actual) ), f"Expected arguments ({arguments}) did not match actual ({actual})"
def assert_orderable(field): def assert_orderable(field):
@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context():
@property @property
def qs(self): def qs(self):
qs = super(ArticleContextFilter, self).qs qs = super().qs
return qs.filter(reporter=self.request.reporter) return qs.filter(reporter=self.request.reporter)
class Query(ObjectType): class Query(ObjectType):
@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context():
editor=r2, editor=r2,
) )
class context(object): class context:
reporter = r2 reporter = r2
query = """ query = """
@ -401,7 +401,7 @@ def test_filterset_descriptions():
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
max_time = field.args["max_time"] max_time = field.args["max_time"]
assert isinstance(max_time, Argument) assert isinstance(max_time, Argument)
assert max_time.type == Float assert max_time.type == Decimal
assert max_time.description == "The maximum time" assert max_time.description == "The maximum time"
@ -1008,7 +1008,7 @@ def test_integer_field_filter_type():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
type Query { type Query {
pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
} }
type PetTypeConnection { type PetTypeConnection {
@ -1056,8 +1056,7 @@ def test_integer_field_filter_type():
interface Node { interface Node {
\"""The ID of the object\""" \"""The ID of the object\"""
id: ID! id: ID!
} }"""
"""
) )
@ -1077,7 +1076,7 @@ def test_other_filter_types():
assert str(schema) == dedent( assert str(schema) == dedent(
"""\ """\
type Query { type Query {
pets(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
} }
type PetTypeConnection { type PetTypeConnection {
@ -1125,8 +1124,7 @@ def test_other_filter_types():
interface Node { interface Node {
\"""The ID of the object\""" \"""The ID of the object\"""
id: ID! id: ID!
} }"""
"""
) )
@ -1226,7 +1224,7 @@ def test_filter_filterset_based_on_mixin():
} }
} }
result = schema.execute(query, variable_values={"email": reporter_1.email},) result = schema.execute(query, variable_values={"email": reporter_1.email})
assert not result.errors assert not result.errors
assert result.data == expected assert result.data == expected
@ -1267,13 +1265,23 @@ def test_filter_string_contains():
result = schema.execute(query, variables={"filter": "Ja"}) result = schema.execute(query, variables={"filter": "Ja"})
assert not result.errors assert not result.errors
assert result.data == { assert result.data == {
"people": {"edges": [{"node": {"name": "Jack"}}, {"node": {"name": "Jane"}},]} "people": {
"edges": [
{"node": {"name": "Jack"}},
{"node": {"name": "Jane"}},
]
}
} }
result = schema.execute(query, variables={"filter": "o"}) result = schema.execute(query, variables={"filter": "o"})
assert not result.errors assert not result.errors
assert result.data == { assert result.data == {
"people": {"edges": [{"node": {"name": "Joe"}}, {"node": {"name": "Bob"}},]} "people": {
"edges": [
{"node": {"name": "Joe"}},
{"node": {"name": "Bob"}},
]
}
} }

View File

@ -349,19 +349,19 @@ def test_fk_id_in_filter(query):
schema = Schema(query=query) schema = Schema(query=query)
query = """ query = """
query { query {{
articles (reporter_In: [%s, %s]) { articles (reporter_In: [{}, {}]) {{
edges { edges {{
node { node {{
headline headline
reporter { reporter {{
lastName lastName
} }}
} }}
} }}
} }}
} }}
""" % ( """.format(
john_doe.id, john_doe.id,
jean_bon.id, jean_bon.id,
) )

View File

@ -98,20 +98,14 @@ def test_typed_filter_schema(schema):
) )
for filter_field, gql_type in filters.items(): for filter_field, gql_type in filters.items():
assert "{}: {} = null".format(filter_field, gql_type) in all_articles_filters assert f"{filter_field}: {gql_type}" in all_articles_filters
def test_typed_filters_work(schema): def test_typed_filters_work(schema):
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="") reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
Article.objects.create( Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
headline="A", reporter=reporter, editor=reporter, lang="es", Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
) Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
Article.objects.create(
headline="B", reporter=reporter, editor=reporter, lang="es",
)
Article.objects.create(
headline="C", reporter=reporter, editor=reporter, lang="en",
)
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }" query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"

View File

@ -97,7 +97,9 @@ def get_filtering_args_from_filterset(filterset_class, type):
field_type = graphene.List(field_type) field_type = graphene.List(field_type)
args[name] = graphene.Argument( args[name] = graphene.Argument(
field_type, description=filter_field.label, required=required, field_type,
description=filter_field.label,
required=required,
) )
return args return args

View File

@ -3,7 +3,19 @@ from functools import singledispatch
from django import forms from django import forms
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time from graphene import (
ID,
Boolean,
Decimal,
Float,
Int,
List,
String,
UUID,
Date,
DateTime,
Time,
)
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -57,12 +69,18 @@ def convert_form_field_to_nullboolean(field):
return Boolean(description=get_form_field_description(field)) return Boolean(description=get_form_field_description(field))
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField) @convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field): def convert_form_field_to_float(field):
return Float(description=get_form_field_description(field), required=field.required) return Float(description=get_form_field_description(field), required=field.required)
@convert_form_field.register(forms.DecimalField)
def convert_form_field_to_decimal(field):
return Decimal(
description=get_form_field_description(field), required=field.required
)
@convert_form_field.register(forms.MultipleChoiceField) @convert_form_field.register(forms.MultipleChoiceField)
def convert_form_field_to_string_list(field): def convert_form_field_to_string_list(field):
return List( return List(

View File

@ -95,7 +95,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoFormMutation, cls).__init_subclass_with_meta__( super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )
@ -117,7 +117,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta: class Meta:
abstract = True abstract = True
errors = graphene.List(ErrorType) errors = graphene.List(graphene.NonNull(ErrorType), required=True)
@classmethod @classmethod
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
@ -127,7 +127,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
return_field_name=None, return_field_name=None,
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
**options **options,
): ):
if not form_class: if not form_class:
@ -147,7 +147,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
registry = get_global_registry() registry = get_global_registry()
model_type = registry.get_type_for_model(model) model_type = registry.get_type_for_model(model)
if not model_type: if not model_type:
raise Exception("No type registered for model: {}".format(model.__name__)) raise Exception(f"No type registered for model: {model.__name__}")
if not return_field_name: if not return_field_name:
model_name = model.__name__ model_name = model.__name__
@ -163,7 +163,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__( super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )

View File

@ -1,11 +1,12 @@
from django import forms from django import forms
from py.test import raises from pytest import raises
import graphene import graphene
from graphene import ( from graphene import (
String, String,
Int, Int,
Boolean, Boolean,
Decimal,
Float, Float,
ID, ID,
UUID, UUID,
@ -97,8 +98,8 @@ def test_should_float_convert_float():
assert_conversion(forms.FloatField, Float) assert_conversion(forms.FloatField, Float)
def test_should_decimal_convert_float(): def test_should_decimal_convert_decimal():
assert_conversion(forms.DecimalField, Float) assert_conversion(forms.DecimalField, Decimal)
def test_should_multiple_choice_convert_list(): def test_should_multiple_choice_convert_list():

View File

@ -1,7 +1,7 @@
import pytest import pytest
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from py.test import raises from pytest import raises
from graphene import Field, ObjectType, Schema, String from graphene import Field, ObjectType, Schema, String
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType

View File

@ -48,7 +48,7 @@ class CommandArguments(BaseCommand):
class Command(CommandArguments): class Command(CommandArguments):
help = "Dump Graphene schema as a JSON or GraphQL file" help = "Dump Graphene schema as a JSON or GraphQL file"
can_import_settings = True can_import_settings = True
requires_system_checks = False requires_system_checks = []
def save_json_file(self, out, schema_dict, indent): def save_json_file(self, out, schema_dict, indent):
with open(out, "w") as outfile: with open(out, "w") as outfile:
@ -73,16 +73,12 @@ class Command(CommandArguments):
elif file_extension == ".json": elif file_extension == ".json":
self.save_json_file(out, schema_dict, indent) self.save_json_file(out, schema_dict, indent)
else: else:
raise CommandError( raise CommandError(f'Unrecognised file format "{file_extension}"')
'Unrecognised file format "{}"'.format(file_extension)
)
style = getattr(self, "style", None) style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x) success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write( self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}"))
success("Successfully dumped GraphQL schema to {}".format(out))
)
def handle(self, *args, **options): def handle(self, *args, **options):
options_schema = options.get("schema") options_schema = options.get("schema")

View File

@ -1,4 +1,4 @@
class Registry(object): class Registry:
def __init__(self): def __init__(self):
self._registry = {} self._registry = {}
self._field_registry = {} self._field_registry = {}

View File

@ -114,7 +114,7 @@ class SerializerMutation(ClientIDMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super(SerializerMutation, cls).__init_subclass_with_meta__( super().__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )

View File

@ -72,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class):
for name, field in serializer.fields.items() for name, field in serializer.fields.items()
} }
ret_type = type( ret_type = type(
"{}Input".format(serializer.__class__.__name__), f"{serializer.__class__.__name__}Input",
(graphene.InputObjectType,), (graphene.InputObjectType,),
items, items,
) )
@ -110,11 +110,15 @@ def convert_serializer_field_to_bool(field):
@get_graphene_type_from_serializer_field.register(serializers.FloatField) @get_graphene_type_from_serializer_field.register(serializers.FloatField)
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_float(field): def convert_serializer_field_to_float(field):
return graphene.Float return graphene.Float
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_decimal(field):
return graphene.Decimal
@get_graphene_type_from_serializer_field.register(serializers.DateTimeField) @get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
def convert_serializer_field_to_datetime_time(field): def convert_serializer_field_to_datetime_time(field):
return graphene.types.datetime.DateTime return graphene.types.datetime.DateTime

View File

@ -3,7 +3,7 @@ import copy
import graphene import graphene
from django.db import models from django.db import models
from graphene import InputObjectType from graphene import InputObjectType
from py.test import raises from pytest import raises
from rest_framework import serializers from rest_framework import serializers
from ..serializer_converter import convert_serializer_field from ..serializer_converter import convert_serializer_field
@ -133,9 +133,9 @@ def test_should_float_convert_float():
assert_conversion(serializers.FloatField, graphene.Float) assert_conversion(serializers.FloatField, graphene.Float)
def test_should_decimal_convert_float(): def test_should_decimal_convert_decimal():
assert_conversion( assert_conversion(
serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2 serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2
) )

View File

@ -1,6 +1,6 @@
import datetime import datetime
from py.test import raises from pytest import raises
from rest_framework import serializers from rest_framework import serializers
from graphene import Field, ResolveInfo from graphene import Field, ResolveInfo

View File

@ -11,7 +11,6 @@ This module provides the `graphene_settings` object, that is used to access
Graphene settings, checking for user settings first, then falling Graphene settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed from django.test.signals import setting_changed
@ -41,7 +40,9 @@ DEFAULTS = {
# This sets headerEditorEnabled GraphiQL option, for details go to # This sets headerEditorEnabled GraphiQL option, for details go to
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
"GRAPHIQL_HEADER_EDITOR_ENABLED": True, "GRAPHIQL_HEADER_EDITOR_ENABLED": True,
"GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
"ATOMIC_MUTATIONS": False, "ATOMIC_MUTATIONS": False,
"TESTING_ENDPOINT": "/graphql",
} }
if settings.DEBUG: if settings.DEBUG:
@ -76,7 +77,7 @@ def import_from_string(val, setting_name):
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for Graphene setting '%s'. %s: %s." % ( msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format(
val, val,
setting_name, setting_name,
e.__class__.__name__, e.__class__.__name__,
@ -85,7 +86,7 @@ def import_from_string(val, setting_name):
raise ImportError(msg) raise ImportError(msg)
class GrapheneSettings(object): class GrapheneSettings:
""" """
A settings object, that allows API settings to be accessed as properties. A settings object, that allows API settings to be accessed as properties.
For example: For example:

View File

@ -10,14 +10,6 @@
history, history,
location, location,
) { ) {
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
// Collect the URL parameters // Collect the URL parameters
var parameters = {}; var parameters = {};
@ -68,9 +60,19 @@
var headers = opts.headers || {}; var headers = opts.headers || {};
headers['Accept'] = headers['Accept'] || 'application/json'; headers['Accept'] = headers['Accept'] || 'application/json';
headers['Content-Type'] = headers['Content-Type'] || 'application/json'; headers['Content-Type'] = headers['Content-Type'] || 'application/json';
// Parse the cookie value for a CSRF token
var csrftoken;
var cookies = ("; " + document.cookie).split("; csrftoken=");
if (cookies.length == 2) {
csrftoken = cookies.pop().split(";").shift();
} else {
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
}
if (csrftoken) { if (csrftoken) {
headers['X-CSRFToken'] = csrftoken headers['X-CSRFToken'] = csrftoken
} }
return fetch(fetchURL, { return fetch(fetchURL, {
method: "post", method: "post",
headers: headers, headers: headers,
@ -176,6 +178,7 @@
onEditVariables: onEditVariables, onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName, onEditOperationName: onEditOperationName,
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
query: parameters.query, query: parameters.query,
}; };
if (parameters.variables) { if (parameters.variables) {

View File

@ -46,6 +46,7 @@ add "&raw" to the end of the URL within a browser.
subscriptionPath: "{{subscription_path}}", subscriptionPath: "{{subscription_path}}",
{% endif %} {% endif %}
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }}, graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
}; };
</script> </script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script> <script src="{% static 'graphene_django/graphiql.js' %}"></script>

View File

@ -8,8 +8,8 @@ import graphene
from graphene import Field, ResolveInfo from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType from graphene.types.inputobjecttype import InputObjectType
from py.test import raises from pytest import raises
from py.test import mark from pytest import mark
from rest_framework import serializers from rest_framework import serializers
from ...types import DjangoObjectType from ...types import DjangoObjectType

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -13,6 +11,9 @@ class Person(models.Model):
class Pet(models.Model): class Pet(models.Model):
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
age = models.PositiveIntegerField() age = models.PositiveIntegerField()
owner = models.ForeignKey(
"Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets"
)
class FilmDetails(models.Model): class FilmDetails(models.Model):
@ -34,7 +35,7 @@ class Film(models.Model):
class DoeReporterManager(models.Manager): class DoeReporterManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe") return super().get_queryset().filter(last_name="Doe")
class Reporter(models.Model): class Reporter(models.Model):
@ -54,7 +55,7 @@ class Reporter(models.Model):
) )
def __str__(self): # __unicode__ on Python 2 def __str__(self): # __unicode__ on Python 2
return "%s %s" % (self.first_name, self.last_name) return f"{self.first_name} {self.last_name}"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -64,7 +65,7 @@ class Reporter(models.Model):
when a CNNReporter is pulled from the database, it is still when a CNNReporter is pulled from the database, it is still
of type Reporter. This was added to test proxy model support. of type Reporter. This was added to test proxy model support.
""" """
super(Reporter, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.reporter_type == 2: # quick and dirty way without enums if self.reporter_type == 2: # quick and dirty way without enums
self.__class__ = CNNReporter self.__class__ = CNNReporter
@ -74,7 +75,7 @@ class Reporter(models.Model):
class CNNReporterManager(models.Manager): class CNNReporterManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2) return super().get_queryset().filter(reporter_type=2)
class CNNReporter(Reporter): class CNNReporter(Reporter):

View File

@ -2,7 +2,7 @@ from textwrap import dedent
from django.core import management from django.core import management
from io import StringIO from io import StringIO
from mock import mock_open, patch from unittest.mock import mock_open, patch
from graphene import ObjectType, Schema, String from graphene import ObjectType, Schema, String
@ -53,6 +53,5 @@ def test_generate_graphql_file_on_call_graphql_schema():
"""\ """\
type Query { type Query {
hi: String hi: String
} }"""
"""
) )

View File

@ -3,13 +3,14 @@ from collections import namedtuple
import pytest import pytest
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from py.test import raises from pytest import raises
import graphene import graphene
from graphene import NonNull from graphene import NonNull
from graphene.relay import ConnectionField, Node from graphene.relay import ConnectionField, Node
from graphene.types.datetime import Date, DateTime, Time from graphene.types.datetime import Date, DateTime, Time
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.types.scalars import BigInt
from ..compat import ( from ..compat import (
ArrayField, ArrayField,
@ -140,8 +141,8 @@ def test_should_small_integer_convert_int():
assert_conversion(models.SmallIntegerField, graphene.Int) assert_conversion(models.SmallIntegerField, graphene.Int)
def test_should_big_integer_convert_int(): def test_should_big_integer_convert_big_int():
assert_conversion(models.BigIntegerField, graphene.Int) assert_conversion(models.BigIntegerField, BigInt)
def test_should_integer_convert_int(): def test_should_integer_convert_int():

View File

@ -1,5 +1,5 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from py.test import raises from pytest import raises
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField

View File

@ -0,0 +1,361 @@
import pytest
import graphene
from graphene.relay import Node
from graphql_relay import to_global_id
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from .models import Article, Reporter
class TestShouldCallGetQuerySetOnForeignKey:
"""
Check that the get_queryset method is called in both forward and reversed direction
of a foreignkey on types.
(see issue #1111)
"""
@pytest.fixture(autouse=True)
def setup_schema(self):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
@classmethod
def get_queryset(cls, queryset, info):
if info.context and info.context.get("admin"):
return queryset
raise Exception("Not authorized to access reporters.")
class ArticleType(DjangoObjectType):
class Meta:
model = Article
@classmethod
def get_queryset(cls, queryset, info):
return queryset.exclude(headline__startswith="Draft")
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType, id=graphene.ID(required=True))
article = graphene.Field(ArticleType, id=graphene.ID(required=True))
def resolve_reporter(self, info, id):
return (
ReporterType.get_queryset(Reporter.objects, info)
.filter(id=id)
.last()
)
def resolve_article(self, info, id):
return (
ArticleType.get_queryset(Article.objects, info).filter(id=id).last()
)
self.schema = graphene.Schema(query=Query)
self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe")
self.articles = [
Article.objects.create(
headline="A fantastic article",
reporter=self.reporter,
editor=self.reporter,
),
Article.objects.create(
headline="Draft: My next best seller",
reporter=self.reporter,
editor=self.reporter,
),
]
def test_get_queryset_called_on_field(self):
# If a user tries to access an article it is fine as long as it's not a draft one
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
}
}
"""
# Non-draft
result = self.schema.execute(query, variables={"id": self.articles[0].id})
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
}
# Draft
result = self.schema.execute(query, variables={"id": self.articles[1].id})
assert not result.errors
assert result.data["article"] is None
# If a non admin user tries to access a reporter they should get our authorization error
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(query, variables={"id": self.reporter.id})
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.reporter.id},
context_value={"admin": True},
)
assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(query, variables={"id": self.articles[0].id})
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.articles[0].id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
headline
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": self.reporter.id},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": [{"headline": "A fantastic article"}],
}
class TestShouldCallGetQuerySetOnForeignKeyNode:
"""
Check that the get_queryset method is called in both forward and reversed direction
of a foreignkey on types using a node interface.
(see issue #1111)
"""
@pytest.fixture(autouse=True)
def setup_schema(self):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
@classmethod
def get_queryset(cls, queryset, info):
if info.context and info.context.get("admin"):
return queryset
raise Exception("Not authorized to access reporters.")
class ArticleType(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node,)
@classmethod
def get_queryset(cls, queryset, info):
return queryset.exclude(headline__startswith="Draft")
class Query(graphene.ObjectType):
reporter = Node.Field(ReporterType)
article = Node.Field(ArticleType)
self.schema = graphene.Schema(query=Query)
self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe")
self.articles = [
Article.objects.create(
headline="A fantastic article",
reporter=self.reporter,
editor=self.reporter,
),
Article.objects.create(
headline="Draft: My next best seller",
reporter=self.reporter,
editor=self.reporter,
),
]
def test_get_queryset_called_on_node(self):
# If a user tries to access an article it is fine as long as it's not a draft one
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
}
}
"""
# Non-draft
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
}
# Draft
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[1].id)}
)
assert not result.errors
assert result.data["article"] is None
# If a non admin user tries to access a reporter they should get our authorization error
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query, variables={"id": to_global_id("ReporterType", self.reporter.id)}
)
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ReporterType", self.reporter.id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data == {"reporter": {"firstName": "Jane"}}
def test_get_queryset_called_on_foreignkey(self):
# If a user tries to access a reporter through an article they should get our authorization error
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}
)
assert len(result.errors) == 1
assert result.errors[0].message == "Not authorized to access reporters."
# An admin user should be able to get reporters through an article
query = """
query getArticle($id: ID!) {
article(id: $id) {
headline
reporter {
firstName
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ArticleType", self.articles[0].id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["article"] == {
"headline": "A fantastic article",
"reporter": {"firstName": "Jane"},
}
# An admin user should not be able to access draft article through a reporter
query = """
query getReporter($id: ID!) {
reporter(id: $id) {
firstName
articles {
edges {
node {
headline
}
}
}
}
}
"""
result = self.schema.execute(
query,
variables={"id": to_global_id("ReporterType", self.reporter.id)},
context_value={"admin": True},
)
assert not result.errors
assert result.data["reporter"] == {
"firstName": "Jane",
"articles": {"edges": [{"node": {"headline": "A fantastic article"}}]},
}

View File

@ -6,7 +6,7 @@ from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from graphql_relay import to_global_id from graphql_relay import to_global_id
from py.test import raises from pytest import raises
import graphene import graphene
from graphene.relay import Node from graphene.relay import Node
@ -15,7 +15,7 @@ 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, Reporter from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter
def test_should_query_only_fields(): def test_should_query_only_fields():
@ -251,8 +251,8 @@ def test_should_node():
def test_should_query_onetoone_fields(): def test_should_query_onetoone_fields():
film = Film(id=1) film = Film.objects.create(id=1)
film_details = FilmDetails(id=1, film=film) film_details = FilmDetails.objects.create(id=1, film=film)
class FilmNode(DjangoObjectType): class FilmNode(DjangoObjectType):
class Meta: class Meta:
@ -1151,9 +1151,9 @@ def test_connection_should_limit_after_to_list_length():
REPORTERS = [ REPORTERS = [
dict( dict(
first_name="First {}".format(i), first_name=f"First {i}",
last_name="Last {}".format(i), last_name=f"Last {i}",
email="johndoe+{}@example.com".format(i), email=f"johndoe+{i}@example.com",
a_choice=1, a_choice=1,
) )
for i in range(6) for i in range(6)
@ -1243,6 +1243,7 @@ def test_should_have_next_page(graphene_settings):
} }
@pytest.mark.parametrize("max_limit", [100, 4])
class TestBackwardPagination: class TestBackwardPagination:
def setup_schema(self, graphene_settings, max_limit): def setup_schema(self, graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
@ -1261,8 +1262,8 @@ class TestBackwardPagination:
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
return schema return schema
def do_queries(self, schema): def test_query_last(self, graphene_settings, max_limit):
# Simply last 3 schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_last = """ query_last = """
query { query {
allReporters(last: 3) { allReporters(last: 3) {
@ -1282,7 +1283,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 3", "First 4", "First 5"] ] == ["First 3", "First 4", "First 5"]
# Use a combination of first and last def test_query_first_and_last(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_and_last = """ query_first_and_last = """
query { query {
allReporters(first: 4, last: 3) { allReporters(first: 4, last: 3) {
@ -1302,7 +1304,8 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 1", "First 2", "First 3"] ] == ["First 1", "First 2", "First 3"]
# Use a combination of first and last and after def test_query_first_last_and_after(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_last_and_after = """ query_first_last_and_after = """
query queryAfter($after: String) { query queryAfter($after: String) {
allReporters(first: 4, last: 3, after: $after) { allReporters(first: 4, last: 3, after: $after) {
@ -1317,7 +1320,8 @@ class TestBackwardPagination:
after = base64.b64encode(b"arrayconnection:0").decode() after = base64.b64encode(b"arrayconnection:0").decode()
result = schema.execute( result = schema.execute(
query_first_last_and_after, variable_values=dict(after=after) query_first_last_and_after,
variable_values=dict(after=after),
) )
assert not result.errors assert not result.errors
assert len(result.data["allReporters"]["edges"]) == 3 assert len(result.data["allReporters"]["edges"]) == 3
@ -1325,20 +1329,35 @@ class TestBackwardPagination:
e["node"]["firstName"] for e in result.data["allReporters"]["edges"] e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
] == ["First 2", "First 3", "First 4"] ] == ["First 2", "First 3", "First 4"]
def test_should_query(self, graphene_settings): def test_query_last_and_before(self, graphene_settings, max_limit):
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
query_first_last_and_after = """
query queryAfter($before: String) {
allReporters(last: 1, before: $before) {
edges {
node {
firstName
}
}
}
}
""" """
Backward pagination should work as expected
"""
schema = self.setup_schema(graphene_settings, max_limit=100)
self.do_queries(schema)
def test_should_query_with_low_max_limit(self, graphene_settings): result = schema.execute(
""" query_first_last_and_after,
When doing backward pagination (using last) in combination with a max limit higher than the number of objects )
we should really retrieve the last ones. assert not result.errors
""" assert len(result.data["allReporters"]["edges"]) == 1
schema = self.setup_schema(graphene_settings, max_limit=4) assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5"
self.do_queries(schema)
before = base64.b64encode(b"arrayconnection:5").decode()
result = schema.execute(
query_first_last_and_after,
variable_values=dict(before=before),
)
assert not result.errors
assert len(result.data["allReporters"]["edges"]) == 1
assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4"
def test_should_preserve_prefetch_related(django_assert_num_queries): def test_should_preserve_prefetch_related(django_assert_num_queries):
@ -1480,7 +1499,11 @@ def test_connection_should_enable_offset_filtering():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]} "allReporters": {
"edges": [
{"node": {"firstName": "Some", "lastName": "Guy"}},
]
}
} }
assert result.data == expected assert result.data == expected
@ -1521,7 +1544,9 @@ def test_connection_should_enable_offset_filtering_higher_than_max_limit(
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": { "allReporters": {
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},] "edges": [
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
} }
} }
assert result.data == expected assert result.data == expected
@ -1590,6 +1615,149 @@ def test_connection_should_allow_offset_filtering_with_after():
result = schema.execute(query, variable_values=dict(after=after)) result = schema.execute(query, variable_values=dict(after=after))
assert not result.errors assert not result.errors
expected = { expected = {
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]} "allReporters": {
"edges": [
{"node": {"firstName": "Jane", "lastName": "Roe"}},
]
}
} }
assert result.data == expected assert result.data == expected
def test_connection_should_succeed_if_last_higher_than_number_of_objects():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
schema = graphene.Schema(query=Query)
query = """
query ReporterPromiseConnectionQuery ($last: Int) {
allReporters(last: $last) {
edges {
node {
firstName
lastName
}
}
}
}
"""
result = schema.execute(query, variable_values=dict(last=2))
assert not result.errors
expected = {"allReporters": {"edges": []}}
assert result.data == expected
Reporter.objects.create(first_name="John", last_name="Doe")
Reporter.objects.create(first_name="Some", last_name="Guy")
Reporter.objects.create(first_name="Jane", last_name="Roe")
Reporter.objects.create(first_name="Some", last_name="Lady")
result = schema.execute(query, variable_values=dict(last=2))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
result = schema.execute(query, variable_values=dict(last=4))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "John", "lastName": "Doe"}},
{"node": {"firstName": "Some", "lastName": "Guy"}},
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
result = schema.execute(query, variable_values=dict(last=20))
assert not result.errors
expected = {
"allReporters": {
"edges": [
{"node": {"firstName": "John", "lastName": "Doe"}},
{"node": {"firstName": "Some", "lastName": "Guy"}},
{"node": {"firstName": "Jane", "lastName": "Roe"}},
{"node": {"firstName": "Some", "lastName": "Lady"}},
]
}
}
assert result.data == expected
def test_should_query_nullable_foreign_key():
class PetType(DjangoObjectType):
class Meta:
model = Pet
class PersonType(DjangoObjectType):
class Meta:
model = Person
class Query(graphene.ObjectType):
pet = graphene.Field(PetType, name=graphene.String(required=True))
person = graphene.Field(PersonType, name=graphene.String(required=True))
def resolve_pet(self, info, name):
return Pet.objects.filter(name=name).first()
def resolve_person(self, info, name):
return Person.objects.filter(name=name).first()
schema = graphene.Schema(query=Query)
person = Person.objects.create(name="Jane")
pets = [
Pet.objects.create(name="Stray dog", age=1),
Pet.objects.create(name="Jane's dog", owner=person, age=1),
]
query_pet = """
query getPet($name: String!) {
pet(name: $name) {
owner {
name
}
}
}
"""
result = schema.execute(query_pet, variables={"name": "Stray dog"})
assert not result.errors
assert result.data["pet"] == {
"owner": None,
}
result = schema.execute(query_pet, variables={"name": "Jane's dog"})
assert not result.errors
assert result.data["pet"] == {
"owner": {"name": "Jane"},
}
query_owner = """
query getOwner($name: String!) {
person(name: $name) {
pets {
name
}
}
}
"""
result = schema.execute(query_owner, variables={"name": "Jane"})
assert not result.errors
assert result.data["person"] == {
"pets": [{"name": "Jane's dog"}],
}

View File

@ -1,4 +1,4 @@
from py.test import raises from pytest import raises
from ..registry import Registry from ..registry import Registry
from ..types import DjangoObjectType from ..types import DjangoObjectType

View File

@ -3,7 +3,7 @@ from textwrap import dedent
import pytest import pytest
from django.db import models from django.db import models
from mock import patch 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
@ -104,7 +104,7 @@ def test_django_objecttype_with_custom_meta():
@classmethod @classmethod
def __init_subclass_with_meta__(cls, **options): def __init_subclass_with_meta__(cls, **options):
options.setdefault("_meta", ArticleTypeOptions(cls)) options.setdefault("_meta", ArticleTypeOptions(cls))
super(ArticleType, cls).__init_subclass_with_meta__(**options) super().__init_subclass_with_meta__(**options)
class Article(ArticleType): class Article(ArticleType):
class Meta: class Meta:
@ -183,7 +183,7 @@ def test_schema_representation():
pets: [Reporter!]! pets: [Reporter!]!
aChoice: TestsReporterAChoiceChoices aChoice: TestsReporterAChoiceChoices
reporterType: TestsReporterReporterTypeChoices reporterType: TestsReporterReporterTypeChoices
articles(offset: Int = null, before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection! articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
} }
\"""An enumeration.\""" \"""An enumeration.\"""
@ -244,8 +244,7 @@ def test_schema_representation():
\"""The ID of the object\""" \"""The ID of the object\"""
id: ID! id: ID!
): Node ): Node
} }"""
"""
) )
assert str(schema) == expected assert str(schema) == expected
@ -485,7 +484,7 @@ def test_django_objecttype_neither_fields_nor_exclude():
def custom_enum_name(field): def custom_enum_name(field):
return "CustomEnum{}".format(field.name.title()) return f"CustomEnum{field.name.title()}"
class TestDjangoObjectType: class TestDjangoObjectType:
@ -525,8 +524,7 @@ class TestDjangoObjectType:
id: ID! id: ID!
kind: String! kind: String!
cuteness: Int! cuteness: Int!
} }"""
"""
) )
def test_django_objecttype_convert_choices_enum_list(self, PetModel): def test_django_objecttype_convert_choices_enum_list(self, PetModel):
@ -560,8 +558,7 @@ class TestDjangoObjectType:
\"""Dog\""" \"""Dog\"""
DOG DOG
} }"""
"""
) )
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
@ -586,8 +583,7 @@ class TestDjangoObjectType:
id: ID! id: ID!
kind: String! kind: String!
cuteness: Int! cuteness: Int!
} }"""
"""
) )
def test_django_objecttype_convert_choices_enum_naming_collisions( def test_django_objecttype_convert_choices_enum_naming_collisions(
@ -621,8 +617,7 @@ class TestDjangoObjectType:
\"""Dog\""" \"""Dog\"""
DOG DOG
} }"""
"""
) )
def test_django_objecttype_choices_custom_enum_name( def test_django_objecttype_choices_custom_enum_name(
@ -660,8 +655,7 @@ class TestDjangoObjectType:
\"""Dog\""" \"""Dog\"""
DOG DOG
} }"""
"""
) )

View File

@ -2,7 +2,7 @@ import json
import pytest import pytest
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
from mock import patch from unittest.mock import patch
from ..utils import camelize, get_model_fields, GraphQLTestCase from ..utils import camelize, get_model_fields, GraphQLTestCase
from .models import Film, Reporter from .models import Film, Reporter
@ -11,11 +11,11 @@ from ..utils.testing import graphql_query
def test_get_model_fields_no_duplication(): def test_get_model_fields_no_duplication():
reporter_fields = get_model_fields(Reporter) reporter_fields = get_model_fields(Reporter)
reporter_name_set = set([field[0] for field in reporter_fields]) reporter_name_set = {field[0] for field in reporter_fields}
assert len(reporter_fields) == len(reporter_name_set) assert len(reporter_fields) == len(reporter_name_set)
film_fields = get_model_fields(Film) film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields]) film_name_set = {field[0] for field in film_fields}
assert len(film_fields) == len(film_name_set) assert len(film_fields) == len(film_name_set)
@ -54,7 +54,7 @@ def test_graphql_test_case_operation_name(post_mock):
tc._pre_setup() tc._pre_setup()
tc.setUpClass() tc.setUpClass()
tc.query("query { }", operation_name="QueryName") tc.query("query { }", operation_name="QueryName")
body = json.loads(post_mock.call_args.args[1]) body = json.loads(post_mock.call_args[0][1])
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert ( assert (
"operationName", "operationName",
@ -66,7 +66,7 @@ def test_graphql_test_case_operation_name(post_mock):
@patch("graphene_django.utils.testing.Client.post") @patch("graphene_django.utils.testing.Client.post")
def test_graphql_query_case_operation_name(post_mock): def test_graphql_query_case_operation_name(post_mock):
graphql_query("query { }", operation_name="QueryName") graphql_query("query { }", operation_name="QueryName")
body = json.loads(post_mock.call_args.args[1]) body = json.loads(post_mock.call_args[0][1])
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
assert ( assert (
"operationName", "operationName",

View File

@ -2,7 +2,7 @@ import json
import pytest import pytest
from mock import patch from unittest.mock import patch
from django.db import connection from django.db import connection
@ -109,12 +109,10 @@ def test_reports_validation_errors(client):
{ {
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
"locations": [{"line": 1, "column": 9}], "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}], "locations": [{"line": 1, "column": 21}],
"path": None,
}, },
] ]
} }
@ -135,8 +133,6 @@ def test_errors_when_missing_operation_name(client):
"errors": [ "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,
} }
] ]
} }
@ -477,7 +473,6 @@ def test_handles_syntax_errors_caught_by_graphql(client):
{ {
"locations": [{"column": 1, "line": 1}], "locations": [{"column": 1, "line": 1}],
"message": "Syntax Error: Unexpected Name 'syntaxerror'.", "message": "Syntax Error: Unexpected Name 'syntaxerror'.",
"path": None,
} }
] ]
} }
@ -512,7 +507,7 @@ def test_handles_invalid_json_bodies(client):
def test_handles_django_request_error(client, monkeypatch): def test_handles_django_request_error(client, monkeypatch):
def mocked_read(*args): def mocked_read(*args):
raise IOError("foo-bar") raise OSError("foo-bar")
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read) monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read)

View File

@ -1,8 +1,8 @@
from django.conf.urls import url from django.urls import path
from ..views import GraphQLView from ..views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r"^graphql/batch", GraphQLView.as_view(batch=True)), path("graphql/batch", GraphQLView.as_view(batch=True)),
url(r"^graphql", GraphQLView.as_view(graphiql=True)), path("graphql", GraphQLView.as_view(graphiql=True)),
] ]

View File

@ -1,4 +1,4 @@
from django.conf.urls import url from django.urls import path
from ..views import GraphQLView from ..views import GraphQLView
from .schema_view import schema from .schema_view import schema
@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView):
pretty = True pretty = True
urlpatterns = [url(r"^graphql/inherited/$", CustomGraphQLView.as_view())] urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())]

View File

@ -1,6 +1,6 @@
from django.conf.urls import url from django.urls import path
from ..views import GraphQLView from ..views import GraphQLView
from .schema_view import schema from .schema_view import schema
urlpatterns = [url(r"^graphql", GraphQLView.as_view(schema=schema, pretty=True))] urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))]

View File

@ -122,7 +122,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields):
class DjangoObjectTypeOptions(ObjectTypeOptions): class DjangoObjectTypeOptions(ObjectTypeOptions):
model = None # type: Model model = None # type: Type[Model]
registry = None # type: Registry registry = None # type: Registry
connection = None # type: Type[Connection] connection = None # type: Type[Connection]
@ -168,10 +168,8 @@ class DjangoObjectType(ObjectType):
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
raise Exception( raise Exception(
( "Can only set filter_fields or filterset_class if "
"Can only set filter_fields or filterset_class if " "Django-Filter is installed"
"Django-Filter is installed"
)
) )
assert not (fields and exclude), ( assert not (fields and exclude), (
@ -216,7 +214,7 @@ class DjangoObjectType(ObjectType):
"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 " "= '__all__'` option on DjangoObjectType {class_name} to use all "
"fields".format(class_name=cls.__name__,), "fields".format(class_name=cls.__name__),
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -228,7 +226,7 @@ class DjangoObjectType(ObjectType):
if use_connection is None and interfaces: if use_connection is None and interfaces:
use_connection = any( use_connection = any(
(issubclass(interface, Node) for interface in interfaces) issubclass(interface, Node) for interface in interfaces
) )
if use_connection and not connection: if use_connection and not connection:
@ -255,7 +253,7 @@ class DjangoObjectType(ObjectType):
_meta.fields = django_fields _meta.fields = django_fields
_meta.connection = connection _meta.connection = connection
super(DjangoObjectType, cls).__init_subclass_with_meta__( super().__init_subclass_with_meta__(
_meta=_meta, interfaces=interfaces, **options _meta=_meta, interfaces=interfaces, **options
) )

View File

@ -3,7 +3,9 @@ import warnings
from django.test import Client, TestCase, TransactionTestCase from django.test import Client, TestCase, TransactionTestCase
DEFAULT_GRAPHQL_URL = "/graphql/" from graphene_django.settings import graphene_settings
DEFAULT_GRAPHQL_URL = "/graphql"
def graphql_query( def graphql_query(
@ -19,7 +21,7 @@ def graphql_query(
Args: Args:
query (string) - GraphQL query to run query (string) - GraphQL query to run
operation_name (string) - If the query is a mutation or named query, you must operation_name (string) - If the query is a mutation or named query, you must
supply the op_name. For annon queries ("{ ... }"), supply the operation_name. For annon queries ("{ ... }"),
should be None (default). should be None (default).
input_data (dict) - If provided, the $input variable in GraphQL will be set 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``,
@ -40,7 +42,7 @@ def graphql_query(
if client is None: if client is None:
client = Client() client = Client()
if not graphql_url: if not graphql_url:
graphql_url = DEFAULT_GRAPHQL_URL graphql_url = graphene_settings.TESTING_ENDPOINT
body = {"query": query} body = {"query": query}
if operation_name: if operation_name:
@ -63,13 +65,13 @@ def graphql_query(
return resp return resp
class GraphQLTestMixin(object): class GraphQLTestMixin:
""" """
Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/
""" """
# URL to graphql endpoint # URL to graphql endpoint
GRAPHQL_URL = DEFAULT_GRAPHQL_URL GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT
def query( def query(
self, query, operation_name=None, input_data=None, variables=None, headers=None self, query, operation_name=None, input_data=None, variables=None, headers=None
@ -78,7 +80,7 @@ class GraphQLTestMixin(object):
Args: Args:
query (string) - GraphQL query to run query (string) - GraphQL query to run
operation_name (string) - If the query is a mutation or named query, you must operation_name (string) - If the query is a mutation or named query, you must
supply the op_name. For annon queries ("{ ... }"), supply the operation_name. For annon queries ("{ ... }"),
should be None (default). should be None (default).
input_data (dict) - If provided, the $input variable in GraphQL will be set 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``,

View File

@ -6,4 +6,4 @@ def test_to_const():
def test_to_const_unicode(): def test_to_const_unicode():
assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"

View File

@ -2,6 +2,7 @@ import pytest
from .. import GraphQLTestCase from .. import GraphQLTestCase
from ...tests.test_types import with_local_registry from ...tests.test_types import with_local_registry
from ...settings import graphene_settings
from django.test import Client from django.test import Client
@ -43,3 +44,11 @@ def test_graphql_test_case_deprecated_client_setter():
with pytest.warns(PendingDeprecationWarning): with pytest.warns(PendingDeprecationWarning):
tc._client = Client() tc._client = Client()
def test_graphql_test_case_imports_endpoint():
"""
GraphQLTestCase class should import the default endpoint from settings file
"""
assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT

View File

@ -11,7 +11,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View from django.views.generic import View
from graphql import OperationType, get_operation_ast, parse, validate from graphql import OperationType, get_operation_ast, parse, validate
from graphql.error import GraphQLError from graphql.error import GraphQLError
from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
from graphene import Schema from graphene import Schema
@ -27,7 +26,7 @@ class HttpError(Exception):
def __init__(self, response, message=None, *args, **kwargs): def __init__(self, response, message=None, *args, **kwargs):
self.response = response self.response = response
self.message = message = message or response.content.decode() self.message = message = message or response.content.decode()
super(HttpError, self).__init__(message, *args, **kwargs) super().__init__(message, *args, **kwargs)
def get_accepted_content_types(request): def get_accepted_content_types(request):
@ -67,9 +66,9 @@ class GraphQLView(View):
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
# The GraphiQL React app. # The GraphiQL React app.
graphiql_version = "1.4.1" # "1.0.3" graphiql_version = "1.4.7" # "1.0.3"
graphiql_sri = "sha256-JUMkXBQWZMfJ7fGEsTXalxVA10lzKOS9loXdLjwZKi4=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk=" graphiql_sri = "sha256-cpZ8w9D/i6XdEbY/Eu7yAXeYzReVw0mxYd7OU3gUcsc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
graphiql_css_sri = "sha256-Md3vdR7PDzWyo/aGfsFVF4tvS5/eAUWuIsg9QHUusCY=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E=" graphiql_css_sri = "sha256-HADQowUuFum02+Ckkv5Yu5ygRoLllHZqg0TFZXY7NHI=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
# The websocket transport library for subscriptions. # The websocket transport library for subscriptions.
subscriptions_transport_ws_version = "0.9.18" subscriptions_transport_ws_version = "0.9.18"
@ -163,6 +162,7 @@ class GraphQLView(View):
subscription_path=self.subscription_path, subscription_path=self.subscription_path,
# GraphiQL headers tab, # GraphiQL headers tab,
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
) )
if self.batch: if self.batch:
@ -387,7 +387,7 @@ class GraphQLView(View):
@staticmethod @staticmethod
def format_error(error): def format_error(error):
if isinstance(error, GraphQLError): if isinstance(error, GraphQLError):
return format_graphql_error(error) return error.formatted
return {"message": str(error)} return {"message": str(error)}

View File

@ -5,7 +5,7 @@ test=pytest
universal=1 universal=1
[flake8] [flake8]
exclude = docs,graphene_django/debug/sql/*,migrations exclude = docs,graphene_django/debug/sql/*
max-line-length = 120 max-line-length = 120
select = select =
# Dictionary key repeated # Dictionary key repeated

View File

@ -14,22 +14,22 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
tests_require = [ tests_require = [
"pytest>=3.6.3", "pytest>=7.1.3",
"pytest-cov", "pytest-cov",
"pytest-random-order", "pytest-random-order",
"coveralls", "coveralls",
"mock", "mock",
"pytz", "pytz",
"django-filter>=2", "django-filter>=22.1",
"pytest-django>=3.3.2", "pytest-django>=4.5.2",
] + rest_framework_require ] + rest_framework_require
dev_requires = [ dev_requires = [
"black==19.10b0", "black==22.8.0",
"flake8==3.7.9", "flake8==5.0.4",
"flake8-black==0.1.1", "flake8-black==0.3.3",
"flake8-bugbear==20.1.4", "flake8-bugbear==22.9.11",
] + tests_require ] + tests_require
setup( setup(
@ -46,23 +46,23 @@ 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.6",
"Programming Language :: Python :: 3.7", "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 :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2", "Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
], ],
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.*"]),
install_requires=[ install_requires=[
"graphene>=3.0.0b5,<4", "graphene>=3.0,<4",
"graphql-core>=3.1.0,<4", "graphql-core>=3.1.0,<4",
"Django>=2.2", "graphql-relay>=3.1.1,<4",
"Django>=3.2",
"promise>=2.1", "promise>=2.1",
"text-unidecode", "text-unidecode",
], ],

35
tox.ini
View File

@ -1,21 +1,21 @@
[tox] [tox]
envlist = envlist =
py{36,37,38,39}-django{22,30,31,32,main}, py{37,38,39,310}-django32,
black,flake8 py{38,39,310}-django{40,41,main},
pre-commit
[gh-actions] [gh-actions]
python = python =
3.6: py36
3.7: py37 3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310
[gh-actions:env] [gh-actions:env]
DJANGO = DJANGO =
2.2: django22
3.0: django30
3.1: django31
3.2: django32 3.2: django32
4.0: django40
4.1: django41
main: djangomain main: djangomain
[testenv] [testenv]
@ -26,23 +26,14 @@ setenv =
deps = deps =
-e.[test] -e.[test]
psycopg2-binary psycopg2-binary
django20: Django>=2.0,<2.1 django32: Django>=3.2,<4.0
django21: Django>=2.1,<2.2 django40: Django>=4.0,<4.1
django22: Django>=2.2,<3.0 django41: Django>=4.1,<4.2
django30: Django>=3.0a1,<3.1
django31: Django>=3.1,<3.2
django32: Django>=3.2a1,<3.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:py.test --cov=graphene_django graphene_django examples}
[testenv:black] [testenv:pre-commit]
basepython = python3.9 skip_install = true
deps = -e.[dev] deps = pre-commit
commands =
black --exclude "/migrations/" graphene_django examples setup.py --check
[testenv:flake8]
basepython = python3.9
deps = -e.[dev]
commands = commands =
flake8 graphene_django examples setup.py pre-commit run --all-files --show-diff-on-failure