Merge remote-tracking branch 'up/master' into drf-choices

This commit is contained in:
Jason Kraus 2019-08-06 11:41:13 -07:00
commit 711d5cce45
80 changed files with 1528 additions and 628 deletions

17
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

2
.gitignore vendored
View File

@ -78,3 +78,5 @@ Session.vim
*~
# auto-generated tag files
tags
.tox/
.pytest_cache/

View File

@ -1,62 +1,60 @@
language: python
sudo: required
cache: pip
dist: xenial
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install -e .[test]
pip install psycopg2==2.8.2 # Required for Django postgres fields testing
pip install django==$DJANGO_VERSION
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8==3.7.7
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene_django
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene_django graphene_django examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
env:
matrix:
- TEST_TYPE=build DJANGO_VERSION=1.11
- pip install tox tox-travis
script:
- tox
after_success:
- pip install coveralls
- coveralls
matrix:
fast_finish: true
include:
- python: '3.4'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '3.7'
env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '2.7'
env: TEST_TYPE=lint
- python: '3.6'
env: TEST_TYPE=lint
- python: '3.7'
env: TEST_TYPE=lint
- python: 2.7
env: DJANGO=1.11
- python: 3.5
env: DJANGO=1.11
- python: 3.5
env: DJANGO=2.0
- python: 3.5
env: DJANGO=2.1
- python: 3.5
env: DJANGO=2.2
- python: 3.6
env: DJANGO=1.11
- python: 3.6
env: DJANGO=2.0
- python: 3.6
env: DJANGO=2.1
- python: 3.6
env: DJANGO=2.2
- python: 3.6
env: DJANGO=master
- python: 3.7
env: DJANGO=1.11
- python: 3.7
env: DJANGO=2.0
- python: 3.7
env: DJANGO=2.1
- python: 3.7
env: DJANGO=2.2
- python: 3.7
env: DJANGO=master
- python: 3.7
env: TOXENV=black,flake8
allow_failures:
- env: DJANGO=master
deploy:
provider: pypi
user: syrusakbary

View File

@ -1,11 +1,29 @@
.PHONY: dev-setup ## Install development dependencies
dev-setup:
pip install -e ".[dev]"
.PHONY: install-dev
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests:
py.test graphene_django --cov=graphene_django -vv
format:
black graphene_django
.PHONY: test
test: tests # Alias test -> tests
.PHONY: format
format:
black --exclude "/migrations/" graphene_django examples setup.py
.PHONY: lint
lint:
flake8 graphene_django
flake8 graphene_django examples
.PHONY: docs ## Generate docs
docs: dev-setup
cd docs && make install && make html
.PHONY: docs-live ## Generate docs with live reloading
docs-live: dev-setup
cd docs && make install && make livehtml

View File

