Merge branch 'master' into debug-wait-for-all-promises

This commit is contained in:
Jonathan Kim 2019-08-04 07:31:50 +01:00
commit 6d56bb4edb
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 # auto-generated tag files
tags tags
.tox/
.pytest_cache/

View File

@ -1,62 +1,60 @@
language: python language: python
sudo: required cache: pip
dist: xenial dist: xenial
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
install: install:
- | - pip install tox tox-travis
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: script:
- | - tox
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: after_success:
- | - pip install coveralls
if [ "$TEST_TYPE" = build ]; then - coveralls
coveralls
fi
env:
matrix:
- TEST_TYPE=build DJANGO_VERSION=1.11
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: '3.4' - python: 2.7
env: TEST_TYPE=build DJANGO_VERSION=2.0 env: DJANGO=1.11
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.0 - python: 3.5
- python: '3.6' env: DJANGO=1.11
env: TEST_TYPE=build DJANGO_VERSION=2.0 - python: 3.5
- python: '3.5' env: DJANGO=2.0
env: TEST_TYPE=build DJANGO_VERSION=2.1 - python: 3.5
- python: '3.6' env: DJANGO=2.1
env: TEST_TYPE=build DJANGO_VERSION=2.1 - python: 3.5
- python: '3.6' env: DJANGO=2.2
env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '3.7' - python: 3.6
env: TEST_TYPE=build DJANGO_VERSION=2.2 env: DJANGO=1.11
- python: '2.7' - python: 3.6
env: TEST_TYPE=lint env: DJANGO=2.0
- python: '3.6' - python: 3.6
env: TEST_TYPE=lint env: DJANGO=2.1
- python: '3.7' - python: 3.6
env: TEST_TYPE=lint 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: deploy:
provider: pypi provider: pypi
user: syrusakbary user: syrusakbary

View File

@ -1,11 +1,29 @@
.PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
.PHONY: install-dev
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests: tests:
py.test graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
format: .PHONY: test
black graphene_django test: tests # Alias test -> tests
.PHONY: format
format:
black --exclude "/migrations/" graphene_django examples setup.py
.PHONY: lint
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. We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
```python ```python
from django.conf.urls import url from django.urls import path
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
# ... # ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)), path('graphql', GraphQLView.as_view(graphiql=True)),
] ]
``` ```

View File

