mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-10 19:57:15 +03:00
Merge branch 'main' into main
This commit is contained in:
commit
eeaa2234b4
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -27,8 +27,8 @@ a github repo, https://repl.it or similar (you can use this template as a starti
|
|||
|
||||
|
||||
* **Please tell us about your environment:**
|
||||
|
||||
- Version:
|
||||
- Platform:
|
||||
|
||||
- Version:
|
||||
- Platform:
|
||||
|
||||
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)
|
||||
|
|
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
|
@ -10,17 +10,17 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: '3.11'
|
||||
- name: Build wheel and source tarball
|
||||
run: |
|
||||
pip install wheel
|
||||
python setup.py sdist bdist_wheel
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.1.0
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_password }}
|
||||
|
|
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
|
@ -7,16 +7,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox
|
||||
- name: Run lint 💅
|
||||
- name: Run pre-commit 💅
|
||||
run: tox
|
||||
env:
|
||||
TOXENV: flake8
|
||||
TOXENV: pre-commit
|
||||
|
|
14
.github/workflows/tests.yml
vendored
14
.github/workflows/tests.yml
vendored
|
@ -8,13 +8,17 @@ jobs:
|
|||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
django: ["2.2", "3.0", "3.1", "3.2"]
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9"]
|
||||
|
||||
django: ["3.2", "4.0", "4.1"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
include:
|
||||
- django: "3.2"
|
||||
python-version: "3.7"
|
||||
- django: "4.1"
|
||||
python-version: "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
|
|
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,30 @@
|
|||
default_language_version:
|
||||
python: python3.11
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.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.3.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
|
@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation:
|
|||
|
||||
```sh
|
||||
make html
|
||||
```
|
||||
```
|
||||
|
|
|
@ -3,4 +3,4 @@ recursive-include graphene_django/templates *
|
|||
recursive-include graphene_django/static *
|
||||
|
||||
include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json
|
||||
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json
|
||||
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json
|
||||
|
|
20
Makefile
20
Makefile
|
@ -1,22 +1,22 @@
|
|||
.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
|
||||
dev-setup:
|
||||
pip install -e ".[dev]"
|
||||
python -m pre_commit install
|
||||
|
||||
.PHONY: install-dev
|
||||
install-dev: dev-setup # Alias install-dev -> dev-setup
|
||||
|
||||
.PHONY: tests
|
||||
.PHONY: tests ## Run unit tests
|
||||
tests:
|
||||
py.test graphene_django --cov=graphene_django -vv
|
||||
|
||||
.PHONY: test
|
||||
test: tests # Alias test -> tests
|
||||
|
||||
.PHONY: format
|
||||
.PHONY: format ## Format code
|
||||
format:
|
||||
black --exclude "/migrations/" graphene_django examples setup.py
|
||||
black graphene_django examples setup.py
|
||||
|
||||
.PHONY: lint
|
||||
.PHONY: lint ## Lint code
|
||||
lint:
|
||||
flake8 graphene_django examples
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ from graphene_django.views import GraphQLView
|
|||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
path('graphql', GraphQLView.as_view(graphiql=True)),
|
||||
path('graphql/', GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ For Django 2.2 and above:
|
|||
|
||||
urlpatterns = [
|
||||
# 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
|
||||
|
|
|
@ -2,8 +2,8 @@ Filtering
|
|||
=========
|
||||
|
||||
Graphene integrates with
|
||||
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
|
||||
See the `usage documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
|
||||
`django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results.
|
||||
See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__
|
||||
for details on the format for ``filter_fields``.
|
||||
|
||||
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
|
||||
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
|
||||
-----------------
|
||||
|
@ -34,7 +34,7 @@ Filterable fields
|
|||
The ``filter_fields`` parameter is used to specify the fields which can
|
||||
be filtered upon. The value specified here is passed directly to
|
||||
``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 example:
|
||||
|
@ -192,7 +192,7 @@ in unison with the ``filter_fields`` parameter:
|
|||
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
|
||||
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||
pre-filter animals owned by the authenticated user (set in ``context.user``).
|
||||
|
|
|
@ -151,7 +151,7 @@ For example the following ``Model`` and ``DjangoObjectType``:
|
|||
|
||||
Results in the following GraphQL schema definition:
|
||||
|
||||
.. code::
|
||||
.. code:: graphql
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
|
@ -178,7 +178,7 @@ You can disable this automatic conversion by setting
|
|||
fields = ("id", "kind",)
|
||||
convert_choices_to_enum = False
|
||||
|
||||
.. code::
|
||||
.. code:: graphql
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
|
@ -313,7 +313,7 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
|
|||
bar=graphene.Int()
|
||||
)
|
||||
|
||||
def resolve_question(root, info, foo, bar):
|
||||
def resolve_question(root, info, foo=None, bar=None):
|
||||
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
|
||||
return Question.objects.filter(foo=foo, bar=bar).first()
|
||||
|
||||
|
@ -336,12 +336,12 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
|
|||
class Query(graphene.ObjectType):
|
||||
questions = graphene.List(QuestionType)
|
||||
|
||||
def resolve_questions(root, info):
|
||||
# See if a user is authenticated
|
||||
if info.context.user.is_authenticated():
|
||||
return Question.objects.all()
|
||||
else:
|
||||
return Question.objects.none()
|
||||
def resolve_questions(root, info):
|
||||
# See if a user is authenticated
|
||||
if info.context.user.is_authenticated():
|
||||
return Question.objects.all()
|
||||
else:
|
||||
return Question.objects.none()
|
||||
|
||||
|
||||
DjangoObjectTypes
|
||||
|
@ -418,29 +418,29 @@ the core graphene pages for more information on customizing the Relay experience
|
|||
You can now execute queries like:
|
||||
|
||||
|
||||
.. code:: python
|
||||
.. code:: graphql
|
||||
|
||||
{
|
||||
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
question_text
|
||||
}
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
question_text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Which returns:
|
||||
|
||||
.. code:: python
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"data": {
|
||||
|
|
|
@ -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 Meta:
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
class CategoryType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IngredientType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = '__all__'
|
||||
class IngredientType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class Query(object):
|
||||
category = graphene.Field(CategoryType,
|
||||
id=graphene.Int(),
|
||||
name=graphene.String())
|
||||
all_categories = graphene.List(CategoryType)
|
||||
class Query:
|
||||
category = graphene.Field(CategoryType, id=graphene.Int(), 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,
|
||||
id=graphene.Int(),
|
||||
name=graphene.String())
|
||||
all_ingredients = graphene.List(IngredientType)
|
||||
def resolve_all_categories(self, info, **kwargs):
|
||||
return Category.objects.all()
|
||||
|
||||
def resolve_all_categories(self, info, **kwargs):
|
||||
return Category.objects.all()
|
||||
def resolve_all_ingredients(self, info, **kwargs):
|
||||
return Ingredient.objects.all()
|
||||
|
||||
def resolve_all_ingredients(self, info, **kwargs):
|
||||
return Ingredient.objects.all()
|
||||
def resolve_category(self, info, **kwargs):
|
||||
id = kwargs.get("id")
|
||||
name = kwargs.get("name")
|
||||
|
||||
def resolve_category(self, info, **kwargs):
|
||||
id = kwargs.get('id')
|
||||
name = kwargs.get('name')
|
||||
if id is not None:
|
||||
return Category.objects.get(pk=id)
|
||||
|
||||
if id is not None:
|
||||
return Category.objects.get(pk=id)
|
||||
if name is not None:
|
||||
return Category.objects.get(name=name)
|
||||
|
||||
if name is not None:
|
||||
return Category.objects.get(name=name)
|
||||
return None
|
||||
|
||||
return None
|
||||
def resolve_ingredient(self, info, **kwargs):
|
||||
id = kwargs.get("id")
|
||||
name = kwargs.get("name")
|
||||
|
||||
def resolve_ingredient(self, info, **kwargs):
|
||||
id = kwargs.get('id')
|
||||
name = kwargs.get('name')
|
||||
if id is not None:
|
||||
return Ingredient.objects.get(pk=id)
|
||||
|
||||
if id is not None:
|
||||
return Ingredient.objects.get(pk=id)
|
||||
if name is not None:
|
||||
return Ingredient.objects.get(name=name)
|
||||
|
||||
if name is not None:
|
||||
return Ingredient.objects.get(name=name)
|
||||
|
||||
return None
|
||||
return None
|
||||
|
|
|
@ -189,7 +189,7 @@ Default: ``None``
|
|||
|
||||
|
||||
``GRAPHIQL_HEADER_EDITOR_ENABLED``
|
||||
---------------------
|
||||
----------------------------------
|
||||
|
||||
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
|
||||
|
||||
|
@ -207,3 +207,36 @@ Default: ``True``
|
|||
GRAPHENE = {
|
||||
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
|
||||
}
|
||||
|
||||
|
||||
``TESTING_ENDPOINT``
|
||||
--------------------
|
||||
|
||||
Define the graphql endpoint url used for the `GraphQLTestCase` class.
|
||||
|
||||
Default: ``/graphql``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'TESTING_ENDPOINT': '/customEndpoint'
|
||||
}
|
||||
|
||||
|
||||
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
||||
---------------------
|
||||
|
||||
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
||||
|
||||
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
|
||||
|
||||
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ Using unittest
|
|||
|
||||
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:
|
||||
|
||||
|
@ -27,7 +28,7 @@ Usage:
|
|||
}
|
||||
}
|
||||
''',
|
||||
op_name='myModel'
|
||||
operation_name='myModel'
|
||||
)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
@ -48,7 +49,7 @@ Usage:
|
|||
}
|
||||
}
|
||||
''',
|
||||
op_name='myModel',
|
||||
operation_name='myModel',
|
||||
variables={'id': 1}
|
||||
)
|
||||
|
||||
|
@ -72,7 +73,7 @@ Usage:
|
|||
}
|
||||
}
|
||||
''',
|
||||
op_name='myMutation',
|
||||
operation_name='myMutation',
|
||||
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'}
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -35,6 +35,7 @@ Now sync your database for the first time:
|
|||
|
||||
.. code:: bash
|
||||
|
||||
cd ..
|
||||
python manage.py migrate
|
||||
|
||||
Let's create a few simple models...
|
||||
|
@ -77,6 +78,18 @@ Add ingredients as INSTALLED_APPS:
|
|||
"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:
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
|||
interfaces = (relay.Node, )
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
class Query(ObjectType):
|
||||
category = relay.Node.Field(CategoryNode)
|
||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ whole Graphene repository:
|
|||
```bash
|
||||
# Get the example project code
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,33 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-04 18:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
name="Category",
|
||||
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(
|
||||
name='Ingredient',
|
||||
name="Ingredient",
|
||||
fields=[
|
||||
('id', models.AutoField(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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2016-11-04 00:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ingredients', '0001_initial'),
|
||||
("ingredients", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='notes',
|
||||
model_name="ingredient",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,14 +4,13 @@ from django.db import migrations
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ingredients', '0002_auto_20161104_0050'),
|
||||
("ingredients", "0002_auto_20161104_0050"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'verbose_name_plural': 'Categories'},
|
||||
name="category",
|
||||
options={"verbose_name_plural": "Categories"},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType):
|
|||
fields = "__all__"
|
||||
|
||||
|
||||
class Query(object):
|
||||
class Query:
|
||||
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
|
||||
all_categories = graphene.List(CategoryType)
|
||||
|
||||
|
|
|
@ -1,36 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-04 18:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('ingredients', '0001_initial'),
|
||||
("ingredients", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Recipe',
|
||||
name="Recipe",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('instructions', models.TextField()),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=100)),
|
||||
("instructions", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeIngredient',
|
||||
name="RecipeIngredient",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2016-11-04 01:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0001_initial'),
|
||||
("recipes", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='recipeingredient',
|
||||
old_name='recipes',
|
||||
new_name='recipe',
|
||||
model_name="recipeingredient",
|
||||
old_name="recipes",
|
||||
new_name="recipe",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipeingredient',
|
||||
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),
|
||||
model_name="recipeingredient",
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,15 +4,22 @@ from django.db import migrations, models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0002_auto_20161104_0106'),
|
||||
("recipes", "0002_auto_20161104_0106"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='recipeingredient',
|
||||
name='unit',
|
||||
field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20),
|
||||
model_name="recipeingredient",
|
||||
name="unit",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("unit", "Units"),
|
||||
("kg", "Kilograms"),
|
||||
("l", "Litres"),
|
||||
("st", "Shots"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType):
|
|||
fields = "__all__"
|
||||
|
||||
|
||||
class Query(object):
|
||||
class Query:
|
||||
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
|
||||
all_recipes = graphene.List(RecipeType)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Cookbook Example Django Project
|
||||
Cookbook Example (Relay) Django Project
|
||||
===============================
|
||||
|
||||
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
|
||||
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,33 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-04 18:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
name="Category",
|
||||
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(
|
||||
name='Ingredient',
|
||||
name="Ingredient",
|
||||
fields=[
|
||||
('id', models.AutoField(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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2016-11-04 00:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ingredients', '0001_initial'),
|
||||
("ingredients", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='notes',
|
||||
model_name="ingredient",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType):
|
|||
}
|
||||
|
||||
|
||||
class Query(object):
|
||||
class Query:
|
||||
category = Node.Field(CategoryNode)
|
||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||
|
||||
|
|
|
@ -1,36 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-04 18:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('ingredients', '0001_initial'),
|
||||
("ingredients", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Recipe',
|
||||
name="Recipe",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('instructions', models.TextField()),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=100)),
|
||||
("instructions", models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeIngredient',
|
||||
name="RecipeIngredient",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, 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')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2016-11-04 01:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0001_initial'),
|
||||
("recipes", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='recipeingredient',
|
||||
old_name='recipes',
|
||||
new_name='recipe',
|
||||
model_name="recipeingredient",
|
||||
old_name="recipes",
|
||||
new_name="recipe",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipeingredient',
|
||||
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),
|
||||
model_name="recipeingredient",
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -25,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType):
|
|||
}
|
||||
|
||||
|
||||
class Query(object):
|
||||
class Query:
|
||||
recipe = Node.Field(RecipeNode)
|
||||
all_recipes = DjangoFilterConnectionField(RecipeNode)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .fields import DjangoConnectionField, DjangoListField
|
||||
from .types import DjangoObjectType
|
||||
|
||||
__version__ = "3.0.0b7"
|
||||
__version__ = "3.0.2"
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class MissingType(object):
|
||||
class MissingType:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
|
|
@ -24,8 +24,15 @@ from graphene import (
|
|||
Decimal,
|
||||
)
|
||||
from graphene.types.json import JSONString
|
||||
from graphene.types.scalars import BigInt
|
||||
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 .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
|
||||
|
@ -55,7 +62,7 @@ class BlankValueField(Field):
|
|||
def convert_choice_name(name):
|
||||
name = to_const(force_str(name))
|
||||
try:
|
||||
assert_valid_name(name)
|
||||
assert_name(name)
|
||||
except GraphQLError:
|
||||
name = "A_%s" % name
|
||||
return name
|
||||
|
@ -67,8 +74,7 @@ def get_choices(choices):
|
|||
choices = choices.items()
|
||||
for value, help_text in choices:
|
||||
if isinstance(help_text, (tuple, list)):
|
||||
for choice in get_choices(help_text):
|
||||
yield choice
|
||||
yield from get_choices(help_text)
|
||||
else:
|
||||
name = convert_choice_name(value)
|
||||
while name in converted_names:
|
||||
|
@ -85,12 +91,17 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
|
|||
named_choices = [(c[0], c[1]) for c in choices]
|
||||
named_choices_descriptions = {c[0]: c[2] for c in choices}
|
||||
|
||||
class EnumWithDescriptionsType(object):
|
||||
class EnumWithDescriptionsType:
|
||||
@property
|
||||
def description(self):
|
||||
return str(named_choices_descriptions[self.name])
|
||||
|
||||
return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||
return_type = Enum(
|
||||
name,
|
||||
list(named_choices),
|
||||
type=EnumWithDescriptionsType,
|
||||
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
|
||||
)
|
||||
return return_type
|
||||
|
||||
|
||||
|
@ -102,7 +113,7 @@ def generate_enum_name(django_model_meta, field):
|
|||
)
|
||||
name = custom_func(field)
|
||||
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:
|
||||
name = "{app_label}{object_name}{field_name}Choices".format(
|
||||
app_label=to_camel_case(django_model_meta.app_label.title()),
|
||||
|
@ -148,7 +159,9 @@ def get_django_field_description(field):
|
|||
@singledispatch
|
||||
def convert_django_field(field, registry=None):
|
||||
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 +199,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.PositiveSmallIntegerField)
|
||||
@convert_django_field.register(models.SmallIntegerField)
|
||||
@convert_django_field.register(models.BigIntegerField)
|
||||
@convert_django_field.register(models.IntegerField)
|
||||
def convert_field_to_int(field, registry=None):
|
||||
return Int(description=get_django_field_description(field), required=not field.null)
|
||||
|
@ -205,7 +222,9 @@ def convert_field_to_boolean(field, registry=None):
|
|||
|
||||
@convert_django_field.register(models.DecimalField)
|
||||
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)
|
||||
|
|
|
@ -11,7 +11,7 @@ def wrap_exception(exception):
|
|||
exc_type=force_str(type(exception)),
|
||||
stack="".join(
|
||||
traceback.format_exception(
|
||||
etype=type(exception), value=exception, tb=exception.__traceback__
|
||||
exception, value=exception, tb=exception.__traceback__
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
@ -7,34 +7,34 @@ from .exception.formating import wrap_exception
|
|||
from .types import DjangoDebug
|
||||
|
||||
|
||||
class DjangoDebugContext(object):
|
||||
class DjangoDebugContext:
|
||||
def __init__(self):
|
||||
self.debug_promise = None
|
||||
self.promises = []
|
||||
self.debug_result = None
|
||||
self.results = []
|
||||
self.object = DjangoDebug(sql=[], exceptions=[])
|
||||
self.enable_instrumentation()
|
||||
|
||||
def get_debug_promise(self):
|
||||
if not self.debug_promise:
|
||||
self.debug_promise = Promise.all(self.promises)
|
||||
self.promises = []
|
||||
return self.debug_promise.then(self.on_resolve_all_promises).get()
|
||||
def get_debug_result(self):
|
||||
if not self.debug_result:
|
||||
self.debug_result = self.results
|
||||
self.results = []
|
||||
return self.on_resolve_all_results()
|
||||
|
||||
def on_resolve_error(self, value):
|
||||
if hasattr(self, "object"):
|
||||
self.object.exceptions.append(wrap_exception(value))
|
||||
return Promise.reject(value)
|
||||
return value
|
||||
|
||||
def on_resolve_all_promises(self, values):
|
||||
if self.promises:
|
||||
self.debug_promise = None
|
||||
return self.get_debug_promise()
|
||||
def on_resolve_all_results(self):
|
||||
if self.results:
|
||||
self.debug_result = None
|
||||
return self.get_debug_result()
|
||||
self.disable_instrumentation()
|
||||
return self.object
|
||||
|
||||
def add_promise(self, promise):
|
||||
if self.debug_promise:
|
||||
self.promises.append(promise)
|
||||
def add_result(self, result):
|
||||
if self.debug_result:
|
||||
self.results.append(result)
|
||||
|
||||
def enable_instrumentation(self):
|
||||
# This is thread-safe because database connections are thread-local.
|
||||
|
@ -46,7 +46,7 @@ class DjangoDebugContext(object):
|
|||
unwrap_cursor(connection)
|
||||
|
||||
|
||||
class DjangoDebugMiddleware(object):
|
||||
class DjangoDebugMiddleware:
|
||||
def resolve(self, next, root, info, **args):
|
||||
context = info.context
|
||||
django_debug = getattr(context, "django_debug", None)
|
||||
|
@ -62,10 +62,10 @@ class DjangoDebugMiddleware(object):
|
|||
)
|
||||
)
|
||||
if info.schema.get_type("DjangoDebug") == info.return_type:
|
||||
return context.django_debug.get_debug_promise()
|
||||
return context.django_debug.get_debug_result()
|
||||
try:
|
||||
promise = next(root, info, **args)
|
||||
result = next(root, info, **args)
|
||||
except Exception as e:
|
||||
return context.django_debug.on_resolve_error(e)
|
||||
context.django_debug.add_promise(promise)
|
||||
return promise
|
||||
context.django_debug.add_result(result)
|
||||
return result
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# Code obtained from django-debug-toolbar sql panel tracking
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import json
|
||||
from threading import local
|
||||
|
@ -50,7 +49,7 @@ def unwrap_cursor(connection):
|
|||
del connection._graphene_cursor
|
||||
|
||||
|
||||
class ExceptionCursorWrapper(object):
|
||||
class ExceptionCursorWrapper:
|
||||
"""
|
||||
Wraps a cursor and raises an exception on any operation.
|
||||
Used in Templates panel.
|
||||
|
@ -63,7 +62,7 @@ class ExceptionCursorWrapper(object):
|
|||
raise SQLQueryTriggered()
|
||||
|
||||
|
||||
class NormalCursorWrapper(object):
|
||||
class NormalCursorWrapper:
|
||||
"""
|
||||
Wraps a cursor and logs queries.
|
||||
"""
|
||||
|
@ -85,7 +84,7 @@ class NormalCursorWrapper(object):
|
|||
if not params:
|
||||
return params
|
||||
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))
|
||||
|
||||
def _decode(self, param):
|
||||
|
|
|
@ -8,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware
|
|||
from ..types import DjangoDebug
|
||||
|
||||
|
||||
class context(object):
|
||||
class context:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from functools import partial
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from graphql_relay.connection.arrayconnection import (
|
||||
|
||||
from graphql_relay import (
|
||||
connection_from_array_slice,
|
||||
cursor_to_offset,
|
||||
get_offset_with_default,
|
||||
offset_to_cursor,
|
||||
)
|
||||
|
||||
from promise import Promise
|
||||
|
||||
from graphene import Int, NonNull
|
||||
|
@ -26,7 +28,7 @@ class DjangoListField(Field):
|
|||
_type = _type.of_type
|
||||
|
||||
# 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(
|
||||
self._underlying_type, DjangoObjectType
|
||||
|
@ -61,13 +63,16 @@ class DjangoListField(Field):
|
|||
return queryset
|
||||
|
||||
def wrap_resolve(self, parent_resolver):
|
||||
resolver = super(DjangoListField, self).wrap_resolve(parent_resolver)
|
||||
resolver = super().wrap_resolve(parent_resolver)
|
||||
_type = self.type
|
||||
if isinstance(_type, NonNull):
|
||||
_type = _type.of_type
|
||||
django_object_type = _type.of_type.of_type
|
||||
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,
|
||||
)
|
||||
kwargs.setdefault("offset", Int())
|
||||
super(DjangoConnectionField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
|
@ -144,36 +149,40 @@ class DjangoConnectionField(ConnectionField):
|
|||
iterable = maybe_queryset(iterable)
|
||||
|
||||
if isinstance(iterable, QuerySet):
|
||||
list_length = iterable.count()
|
||||
array_length = iterable.count()
|
||||
else:
|
||||
list_length = len(iterable)
|
||||
list_slice_length = (
|
||||
min(max_limit, list_length) if max_limit is not None else list_length
|
||||
)
|
||||
array_length = len(iterable)
|
||||
|
||||
# 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
|
||||
# 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:
|
||||
if args.get("last", None) is not None:
|
||||
after = list_length - args["last"]
|
||||
else:
|
||||
args["first"] = max_limit
|
||||
# Impose the maximum limit via the `first` field if neither first or last are already provided
|
||||
# (note that if any of them is provided they must be under max_limit otherwise an error is raised).
|
||||
if (
|
||||
max_limit is not None
|
||||
and args.get("first", None) is None
|
||||
and args.get("last", None) is None
|
||||
):
|
||||
args["first"] = max_limit
|
||||
|
||||
connection = connection_from_array_slice(
|
||||
iterable[after:],
|
||||
iterable[slice_start:],
|
||||
args,
|
||||
slice_start=after,
|
||||
array_length=list_length,
|
||||
array_slice_length=list_slice_length,
|
||||
slice_start=slice_start,
|
||||
array_length=array_length,
|
||||
array_slice_length=array_slice_length,
|
||||
connection_type=partial(connection_adapter, connection),
|
||||
edge_type=connection.Edge,
|
||||
page_info_type=page_info_adapter,
|
||||
)
|
||||
connection.iterable = iterable
|
||||
connection.length = list_length
|
||||
connection.length = array_length
|
||||
return connection
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -30,7 +30,7 @@ def convert_enum(data):
|
|||
class DjangoFilterConnectionField(DjangoConnectionField):
|
||||
def __init__(
|
||||
self,
|
||||
type,
|
||||
type_,
|
||||
fields=None,
|
||||
order_by=None,
|
||||
extra_filter_meta=None,
|
||||
|
@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
self._filtering_args = None
|
||||
self._extra_filter_meta = extra_filter_meta
|
||||
self._base_args = None
|
||||
super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
|
||||
super().__init__(type_, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
|
@ -90,9 +90,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
kwargs[k] = convert_enum(v)
|
||||
return kwargs
|
||||
|
||||
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
|
||||
connection, iterable, info, args
|
||||
)
|
||||
qs = super().resolve_queryset(connection, iterable, info, args)
|
||||
|
||||
filterset = filterset_class(
|
||||
data=filter_kwargs(), queryset=qs, request=info.context
|
||||
|
|
|
@ -22,6 +22,6 @@ class ArrayFilter(TypedFilter):
|
|||
return qs
|
||||
if self.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})
|
||||
return qs
|
||||
|
|
|
@ -13,11 +13,11 @@ class GlobalIDFilter(Filter):
|
|||
field_class = GlobalIDFormField
|
||||
|
||||
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
|
||||
if value is not None:
|
||||
_, _id = from_global_id(value)
|
||||
return super(GlobalIDFilter, self).filter(qs, _id)
|
||||
return super().filter(qs, _id)
|
||||
|
||||
|
||||
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
|
||||
|
@ -25,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
|
|||
|
||||
def filter(self, qs, value):
|
||||
gids = [from_global_id(v)[1] for v in value]
|
||||
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
|
||||
return super().filter(qs, gids)
|
||||
|
|
|
@ -23,4 +23,4 @@ class ListFilter(TypedFilter):
|
|||
else:
|
||||
return qs.none()
|
||||
else:
|
||||
return super(ListFilter, self).filter(qs, value)
|
||||
return super().filter(qs, value)
|
||||
|
|
|
@ -12,7 +12,7 @@ class TypedFilter(Filter):
|
|||
|
||||
def __init__(self, input_type=None, *args, **kwargs):
|
||||
self._input_type = input_type
|
||||
super(TypedFilter, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def input_type(self):
|
||||
|
|
|
@ -18,8 +18,8 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
|
|||
|
||||
|
||||
class GrapheneFilterSetMixin(BaseFilterSet):
|
||||
""" A django_filters.filterset.BaseFilterSet with default filter overrides
|
||||
to handle global IDs """
|
||||
"""A django_filters.filterset.BaseFilterSet with default filter overrides
|
||||
to handle global IDs"""
|
||||
|
||||
FILTER_DEFAULTS = dict(
|
||||
itertools.chain(
|
||||
|
@ -29,20 +29,18 @@ class GrapheneFilterSetMixin(BaseFilterSet):
|
|||
|
||||
|
||||
def setup_filterset(filterset_class):
|
||||
""" Wrap a provided filterset in Graphene-specific functionality
|
||||
"""
|
||||
"""Wrap a provided filterset in Graphene-specific functionality"""
|
||||
return type(
|
||||
"Graphene{}".format(filterset_class.__name__),
|
||||
f"Graphene{filterset_class.__name__}",
|
||||
(filterset_class, GrapheneFilterSetMixin),
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
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_class = type(str("Meta"), (object,), meta)
|
||||
meta_class = type("Meta", (object,), meta)
|
||||
filterset = type(
|
||||
str("%sFilterSet" % model._meta.object_name),
|
||||
(filterset_base_class, GrapheneFilterSetMixin),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from mock import MagicMock
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from django.db import models
|
||||
|
@ -87,12 +87,11 @@ def Query(EventType):
|
|||
events = DjangoFilterConnectionField(EventType)
|
||||
|
||||
def resolve_events(self, info, **kwargs):
|
||||
|
||||
events = [
|
||||
Event(name="Live Show", tags=["concert", "music", "rock"],),
|
||||
Event(name="Musical", tags=["movie", "music"],),
|
||||
Event(name="Ballet", tags=["concert", "dance"],),
|
||||
Event(name="Speech", tags=[],),
|
||||
Event(name="Live Show", tags=["concert", "music", "rock"]),
|
||||
Event(name="Musical", tags=["movie", "music"]),
|
||||
Event(name="Ballet", tags=["concert", "dance"]),
|
||||
Event(name="Speech", tags=[]),
|
||||
]
|
||||
|
||||
STORE["events"] = events
|
||||
|
|
|
@ -120,10 +120,7 @@ def test_array_field_filter_schema_type(Query):
|
|||
"randomField": "[Boolean!]",
|
||||
}
|
||||
filters_str = ", ".join(
|
||||
[
|
||||
f"{filter_field}: {gql_type} = null"
|
||||
for filter_field, gql_type in filters.items()
|
||||
]
|
||||
[f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
|
||||
)
|
||||
assert (
|
||||
f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str
|
||||
|
|
|
@ -54,13 +54,13 @@ def reporter_article_data():
|
|||
first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2
|
||||
)
|
||||
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(
|
||||
headline="Article Node 2", reporter=john, editor=john, lang="en",
|
||||
headline="Article Node 2", reporter=john, editor=john, lang="en"
|
||||
)
|
||||
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)
|
||||
assert not result.errors
|
||||
|
@ -152,9 +158,6 @@ def test_filter_enum_field_schema_type(schema):
|
|||
"reporter_AChoice_In": "[TestsReporterAChoiceChoices]",
|
||||
}
|
||||
filters_str = ", ".join(
|
||||
[
|
||||
f"{filter_field}: {gql_type} = null"
|
||||
for filter_field, gql_type in filters.items()
|
||||
]
|
||||
[f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
|
||||
)
|
||||
assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str
|
||||
|
|
|
@ -5,7 +5,7 @@ import pytest
|
|||
from django.db.models import TextField, Value
|
||||
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_django import DjangoObjectType
|
||||
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("_")]
|
||||
assert set(arguments) == set(
|
||||
actual
|
||||
), "Expected arguments ({}) did not match actual ({})".format(arguments, actual)
|
||||
), f"Expected arguments ({arguments}) did not match actual ({actual})"
|
||||
|
||||
|
||||
def assert_orderable(field):
|
||||
|
@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context():
|
|||
|
||||
@property
|
||||
def qs(self):
|
||||
qs = super(ArticleContextFilter, self).qs
|
||||
qs = super().qs
|
||||
return qs.filter(reporter=self.request.reporter)
|
||||
|
||||
class Query(ObjectType):
|
||||
|
@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context():
|
|||
editor=r2,
|
||||
)
|
||||
|
||||
class context(object):
|
||||
class context:
|
||||
reporter = r2
|
||||
|
||||
query = """
|
||||
|
@ -401,7 +401,7 @@ def test_filterset_descriptions():
|
|||
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
|
||||
max_time = field.args["max_time"]
|
||||
assert isinstance(max_time, Argument)
|
||||
assert max_time.type == Float
|
||||
assert max_time.type == Decimal
|
||||
assert max_time.description == "The maximum time"
|
||||
|
||||
|
||||
|
@ -1008,7 +1008,7 @@ def test_integer_field_filter_type():
|
|||
assert str(schema) == dedent(
|
||||
"""\
|
||||
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 {
|
||||
|
@ -1056,8 +1056,7 @@ def test_integer_field_filter_type():
|
|||
interface Node {
|
||||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
|
@ -1077,7 +1076,7 @@ def test_other_filter_types():
|
|||
assert str(schema) == dedent(
|
||||
"""\
|
||||
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 {
|
||||
|
@ -1125,8 +1124,7 @@ def test_other_filter_types():
|
|||
interface Node {
|
||||
\"""The ID of the object\"""
|
||||
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 result.data == expected
|
||||
|
@ -1267,13 +1265,23 @@ def test_filter_string_contains():
|
|||
result = schema.execute(query, variables={"filter": "Ja"})
|
||||
assert not result.errors
|
||||
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"})
|
||||
assert not result.errors
|
||||
assert result.data == {
|
||||
"people": {"edges": [{"node": {"name": "Joe"}}, {"node": {"name": "Bob"}},]}
|
||||
"people": {
|
||||
"edges": [
|
||||
{"node": {"name": "Joe"}},
|
||||
{"node": {"name": "Bob"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -349,19 +349,19 @@ def test_fk_id_in_filter(query):
|
|||
schema = Schema(query=query)
|
||||
|
||||
query = """
|
||||
query {
|
||||
articles (reporter_In: [%s, %s]) {
|
||||
edges {
|
||||
node {
|
||||
query {{
|
||||
articles (reporter_In: [{}, {}]) {{
|
||||
edges {{
|
||||
node {{
|
||||
headline
|
||||
reporter {
|
||||
reporter {{
|
||||
lastName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" % (
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
""".format(
|
||||
john_doe.id,
|
||||
jean_bon.id,
|
||||
)
|
||||
|
|
|
@ -98,20 +98,14 @@ def test_typed_filter_schema(schema):
|
|||
)
|
||||
|
||||
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):
|
||||
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
|
||||
Article.objects.create(
|
||||
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="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")
|
||||
|
||||
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"
|
||||
|
||||
|
|
|
@ -97,7 +97,9 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
|||
field_type = graphene.List(field_type)
|
||||
|
||||
args[name] = graphene.Argument(
|
||||
field_type, description=filter_field.label, required=required,
|
||||
field_type,
|
||||
description=filter_field.label,
|
||||
required=required,
|
||||
)
|
||||
|
||||
return args
|
||||
|
|
|
@ -3,7 +3,19 @@ from functools import singledispatch
|
|||
from django import forms
|
||||
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
|
||||
|
||||
|
@ -57,12 +69,18 @@ def convert_form_field_to_nullboolean(field):
|
|||
return Boolean(description=get_form_field_description(field))
|
||||
|
||||
|
||||
@convert_form_field.register(forms.DecimalField)
|
||||
@convert_form_field.register(forms.FloatField)
|
||||
def convert_form_field_to_float(field):
|
||||
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)
|
||||
def convert_form_field_to_string_list(field):
|
||||
return List(
|
||||
|
|
|
@ -82,7 +82,6 @@ class DjangoFormMutation(BaseDjangoFormMutation):
|
|||
def __init_subclass_with_meta__(
|
||||
cls, form_class=None, only_fields=(), exclude_fields=(), **options
|
||||
):
|
||||
|
||||
if not form_class:
|
||||
raise Exception("form_class is required for DjangoFormMutation")
|
||||
|
||||
|
@ -95,7 +94,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
|
|||
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -117,7 +116,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
errors = graphene.List(ErrorType)
|
||||
errors = graphene.List(graphene.NonNull(ErrorType), required=True)
|
||||
|
||||
@classmethod
|
||||
def __init_subclass_with_meta__(
|
||||
|
@ -127,9 +126,8 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
|||
return_field_name=None,
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
**options
|
||||
**options,
|
||||
):
|
||||
|
||||
if not form_class:
|
||||
raise Exception("form_class is required for DjangoModelFormMutation")
|
||||
|
||||
|
@ -147,7 +145,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
|||
registry = get_global_registry()
|
||||
model_type = registry.get_type_for_model(model)
|
||||
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:
|
||||
model_name = model.__name__
|
||||
|
@ -163,7 +161,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
|||
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from django import forms
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
import graphene
|
||||
from graphene import (
|
||||
String,
|
||||
Int,
|
||||
Boolean,
|
||||
Decimal,
|
||||
Float,
|
||||
ID,
|
||||
UUID,
|
||||
|
@ -97,8 +98,8 @@ def test_should_float_convert_float():
|
|||
assert_conversion(forms.FloatField, Float)
|
||||
|
||||
|
||||
def test_should_decimal_convert_float():
|
||||
assert_conversion(forms.DecimalField, Float)
|
||||
def test_should_decimal_convert_decimal():
|
||||
assert_conversion(forms.DecimalField, Decimal)
|
||||
|
||||
|
||||
def test_should_multiple_choice_convert_list():
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import pytest
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
from graphene import Field, ObjectType, Schema, String
|
||||
from graphene_django import DjangoObjectType
|
||||
|
|
|
@ -48,7 +48,7 @@ class CommandArguments(BaseCommand):
|
|||
class Command(CommandArguments):
|
||||
help = "Dump Graphene schema as a JSON or GraphQL file"
|
||||
can_import_settings = True
|
||||
requires_system_checks = False
|
||||
requires_system_checks = []
|
||||
|
||||
def save_json_file(self, out, schema_dict, indent):
|
||||
with open(out, "w") as outfile:
|
||||
|
@ -63,7 +63,7 @@ class Command(CommandArguments):
|
|||
if out == "-" or out == "-.json":
|
||||
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
||||
elif out == "-.graphql":
|
||||
self.stdout.write(print_schema(schema))
|
||||
self.stdout.write(print_schema(schema.graphql_schema))
|
||||
else:
|
||||
# Determine format
|
||||
_, file_extension = os.path.splitext(out)
|
||||
|
@ -73,16 +73,12 @@ class Command(CommandArguments):
|
|||
elif file_extension == ".json":
|
||||
self.save_json_file(out, schema_dict, indent)
|
||||
else:
|
||||
raise CommandError(
|
||||
'Unrecognised file format "{}"'.format(file_extension)
|
||||
)
|
||||
raise CommandError(f'Unrecognised file format "{file_extension}"')
|
||||
|
||||
style = getattr(self, "style", None)
|
||||
success = getattr(style, "SUCCESS", lambda x: x)
|
||||
|
||||
self.stdout.write(
|
||||
success("Successfully dumped GraphQL schema to {}".format(out))
|
||||
)
|
||||
self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}"))
|
||||
|
||||
def handle(self, *args, **options):
|
||||
options_schema = options.get("schema")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Registry(object):
|
||||
class Registry:
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
self._field_registry = {}
|
||||
|
|
|
@ -72,7 +72,6 @@ class SerializerMutation(ClientIDMutation):
|
|||
_meta=None,
|
||||
**options
|
||||
):
|
||||
|
||||
if not serializer_class:
|
||||
raise Exception("serializer_class is required for the SerializerMutation")
|
||||
|
||||
|
@ -114,7 +113,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class):
|
|||
for name, field in serializer.fields.items()
|
||||
}
|
||||
ret_type = type(
|
||||
"{}Input".format(serializer.__class__.__name__),
|
||||
f"{serializer.__class__.__name__}Input",
|
||||
(graphene.InputObjectType,),
|
||||
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.DecimalField)
|
||||
def convert_serializer_field_to_float(field):
|
||||
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)
|
||||
def convert_serializer_field_to_datetime_time(field):
|
||||
return graphene.types.datetime.DateTime
|
||||
|
|
|
@ -3,7 +3,7 @@ import copy
|
|||
import graphene
|
||||
from django.db import models
|
||||
from graphene import InputObjectType
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..serializer_converter import convert_serializer_field
|
||||
|
@ -133,9 +133,9 @@ def test_should_float_convert_float():
|
|||
assert_conversion(serializers.FloatField, graphene.Float)
|
||||
|
||||
|
||||
def test_should_decimal_convert_float():
|
||||
def test_should_decimal_convert_decimal():
|
||||
assert_conversion(
|
||||
serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2
|
||||
serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import datetime
|
||||
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from graphene import Field, ResolveInfo
|
||||
|
|
|
@ -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
|
||||
back to the defaults.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.signals import setting_changed
|
||||
|
@ -41,7 +40,9 @@ DEFAULTS = {
|
|||
# This sets headerEditorEnabled GraphiQL option, for details go to
|
||||
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
||||
"GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
|
||||
"ATOMIC_MUTATIONS": False,
|
||||
"TESTING_ENDPOINT": "/graphql",
|
||||
}
|
||||
|
||||
if settings.DEBUG:
|
||||
|
@ -76,7 +77,7 @@ def import_from_string(val, setting_name):
|
|||
module = importlib.import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
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,
|
||||
setting_name,
|
||||
e.__class__.__name__,
|
||||
|
@ -85,7 +86,7 @@ def import_from_string(val, setting_name):
|
|||
raise ImportError(msg)
|
||||
|
||||
|
||||
class GrapheneSettings(object):
|
||||
class GrapheneSettings:
|
||||
"""
|
||||
A settings object, that allows API settings to be accessed as properties.
|
||||
For example:
|
||||
|
|
|
@ -5,19 +5,12 @@
|
|||
GraphiQL,
|
||||
React,
|
||||
ReactDOM,
|
||||
SubscriptionsTransportWs,
|
||||
graphqlWs,
|
||||
GraphiQLPluginExplorer,
|
||||
fetch,
|
||||
history,
|
||||
location,
|
||||
) {
|
||||
// Parse the cookie value for a CSRF token
|
||||
var csrftoken;
|
||||
var cookies = ("; " + document.cookie).split("; csrftoken=");
|
||||
if (cookies.length == 2) {
|
||||
csrftoken = cookies.pop().split(";").shift();
|
||||
} else {
|
||||
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||
}
|
||||
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
|
@ -60,98 +53,34 @@
|
|||
|
||||
var fetchURL = locationQuery(otherParams);
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function httpClient(graphQLParams, opts) {
|
||||
if (typeof opts === 'undefined') {
|
||||
opts = {};
|
||||
}
|
||||
var headers = opts.headers || {};
|
||||
headers['Accept'] = headers['Accept'] || 'application/json';
|
||||
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
||||
if (csrftoken) {
|
||||
headers['X-CSRFToken'] = csrftoken
|
||||
}
|
||||
return fetch(fetchURL, {
|
||||
method: "post",
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: "include",
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.text();
|
||||
})
|
||||
.then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
|
||||
// assumes the current window location with an appropriate websocket protocol.
|
||||
var subscribeURL =
|
||||
location.origin.replace(/^http/, "ws") +
|
||||
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
|
||||
|
||||
// Create a subscription client.
|
||||
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
|
||||
subscribeURL,
|
||||
{
|
||||
// Reconnect after any interruptions.
|
||||
reconnect: true,
|
||||
// Delay socket initialization until the first subscription is started.
|
||||
function trueLambda() { return true; };
|
||||
|
||||
var headers = {};
|
||||
var cookies = ("; " + document.cookie).split("; csrftoken=");
|
||||
if (cookies.length == 2) {
|
||||
csrftoken = cookies.pop().split(";").shift();
|
||||
} else {
|
||||
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||
}
|
||||
if (csrftoken) {
|
||||
headers['X-CSRFToken'] = csrftoken
|
||||
}
|
||||
|
||||
var graphQLFetcher = GraphiQL.createFetcher({
|
||||
url: fetchURL,
|
||||
wsClient: graphqlWs.createClient({
|
||||
url: subscribeURL,
|
||||
shouldRetry: trueLambda,
|
||||
lazy: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Keep a reference to the currently-active subscription, if available.
|
||||
var activeSubscription = null;
|
||||
|
||||
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
|
||||
function graphQLFetcher(graphQLParams, opts) {
|
||||
var operationType = getOperationType(graphQLParams);
|
||||
|
||||
// If we're about to execute a new operation, and we have an active subscription,
|
||||
// unsubscribe before continuing.
|
||||
if (activeSubscription) {
|
||||
activeSubscription.unsubscribe();
|
||||
activeSubscription = null;
|
||||
}
|
||||
|
||||
if (operationType === "subscription") {
|
||||
return {
|
||||
subscribe: function (observer) {
|
||||
activeSubscription = subscriptionClient;
|
||||
return subscriptionClient.request(graphQLParams, opts).subscribe(observer);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return httpClient(graphQLParams, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the type of operation being executed for a given set of GraphQL parameters.
|
||||
function getOperationType(graphQLParams) {
|
||||
// Run a regex against the query to determine the operation type (query, mutation, subscription).
|
||||
var operationRegex = new RegExp(
|
||||
// Look for lines that start with an operation keyword, ignoring whitespace.
|
||||
"^\\s*(query|mutation|subscription)\\s*" +
|
||||
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available).
|
||||
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") +
|
||||
// The line should eventually encounter an opening curly brace.
|
||||
"[^\\{]*\\{",
|
||||
// Enable multiline matching.
|
||||
"m",
|
||||
);
|
||||
var match = operationRegex.exec(graphQLParams.query);
|
||||
if (!match) {
|
||||
return "query";
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
}),
|
||||
headers: headers
|
||||
})
|
||||
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
|
@ -170,23 +99,44 @@
|
|||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
var options = {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
|
||||
query: parameters.query,
|
||||
};
|
||||
if (parameters.variables) {
|
||||
options.variables = parameters.variables;
|
||||
}
|
||||
if (parameters.operation_name) {
|
||||
options.operationName = parameters.operation_name;
|
||||
|
||||
function GraphiQLWithExplorer() {
|
||||
var [query, setQuery] = React.useState(parameters.query);
|
||||
|
||||
function handleQuery(query) {
|
||||
setQuery(query);
|
||||
onEditQuery(query);
|
||||
}
|
||||
|
||||
var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({
|
||||
query: query,
|
||||
onEdit: handleQuery,
|
||||
});
|
||||
|
||||
var options = {
|
||||
fetcher: graphQLFetcher,
|
||||
plugins: [explorerPlugin],
|
||||
defaultEditorToolsVisibility: true,
|
||||
onEditQuery: handleQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
|
||||
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
|
||||
query: query,
|
||||
};
|
||||
if (parameters.variables) {
|
||||
options.variables = parameters.variables;
|
||||
}
|
||||
if (parameters.operation_name) {
|
||||
options.operationName = parameters.operation_name;
|
||||
}
|
||||
|
||||
return React.createElement(GraphiQL, options);
|
||||
}
|
||||
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, options),
|
||||
React.createElement(GraphiQLWithExplorer),
|
||||
document.getElementById("editor"),
|
||||
);
|
||||
})(
|
||||
|
@ -196,7 +146,8 @@
|
|||
window.GraphiQL,
|
||||
window.React,
|
||||
window.ReactDOM,
|
||||
window.SubscriptionsTransportWs,
|
||||
window.graphqlWs,
|
||||
window.GraphiQLPluginExplorer,
|
||||
window.fetch,
|
||||
window.history,
|
||||
window.location,
|
||||
|
|
|
@ -33,9 +33,12 @@ add "&raw" to the end of the URL within a browser.
|
|||
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
||||
integrity="{{graphiql_sri}}"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js"
|
||||
<script src="https://cdn.jsdelivr.net/npm/graphql-ws@{{subscriptions_transport_ws_version}}/umd/graphql-ws.min.js"
|
||||
integrity="{{subscriptions_transport_ws_sri}}"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/graphiql-plugin-explorer.umd.js"
|
||||
integrity="{{graphiql_plugin_explorer_sri}}"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
|
@ -46,6 +49,7 @@ add "&raw" to the end of the URL within a browser.
|
|||
subscriptionPath: "{{subscription_path}}",
|
||||
{% endif %}
|
||||
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
|
||||
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
|
||||
};
|
||||
</script>
|
||||
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||
|
|
|
@ -8,8 +8,8 @@ import graphene
|
|||
|
||||
from graphene import Field, ResolveInfo
|
||||
from graphene.types.inputobjecttype import InputObjectType
|
||||
from py.test import raises
|
||||
from py.test import mark
|
||||
from pytest import raises
|
||||
from pytest import mark
|
||||
from rest_framework import serializers
|
||||
|
||||
from ...types import DjangoObjectType
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -13,6 +11,9 @@ class Person(models.Model):
|
|||
class Pet(models.Model):
|
||||
name = models.CharField(max_length=30)
|
||||
age = models.PositiveIntegerField()
|
||||
owner = models.ForeignKey(
|
||||
"Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets"
|
||||
)
|
||||
|
||||
|
||||
class FilmDetails(models.Model):
|
||||
|
@ -34,7 +35,7 @@ class Film(models.Model):
|
|||
|
||||
class DoeReporterManager(models.Manager):
|
||||
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):
|
||||
|
@ -54,7 +55,7 @@ class Reporter(models.Model):
|
|||
)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -64,7 +65,7 @@ class Reporter(models.Model):
|
|||
when a CNNReporter is pulled from the database, it is still
|
||||
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
|
||||
self.__class__ = CNNReporter
|
||||
|
||||
|
@ -74,7 +75,7 @@ class Reporter(models.Model):
|
|||
|
||||
class CNNReporterManager(models.Manager):
|
||||
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):
|
||||
|
|
|
@ -5,7 +5,6 @@ from .mutations import PetFormMutation, PetMutation
|
|||
|
||||
|
||||
class QueryRoot(ObjectType):
|
||||
|
||||
thrower = graphene.String(required=True)
|
||||
request = graphene.String(required=True)
|
||||
test = graphene.String(who=graphene.String())
|
||||
|
|
|
@ -2,7 +2,7 @@ from textwrap import dedent
|
|||
|
||||
from django.core import management
|
||||
from io import StringIO
|
||||
from mock import mock_open, patch
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
from graphene import ObjectType, Schema, String
|
||||
|
||||
|
@ -53,6 +53,5 @@ def test_generate_graphql_file_on_call_graphql_schema():
|
|||
"""\
|
||||
type Query {
|
||||
hi: String
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
|
|
@ -3,13 +3,14 @@ from collections import namedtuple
|
|||
import pytest
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
import graphene
|
||||
from graphene import NonNull
|
||||
from graphene.relay import ConnectionField, Node
|
||||
from graphene.types.datetime import Date, DateTime, Time
|
||||
from graphene.types.json import JSONString
|
||||
from graphene.types.scalars import BigInt
|
||||
|
||||
from ..compat import (
|
||||
ArrayField,
|
||||
|
@ -140,8 +141,8 @@ def test_should_small_integer_convert_int():
|
|||
assert_conversion(models.SmallIntegerField, graphene.Int)
|
||||
|
||||
|
||||
def test_should_big_integer_convert_int():
|
||||
assert_conversion(models.BigIntegerField, graphene.Int)
|
||||
def test_should_big_integer_convert_big_int():
|
||||
assert_conversion(models.BigIntegerField, BigInt)
|
||||
|
||||
|
||||
def test_should_integer_convert_int():
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
from django.db.models import Count
|
||||
import re
|
||||
from django.db.models import Count, Prefetch
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -7,8 +8,12 @@ from graphene import List, NonNull, ObjectType, Schema, String
|
|||
|
||||
from ..fields import DjangoListField
|
||||
from ..types import DjangoObjectType
|
||||
from .models import Article as ArticleModel
|
||||
from .models import Reporter as ReporterModel
|
||||
from .models import (
|
||||
Article as ArticleModel,
|
||||
Film as FilmModel,
|
||||
FilmDetails as FilmDetailsModel,
|
||||
Reporter as ReporterModel,
|
||||
)
|
||||
|
||||
|
||||
class TestDjangoListField:
|
||||
|
@ -500,3 +505,145 @@ class TestDjangoListField:
|
|||
|
||||
assert not result.errors
|
||||
assert result.data == {"reporters": [{"firstName": "Tara"}]}
|
||||
|
||||
def test_select_related_and_prefetch_related_are_respected(
|
||||
self, django_assert_num_queries
|
||||
):
|
||||
class Article(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ArticleModel
|
||||
fields = ("headline", "editor", "reporter")
|
||||
|
||||
class Film(DjangoObjectType):
|
||||
class Meta:
|
||||
model = FilmModel
|
||||
fields = ("genre", "details")
|
||||
|
||||
class FilmDetail(DjangoObjectType):
|
||||
class Meta:
|
||||
model = FilmDetailsModel
|
||||
fields = ("location",)
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = ("first_name", "articles", "films")
|
||||
|
||||
class Query(ObjectType):
|
||||
articles = DjangoListField(Article)
|
||||
|
||||
@staticmethod
|
||||
def resolve_articles(root, info):
|
||||
# Optimize for querying associated editors and reporters, and the films and film
|
||||
# details of those reporters. This is similar to what would happen using a library
|
||||
# like https://github.com/tfoxy/graphene-django-optimizer for a query like the one
|
||||
# below (albeit simplified and hardcoded here).
|
||||
return ArticleModel.objects.select_related(
|
||||
"editor", "reporter"
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"reporter__films",
|
||||
queryset=FilmModel.objects.select_related("details"),
|
||||
),
|
||||
)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
query = """
|
||||
query {
|
||||
articles {
|
||||
headline
|
||||
|
||||
editor {
|
||||
firstName
|
||||
}
|
||||
|
||||
reporter {
|
||||
firstName
|
||||
|
||||
films {
|
||||
genre
|
||||
|
||||
details {
|
||||
location
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
|
||||
r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne")
|
||||
|
||||
ArticleModel.objects.create(
|
||||
headline="Amazing news",
|
||||
reporter=r1,
|
||||
pub_date=datetime.date.today(),
|
||||
pub_date_time=datetime.datetime.now(),
|
||||
editor=r2,
|
||||
)
|
||||
ArticleModel.objects.create(
|
||||
headline="Not so good news",
|
||||
reporter=r2,
|
||||
pub_date=datetime.date.today(),
|
||||
pub_date_time=datetime.datetime.now(),
|
||||
editor=r1,
|
||||
)
|
||||
|
||||
film1 = FilmModel.objects.create(genre="ac")
|
||||
film2 = FilmModel.objects.create(genre="ot")
|
||||
film3 = FilmModel.objects.create(genre="do")
|
||||
FilmDetailsModel.objects.create(location="Hollywood", film=film1)
|
||||
FilmDetailsModel.objects.create(location="Antarctica", film=film3)
|
||||
r1.films.add(film1, film2)
|
||||
r2.films.add(film3)
|
||||
|
||||
# We expect 2 queries to be performed based on the above resolver definition: one for all
|
||||
# articles joined with the reporters model (for associated editors and reporters), and one
|
||||
# for the films prefetch (which includes its `select_related` JOIN logic in its queryset)
|
||||
with django_assert_num_queries(2) as captured:
|
||||
result = schema.execute(query)
|
||||
|
||||
assert not result.errors
|
||||
assert result.data == {
|
||||
"articles": [
|
||||
{
|
||||
"headline": "Amazing news",
|
||||
"editor": {"firstName": "Debra"},
|
||||
"reporter": {
|
||||
"firstName": "Tara",
|
||||
"films": [
|
||||
{"genre": "AC", "details": {"location": "Hollywood"}},
|
||||
{"genre": "OT", "details": None},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"headline": "Not so good news",
|
||||
"editor": {"firstName": "Tara"},
|
||||
"reporter": {
|
||||
"firstName": "Debra",
|
||||
"films": [
|
||||
{"genre": "DO", "details": {"location": "Antarctica"}},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
assert len(captured.captured_queries) == 2 # Sanity-check
|
||||
|
||||
# First we should have queried for all articles in a single query, joining on the reporters
|
||||
# model (for the editors and reporters ForeignKeys)
|
||||
assert re.match(
|
||||
r'SELECT .* "tests_article" INNER JOIN "tests_reporter"',
|
||||
captured.captured_queries[0]["sql"],
|
||||
)
|
||||
|
||||
# Then we should have queried for all of the films of all reporters, joined with the film
|
||||
# details for each film, using a single query
|
||||
assert re.match(
|
||||
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
|
||||
captured.captured_queries[1]["sql"],
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||
|
||||
|
|
235
graphene_django/tests/test_get_queryset.py
Normal file
235
graphene_django/tests/test_get_queryset.py
Normal file
|
@ -0,0 +1,235 @@
|
|||
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)
|
||||
|
||||
NOTE: For now, we do not expect this get_queryset method to be called for nested
|
||||
objects, as the original attempt to do so prevented SQL query-optimization with
|
||||
`select_related`/`prefetch_related` and caused N+1 queries. See discussions here
|
||||
https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857
|
||||
and here https://github.com/graphql-python/graphene-django/pull/1401.
|
||||
"""
|
||||
|
||||
@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"}}
|
||||
|
||||
|
||||
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"}}
|
|
@ -6,7 +6,7 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from graphql_relay import to_global_id
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
import graphene
|
||||
from graphene.relay import Node
|
||||
|
@ -15,7 +15,7 @@ from ..compat import IntegerRangeField, MissingType
|
|||
from ..fields import DjangoConnectionField
|
||||
from ..types import DjangoObjectType
|
||||
from ..utils import DJANGO_FILTER_INSTALLED
|
||||
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
|
||||
from .models import Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter
|
||||
|
||||
|
||||
def test_should_query_only_fields():
|
||||
|
@ -251,8 +251,8 @@ def test_should_node():
|
|||
|
||||
|
||||
def test_should_query_onetoone_fields():
|
||||
film = Film(id=1)
|
||||
film_details = FilmDetails(id=1, film=film)
|
||||
film = Film.objects.create(id=1)
|
||||
film_details = FilmDetails.objects.create(id=1, film=film)
|
||||
|
||||
class FilmNode(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -780,7 +780,6 @@ def test_should_query_promise_connectionfields():
|
|||
|
||||
|
||||
def test_should_query_connectionfields_with_last():
|
||||
|
||||
r = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
@ -818,7 +817,6 @@ def test_should_query_connectionfields_with_last():
|
|||
|
||||
|
||||
def test_should_query_connectionfields_with_manager():
|
||||
|
||||
r = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
@ -1151,9 +1149,9 @@ def test_connection_should_limit_after_to_list_length():
|
|||
|
||||
REPORTERS = [
|
||||
dict(
|
||||
first_name="First {}".format(i),
|
||||
last_name="Last {}".format(i),
|
||||
email="johndoe+{}@example.com".format(i),
|
||||
first_name=f"First {i}",
|
||||
last_name=f"Last {i}",
|
||||
email=f"johndoe+{i}@example.com",
|
||||
a_choice=1,
|
||||
)
|
||||
for i in range(6)
|
||||
|
@ -1243,6 +1241,7 @@ def test_should_have_next_page(graphene_settings):
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_limit", [100, 4])
|
||||
class TestBackwardPagination:
|
||||
def setup_schema(self, graphene_settings, max_limit):
|
||||
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
|
||||
|
@ -1261,8 +1260,8 @@ class TestBackwardPagination:
|
|||
schema = graphene.Schema(query=Query)
|
||||
return schema
|
||||
|
||||
def do_queries(self, schema):
|
||||
# Simply last 3
|
||||
def test_query_last(self, graphene_settings, max_limit):
|
||||
schema = self.setup_schema(graphene_settings, max_limit=max_limit)
|
||||
query_last = """
|
||||
query {
|
||||
allReporters(last: 3) {
|
||||
|
@ -1282,7 +1281,8 @@ class TestBackwardPagination:
|
|||
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
|
||||
] == ["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 {
|
||||
allReporters(first: 4, last: 3) {
|
||||
|
@ -1302,7 +1302,8 @@ class TestBackwardPagination:
|
|||
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
|
||||
] == ["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 queryAfter($after: String) {
|
||||
allReporters(first: 4, last: 3, after: $after) {
|
||||
|
@ -1317,7 +1318,8 @@ class TestBackwardPagination:
|
|||
|
||||
after = base64.b64encode(b"arrayconnection:0").decode()
|
||||
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 len(result.data["allReporters"]["edges"]) == 3
|
||||
|
@ -1325,20 +1327,35 @@ class TestBackwardPagination:
|
|||
e["node"]["firstName"] for e in result.data["allReporters"]["edges"]
|
||||
] == ["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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
schema = self.setup_schema(graphene_settings, max_limit=4)
|
||||
self.do_queries(schema)
|
||||
result = schema.execute(
|
||||
query_first_last_and_after,
|
||||
)
|
||||
assert not result.errors
|
||||
assert len(result.data["allReporters"]["edges"]) == 1
|
||||
assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5"
|
||||
|
||||
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):
|
||||
|
@ -1480,7 +1497,11 @@ def test_connection_should_enable_offset_filtering():
|
|||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
expected = {
|
||||
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
|
||||
"allReporters": {
|
||||
"edges": [
|
||||
{"node": {"firstName": "Some", "lastName": "Guy"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
assert result.data == expected
|
||||
|
||||
|
@ -1521,7 +1542,9 @@ def test_connection_should_enable_offset_filtering_higher_than_max_limit(
|
|||
assert not result.errors
|
||||
expected = {
|
||||
"allReporters": {
|
||||
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
|
||||
"edges": [
|
||||
{"node": {"firstName": "Some", "lastName": "Lady"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
assert result.data == expected
|
||||
|
@ -1590,6 +1613,149 @@ def test_connection_should_allow_offset_filtering_with_after():
|
|||
result = schema.execute(query, variable_values=dict(after=after))
|
||||
assert not result.errors
|
||||
expected = {
|
||||
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
|
||||
"allReporters": {
|
||||
"edges": [
|
||||
{"node": {"firstName": "Jane", "lastName": "Roe"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
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"}],
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from py.test import raises
|
||||
from pytest import raises
|
||||
|
||||
from ..registry import Registry
|
||||
from ..types import DjangoObjectType
|
||||
|
|
|
@ -3,7 +3,7 @@ from textwrap import dedent
|
|||
|
||||
import pytest
|
||||
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.relay import Node
|
||||
|
@ -104,7 +104,7 @@ def test_django_objecttype_with_custom_meta():
|
|||
@classmethod
|
||||
def __init_subclass_with_meta__(cls, **options):
|
||||
options.setdefault("_meta", ArticleTypeOptions(cls))
|
||||
super(ArticleType, cls).__init_subclass_with_meta__(**options)
|
||||
super().__init_subclass_with_meta__(**options)
|
||||
|
||||
class Article(ArticleType):
|
||||
class Meta:
|
||||
|
@ -183,7 +183,7 @@ def test_schema_representation():
|
|||
pets: [Reporter!]!
|
||||
aChoice: TestsReporterAChoiceChoices
|
||||
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.\"""
|
||||
|
@ -244,8 +244,7 @@ def test_schema_representation():
|
|||
\"""The ID of the object\"""
|
||||
id: ID!
|
||||
): Node
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
assert str(schema) == expected
|
||||
|
||||
|
@ -485,7 +484,7 @@ def test_django_objecttype_neither_fields_nor_exclude():
|
|||
|
||||
|
||||
def custom_enum_name(field):
|
||||
return "CustomEnum{}".format(field.name.title())
|
||||
return f"CustomEnum{field.name.title()}"
|
||||
|
||||
|
||||
class TestDjangoObjectType:
|
||||
|
@ -525,8 +524,7 @@ class TestDjangoObjectType:
|
|||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
||||
|
@ -560,8 +558,7 @@ class TestDjangoObjectType:
|
|||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
||||
|
@ -586,8 +583,7 @@ class TestDjangoObjectType:
|
|||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_naming_collisions(
|
||||
|
@ -621,8 +617,7 @@ class TestDjangoObjectType:
|
|||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_choices_custom_enum_name(
|
||||
|
@ -660,8 +655,7 @@ class TestDjangoObjectType:
|
|||
|
||||
\"""Dog\"""
|
||||
DOG
|
||||
}
|
||||
"""
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import json
|
|||
|
||||
import pytest
|
||||
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 .models import Film, Reporter
|
||||
|
@ -11,11 +11,11 @@ from ..utils.testing import graphql_query
|
|||
|
||||
def test_get_model_fields_no_duplication():
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -54,7 +54,7 @@ def test_graphql_test_case_operation_name(post_mock):
|
|||
tc._pre_setup()
|
||||
tc.setUpClass()
|
||||
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
|
||||
assert (
|
||||
"operationName",
|
||||
|
@ -66,7 +66,7 @@ def test_graphql_test_case_operation_name(post_mock):
|
|||
@patch("graphene_django.utils.testing.Client.post")
|
||||
def test_graphql_query_case_operation_name(post_mock):
|
||||
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
|
||||
assert (
|
||||
"operationName",
|
||||
|
|
|
@ -2,7 +2,7 @@ import json
|
|||
|
||||
import pytest
|
||||
|
||||
from mock import patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.db import connection
|
||||
|
||||
|
@ -109,12 +109,10 @@ def test_reports_validation_errors(client):
|
|||
{
|
||||
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
|
||||
"locations": [{"line": 1, "column": 9}],
|
||||
"path": None,
|
||||
},
|
||||
{
|
||||
"message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
|
||||
"locations": [{"line": 1, "column": 21}],
|
||||
"path": None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -135,8 +133,6 @@ def test_errors_when_missing_operation_name(client):
|
|||
"errors": [
|
||||
{
|
||||
"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}],
|
||||
"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 mocked_read(*args):
|
||||
raise IOError("foo-bar")
|
||||
raise OSError("foo-bar")
|
||||
|
||||
monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from ..views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^graphql/batch", GraphQLView.as_view(batch=True)),
|
||||
url(r"^graphql", GraphQLView.as_view(graphiql=True)),
|
||||
path("graphql/batch", GraphQLView.as_view(batch=True)),
|
||||
path("graphql", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from ..views import GraphQLView
|
||||
from .schema_view import schema
|
||||
|
@ -10,4 +10,4 @@ class CustomGraphQLView(GraphQLView):
|
|||
pretty = True
|
||||
|
||||
|
||||
urlpatterns = [url(r"^graphql/inherited/$", CustomGraphQLView.as_view())]
|
||||
urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from ..views import GraphQLView
|
||||
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))]
|
||||
|
|
|
@ -122,7 +122,7 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields):
|
|||
|
||||
|
||||
class DjangoObjectTypeOptions(ObjectTypeOptions):
|
||||
model = None # type: Model
|
||||
model = None # type: Type[Model]
|
||||
registry = None # type: Registry
|
||||
connection = None # type: Type[Connection]
|
||||
|
||||
|
@ -168,10 +168,8 @@ class DjangoObjectType(ObjectType):
|
|||
|
||||
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
|
||||
raise Exception(
|
||||
(
|
||||
"Can only set filter_fields or filterset_class if "
|
||||
"Django-Filter is installed"
|
||||
)
|
||||
"Can only set filter_fields or filterset_class if "
|
||||
"Django-Filter is installed"
|
||||
)
|
||||
|
||||
assert not (fields and exclude), (
|
||||
|
@ -216,7 +214,7 @@ class DjangoObjectType(ObjectType):
|
|||
"Creating a DjangoObjectType without either the `fields` "
|
||||
"or the `exclude` option is deprecated. Add an explicit `fields "
|
||||
"= '__all__'` option on DjangoObjectType {class_name} to use all "
|
||||
"fields".format(class_name=cls.__name__,),
|
||||
"fields".format(class_name=cls.__name__),
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
@ -228,7 +226,7 @@ class DjangoObjectType(ObjectType):
|
|||
|
||||
if use_connection is None and interfaces:
|
||||
use_connection = any(
|
||||
(issubclass(interface, Node) for interface in interfaces)
|
||||
issubclass(interface, Node) for interface in interfaces
|
||||
)
|
||||
|
||||
if use_connection and not connection:
|
||||
|
@ -255,7 +253,7 @@ class DjangoObjectType(ObjectType):
|
|||
_meta.fields = django_fields
|
||||
_meta.connection = connection
|
||||
|
||||
super(DjangoObjectType, cls).__init_subclass_with_meta__(
|
||||
super().__init_subclass_with_meta__(
|
||||
_meta=_meta, interfaces=interfaces, **options
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ import warnings
|
|||
|
||||
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(
|
||||
|
@ -19,7 +21,7 @@ def graphql_query(
|
|||
Args:
|
||||
query (string) - GraphQL query to run
|
||||
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).
|
||||
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||
to this value. If both ``input_data`` and ``variables``,
|
||||
|
@ -40,7 +42,7 @@ def graphql_query(
|
|||
if client is None:
|
||||
client = Client()
|
||||
if not graphql_url:
|
||||
graphql_url = DEFAULT_GRAPHQL_URL
|
||||
graphql_url = graphene_settings.TESTING_ENDPOINT
|
||||
|
||||
body = {"query": query}
|
||||
if operation_name:
|
||||
|
@ -63,13 +65,13 @@ def graphql_query(
|
|||
return resp
|
||||
|
||||
|
||||
class GraphQLTestMixin(object):
|
||||
class GraphQLTestMixin:
|
||||
"""
|
||||
Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/
|
||||
"""
|
||||
|
||||
# URL to graphql endpoint
|
||||
GRAPHQL_URL = DEFAULT_GRAPHQL_URL
|
||||
GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT
|
||||
|
||||
def query(
|
||||
self, query, operation_name=None, input_data=None, variables=None, headers=None
|
||||
|
@ -78,7 +80,7 @@ class GraphQLTestMixin(object):
|
|||
Args:
|
||||
query (string) - GraphQL query to run
|
||||
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).
|
||||
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||
to this value. If both ``input_data`` and ``variables``,
|
||||
|
@ -89,7 +91,7 @@ class GraphQLTestMixin(object):
|
|||
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
||||
will be set to this value. Keys should be prepended with
|
||||
"HTTP_" (e.g. to specify the "Authorization" HTTP header,
|
||||
use "HTTP_AUTHORIZATION" as the key).
|
||||
use "HTTP_AUTHORIZATION" as the key).
|
||||
|
||||
Returns:
|
||||
Response object from client
|
||||
|
|
|
@ -6,4 +6,4 @@ def test_to_const():
|
|||
|
||||
|
||||
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"
|
||||
|
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
|
||||
from .. import GraphQLTestCase
|
||||
from ...tests.test_types import with_local_registry
|
||||
from ...settings import graphene_settings
|
||||
from django.test import Client
|
||||
|
||||
|
||||
|
@ -43,3 +44,11 @@ def test_graphql_test_case_deprecated_client_setter():
|
|||
|
||||
with pytest.warns(PendingDeprecationWarning):
|
||||
tc._client = Client()
|
||||
|
||||
|
||||
def test_graphql_test_case_imports_endpoint():
|
||||
"""
|
||||
GraphQLTestCase class should import the default endpoint from settings file
|
||||
"""
|
||||
|
||||
assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT
|
||||
|
|
|
@ -11,7 +11,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
|||
from django.views.generic import View
|
||||
from graphql import OperationType, get_operation_ast, parse, validate
|
||||
from graphql.error import GraphQLError
|
||||
from graphql.error import format_error as format_graphql_error
|
||||
from graphql.execution import ExecutionResult
|
||||
|
||||
from graphene import Schema
|
||||
|
@ -27,7 +26,7 @@ class HttpError(Exception):
|
|||
def __init__(self, response, message=None, *args, **kwargs):
|
||||
self.response = response
|
||||
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):
|
||||
|
@ -67,16 +66,19 @@ class GraphQLView(View):
|
|||
react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0="
|
||||
|
||||
# The GraphiQL React app.
|
||||
graphiql_version = "1.4.1" # "1.0.3"
|
||||
graphiql_sri = "sha256-JUMkXBQWZMfJ7fGEsTXalxVA10lzKOS9loXdLjwZKi4=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
||||
graphiql_css_sri = "sha256-Md3vdR7PDzWyo/aGfsFVF4tvS5/eAUWuIsg9QHUusCY=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
||||
graphiql_version = "2.4.1" # "1.0.3"
|
||||
graphiql_sri = "sha256-s+f7CFAPSUIygFnRC2nfoiEKd3liCUy+snSdYFAoLUc=" # "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
||||
graphiql_css_sri = "sha256-88yn8FJMyGboGs4Bj+Pbb3kWOWXo7jmb+XCRHE+282k=" # "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
||||
|
||||
# The websocket transport library for subscriptions.
|
||||
subscriptions_transport_ws_version = "0.9.18"
|
||||
subscriptions_transport_ws_version = "5.12.1"
|
||||
subscriptions_transport_ws_sri = (
|
||||
"sha256-i0hAXd4PdJ/cHX3/8tIy/Q/qKiWr5WSTxMFuL9tACkw="
|
||||
"sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw="
|
||||
)
|
||||
|
||||
graphiql_plugin_explorer_version = "0.1.15"
|
||||
graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE="
|
||||
|
||||
schema = None
|
||||
graphiql = False
|
||||
middleware = None
|
||||
|
@ -159,10 +161,13 @@ class GraphQLView(View):
|
|||
graphiql_css_sri=self.graphiql_css_sri,
|
||||
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
|
||||
subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri,
|
||||
graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version,
|
||||
graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri,
|
||||
# The SUBSCRIPTION_PATH setting.
|
||||
subscription_path=self.subscription_path,
|
||||
# GraphiQL headers tab,
|
||||
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
|
||||
graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS,
|
||||
)
|
||||
|
||||
if self.batch:
|
||||
|
@ -387,7 +392,7 @@ class GraphQLView(View):
|
|||
@staticmethod
|
||||
def format_error(error):
|
||||
if isinstance(error, GraphQLError):
|
||||
return format_graphql_error(error)
|
||||
return error.formatted
|
||||
|
||||
return {"message": str(error)}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ test=pytest
|
|||
universal=1
|
||||
|
||||
[flake8]
|
||||
exclude = docs,graphene_django/debug/sql/*,migrations
|
||||
exclude = docs,graphene_django/debug/sql/*
|
||||
max-line-length = 120
|
||||
select =
|
||||
# Dictionary key repeated
|
||||
|
|
28
setup.py
28
setup.py
|
@ -14,22 +14,23 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
|
|||
|
||||
|
||||
tests_require = [
|
||||
"pytest>=3.6.3",
|
||||
"pytest>=7.3.1",
|
||||
"pytest-cov",
|
||||
"pytest-random-order",
|
||||
"coveralls",
|
||||
"mock",
|
||||
"pytz",
|
||||
"django-filter>=2",
|
||||
"pytest-django>=3.3.2",
|
||||
"django-filter>=22.1",
|
||||
"pytest-django>=4.5.2",
|
||||
] + rest_framework_require
|
||||
|
||||
|
||||
dev_requires = [
|
||||
"black==19.10b0",
|
||||
"flake8==3.7.9",
|
||||
"flake8-black==0.1.1",
|
||||
"flake8-bugbear==20.1.4",
|
||||
"black==23.3.0",
|
||||
"flake8==6.0.0",
|
||||
"flake8-black==0.3.6",
|
||||
"flake8-bugbear==23.3.23",
|
||||
"pre-commit",
|
||||
] + tests_require
|
||||
|
||||
setup(
|
||||
|
@ -46,23 +47,24 @@ setup(
|
|||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 2.2",
|
||||
"Framework :: Django :: 3.0",
|
||||
"Framework :: Django :: 3.1",
|
||||
"Framework :: Django :: 3.2",
|
||||
"Framework :: Django :: 4.0",
|
||||
"Framework :: Django :: 4.1",
|
||||
],
|
||||
keywords="api graphql protocol rest relay graphene",
|
||||
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
||||
install_requires=[
|
||||
"graphene>=3.0.0b5,<4",
|
||||
"graphene>=3.0,<4",
|
||||
"graphql-core>=3.1.0,<4",
|
||||
"Django>=2.2",
|
||||
"graphql-relay>=3.1.1,<4",
|
||||
"Django>=3.2",
|
||||
"promise>=2.1",
|
||||
"text-unidecode",
|
||||
],
|
||||
|
|
38
tox.ini
38
tox.ini
|
@ -1,21 +1,23 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py{36,37,38,39}-django{22,30,31,32,main},
|
||||
black,flake8
|
||||
py{37,38,39,310}-django32,
|
||||
py{38,39,310}-django{40,41,main},
|
||||
py311-django{41,main}
|
||||
pre-commit
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
|
||||
[gh-actions:env]
|
||||
DJANGO =
|
||||
2.2: django22
|
||||
3.0: django30
|
||||
3.1: django31
|
||||
3.2: django32
|
||||
4.0: django40
|
||||
4.1: django41
|
||||
main: djangomain
|
||||
|
||||
[testenv]
|
||||
|
@ -23,26 +25,18 @@ passenv = *
|
|||
usedevelop = True
|
||||
setenv =
|
||||
DJANGO_SETTINGS_MODULE=examples.django_test_settings
|
||||
PYTHONPATH=.
|
||||
deps =
|
||||
-e.[test]
|
||||
psycopg2-binary
|
||||
django20: Django>=2.0,<2.1
|
||||
django21: Django>=2.1,<2.2
|
||||
django22: Django>=2.2,<3.0
|
||||
django30: Django>=3.0a1,<3.1
|
||||
django31: Django>=3.1,<3.2
|
||||
django32: Django>=3.2a1,<3.3
|
||||
django32: Django>=3.2,<4.0
|
||||
django40: Django>=4.0,<4.1
|
||||
django41: Django>=4.1,<4.2
|
||||
djangomain: https://github.com/django/django/archive/main.zip
|
||||
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
||||
|
||||
[testenv:black]
|
||||
basepython = python3.9
|
||||
deps = -e.[dev]
|
||||
commands =
|
||||
black --exclude "/migrations/" graphene_django examples setup.py --check
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python3.9
|
||||
deps = -e.[dev]
|
||||
[testenv:pre-commit]
|
||||
skip_install = true
|
||||
deps = pre-commit
|
||||
commands =
|
||||
flake8 graphene_django examples setup.py
|
||||
pre-commit run --all-files --show-diff-on-failure
|
||||
|
|
Loading…
Reference in New Issue
Block a user