Merge remote-tracking branch 'up/master' into enum_conversion_fixes

This commit is contained in:
Jason Kraus 2019-06-17 11:38:16 -07:00
commit 4ba3b544c9
54 changed files with 608 additions and 459 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

View File

@ -1,58 +1,58 @@
language: python language: python
sudo: required cache: pip
dist: xenial dist: xenial
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
env:
matrix:
- DJANGO=1.11
- DJANGO=2.1
- DJANGO=2.2
- DJANGO=master
install: install:
- TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO} - pip install tox tox-travis
- pip install tox
- tox -e $TOX_ENV --notest
script: script:
- tox -e $TOX_ENV - tox
after_success: after_success:
- tox -e $TOX_ENV -- pip install coveralls - pip install coveralls
- tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION - coveralls
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: 3.5
script: tox -e lint
exclude:
- python: 2.7 - python: 2.7
env: DJANGO=2.1 env: DJANGO=1.11
- python: 2.7
env: DJANGO=2.2
- python: 2.7
env: DJANGO=master
- python: 3.4
env: DJANGO=2.1
- python: 3.4
env: DJANGO=2.2
- python: 3.4
env: DJANGO=master
- python: 3.5 - python: 3.5
env: DJANGO=1.11
- python: 3.5
env: DJANGO=2.0
- python: 3.5
env: DJANGO=2.1
- python: 3.5
env: DJANGO=2.2
- python: 3.6
env: DJANGO=1.11
- python: 3.6
env: DJANGO=2.0
- python: 3.6
env: DJANGO=2.1
- python: 3.6
env: DJANGO=2.2
- python: 3.6
env: DJANGO=master env: DJANGO=master
- python: 3.7
env: DJANGO=1.10
- python: 3.7 - python: 3.7
env: DJANGO=1.11 env: DJANGO=1.11
allow_failures:
- python: 3.7 - 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 - env: DJANGO=master
deploy: deploy:

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
.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

@ -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,7 +154,8 @@ Adding Login Required
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.: 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
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView

View File

@ -92,6 +92,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
-------------- --------------

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.

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.recipes.schema.Query, cookbook.ingredients.schema.Query,
graphene.ObjectType): cookbook.recipes.schema.Query,
debug = graphene.Field(DjangoDebug, name='_debug') graphene.ObjectType,
):
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.9

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,7 @@ 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")
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,15 @@ 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")
ingredient = models.ForeignKey(Ingredient, related_name='used_by') ingredient = models.ForeignKey(Ingredient, related_name="used_by")
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.recipes.schema.Query, cookbook.ingredients.schema.Query,
graphene.ObjectType): cookbook.recipes.schema.Query,
debug = graphene.Field(DjangoDebug, name='_debug') graphene.ObjectType,
):
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,62 @@ 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_CLASSES = [
'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.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 +97,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 +122,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.20 django==1.11.21
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,18 +2,16 @@ 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,)
@classmethod @classmethod
def get_node(cls, info, id): def get_node(cls, info, id):
@ -22,16 +20,14 @@ 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,)
@classmethod @classmethod
def get_node(cls, info, id): def get_node(cls, info, id):
@ -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,49 +29,23 @@ 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": {
'faction': { "edges": [
'name': 'Alliance to Restore the Republic', {"node": {"id": "U2hpcDox", "name": "X-Wing"}},
'ships': { {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
'edges': [{ {"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
'node': { {"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
'id': 'U2hpcDox', {"node": {"id": "U2hpcDo1", "name": "Home One"}},
'name': 'X-Wing' {"node": {"id": "U2hpcDo5", "name": "Peter"}},
} ]
}, {
'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)

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

@ -71,13 +71,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))
@ -196,7 +198,11 @@ 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 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

View File

@ -41,10 +41,9 @@ class DjangoFilterConnectionField(DjangoConnectionField):
meta.update(self._extra_filter_meta) meta.update(self._extra_filter_meta)
filterset_class = self._provided_filterset_class or ( filterset_class = self._provided_filterset_class or (
self.node_type._meta.filterset_class) self.node_type._meta.filterset_class
self._filterset_class = get_filterset_class(
filterset_class, **meta
) )
self._filterset_class = get_filterset_class(filterset_class, **meta)
return self._filterset_class return self._filterset_class

View File

@ -229,6 +229,7 @@ def test_filter_filterset_information_on_meta_related():
def test_filter_filterset_class_filter_fields_exception(): def test_filter_filterset_class_filter_fields_exception():
with pytest.raises(Exception): with pytest.raises(Exception):
class ReporterFilter(FilterSet): class ReporterFilter(FilterSet):
class Meta: class Meta:
model = Reporter model = Reporter

View File

@ -104,7 +104,9 @@ def test_write_only_field():
) )
assert hasattr(result, "cool_name") assert hasattr(result, "cool_name")
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" assert not hasattr(
result, "password"
), "'password' is write_only field and shouldn't be visible"
@mark.django_db @mark.django_db
@ -124,7 +126,9 @@ def test_write_only_field_using_extra_kwargs():
) )
assert hasattr(result, "cool_name") assert hasattr(result, "cool_name")
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible" assert not hasattr(
result, "password"
), "'password' is write_only field and shouldn't be visible"
def test_nested_model(): def test_nested_model():