@ -38,12 +38,12 @@ GRAPHENE = {
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
```python
from django.conf.urls import url
from django.urls import path
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
path('graphql', GraphQLView.as_view(graphiql=True)),
]
```
@ -100,4 +100,4 @@ To learn more check out the following [examples](examples/):
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
See [CONTRIBUTING.md](CONTRIBUTING.md)

View File

@ -48,12 +48,20 @@ help:
clean:
rm -rf $(BUILDDIR)/*
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: livehtml ## to build and serve live-reloading documentation
livehtml:
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml

0
docs/_static/.gitkeep vendored Normal file
View File

View File

@ -154,7 +154,8 @@ Adding Login Required
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
.. code:: python
#views.py
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView

View File

@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
``filter_fields``.
However, you may find this to be insufficient. In these cases you can
create your own ``Filterset`` as follows:
create your own ``FilterSet``. You can pass it directly as follows:
.. code:: python
@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
You can also specify the ``FilterSet`` class using the ``filerset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter:
.. code:: python
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
fields = ['name', 'genus', 'is_domesticated']
class AnimalNode(DjangoObjectType):
class Meta:
model = Animal
filterset_class = AnimalFilter
interfaces = (relay.Node, )
class Query(ObjectType):
animal = relay.Node.Field(AnimalNode)
all_animals = DjangoFilterConnectionField(AnimalNode)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to

View File

@ -35,6 +35,8 @@ Advanced Usage
The ``--indent`` option can be used to specify the number of indentation spaces to
be used in the output. Defaults to `None` which displays all data on a single line.
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
To simplify the command to ``./manage.py graphql_schema``, you can
specify the parameters in your settings.py:

View File

@ -199,7 +199,9 @@ You can use relay with mutations. A Relay mutation must inherit from
.. code:: python
import graphene import relay, DjangoObjectType
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from graphql_relay import from_global_id
from .queries import QuestionType
@ -214,7 +216,7 @@ You can use relay with mutations. A Relay mutation must inherit from
@classmethod
def mutate_and_get_payload(cls, root, info, text, id):
question = Question.objects.get(pk=from_global_id(id))
question = Question.objects.get(pk=from_global_id(id)[1])
question.text = text
question.save()
return QuestionMutation(question=question)
@ -226,4 +228,4 @@ Relay ClientIDMutation accept a ``clientIDMutation`` argument.
This argument is also sent back to the client with the mutation result
(you do not have to do anything). For services that manage
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
allows you to match up a specific mutation with the response.
allows you to match up a specific mutation with the response.

View File

@ -41,14 +41,18 @@ Full example
return Question.objects.get(pk=question_id)
Fields
------
Specifying which fields to include
----------------------------------
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``.
If you only want a subset of fields to be present, you can do so using
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
all fields that should be exposed using the fields attribute.
This will make it less likely to result in unintentionally exposing data when
your models change.
only_fields
~~~~~~~~~~~
``fields``
~~~~~~~~~~
Show **only** these fields on the model:
@ -57,24 +61,35 @@ Show **only** these fields on the model:
class QuestionType(DjangoObjectType):
class Meta:
model = Question
only_fields = ('question_text')
fields = ('id', 'question_text')
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
exclude_fields
~~~~~~~~~~~~~~
Show all fields **except** those in ``exclude_fields``:
For example:
.. code:: python
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude_fields = ('question_text')
fields = '__all__'
Customised fields
~~~~~~~~~~~~~~~~~
``exclude``
~~~~~~~~~~~
Show all fields **except** those in ``exclude``:
.. code:: python
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude = ('question_text',)
Customising fields
------------------
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
class Meta:
model = Question
exclude_fields = ('question_text')
fields = ('id', 'question_text')
extra_field = graphene.String()
@ -92,6 +107,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
return 'hello!'
Choices to Enum conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~
By default Graphene-Django will convert any Django fields that have `choices`_
defined into a GraphQL enum type.
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
For example the following ``Model`` and ``DjangoObjectType``:
.. code:: python
class PetModel(models.Model):
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
class Pet(DjangoObjectType):
class Meta:
model = PetModel
Results in the following GraphQL schema definition:
.. code::
type Pet {
id: ID!
kind: PetModelKind!
}
enum PetModelKind {
CAT
DOG
}
You can disable this automatic conversion by setting
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
``Meta`` class.
.. code:: python
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False
.. code::
type Pet {
id: ID!
kind: String!
}
You can also set ``convert_choices_to_enum`` to a list of fields that should be
automatically converted into enums:
.. code:: python
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ['kind']
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
``False``.
Related models
--------------
@ -113,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
class QuestionType(DjangoObjectType):
class Meta:
model = Question
only_fields = ('category',)
fields = ('category',)
Then all query-able related models must be defined as DjangoObjectType subclass,
or they will fail to show if you are trying to query those relation fields. You only

View File

@ -1,3 +1,4 @@
sphinx
Sphinx==1.5.3
sphinx-autobuild==0.7.1
# Docs template
http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -30,7 +30,7 @@ Default: ``None``
``SCHEMA_OUTPUT``
----------
-----------------
The name of the file where the GraphQL schema output will go.
@ -44,7 +44,7 @@ Default: ``schema.json``
``SCHEMA_INDENT``
----------
-----------------
The indentation level of the schema output.
@ -58,7 +58,7 @@ Default: ``2``
``MIDDLEWARE``
----------
--------------
A tuple of middleware that will be executed for each GraphQL query.
@ -76,7 +76,7 @@ Default: ``()``
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
----------
------------------------------------------
Enforces relay queries to have the ``first`` or ``last`` argument.
@ -90,7 +90,7 @@ Default: ``False``
``RELAY_CONNECTION_MAX_LIMIT``
----------
------------------------------
The maximum size of objects that can be requested through a relay connection.
@ -101,3 +101,42 @@ Default: ``100``
GRAPHENE = {
'RELAY_CONNECTION_MAX_LIMIT': 100,
}
``CAMELCASE_ERRORS``
------------------------------------
When set to ``True`` field names in the ``errors`` object will be camel case.
By default they will be snake case.
Default: ``False``
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': False,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'test_field',
# 'messages': ['This field is required.'],
# }
# ]
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': True,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'testField',
# 'messages': ['This field is required.'],
# }
# ]

View File

@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category')
list_editable = ('name', 'category')
list_display = ("id", "name", "category")
list_editable = ("name", "category")
admin.site.register(Category)

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'
name = "cookbook.ingredients"
label = "ingredients"
verbose_name = "Ingredients"

View File

@ -3,7 +3,8 @@ from django.db import models
class Category(models.Model):
class Meta:
verbose_name_plural = 'Categories'
verbose_name_plural = "Categories"
name = models.CharField(max_length=100)
def __str__(self):
@ -13,7 +14,9 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)
category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self):
return self.name

View File

@ -15,14 +15,12 @@ class IngredientType(DjangoObjectType):
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(IngredientType,
id=graphene.Int(),
name=graphene.String())
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, context):
@ -30,7 +28,7 @@ class Query(object):
def resolve_all_ingredients(self, context):
# We can easily optimize query count in the resolve method
return Ingredient.objects.select_related('category').all()
return Ingredient.objects.select_related("category").all()
def resolve_category(self, context, id=None, name=None):
if id is not None:

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'
name = "cookbook.recipes"
label = "recipes"
verbose_name = "Recipes"

View File

@ -6,17 +6,23 @@ from ..ingredients.models import Ingredient
class Recipe(models.Model):
title = models.CharField(max_length=100)
instructions = models.TextField()
def __str__(self):
return self.title
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('unit', 'Units'),
('kg', 'Kilograms'),
('l', 'Litres'),
('st', 'Shots'),
))
unit = models.CharField(
max_length=20,
choices=(
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
),
)

View File

@ -15,13 +15,10 @@ class RecipeIngredientType(DjangoObjectType):
class Query(object):
recipe = graphene.Field(RecipeType,
id=graphene.Int(),
title=graphene.String())
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType)
recipeingredient = graphene.Field(RecipeIngredientType,
id=graphene.Int())
recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int())
all_recipeingredients = graphene.List(RecipeIngredientType)
def resolve_recipe(self, context, id=None, title=None):
@ -43,5 +40,5 @@ class Query(object):
return Recipe.objects.all()
def resolve_all_recipeingredients(self, context):
related = ['recipe', 'ingredient']
related = ["recipe", "ingredient"]
return RecipeIngredient.objects.select_related(*related).all()

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -5,10 +5,12 @@ import graphene
from graphene_django.debug import DjangoDebug
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='_debug')
class Query(
cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType,
):
debug = graphene.Field(DjangoDebug, name="_debug")
schema = graphene.Schema(query=Query)

View File

@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -32,64 +32,61 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django',
'cookbook.ingredients.apps.IngredientsConfig',
'cookbook.recipes.apps.RecipesConfig',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"graphene_django",
"cookbook.ingredients.apps.IngredientsConfig",
"cookbook.recipes.apps.RecipesConfig",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
"SCHEMA": "cookbook.schema.schema",
"SCHEMA_INDENT": 2,
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
ROOT_URLCONF = 'cookbook.urls'
ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
},
}
]
WSGI_APPLICATION = 'cookbook.wsgi.application'
WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@ -99,26 +96,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -130,4 +121,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = "/static/"

View File

@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(graphiql=True)),
path("admin/", admin.site.urls),
path("graphql/", GraphQLView.as_view(graphiql=True)),
]

View File

@ -1,4 +1,4 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==2.1.6
django==2.1.10

View File

@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category')
list_editable = ('name', 'category')
list_display = ("id", "name", "category")
list_editable = ("name", "category")
admin.site.register(Category)

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'
name = "cookbook.ingredients"
label = "ingredients"
verbose_name = "Ingredients"

View File

@ -11,7 +11,9 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, related_name='ingredients')
category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self):
return self.name

View File

@ -7,24 +7,22 @@ from graphene_django.types import DjangoObjectType
# Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below)
class CategoryNode(DjangoObjectType):
class Meta:
model = Category
interfaces = (Node, )
filter_fields = ['name', 'ingredients']
interfaces = (Node,)
filter_fields = ["name", "ingredients"]
class IngredientNode(DjangoObjectType):
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
interfaces = (Node, )
interfaces = (Node,)
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'notes': ['exact', 'icontains'],
'category': ['exact'],
'category__name': ['exact'],
"name": ["exact", "icontains", "istartswith"],
"notes": ["exact", "icontains"],
"category": ["exact"],
"category__name": ["exact"],
}

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'
name = "cookbook.recipes"
label = "recipes"
verbose_name = "Recipes"

View File

@ -10,12 +10,17 @@ class Recipe(models.Model):
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts')
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('unit', 'Units'),
('kg', 'Kilograms'),
('l', 'Litres'),
('st', 'Shots'),
))
unit = models.CharField(
max_length=20,
choices=(
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
),
)

View File

@ -3,24 +3,23 @@ from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
class RecipeNode(DjangoObjectType):
class RecipeNode(DjangoObjectType):
class Meta:
model = Recipe
interfaces = (Node, )
filter_fields = ['title','amounts']
interfaces = (Node,)
filter_fields = ["title", "amounts"]
class RecipeIngredientNode(DjangoObjectType):
class Meta:
model = RecipeIngredient
# Allow for some more advanced filtering here
interfaces = (Node, )
interfaces = (Node,)
filter_fields = {
'ingredient__name': ['exact', 'icontains', 'istartswith'],
'recipe': ['exact'],
'recipe__title': ['icontains'],
"ingredient__name": ["exact", "icontains", "istartswith"],
"recipe": ["exact"],
"recipe__title": ["icontains"],
}

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -5,10 +5,12 @@ import graphene
from graphene_django.debug import DjangoDebug
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='_debug')
class Query(
cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType,
):
debug = graphene.Field(DjangoDebug, name="_debug")
schema = graphene.Schema(query=Query)

View File

@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -32,65 +32,61 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django',
'cookbook.ingredients.apps.IngredientsConfig',
'cookbook.recipes.apps.RecipesConfig',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"graphene_django",
"cookbook.ingredients.apps.IngredientsConfig",
"cookbook.recipes.apps.RecipesConfig",
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
"SCHEMA": "cookbook.schema.schema",
"SCHEMA_INDENT": 2,
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
ROOT_URLCONF = 'cookbook.urls'
ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
},
}
]
WSGI_APPLICATION = 'cookbook.wsgi.application'
WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@ -100,26 +96,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -131,4 +121,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = "/static/"

View File

@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
url(r"^admin/", admin.site.urls),
url(r"^graphql$", GraphQLView.as_view(graphiql=True)),
]

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==1.11.19
django==2.2.3
django-filter>=2

View File

@ -2,97 +2,50 @@ from .models import Character, Faction, Ship
def initialize():
human = Character(
name='Human'
)
human = Character(name="Human")
human.save()
droid = Character(
name='Droid'
)
droid = Character(name="Droid")
droid.save()
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
hero=human
)
rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human)
rebels.save()
empire = Faction(
id='2',
name='Galactic Empire',
hero=droid
)
empire = Faction(id="2", name="Galactic Empire", hero=droid)
empire.save()
xwing = Ship(
id='1',
name='X-Wing',
faction=rebels,
)
xwing = Ship(id="1", name="X-Wing", faction=rebels)
xwing.save()
human.ship = xwing
human.save()
ywing = Ship(
id='2',
name='Y-Wing',
faction=rebels,
)
ywing = Ship(id="2", name="Y-Wing", faction=rebels)
ywing.save()
awing = Ship(
id='3',
name='A-Wing',
faction=rebels,
)
awing = Ship(id="3", name="A-Wing", faction=rebels)
awing.save()
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship.
falcon = Ship(
id='4',
name='Millenium Falcon',
faction=rebels,
)
falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
falcon.save()
homeOne = Ship(
id='5',
name='Home One',
faction=rebels,
)
homeOne = Ship(id="5", name="Home One", faction=rebels)
homeOne.save()
tieFighter = Ship(
id='6',
name='TIE Fighter',
faction=empire,
)
tieFighter = Ship(id="6", name="TIE Fighter", faction=empire)
tieFighter.save()
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
faction=empire,
)
tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire)
tieInterceptor.save()
executor = Ship(
id='8',
name='Executor',
faction=empire,
)
executor = Ship(id="8", name="Executor", faction=empire)
executor.save()
def create_ship(ship_name, faction_id):
new_ship = Ship(
name=ship_name,
faction_id=faction_id
)
new_ship = Ship(name=ship_name, faction_id=faction_id)
new_ship.save()
return new_ship

View File

@ -5,7 +5,13 @@ from django.db import models
class Character(models.Model):
name = models.CharField(max_length=50)
ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters')
ship = models.ForeignKey(
"Ship",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="characters",
)
def __str__(self):
return self.name
@ -21,7 +27,7 @@ class Faction(models.Model):
class Ship(models.Model):
name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships')
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
def __str__(self):
return self.name

View File

@ -2,18 +2,16 @@ import graphene
from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
get_ships)
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
from .models import Character as CharacterModel
from .models import Faction as FactionModel
from .models import Ship as ShipModel
class Ship(DjangoObjectType):
class Meta:
model = ShipModel
interfaces = (relay.Node, )
interfaces = (relay.Node,)
@classmethod
def get_node(cls, info, id):
@ -22,16 +20,14 @@ class Ship(DjangoObjectType):
class Character(DjangoObjectType):
class Meta:
model = CharacterModel
class Faction(DjangoObjectType):
class Meta:
model = FactionModel
interfaces = (relay.Node, )
interfaces = (relay.Node,)
@classmethod
def get_node(cls, info, id):
@ -39,7 +35,6 @@ class Faction(DjangoObjectType):
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
def mutate_and_get_payload(
cls, root, info, ship_name, faction_id, client_mutation_id=None
):
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@ -58,7 +55,7 @@ class Query(graphene.ObjectType):
rebels = graphene.Field(Faction)
empire = graphene.Field(Faction)
node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description='All the ships.')
ships = DjangoConnectionField(Ship, description="All the ships.")
@resolve_only_args
def resolve_ships(self):

View File

@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db
def test_correct_fetch_first_ship_rebels():
initialize()
query = '''
query = """
query RebelsShipsQuery {
rebels {
name,
@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels():
}
}
}
'''
"""
expected = {
'rebels': {
'name': 'Alliance to Restore the Republic',
'hero': {
'name': 'Human'
},
'ships': {
'edges': [
{
'node': {
'name': 'X-Wing'
}
}
]
}
"rebels": {
"name": "Alliance to Restore the Republic",
"hero": {"name": "Human"},
"ships": {"edges": [{"node": {"name": "X-Wing"}}]},
}
}
result = schema.execute(query)
@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels():
def test_correct_list_characters():
initialize()
query = '''
query = """
query RebelsShipsQuery {
node(id: "U2hpcDox") {
... on Ship {
@ -60,15 +50,8 @@ def test_correct_list_characters():
}
}
}
'''
expected = {
'node': {
'name': 'X-Wing',
'characters': [{
'name': 'Human'
}],
}
}
"""
expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
def test_mutations():
initialize()
query = '''
query = """
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
@ -29,49 +29,23 @@ def test_mutations():
}
}
}
'''
"""
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
"introduceShip": {
"ship": {"id": "U2hpcDo5", "name": "Peter"},
"faction": {
"name": "Alliance to Restore the Republic",
"ships": {
"edges": [
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
{"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
]
},
}
},
}
}
result = schema.execute(query)

View File

@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db
def test_correctly_fetches_id_name_rebels():
initialize()
query = '''
query = """
query RebelsQuery {
rebels {
id
name
}
}
'''
"""
expected = {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels():
def test_correctly_refetches_rebels():
initialize()
query = '''
query = """
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
@ -38,12 +35,9 @@ def test_correctly_refetches_rebels():
}
}
}
'''
"""
expected = {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@ -52,20 +46,15 @@ def test_correctly_refetches_rebels():
def test_correctly_fetches_id_name_empire():
initialize()
query = '''
query = """
query EmpireQuery {
empire {
id
name
}
}
'''
expected = {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
"""
expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire():
def test_correctly_refetches_empire():
initialize()
query = '''
query = """
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
@ -82,13 +71,8 @@ def test_correctly_refetches_empire():
}
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
"""
expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@ -96,7 +80,7 @@ def test_correctly_refetches_empire():
def test_correctly_refetches_xwing():
initialize()
query = '''
query = """
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
@ -105,13 +89,8 @@ def test_correctly_refetches_xwing():
}
}
}
'''
expected = {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
"""
expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -1,6 +1,6 @@
from .types import DjangoObjectType
from .fields import DjangoConnectionField
__version__ = "2.2.0"
__version__ = "2.4.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

View File

@ -68,17 +68,20 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
def convert_django_field_with_choices(field, registry=None):
def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True
):
if registry is not None:
converted = registry.get_converted_field(field)
if converted:
return converted
choices = getattr(field, "choices", None)
if choices:
if choices and convert_choices_to_enum:
meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
converted = enum(description=field.help_text, required=not field.null)
required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required)
else:
converted = convert_django_field(field, registry)
if registry is not None:
@ -184,19 +187,32 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type:
return
description = (
field.help_text
if isinstance(field, models.ManyToManyField)
else field.field.help_text
)
# If there is a connection, we should transform the field
# into a DjangoConnectionField
if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are
# defined filter_fields in the DjangoObjectType Meta
if _type._meta.filter_fields:
# defined filter_fields or a filterset_class in the
# DjangoObjectType Meta
if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(_type)
return DjangoFilterConnectionField(
_type, required=True, description=description
)
return DjangoConnectionField(_type)
return DjangoConnectionField(_type, required=True, description=description)
return DjangoListField(_type)
return DjangoListField(
_type,
required=True, # A Set is always returned, never None.
description=description,
)
return Dynamic(dynamic_type)

View File

@ -50,9 +50,7 @@ def test_should_query_field():
"""
expected = {
"reporter": {"lastName": "ABA"},
"_debug": {
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
},
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
}
schema = graphene.Schema(query=Query)
result = schema.execute(

View File

@ -15,7 +15,8 @@ from .utils import maybe_queryset
class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs):
super(DjangoListField, self).__init__(List(_type), *args, **kwargs)
# Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
@property
def model(self):
@ -100,7 +101,7 @@ class DjangoConnectionField(ConnectionField):
iterable = default_manager
iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet):
if iterable is not default_manager:
if iterable.model.objects is not default_manager:
default_queryset = maybe_queryset(default_manager)
iterable = cls.merge_querysets(default_queryset, iterable)
_len = iterable.count()

View File

@ -40,9 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
if self._extra_filter_meta:
meta.update(self._extra_filter_meta)
self._filterset_class = get_filterset_class(
self._provided_filterset_class, **meta
filterset_class = self._provided_filterset_class or (
self.node_type._meta.filterset_class
)
self._filterset_class = get_filterset_class(filterset_class, **meta)
return self._filterset_class
@ -110,7 +111,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
return partial(
self.connection_resolver,
parent_resolver,
self.type,
self.connection_type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,

View File

@ -1,18 +1,17 @@
from datetime import datetime
from textwrap import dedent
import pytest
from django.db.models import TextField, Value
from django.db.models.functions import Concat
from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String
from graphene.relay import Node
from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED
# for annotation test
from django.db.models import TextField, Value
from django.db.models.functions import Concat
pytestmark = []
if DJANGO_FILTER_INSTALLED:
@ -183,7 +182,7 @@ def test_filter_shortcut_filterset_context():
}
"""
schema = Schema(query=Query)
result = schema.execute(query, context_value=context())
result = schema.execute(query, context=context())
assert not result.errors
assert len(result.data["contextArticles"]["edges"]) == 1
@ -227,6 +226,74 @@ def test_filter_filterset_information_on_meta_related():
assert_not_orderable(articles_field)
def test_filter_filterset_class_filter_fields_exception():
with pytest.raises(Exception):
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
filter_fields = ["first_name", "articles"]
def test_filter_filterset_class_information_on_meta():
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, "first_name", "articles")
assert_not_orderable(field)
def test_filter_filterset_class_information_on_meta_related():
class ReporterFilter(FilterSet):
class Meta:
model = Reporter
fields = ["first_name", "articles"]
class ArticleFilter(FilterSet):
class Meta:
model = Article
fields = ["headline", "reporter"]
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filterset_class = ReporterFilter
class ArticleFilterNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node,)
filterset_class = ArticleFilter
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode)
schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
assert_arguments(articles_field, "headline", "reporter")
assert_not_orderable(articles_field)
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType):
class Meta:
@ -253,12 +320,14 @@ def test_filter_filterset_related_results():
pub_date=datetime.now(),
pub_date_time=datetime.now(),
reporter=r1,
editor=r1,
)
Article.objects.create(
headline="a2",
pub_date=datetime.now(),
pub_date_time=datetime.now(),
reporter=r2,
editor=r2,
)
query = """
@ -382,7 +451,7 @@ def test_global_id_multiple_field_explicit_reverse():
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_filter_filterset_related_results():
def test_filter_filterset_related_results_with_filter():
class ReporterFilterNode(DjangoObjectType):
class Meta:
model = Reporter
@ -392,15 +461,15 @@ def test_filter_filterset_related_results():
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
r1 = Reporter.objects.create(
Reporter.objects.create(
first_name="A test user", last_name="Last Name", email="test1@test.com"
)
r2 = Reporter.objects.create(
Reporter.objects.create(
first_name="Other test user",
last_name="Other Last Name",
email="test2@test.com",
)
r3 = Reporter.objects.create(
Reporter.objects.create(
first_name="Random", last_name="RandomLast", email="random@test.com"
)
@ -568,7 +637,7 @@ def test_should_query_filter_node_double_limit_raises():
Reporter.objects.create(
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
)
r = Reporter.objects.create(
Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
@ -614,7 +683,7 @@ def test_order_by_is_perserved():
return reporters
Reporter.objects.create(first_name="b")
r = Reporter.objects.create(first_name="a")
Reporter.objects.create(first_name="a")
schema = Schema(query=Query)
query = """
@ -697,3 +766,55 @@ def test_annotation_is_perserved():
assert not result.errors
assert result.data == expected
def test_integer_field_filter_type():
class PetType(DjangoObjectType):
class Meta:
model = Pet
interfaces = (Node,)
filter_fields = {"age": ["exact"]}
fields = ("age",)
class Query(ObjectType):
pets = DjangoFilterConnectionField(PetType)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
}
type PetTypeConnection {
pageInfo: PageInfo!
edges: [PetTypeEdge]!
}
type PetTypeEdge {
node: PetType
cursor: String!
}
type Query {
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
}
"""
)

View File

@ -11,8 +11,25 @@ def get_filtering_args_from_filterset(filterset_class, type):
from ..forms.converter import convert_form_field
args = {}
model = filterset_class._meta.model
for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = convert_form_field(filter_field.field).Argument()
if name in filterset_class.declared_filters:
form_field = filter_field.field
else:
field_name = name.split("__", 1)[0]
model_field = model._meta.get_field(field_name)
if hasattr(model_field, "formfield"):
form_field = model_field.formfield(
required=filter_field.extra.get("required", False)
)
# Fallback to field defined on filter if we can't get it from the
# model field
if not form_field:
form_field = filter_field.field
field_type = convert_form_field(form_field).Argument()
field_type.description = filter_field.label
args[name] = field_type

View File

@ -13,8 +13,8 @@ from graphene.types.mutation import MutationOptions
from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry
from .converter import convert_form_field
from ..types import ErrorType
from .converter import convert_form_field
def fields_for_form(form, only_fields, exclude_fields):
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
if form.is_valid():
return cls.perform_mutate(form, info)
else:
errors = [
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
errors = ErrorType.from_errors(form.errors)
return cls(errors=errors)

View File

@ -2,7 +2,9 @@ from django import forms
from django.test import TestCase
from py.test import raises
from graphene_django.tests.models import Pet, Film, FilmDetails
from graphene_django.tests.models import Film, FilmDetails, Pet
from ...settings import graphene_settings
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
@ -41,6 +43,22 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.fields
def test_mutation_error_camelcased():
class ExtraPetForm(PetForm):
test_field = forms.CharField(required=True)
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = ExtraPetForm
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
graphene_settings.CAMELCASE_ERRORS = True
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "testField"}
graphene_settings.CAMELCASE_ERRORS = False
class ModelFormMutationTests(TestCase):
def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation):

View File

@ -1,6 +1,3 @@
import graphene
class ErrorType(graphene.ObjectType):
field = graphene.String()
messages = graphene.List(graphene.String)
from ..types import ErrorType # noqa Import ErrorType for backwards compatability

View File

@ -1,7 +1,9 @@
import importlib
import json
import functools
from django.core.management.base import BaseCommand, CommandError
from django.utils import autoreload
from graphene_django.settings import graphene_settings
@ -32,6 +34,14 @@ class CommandArguments(BaseCommand):
help="Output file indent (default: None)",
)
parser.add_argument(
"--watch",
dest="watch",
default=False,
action="store_true",
help="Updates the schema on file changes (default: False)",
)
class Command(CommandArguments):
help = "Dump Graphene schema JSON to file"
@ -41,6 +51,18 @@ class Command(CommandArguments):
with open(out, "w") as outfile:
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
def get_schema(self, schema, out, indent):
schema_dict = {"data": schema.introspect()}
if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
else:
self.save_file(out, schema_dict, indent)
style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
def handle(self, *args, **options):
options_schema = options.get("schema")
@ -63,13 +85,10 @@ class Command(CommandArguments):
)
indent = options.get("indent")
schema_dict = {"data": schema.introspect()}
if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
watch = options.get("watch")
if watch:
autoreload.run_with_reloader(
functools.partial(self.get_schema, schema, out, indent)
)
else:
self.save_file(out, schema_dict, indent)
style = getattr(self, "style", None)
success = getattr(style, "SUCCESS", lambda x: x)
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
self.get_schema(schema, out, indent)

View File

@ -4,3 +4,8 @@ from django.db import models
class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
class MyFakeModelWithPassword(models.Model):
cool_name = models.CharField(max_length=50)
password = models.CharField(max_length=50)

View File

@ -3,13 +3,13 @@ from collections import OrderedDict
from django.shortcuts import get_object_or_404
import graphene
from graphene.relay.mutation import ClientIDMutation
from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions
from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs
from .serializer_converter import convert_serializer_field
from ..types import ErrorType
from .serializer_converter import convert_serializer_field
class SerializerMutationOptions(MutationOptions):
@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
name
in exclude_fields # or
# name in already_created_fields
) or (
field.write_only and not is_input # don't show write_only fields in Query
)
if is_not_in_only or is_excluded:
@ -50,7 +52,7 @@ class SerializerMutation(ClientIDMutation):
lookup_field=None,
serializer_class=None,
model_class=None,
model_operations=["create", "update"],
model_operations=("create", "update"),
only_fields=(),
exclude_fields=(),
**options
@ -125,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
if serializer.is_valid():
return cls.perform_mutate(serializer, info)
else:
errors = [
ErrorType(field=key, messages=value)
for key, value in serializer.errors.items()
]
errors = ErrorType.from_errors(serializer.errors)
return cls(errors=errors)
@ -138,6 +137,7 @@ class SerializerMutation(ClientIDMutation):
kwargs = {}
for f, field in serializer.fields.items():
kwargs[f] = field.get_attribute(obj)
if not field.write_only:
kwargs[f] = field.get_attribute(obj)
return cls(errors=None, **kwargs)

View File

@ -58,18 +58,27 @@ def convert_serializer_field(field, is_input=True):
def convert_serializer_to_input_type(serializer_class):
cached_type = convert_serializer_to_input_type.cache.get(
serializer_class.__name__, None
)
if cached_type:
return cached_type
serializer = serializer_class()
items = {
name: convert_serializer_field(field)
for name, field in serializer.fields.items()
}
return type(
ret_type = type(
"{}Input".format(serializer.__class__.__name__),
(graphene.InputObjectType,),
items,
)
convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type
return ret_type
convert_serializer_to_input_type.cache = {}
@get_graphene_type_from_serializer_field.register(serializers.Field)

View File

@ -0,0 +1,67 @@
import graphene
import pytest
from django.db import models
from graphene import Schema
from rest_framework import serializers
from graphene_django import DjangoObjectType
from graphene_django.rest_framework.mutation import SerializerMutation
pytestmark = pytest.mark.django_db
class MyFakeChildModel(models.Model):
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
class MyFakeParentModel(models.Model):
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
child1 = models.OneToOneField(
MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE
)
child2 = models.OneToOneField(
MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE
)
class ParentType(DjangoObjectType):
class Meta:
model = MyFakeParentModel
interfaces = (graphene.relay.Node,)
class ChildType(DjangoObjectType):
class Meta:
model = MyFakeChildModel
interfaces = (graphene.relay.Node,)
class MyModelChildSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeChildModel
fields = "__all__"
class MyModelParentSerializer(serializers.ModelSerializer):
child1 = MyModelChildSerializer()
child2 = MyModelChildSerializer()
class Meta:
model = MyFakeParentModel
fields = "__all__"
class MyParentModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelParentSerializer
class Mutation(graphene.ObjectType):
createParentWithChild = MyParentModelMutation.Field()
def test_create_schema():
schema = Schema(mutation=Mutation, types=[ParentType, ChildType])
assert schema

View File

@ -1,13 +1,14 @@
import datetime
from py.test import mark, raises
from rest_framework import serializers
from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...settings import graphene_settings
from ...types import DjangoObjectType
from ..models import MyFakeModel
from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation
@ -86,6 +87,51 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.fields
@mark.django_db
def test_write_only_field():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = MyFakeModelWithPassword
fields = ["cool_name", "password"]
class MyMutation(SerializerMutation):
class Meta:
serializer_class = WriteOnlyFieldModelSerializer
result = MyMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
)
assert hasattr(result, "cool_name")
assert not hasattr(
result, "password"
), "'password' is write_only field and shouldn't be visible"
@mark.django_db
def test_write_only_field_using_extra_kwargs():
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeModelWithPassword
fields = ["cool_name", "password"]
extra_kwargs = {"password": {"write_only": True}}
class MyMutation(SerializerMutation):
class Meta:
serializer_class = WriteOnlyFieldModelSerializer
result = MyMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
)
assert hasattr(result, "cool_name")
assert not hasattr(
result, "password"
), "'password' is write_only field and shouldn't be visible"
def test_nested_model():
class MyFakeModelGrapheneType(DjangoObjectType):
class Meta:
@ -168,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0
def test_mutation_error_camelcased():
graphene_settings.CAMELCASE_ERRORS = True
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors[0].field == "coolName"
graphene_settings.CAMELCASE_ERRORS = False
def test_invalid_serializer_operations():
with raises(Exception) as exc:

View File

@ -35,6 +35,7 @@ DEFAULTS = {
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
# Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100,
"CAMELCASE_ERRORS": False,
}
if settings.DEBUG:

View File

@ -38,7 +38,7 @@ class Reporter(models.Model):
last_name = models.CharField(max_length=30)
email = models.EmailField()
pets = models.ManyToManyField("self")
a_choice = models.CharField(max_length=30, choices=CHOICES)
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
objects = models.Manager()
doe_objects = DoeReporterManager()
@ -65,6 +65,11 @@ class Reporter(models.Model):
self.__class__ = CNNReporter
class CNNReporterManager(models.Manager):
def get_queryset(self):
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
class CNNReporter(Reporter):
"""
This class is a proxy model for Reporter, used for testing
@ -74,6 +79,8 @@ class CNNReporter(Reporter):
class Meta:
proxy = True
objects = CNNReporterManager()
class Article(models.Model):
headline = models.CharField(max_length=100)

View File

@ -1,6 +1,7 @@
import pytest
from django.db import models
from django.utils.translation import ugettext_lazy as _
from graphene import NonNull
from py.test import raises
import graphene
@ -196,6 +197,23 @@ def test_field_with_choices_collision():
convert_django_field_with_choices(field)
def test_field_with_choices_convert_enum_false():
field = models.CharField(
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
)
class TranslatedModel(models.Model):
language = field
class Meta:
app_label = "test"
graphene_type = convert_django_field_with_choices(
field, convert_choices_to_enum=False
)
assert isinstance(graphene_type, graphene.String)
def test_should_float_convert_float():
assert_conversion(models.FloatField, graphene.Float)
@ -217,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list():
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List)
assert dynamic_field.type.of_type == A
# A NonNull List of NonNull A ([A!]!)
# https://github.com/graphql-python/graphene-django/issues/448
assert isinstance(dynamic_field.type, NonNull)
assert isinstance(dynamic_field.type.of_type, graphene.List)
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
assert dynamic_field.type.of_type.of_type.of_type == A
def test_should_manytomany_convert_connectionorlist_connection():
@ -233,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection():
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, ConnectionField)
assert dynamic_field.type == A._meta.connection
assert dynamic_field.type.of_type == A._meta.connection
def test_should_manytoone_convert_connectionorlist():
@ -245,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist():
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List)
assert dynamic_field.type.of_type == A
# a NonNull List of NonNull A ([A!]!)
assert isinstance(dynamic_field.type, NonNull)
assert isinstance(dynamic_field.type.of_type, graphene.List)
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
assert dynamic_field.type.of_type.of_type.of_type == A
def test_should_onetoone_reverse_convert_model():

View File

@ -1,3 +1,4 @@
import base64
import datetime
import pytest
@ -7,6 +8,7 @@ from py.test import raises
from django.db.models import Q
from graphql_relay import to_global_id
import graphene
from graphene.relay import Node
@ -26,7 +28,7 @@ def test_should_query_only_fields():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
only_fields = ("articles",)
fields = ("articles",)
schema = graphene.Schema(query=ReporterType)
query = """
@ -42,7 +44,7 @@ def test_should_query_simplelazy_objects():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
only_fields = ("id",)
fields = ("id",)
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
@ -226,12 +228,68 @@ def test_should_node():
assert result.data == expected
def test_should_query_onetoone_fields():
film = Film(id=1)
film_details = FilmDetails(id=1, film=film)
class FilmNode(DjangoObjectType):
class Meta:
model = Film
interfaces = (Node,)
class FilmDetailsNode(DjangoObjectType):
class Meta:
model = FilmDetails
interfaces = (Node,)
class Query(graphene.ObjectType):
film = graphene.Field(FilmNode)
film_details = graphene.Field(FilmDetailsNode)
def resolve_film(root, info):
return film
def resolve_film_details(root, info):
return film_details
query = """
query FilmQuery {
filmDetails {
id
film {
id
}
}
film {
id
details {
id
}
}
}
"""
expected = {
"filmDetails": {
"id": "RmlsbURldGFpbHNOb2RlOjE=",
"film": {"id": "RmlsbU5vZGU6MQ=="},
},
"film": {
"id": "RmlsbU5vZGU6MQ==",
"details": {"id": "RmlsbURldGFpbHNOb2RlOjE="},
},
}
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_query_connectionfields():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
only_fields = ("articles",)
fields = ("articles",)
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
@ -271,7 +329,7 @@ def test_should_keep_annotations():
class Meta:
model = Reporter
interfaces = (Node,)
only_fields = ("articles",)
fields = ("articles",)
class ArticleType(DjangoObjectType):
class Meta:
@ -895,8 +953,7 @@ def test_should_handle_inherited_choices():
def test_proxy_model_support():
"""
This test asserts that we can query for all Reporters,
even if some are of a proxy model type at runtime.
This test asserts that we can query for all Reporters and proxied Reporters.
"""
class ReporterType(DjangoObjectType):
@ -905,11 +962,17 @@ def test_proxy_model_support():
interfaces = (Node,)
use_connection = True
reporter_1 = Reporter.objects.create(
class CNNReporterType(DjangoObjectType):
class Meta:
model = CNNReporter
interfaces = (Node,)
use_connection = True
reporter = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
reporter_2 = CNNReporter.objects.create(
cnn_reporter = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
email="someguy@cnn.com",
@ -919,6 +982,7 @@ def test_proxy_model_support():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
cnn_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query)
query = """
@ -930,16 +994,28 @@ def test_proxy_model_support():
}
}
}
cnnReporters {
edges {
node {
id
}
}
}
}
"""
expected = {
"allReporters": {
"edges": [
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
{"node": {"id": to_global_id("ReporterType", reporter.id)}},
{"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
]
}
},
"cnnReporters": {
"edges": [
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
]
},
}
result = schema.execute(query)
@ -947,68 +1023,6 @@ def test_proxy_model_support():
assert result.data == expected
def test_proxy_model_fails():
"""
This test asserts that if you try to query for a proxy model,
that query will fail with:
GraphQLError('Expected value of type "CNNReporterType" but got:
CNNReporter.',)
This is because a proxy model has the identical model definition
to its superclass, and defines its behavior at runtime, rather than
at the database level. Currently, filtering objects of the proxy models'
type isn't supported. It would require a field on the model that would
represent the type, and it doesn't seem like there is a clear way to
enforce this pattern across all projects
"""
class CNNReporterType(DjangoObjectType):
class Meta:
model = CNNReporter
interfaces = (Node,)
use_connection = True
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
reporter_2 = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
email="someguy@cnn.com",
a_choice=1,
reporter_type=2, # set this guy to be CNN
)
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query)
query = """
query ProxyModelQuery {
allReporters {
edges {
node {
id
}
}
}
}
"""
expected = {
"allReporters": {
"edges": [
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
]
}
}
result = schema.execute(query)
assert result.errors
def test_should_resolve_get_queryset_connectionfields():
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
@ -1051,3 +1065,54 @@ def test_should_resolve_get_queryset_connectionfields():
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_should_preserve_prefetch_related(django_assert_num_queries):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (graphene.relay.Node,)
class FilmType(DjangoObjectType):
reporters = DjangoConnectionField(ReporterType)
class Meta:
model = Film
interfaces = (graphene.relay.Node,)
class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType)
def resolve_films(root, info):
qs = Film.objects.prefetch_related("reporters")
return qs
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
f1 = Film.objects.create()
f1.reporters.set([r1, r2])
f2 = Film.objects.create()
f2.reporters.set([r2])
query = """
query {
films {
edges {
node {
reporters {
edges {
node {
firstName
}
}
}
}
}
}
}
"""
schema = graphene.Schema(query=Query)
with django_assert_num_queries(3) as captured:
result = schema.execute(query)
assert not result.errors

View File

@ -48,6 +48,6 @@ def test_should_map_only_few_fields():
class Reporter2(DjangoObjectType):
class Meta:
model = Reporter
only_fields = ("id", "email")
fields = ("id", "email")
assert list(Reporter2._meta.fields.keys()) == ["id", "email"]

View File

@ -1,6 +1,11 @@
from collections import OrderedDict, defaultdict
from textwrap import dedent
import pytest
from django.db import models
from mock import patch
from graphene import Interface, ObjectType, Schema, Connection, String
from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node
from .. import registry
@ -165,10 +170,10 @@ type Reporter {
firstName: String!
lastName: String!
email: String!
pets: [Reporter]
aChoice: ReporterAChoice!
pets: [Reporter!]!
aChoice: ReporterAChoice
reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
}
enum ReporterAChoice {
@ -206,21 +211,216 @@ def with_local_registry(func):
@with_local_registry
def test_django_objecttype_only_fields():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
only_fields = ("id", "email", "films")
with pytest.warns(PendingDeprecationWarning):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
only_fields = ("id", "email", "films")
fields = list(Reporter._meta.fields.keys())
assert fields == ["id", "email", "films"]
@with_local_registry
def test_django_objecttype_exclude_fields():
def test_django_objecttype_fields():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude_fields = "email"
fields = ("id", "email", "films")
fields = list(Reporter._meta.fields.keys())
assert fields == ["id", "email", "films"]
@with_local_registry
def test_django_objecttype_only_fields_and_fields():
with pytest.raises(Exception):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
only_fields = ("id", "email", "films")
fields = ("id", "email", "films")
@with_local_registry
def test_django_objecttype_all_fields():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "__all__"
fields = list(Reporter._meta.fields.keys())
assert len(fields) == len(ReporterModel._meta.get_fields())
@with_local_registry
def test_django_objecttype_exclude_fields():
with pytest.warns(PendingDeprecationWarning):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude_fields = ["email"]
fields = list(Reporter._meta.fields.keys())
assert "email" not in fields
@with_local_registry
def test_django_objecttype_exclude():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
fields = list(Reporter._meta.fields.keys())
assert "email" not in fields
@with_local_registry
def test_django_objecttype_exclude_fields_and_exclude():
with pytest.raises(Exception):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
exclude_fields = ["email"]
@with_local_registry
def test_django_objecttype_exclude_and_only():
with pytest.raises(AssertionError):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
fields = ["id"]
@with_local_registry
def test_django_objecttype_fields_exclude_type_checking():
with pytest.raises(TypeError):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "foo"
with pytest.raises(TypeError):
class Reporter2(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "foo"
class TestDjangoObjectType:
@pytest.fixture
def PetModel(self):
class PetModel(models.Model):
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
cuteness = models.IntegerField(
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
)
yield PetModel
# Clear Django model cache so we don't get warnings when creating the
# model multiple times
PetModel._meta.apps.all_models = defaultdict(OrderedDict)
def test_django_objecttype_convert_choices_enum_false(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: String!
cuteness: Int!
}
type Query {
pet: Pet
}
"""
)
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ["kind"]
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: PetModelKind!
cuteness: Int!
}
enum PetModelKind {
CAT
DOG
}
type Query {
pet: Pet
}
"""
)
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = []
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: String!
cuteness: Int!
}
type Query {
pet: Pet
}
"""
)

View File

@ -1,4 +1,6 @@
from ..utils import get_model_fields
from django.utils.translation import gettext_lazy
from ..utils import camelize, get_model_fields
from .models import Film, Reporter
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields])
assert len(film_fields) == len(film_name_set)
def test_camelize():
assert camelize({}) == {}
assert camelize("value_a") == "value_a"
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
}
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
assert camelize(gettext_lazy("value_a")) == "value_a"
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
"valueA": "value_b"
}
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}

View File

@ -1,8 +1,10 @@
import six
import warnings
from collections import OrderedDict
import six
from django.db.models import Model
from django.utils.functional import SimpleLazyObject
import graphene
from graphene import Field
from graphene.relay import Connection, Node
@ -11,14 +13,24 @@ from graphene.types.utils import yank_fields_from_attrs
from .converter import convert_django_field_with_choices
from .registry import Registry, get_global_registry
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
from .settings import graphene_settings
from .utils import (
DJANGO_FILTER_INSTALLED,
camelize,
get_model_fields,
is_valid_django_model,
)
if six.PY3:
from typing import Type
def construct_fields(model, registry, only_fields, exclude_fields):
ALL_FIELDS = "__all__"
def construct_fields(
model, registry, only_fields, exclude_fields, convert_choices_to_enum
):
_model_fields = get_model_fields(model)
fields = OrderedDict()
@ -33,7 +45,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
# in there. Or when we exclude this field in exclude_fields.
# Or when there is no back reference.
continue
converted = convert_django_field_with_choices(field, registry)
_convert_choices_to_enum = convert_choices_to_enum
if not isinstance(_convert_choices_to_enum, bool):
# then `convert_choices_to_enum` is a list of field names to convert
if name in _convert_choices_to_enum:
_convert_choices_to_enum = True
else:
_convert_choices_to_enum = False
converted = convert_django_field_with_choices(
field, registry, convert_choices_to_enum=_convert_choices_to_enum
)
fields[name] = converted
return fields
@ -45,6 +68,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
connection = None # type: Type[Connection]
filter_fields = ()
filterset_class = None
class DjangoObjectType(ObjectType):
@ -54,13 +78,17 @@ class DjangoObjectType(ObjectType):
model=None,
registry=None,
skip_registry=False,
only_fields=(),
exclude_fields=(),
only_fields=(), # deprecated in favour of `fields`
fields=(),
exclude_fields=(), # deprecated in favour of `exclude`
exclude=(),
filter_fields=None,
filterset_class=None,
connection=None,
connection_class=None,
use_connection=None,
interfaces=(),
convert_choices_to_enum=True,
_meta=None,
**options
):
@ -76,11 +104,60 @@ class DjangoObjectType(ObjectType):
'Registry, received "{}".'
).format(cls.__name__, registry)
if not DJANGO_FILTER_INSTALLED and filter_fields:
raise Exception("Can only set filter_fields if Django-Filter is installed")
if filter_fields and filterset_class:
raise Exception("Can't set both filter_fields and filterset_class")
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"
)
)
assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on "
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
)
# Alias only_fields -> fields
if only_fields and fields:
raise Exception("Can't set both only_fields and fields")
if only_fields:
warnings.warn(
"Defining `only_fields` is deprecated in favour of `fields`.",
PendingDeprecationWarning,
stacklevel=2,
)
fields = only_fields
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple or "__all__". '
"Got %s." % type(fields).__name__
)
if fields == ALL_FIELDS:
fields = None
# Alias exclude_fields -> exclude
if exclude_fields and exclude:
raise Exception("Can't set both exclude_fields and exclude")
if exclude_fields:
warnings.warn(
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
PendingDeprecationWarning,
stacklevel=2,
)
exclude = exclude_fields
if exclude and not isinstance(exclude, (list, tuple)):
raise TypeError(
"The `exclude` option must be a list or tuple. Got %s."
% type(exclude).__name__
)
django_fields = yank_fields_from_attrs(
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
_as=Field,
)
if use_connection is None and interfaces:
@ -108,6 +185,7 @@ class DjangoObjectType(ObjectType):
_meta.model = model
_meta.registry = registry
_meta.filter_fields = filter_fields
_meta.filterset_class = filterset_class
_meta.fields = django_fields
_meta.connection = connection
@ -131,7 +209,11 @@ class DjangoObjectType(ObjectType):
if not is_valid_django_model(type(root)):
raise Exception(('Received incompatible instance "{}".').format(root))
model = root._meta.model._meta.concrete_model
if cls._meta.model._meta.proxy:
model = root._meta.model
else:
model = root._meta.model._meta.concrete_model
return model == cls._meta.model
@classmethod
@ -150,3 +232,8 @@ class DjangoObjectType(ObjectType):
class ErrorType(ObjectType):
field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
@classmethod
def from_errors(cls, errors):
data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors
return [cls(field=key, messages=value) for key, value in data.items()]

View File

@ -1,18 +1,20 @@
from .testing import GraphQLTestCase
from .utils import (
DJANGO_FILTER_INSTALLED,
get_reverse_fields,
maybe_queryset,
camelize,
get_model_fields,
is_valid_django_model,
get_reverse_fields,
import_single_dispatch,
is_valid_django_model,
maybe_queryset,
)
from .testing import GraphQLTestCase
__all__ = [
"DJANGO_FILTER_INSTALLED",
"get_reverse_fields",
"maybe_queryset",
"get_model_fields",
"camelize",
"is_valid_django_model",
"import_single_dispatch",
"GraphQLTestCase",

View File

@ -22,7 +22,7 @@ class GraphQLTestCase(TestCase):
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
)
cls._client = Client(cls.GRAPHQL_SCHEMA)
cls._client = Client()
def query(self, query, op_name=None, input_data=None):
"""
@ -54,8 +54,8 @@ class GraphQLTestCase(TestCase):
the call was fine.
:resp HttpResponse: Response
"""
content = json.loads(resp.content)
self.assertEqual(resp.status_code, 200)
content = json.loads(resp.content)
self.assertNotIn("errors", list(content.keys()))
def assertResponseHasErrors(self, resp):

View File

@ -2,7 +2,11 @@ import inspect
from django.db import models
from django.db.models.manager import Manager
from django.utils import six
from django.utils.encoding import force_text
from django.utils.functional import Promise
from graphene.utils.str_converters import to_camel_case
try:
import django_filters # noqa
@ -12,13 +16,36 @@ except ImportError:
DJANGO_FILTER_INSTALLED = False
def isiterable(value):
try:
iter(value)
except TypeError:
return False
return True
def _camelize_django_str(s):
if isinstance(s, Promise):
s = force_text(s)
return to_camel_case(s) if isinstance(s, six.string_types) else s
def camelize(data):
if isinstance(data, dict):
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
return [camelize(d) for d in data]
return data
def get_reverse_fields(model, local_field_names):
for name, attr in model.__dict__.items():
# Don't duplicate any local fields
if name in local_field_names:
continue
related = getattr(attr, "rel", None)
# "rel" for FK and M2M relations and "related" for O2O Relations
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
if isinstance(related, models.ManyToOneRel):
yield (name, related)
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:

View File

@ -5,11 +5,41 @@ test=pytest
universal=1
[flake8]
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
exclude = docs,graphene_django/debug/sql/*,migrations
max-line-length = 120
select =
# Dictionary key repeated
F601,
# Ensure use of ==/!= to compare with str, bytes and int literals
F632,
# Redefinition of unused name
F811,
# Using an undefined variable
F821,
# Defining an undefined variable in __all__
F822,
# Using a variable before it is assigned
F823,
# Duplicate argument in function declaration
F831,
# Black would format this line
BLK,
# Do not use bare except
B001,
# Don't allow ++n. You probably meant n += 1
B002,
# Do not use mutable structures for argument defaults
B006,
# Do not perform calls in argument defaults
B008
[coverage:run]
omit = */tests/*
[isort]
known_first_party=graphene,graphene_django
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

View File

@ -28,6 +28,8 @@ tests_require = [
dev_requires = [
"black==19.3b0",
"flake8==3.7.7",
"flake8-black==0.1.0",
"flake8-bugbear==19.3.0",
] + tests_require
setup(
@ -64,7 +66,11 @@ setup(
setup_requires=["pytest-runner"],
tests_require=tests_require,
rest_framework_require=rest_framework_require,
extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires},
extras_require={
"test": tests_require,
"rest_framework": rest_framework_require,
"dev": dev_requires,
},
include_package_data=True,
zip_safe=False,
platforms="any",

39
tox.ini Normal file
View File

@ -0,0 +1,39 @@
[tox]
envlist =
py{27,35,36,37}-django{111,20,21,22,master},
black,flake8
[travis:env]
DJANGO =
1.11: django111
2.0: django20
2.1: django21
2.2: django22
master: djangomaster
[testenv]
passenv = *
usedevelop = True
setenv =
DJANGO_SETTINGS_MODULE=django_test_settings
deps =
-e.[test]
psycopg2
django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<3.0
djangomaster: https://github.com/django/django/archive/master.zip
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
[testenv:black]
basepython = python3.7
deps = -e.[dev]
commands =
black --exclude "/migrations/" graphene_django examples setup.py --check
[testenv:flake8]
basepython = python3.7
deps = -e.[dev]
commands =
flake8 graphene_django examples