@ -48,12 +48,20 @@ help:
clean: clean:
rm -rf $(BUILDDIR)/* rm -rf $(BUILDDIR)/*
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: html .PHONY: html
html: html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo @echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @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 .PHONY: dirhtml
dirhtml: dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml

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

View File

@ -154,6 +154,7 @@ 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``.: 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 .. code:: python
# views.py # views.py
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin

View File

@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
``filter_fields``. ``filter_fields``.
However, you may find this to be insufficient. In these cases you can 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 .. code:: python
@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode, all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter) 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>`__ 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 in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to filters to be context-dependent. We could modify the ``AnimalFilter`` above to

View File

@ -35,6 +35,8 @@ Advanced Usage
The ``--indent`` option can be used to specify the number of indentation spaces to 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. 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 To simplify the command to ``./manage.py graphql_schema``, you can
specify the parameters in your settings.py: 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 .. 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 graphql_relay import from_global_id
from .queries import QuestionType from .queries import QuestionType
@ -214,7 +216,7 @@ You can use relay with mutations. A Relay mutation must inherit from
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, text, id): 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.text = text
question.save() question.save()
return QuestionMutation(question=question) return QuestionMutation(question=question)

View File

@ -41,14 +41,18 @@ Full example
return Question.objects.get(pk=question_id) 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. 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: Show **only** these fields on the model:
@ -57,24 +61,35 @@ Show **only** these fields on the model:
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question 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 For example:
~~~~~~~~~~~~~~
Show all fields **except** those in ``exclude_fields``:
.. code:: python .. code:: python
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question 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: 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: class Meta:
model = Question model = Question
exclude_fields = ('question_text') fields = ('id', 'question_text')
extra_field = graphene.String() extra_field = graphene.String()
@ -92,6 +107,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
return 'hello!' 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 Related models
-------------- --------------
@ -113,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
only_fields = ('category',) fields = ('category',)
Then all query-able related models must be defined as DjangoObjectType subclass, 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 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 # Docs template
http://graphene-python.org/sphinx_graphene_theme.zip http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -30,7 +30,7 @@ Default: ``None``
``SCHEMA_OUTPUT`` ``SCHEMA_OUTPUT``
---------- -----------------
The name of the file where the GraphQL schema output will go. The name of the file where the GraphQL schema output will go.
@ -44,7 +44,7 @@ Default: ``schema.json``
``SCHEMA_INDENT`` ``SCHEMA_INDENT``
---------- -----------------
The indentation level of the schema output. The indentation level of the schema output.
@ -58,7 +58,7 @@ Default: ``2``
``MIDDLEWARE`` ``MIDDLEWARE``
---------- --------------
A tuple of middleware that will be executed for each GraphQL query. A tuple of middleware that will be executed for each GraphQL query.
@ -76,7 +76,7 @@ Default: ``()``
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
---------- ------------------------------------------
Enforces relay queries to have the ``first`` or ``last`` argument. Enforces relay queries to have the ``first`` or ``last`` argument.
@ -90,7 +90,7 @@ Default: ``False``
``RELAY_CONNECTION_MAX_LIMIT`` ``RELAY_CONNECTION_MAX_LIMIT``
---------- ------------------------------
The maximum size of objects that can be requested through a relay connection. The maximum size of objects that can be requested through a relay connection.
@ -101,3 +101,42 @@ Default: ``100``
GRAPHENE = { GRAPHENE = {
'RELAY_CONNECTION_MAX_LIMIT': 100, '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) @admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin): class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category') list_display = ("id", "name", "category")
list_editable = ('name', 'category') list_editable = ("name", "category")
admin.site.register(Category) admin.site.register(Category)

View File

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

View File

@ -3,7 +3,8 @@ from django.db import models
class Category(models.Model): class Category(models.Model):
class Meta: class Meta:
verbose_name_plural = 'Categories' verbose_name_plural = "Categories"
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
def __str__(self): def __str__(self):
@ -13,7 +14,9 @@ class Category(models.Model):
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True) 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): def __str__(self):
return self.name return self.name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
graphene graphene
graphene-django graphene-django
graphql-core>=2.1rc1 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) @admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin): class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category') list_display = ("id", "name", "category")
list_editable = ('name', 'category') list_editable = ("name", "category")
admin.site.register(Category) admin.site.register(Category)

View File

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

View File

@ -11,7 +11,9 @@ class Category(models.Model):
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True) 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): def __str__(self):
return self.name 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. # Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below) # This is configured in the CategoryNode's Meta class (as you can see below)
class CategoryNode(DjangoObjectType): class CategoryNode(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
interfaces = (Node,) interfaces = (Node,)
filter_fields = ['name', 'ingredients'] filter_fields = ["name", "ingredients"]
class IngredientNode(DjangoObjectType): class IngredientNode(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
# Allow for some more advanced filtering here # Allow for some more advanced filtering here
interfaces = (Node,) interfaces = (Node,)
filter_fields = { filter_fields = {
'name': ['exact', 'icontains', 'istartswith'], "name": ["exact", "icontains", "istartswith"],
'notes': ['exact', 'icontains'], "notes": ["exact", "icontains"],
'category': ['exact'], "category": ["exact"],
'category__name': ['exact'], "category__name": ["exact"],
} }

View File

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

View File

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

View File

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

View File

@ -10,12 +10,17 @@ class Recipe(models.Model):
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts') recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name='used_by') ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField() amount = models.FloatField()
unit = models.CharField(max_length=20, choices=( unit = models.CharField(
('unit', 'Units'), max_length=20,
('kg', 'Kilograms'), choices=(
('l', 'Litres'), ("unit", "Units"),
('st', 'Shots'), ("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.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
class RecipeNode(DjangoObjectType):
class RecipeNode(DjangoObjectType):
class Meta: class Meta:
model = Recipe model = Recipe
interfaces = (Node,) interfaces = (Node,)
filter_fields = ['title','amounts'] filter_fields = ["title", "amounts"]
class RecipeIngredientNode(DjangoObjectType): class RecipeIngredientNode(DjangoObjectType):
class Meta: class Meta:
model = RecipeIngredient model = RecipeIngredient
# Allow for some more advanced filtering here # Allow for some more advanced filtering here
interfaces = (Node,) interfaces = (Node,)
filter_fields = { filter_fields = {
'ingredient__name': ['exact', 'icontains', 'istartswith'], "ingredient__name": ["exact", "icontains", "istartswith"],
'recipe': ['exact'], "recipe": ["exact"],
'recipe__title': ['icontains'], "recipe__title": ["icontains"],
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,13 @@ from django.db import models
class Character(models.Model): class Character(models.Model):
name = models.CharField(max_length=50) 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): def __str__(self):
return self.name return self.name
@ -21,7 +27,7 @@ class Faction(models.Model):
class Ship(models.Model): class Ship(models.Model):
name = models.CharField(max_length=50) 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): def __str__(self):
return self.name return self.name

View File

@ -2,15 +2,13 @@ import graphene
from graphene import Schema, relay, resolve_only_args from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType from graphene_django import DjangoConnectionField, DjangoObjectType
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship, from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
get_ships)
from .models import Character as CharacterModel from .models import Character as CharacterModel
from .models import Faction as FactionModel from .models import Faction as FactionModel
from .models import Ship as ShipModel from .models import Ship as ShipModel
class Ship(DjangoObjectType): class Ship(DjangoObjectType):
class Meta: class Meta:
model = ShipModel model = ShipModel
interfaces = (relay.Node,) interfaces = (relay.Node,)
@ -22,13 +20,11 @@ class Ship(DjangoObjectType):
class Character(DjangoObjectType): class Character(DjangoObjectType):
class Meta: class Meta:
model = CharacterModel model = CharacterModel
class Faction(DjangoObjectType): class Faction(DjangoObjectType):
class Meta: class Meta:
model = FactionModel model = FactionModel
interfaces = (relay.Node,) interfaces = (relay.Node,)
@ -39,7 +35,6 @@ class Faction(DjangoObjectType):
class IntroduceShip(relay.ClientIDMutation): class IntroduceShip(relay.ClientIDMutation):
class Input: class Input:
ship_name = graphene.String(required=True) ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True) faction_id = graphene.String(required=True)
@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction) faction = graphene.Field(Faction)
@classmethod @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) ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id) faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction) return IntroduceShip(ship=ship, faction=faction)
@ -58,7 +55,7 @@ class Query(graphene.ObjectType):
rebels = graphene.Field(Faction) rebels = graphene.Field(Faction)
empire = graphene.Field(Faction) empire = graphene.Field(Faction)
node = relay.Node.Field() node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description='All the ships.') ships = DjangoConnectionField(Ship, description="All the ships.")
@resolve_only_args @resolve_only_args
def resolve_ships(self): def resolve_ships(self):

View File

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

View File

@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
def test_mutations(): def test_mutations():
initialize() initialize()
query = ''' query = """
mutation MyMutation { mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship { ship {
@ -29,51 +29,25 @@ def test_mutations():
} }
} }
} }
''' """
expected = { expected = {
'introduceShip': { "introduceShip": {
'ship': { "ship": {"id": "U2hpcDo5", "name": "Peter"},
'id': 'U2hpcDo5', "faction": {
'name': 'Peter' "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"}},
]
}, },
'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) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == expected assert result.data == expected

View File

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

View File

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

View File

@ -52,13 +52,15 @@ def get_choices(choices):
yield name, value, description yield name, value, description
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: if registry is not None:
converted = registry.get_converted_field(field) converted = registry.get_converted_field(field)
if converted: if converted:
return converted return converted
choices = getattr(field, "choices", None) choices = getattr(field, "choices", None)
if choices: if choices and convert_choices_to_enum:
meta = field.model._meta meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name)) name = to_camel_case("{}_{}".format(meta.object_name, field.name))
choices = list(get_choices(choices)) choices = list(get_choices(choices))
@ -71,7 +73,8 @@ def convert_django_field_with_choices(field, registry=None):
return named_choices_descriptions[self.name] return named_choices_descriptions[self.name]
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
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: else:
converted = convert_django_field(field, registry) converted = convert_django_field(field, registry)
if registry is not None: if registry is not None:
@ -177,19 +180,32 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type: if not _type:
return 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 # If there is a connection, we should transform the field
# into a DjangoConnectionField # into a DjangoConnectionField
if _type._meta.connection: if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are # Use a DjangoFilterConnectionField if there are
# defined filter_fields in the DjangoObjectType Meta # defined filter_fields or a filterset_class in the
if _type._meta.filter_fields: # DjangoObjectType Meta
if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField 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) return Dynamic(dynamic_type)

View File

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

View File

@ -15,7 +15,8 @@ from .utils import maybe_queryset
class DjangoListField(Field): class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs): 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 @property
def model(self): def model(self):
@ -100,7 +101,7 @@ class DjangoConnectionField(ConnectionField):
iterable = default_manager iterable = default_manager
iterable = maybe_queryset(iterable) iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet): if isinstance(iterable, QuerySet):
if iterable is not default_manager: if iterable.model.objects is not default_manager:
default_queryset = maybe_queryset(default_manager) default_queryset = maybe_queryset(default_manager)
iterable = cls.merge_querysets(default_queryset, iterable) iterable = cls.merge_querysets(default_queryset, iterable)
_len = iterable.count() _len = iterable.count()

View File

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

View File

@ -1,18 +1,17 @@
from datetime import datetime from datetime import datetime
from textwrap import dedent
import pytest 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.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED 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 = [] pytestmark = []
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
@ -183,7 +182,7 @@ def test_filter_shortcut_filterset_context():
} }
""" """
schema = Schema(query=Query) schema = Schema(query=Query)
result = schema.execute(query, context_value=context()) result = schema.execute(query, context=context())
assert not result.errors assert not result.errors
assert len(result.data["contextArticles"]["edges"]) == 1 assert len(result.data["contextArticles"]["edges"]) == 1
@ -227,6 +226,74 @@ def test_filter_filterset_information_on_meta_related():
assert_not_orderable(articles_field) 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(): def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
@ -253,12 +320,14 @@ def test_filter_filterset_related_results():
pub_date=datetime.now(), pub_date=datetime.now(),
pub_date_time=datetime.now(), pub_date_time=datetime.now(),
reporter=r1, reporter=r1,
editor=r1,
) )
Article.objects.create( Article.objects.create(
headline="a2", headline="a2",
pub_date=datetime.now(), pub_date=datetime.now(),
pub_date_time=datetime.now(), pub_date_time=datetime.now(),
reporter=r2, reporter=r2,
editor=r2,
) )
query = """ query = """
@ -382,7 +451,7 @@ def test_global_id_multiple_field_explicit_reverse():
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_filter_filterset_related_results(): def test_filter_filterset_related_results_with_filter():
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
@ -392,15 +461,15 @@ def test_filter_filterset_related_results():
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
r1 = Reporter.objects.create( Reporter.objects.create(
first_name="A test user", last_name="Last Name", email="test1@test.com" 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", first_name="Other test user",
last_name="Other Last Name", last_name="Other Last Name",
email="test2@test.com", email="test2@test.com",
) )
r3 = Reporter.objects.create( Reporter.objects.create(
first_name="Random", last_name="RandomLast", email="random@test.com" 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( Reporter.objects.create(
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2 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 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 return reporters
Reporter.objects.create(first_name="b") Reporter.objects.create(first_name="b")
r = Reporter.objects.create(first_name="a") Reporter.objects.create(first_name="a")
schema = Schema(query=Query) schema = Schema(query=Query)
query = """ query = """
@ -697,3 +766,55 @@ def test_annotation_is_perserved():
assert not result.errors assert not result.errors
assert result.data == expected 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 from ..forms.converter import convert_form_field
args = {} args = {}
model = filterset_class._meta.model
for name, filter_field in six.iteritems(filterset_class.base_filters): 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 field_type.description = filter_field.label
args[name] = field_type 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.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry from graphene_django.registry import get_global_registry
from .converter import convert_form_field
from ..types import ErrorType from ..types import ErrorType
from .converter import convert_form_field
def fields_for_form(form, only_fields, exclude_fields): def fields_for_form(form, only_fields, exclude_fields):
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
if form.is_valid(): if form.is_valid():
return cls.perform_mutate(form, info) return cls.perform_mutate(form, info)
else: else:
errors = [ errors = ErrorType.from_errors(form.errors)
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
return cls(errors=errors) return cls(errors=errors)

View File

@ -2,7 +2,9 @@ from django import forms
from django.test import TestCase from django.test import TestCase
from py.test import raises 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 from ..mutation import DjangoFormMutation, DjangoModelFormMutation
@ -41,6 +43,22 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.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): class ModelFormMutationTests(TestCase):
def test_default_meta_fields(self): def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation): class PetMutation(DjangoModelFormMutation):

View File

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

View File

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

View File

@ -4,3 +4,8 @@ from django.db import models
class MyFakeModel(models.Model): class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50) cool_name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True) 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 from django.shortcuts import get_object_or_404
import graphene import graphene
from graphene.relay.mutation import ClientIDMutation
from graphene.types import Field, InputField from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions from graphene.types.mutation import MutationOptions
from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs from graphene.types.objecttype import yank_fields_from_attrs
from .serializer_converter import convert_serializer_field
from ..types import ErrorType from ..types import ErrorType
from .serializer_converter import convert_serializer_field
class SerializerMutationOptions(MutationOptions): class SerializerMutationOptions(MutationOptions):
@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
name name
in exclude_fields # or in exclude_fields # or
# name in already_created_fields # 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: if is_not_in_only or is_excluded:
@ -50,7 +52,7 @@ class SerializerMutation(ClientIDMutation):
lookup_field=None, lookup_field=None,
serializer_class=None, serializer_class=None,
model_class=None, model_class=None,
model_operations=["create", "update"], model_operations=("create", "update"),
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
**options **options
@ -125,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
if serializer.is_valid(): if serializer.is_valid():
return cls.perform_mutate(serializer, info) return cls.perform_mutate(serializer, info)
else: else:
errors = [ errors = ErrorType.from_errors(serializer.errors)
ErrorType(field=key, messages=value)
for key, value in serializer.errors.items()
]
return cls(errors=errors) return cls(errors=errors)
@ -138,6 +137,7 @@ class SerializerMutation(ClientIDMutation):
kwargs = {} kwargs = {}
for f, field in serializer.fields.items(): for f, field in serializer.fields.items():
if not field.write_only:
kwargs[f] = field.get_attribute(obj) kwargs[f] = field.get_attribute(obj)
return cls(errors=None, **kwargs) return cls(errors=None, **kwargs)

View File

@ -57,18 +57,27 @@ def convert_serializer_field(field, is_input=True):
def convert_serializer_to_input_type(serializer_class): 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() serializer = serializer_class()
items = { items = {
name: convert_serializer_field(field) name: convert_serializer_field(field)
for name, field in serializer.fields.items() for name, field in serializer.fields.items()
} }
ret_type = type(
return type(
"{}Input".format(serializer.__class__.__name__), "{}Input".format(serializer.__class__.__name__),
(graphene.InputObjectType,), (graphene.InputObjectType,),
items, 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) @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 import datetime
from py.test import mark, raises
from rest_framework import serializers
from graphene import Field, ResolveInfo from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...settings import graphene_settings
from ...types import DjangoObjectType from ...types import DjangoObjectType
from ..models import MyFakeModel from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation from ..mutation import SerializerMutation
@ -86,6 +87,51 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.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(): def test_nested_model():
class MyFakeModelGrapheneType(DjangoObjectType): class MyFakeModelGrapheneType(DjangoObjectType):
class Meta: class Meta:
@ -168,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0 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(): def test_invalid_serializer_operations():
with raises(Exception) as exc: with raises(Exception) as exc:

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import pytest import pytest
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from graphene import NonNull
from py.test import raises from py.test import raises
import graphene import graphene
@ -196,6 +197,23 @@ def test_field_with_choices_collision():
convert_django_field_with_choices(field) 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(): def test_should_float_convert_float():
assert_conversion(models.FloatField, graphene.Float) assert_conversion(models.FloatField, graphene.Float)
@ -217,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List) # A NonNull List of NonNull A ([A!]!)
assert dynamic_field.type.of_type == 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(): def test_should_manytomany_convert_connectionorlist_connection():
@ -233,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, ConnectionField) 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(): def test_should_manytoone_convert_connectionorlist():
@ -245,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List) # a NonNull List of NonNull A ([A!]!)
assert dynamic_field.type.of_type == 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(): def test_should_onetoone_reverse_convert_model():

View File

@ -1,3 +1,4 @@
import base64
import datetime import datetime
import pytest import pytest
@ -7,6 +8,7 @@ from py.test import raises
from django.db.models import Q from django.db.models import Q
from graphql_relay import to_global_id
import graphene import graphene
from graphene.relay import Node from graphene.relay import Node
@ -26,7 +28,7 @@ def test_should_query_only_fields():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("articles",) fields = ("articles",)
schema = graphene.Schema(query=ReporterType) schema = graphene.Schema(query=ReporterType)
query = """ query = """
@ -42,7 +44,7 @@ def test_should_query_simplelazy_objects():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("id",) fields = ("id",)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType) reporter = graphene.Field(ReporterType)
@ -226,12 +228,68 @@ def test_should_node():
assert result.data == expected 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(): def test_should_query_connectionfields():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
only_fields = ("articles",) fields = ("articles",)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
@ -271,7 +329,7 @@ def test_should_keep_annotations():
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
only_fields = ("articles",) fields = ("articles",)
class ArticleType(DjangoObjectType): class ArticleType(DjangoObjectType):
class Meta: class Meta:
@ -895,8 +953,7 @@ def test_should_handle_inherited_choices():
def test_proxy_model_support(): def test_proxy_model_support():
""" """
This test asserts that we can query for all Reporters, This test asserts that we can query for all Reporters and proxied Reporters.
even if some are of a proxy model type at runtime.
""" """
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
@ -905,11 +962,17 @@ def test_proxy_model_support():
interfaces = (Node,) interfaces = (Node,)
use_connection = True 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 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", first_name="Some",
last_name="Guy", last_name="Guy",
email="someguy@cnn.com", email="someguy@cnn.com",
@ -919,6 +982,7 @@ def test_proxy_model_support():
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
cnn_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
query = """ query = """
@ -930,16 +994,28 @@ def test_proxy_model_support():
} }
} }
} }
cnnReporters {
edges {
node {
id
}
}
}
} }
""" """
expected = { expected = {
"allReporters": { "allReporters": {
"edges": [ "edges": [
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}, {"node": {"id": to_global_id("ReporterType", reporter.id)}},
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}, {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
] ]
} },
"cnnReporters": {
"edges": [
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
]
},
} }
result = schema.execute(query) result = schema.execute(query)
@ -947,68 +1023,6 @@ def test_proxy_model_support():
assert result.data == expected 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(): def test_should_resolve_get_queryset_connectionfields():
reporter_1 = Reporter.objects.create( reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
@ -1051,3 +1065,54 @@ def test_should_resolve_get_queryset_connectionfields():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == expected 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 Reporter2(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("id", "email") fields = ("id", "email")
assert list(Reporter2._meta.fields.keys()) == ["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 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 graphene.relay import Node
from .. import registry from .. import registry
@ -165,10 +170,10 @@ type Reporter {
firstName: String! firstName: String!
lastName: String! lastName: String!
email: String! email: String!
pets: [Reporter] pets: [Reporter!]!
aChoice: ReporterAChoice! aChoice: ReporterAChoice
reporterType: ReporterReporterType reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
} }
enum ReporterAChoice { enum ReporterAChoice {
@ -206,6 +211,8 @@ def with_local_registry(func):
@with_local_registry @with_local_registry
def test_django_objecttype_only_fields(): def test_django_objecttype_only_fields():
with pytest.warns(PendingDeprecationWarning):
class Reporter(DjangoObjectType): class Reporter(DjangoObjectType):
class Meta: class Meta:
model = ReporterModel model = ReporterModel
@ -216,11 +223,204 @@ def test_django_objecttype_only_fields():
@with_local_registry @with_local_registry
def test_django_objecttype_exclude_fields(): def test_django_objecttype_fields():
class Reporter(DjangoObjectType): class Reporter(DjangoObjectType):
class Meta: class Meta:
model = ReporterModel 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()) fields = list(Reporter._meta.fields.keys())
assert "email" not in fields 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 from .models import Film, Reporter
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
film_fields = get_model_fields(Film) film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields]) film_name_set = set([field[0] for field in film_fields])
assert len(film_fields) == len(film_name_set) 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 from collections import OrderedDict
import six
from django.db.models import Model from django.db.models import Model
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
import graphene import graphene
from graphene import Field from graphene import Field
from graphene.relay import Connection, Node 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 .converter import convert_django_field_with_choices
from .registry import Registry, get_global_registry 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: if six.PY3:
from typing import Type 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) _model_fields = get_model_fields(model)
fields = OrderedDict() 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. # in there. Or when we exclude this field in exclude_fields.
# Or when there is no back reference. # Or when there is no back reference.
continue 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 fields[name] = converted
return fields return fields
@ -45,6 +68,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
connection = None # type: Type[Connection] connection = None # type: Type[Connection]
filter_fields = () filter_fields = ()
filterset_class = None
class DjangoObjectType(ObjectType): class DjangoObjectType(ObjectType):
@ -54,13 +78,17 @@ class DjangoObjectType(ObjectType):
model=None, model=None,
registry=None, registry=None,
skip_registry=False, skip_registry=False,
only_fields=(), only_fields=(), # deprecated in favour of `fields`
exclude_fields=(), fields=(),
exclude_fields=(), # deprecated in favour of `exclude`
exclude=(),
filter_fields=None, filter_fields=None,
filterset_class=None,
connection=None, connection=None,
connection_class=None, connection_class=None,
use_connection=None, use_connection=None,
interfaces=(), interfaces=(),
convert_choices_to_enum=True,
_meta=None, _meta=None,
**options **options
): ):
@ -76,11 +104,60 @@ class DjangoObjectType(ObjectType):
'Registry, received "{}".' 'Registry, received "{}".'
).format(cls.__name__, registry) ).format(cls.__name__, registry)
if not DJANGO_FILTER_INSTALLED and filter_fields: if filter_fields and filterset_class:
raise Exception("Can only set filter_fields if Django-Filter is installed") 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( 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: if use_connection is None and interfaces:
@ -108,6 +185,7 @@ class DjangoObjectType(ObjectType):
_meta.model = model _meta.model = model
_meta.registry = registry _meta.registry = registry
_meta.filter_fields = filter_fields _meta.filter_fields = filter_fields
_meta.filterset_class = filterset_class
_meta.fields = django_fields _meta.fields = django_fields
_meta.connection = connection _meta.connection = connection
@ -131,7 +209,11 @@ class DjangoObjectType(ObjectType):
if not is_valid_django_model(type(root)): if not is_valid_django_model(type(root)):
raise Exception(('Received incompatible instance "{}".').format(root)) raise Exception(('Received incompatible instance "{}".').format(root))
if cls._meta.model._meta.proxy:
model = root._meta.model
else:
model = root._meta.model._meta.concrete_model model = root._meta.model._meta.concrete_model
return model == cls._meta.model return model == cls._meta.model
@classmethod @classmethod
@ -150,3 +232,8 @@ class DjangoObjectType(ObjectType):
class ErrorType(ObjectType): class ErrorType(ObjectType):
field = graphene.String(required=True) field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(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 ( from .utils import (
DJANGO_FILTER_INSTALLED, DJANGO_FILTER_INSTALLED,
get_reverse_fields, camelize,
maybe_queryset,
get_model_fields, get_model_fields,
is_valid_django_model, get_reverse_fields,
import_single_dispatch, import_single_dispatch,
is_valid_django_model,
maybe_queryset,
) )
from .testing import GraphQLTestCase
__all__ = [ __all__ = [
"DJANGO_FILTER_INSTALLED", "DJANGO_FILTER_INSTALLED",
"get_reverse_fields", "get_reverse_fields",
"maybe_queryset", "maybe_queryset",
"get_model_fields", "get_model_fields",
"camelize",
"is_valid_django_model", "is_valid_django_model",
"import_single_dispatch", "import_single_dispatch",
"GraphQLTestCase", "GraphQLTestCase",

View File

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

View File

@ -2,7 +2,11 @@ import inspect
from django.db import models from django.db import models
from django.db.models.manager import Manager 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: try:
import django_filters # noqa import django_filters # noqa
@ -12,13 +16,36 @@ except ImportError:
DJANGO_FILTER_INSTALLED = False 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): def get_reverse_fields(model, local_field_names):
for name, attr in model.__dict__.items(): for name, attr in model.__dict__.items():
# Don't duplicate any local fields # Don't duplicate any local fields
if name in local_field_names: if name in local_field_names:
continue 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): if isinstance(related, models.ManyToOneRel):
yield (name, related) yield (name, related)
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical: elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:

View File

@ -5,11 +5,41 @@ test=pytest
universal=1 universal=1
[flake8] [flake8]
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/* exclude = docs,graphene_django/debug/sql/*,migrations
max-line-length = 120 max-line-length = 120
select =
# Dictionary key repeated
F601,
# Ensure use of ==/!= to compare with str, bytes and int literals
F632,
# Redefinition of unused name
F811,
# Using an undefined variable
F821,
# Defining an undefined variable in __all__
F822,
# Using a variable before it is assigned
F823,
# Duplicate argument in function declaration
F831,
# Black would format this line
BLK,
# Do not use bare except
B001,
# Don't allow ++n. You probably meant n += 1
B002,
# Do not use mutable structures for argument defaults
B006,
# Do not perform calls in argument defaults
B008
[coverage:run] [coverage:run]
omit = */tests/* omit = */tests/*
[isort] [isort]
known_first_party=graphene,graphene_django 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 = [ dev_requires = [
"black==19.3b0", "black==19.3b0",
"flake8==3.7.7", "flake8==3.7.7",
"flake8-black==0.1.0",
"flake8-bugbear==19.3.0",
] + tests_require ] + tests_require
setup( setup(
@ -64,7 +66,11 @@ setup(
setup_requires=["pytest-runner"], setup_requires=["pytest-runner"],
tests_require=tests_require, tests_require=tests_require,
rest_framework_require=rest_framework_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, include_package_data=True,
zip_safe=False, zip_safe=False,
platforms="any", 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