View File

@ -203,15 +203,12 @@ def test_field_with_choices_underscore():
("__percentage__", "Percentage"), ("__percentage__", "Percentage"),
("_not_sunder__", "Not Single Underscore"), ("_not_sunder__", "Not Single Underscore"),
("__not_dunder", "Not Double Underscore"), ("__not_dunder", "Not Double Underscore"),
), )
) )
class UnderscoreChoicesModel(models.Model): class UnderscoreChoicesModel(models.Model):
ourfield = field ourfield = field
class Meta:
app_label = "test"
graphene_type = convert_django_field_with_choices(field) graphene_type = convert_django_field_with_choices(field)
assert len(graphene_type._meta.enum.__members__) == 4 assert len(graphene_type._meta.enum.__members__) == 4
assert "A_AMOUNT_" in graphene_type._meta.enum.__members__ assert "A_AMOUNT_" in graphene_type._meta.enum.__members__
@ -219,6 +216,24 @@ def test_field_with_choices_underscore():
assert "_NOT_SUNDER__" in graphene_type._meta.enum.__members__ assert "_NOT_SUNDER__" in graphene_type._meta.enum.__members__
assert "__NOT_DUNDER" in graphene_type._meta.enum.__members__ assert "__NOT_DUNDER" in graphene_type._meta.enum.__members__
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)

View File

@ -1015,7 +1015,7 @@ def test_proxy_model_support():
"edges": [ "edges": [
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
] ]
} },
} }
result = schema.execute(query) result = schema.execute(query)

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
@ -225,3 +230,111 @@ def test_django_objecttype_exclude_fields():
fields = list(Reporter._meta.fields.keys()) fields = list(Reporter._meta.fields.keys())
assert "email" not in fields assert "email" not in fields
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

@ -18,7 +18,9 @@ if six.PY3:
from typing import Type from typing import Type
def construct_fields(model, registry, only_fields, exclude_fields): 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 +35,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
@ -63,6 +76,7 @@ class DjangoObjectType(ObjectType):
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
): ):
@ -82,13 +96,18 @@ class DjangoObjectType(ObjectType):
raise Exception("Can't set both filter_fields and filterset_class") raise Exception("Can't set both filter_fields and filterset_class")
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
raise Exception(( raise Exception(
"Can only set filter_fields or filterset_class if " (
"Django-Filter is installed" "Can only set filter_fields or filterset_class if "
)) "Django-Filter is installed"
)
)
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, only_fields, exclude_fields, convert_choices_to_enum
),
_as=Field,
) )
if use_connection is None and interfaces: if use_connection is None and interfaces:

44
tox.ini
View File

@ -1,31 +1,39 @@
[tox] [tox]
envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint 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] [testenv]
passenv = * passenv = *
usedevelop = True usedevelop = True
setenv = setenv =
DJANGO_SETTINGS_MODULE=django_test_settings DJANGO_SETTINGS_MODULE=django_test_settings
basepython =
py2.7: python2.7
py3.4: python3.4
py3.5: python3.5
py3.6: python3.6
py3.7: python3.7
pypypy: pypy
pypypy3: pypy3
deps = deps =
-e.[test] -e.[test]
psycopg2 psycopg2
django1.10: Django>=1.10,<1.11 django111: Django>=1.11,<2.0
django1.11: Django>=1.11,<1.12 django20: Django>=2.0,<2.1
django2.0: Django>=2.0 django21: Django>=2.1,<2.2
django2.1: Django>=2.1 django22: Django>=2.2,<3.0
djangomaster: https://github.com/django/django/archive/master.zip djangomaster: https://github.com/django/django/archive/master.zip
commands = {posargs:py.test --cov=graphene_django graphene_django examples} commands = {posargs:py.test --cov=graphene_django graphene_django examples}
[testenv:lint] [testenv:black]
basepython = python basepython = python3.7
deps = deps = black
prospector commands =
commands = prospector graphene_django -0 black --exclude "/migrations/" graphene_django examples --check
[testenv:flake8]
basepython = python3.7
deps = flake8
commands =
flake8 graphene_django examples