mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-23 02:22:08 +03:00
Merge remote-tracking branch 'up/master' into drf-choices
This commit is contained in:
commit
711d5cce45
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal 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
2
.gitignore
vendored
|
@ -78,3 +78,5 @@ Session.vim
|
|||
*~
|
||||
# auto-generated tag files
|
||||
tags
|
||||
.tox/
|
||||
.pytest_cache/
|
||||
|
|
104
.travis.yml
104
.travis.yml
|
@ -1,62 +1,60 @@
|
|||
language: python
|
||||
sudo: required
|
||||
cache: pip
|
||||
dist: xenial
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = build ]; then
|
||||
pip install -e .[test]
|
||||
pip install psycopg2==2.8.2 # Required for Django postgres fields testing
|
||||
pip install django==$DJANGO_VERSION
|
||||
python setup.py develop
|
||||
elif [ "$TEST_TYPE" = lint ]; then
|
||||
pip install flake8==3.7.7
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = lint ]; then
|
||||
echo "Checking Python code lint."
|
||||
flake8 graphene_django
|
||||
exit
|
||||
elif [ "$TEST_TYPE" = build ]; then
|
||||
py.test --cov=graphene_django graphene_django examples
|
||||
fi
|
||||
after_success:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = build ]; then
|
||||
coveralls
|
||||
fi
|
||||
env:
|
||||
matrix:
|
||||
- TEST_TYPE=build DJANGO_VERSION=1.11
|
||||
- pip install tox tox-travis
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- pip install coveralls
|
||||
- coveralls
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: '3.4'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.5'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.5'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.2
|
||||
- python: '3.7'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.2
|
||||
- python: '2.7'
|
||||
env: TEST_TYPE=lint
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=lint
|
||||
- python: '3.7'
|
||||
env: TEST_TYPE=lint
|
||||
- python: 2.7
|
||||
env: DJANGO=1.11
|
||||
|
||||
- python: 3.5
|
||||
env: DJANGO=1.11
|
||||
- python: 3.5
|
||||
env: DJANGO=2.0
|
||||
- python: 3.5
|
||||
env: DJANGO=2.1
|
||||
- python: 3.5
|
||||
env: DJANGO=2.2
|
||||
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11
|
||||
- python: 3.6
|
||||
env: DJANGO=2.0
|
||||
- python: 3.6
|
||||
env: DJANGO=2.1
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2
|
||||
- python: 3.6
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.7
|
||||
env: DJANGO=1.11
|
||||
- python: 3.7
|
||||
env: DJANGO=2.0
|
||||
- python: 3.7
|
||||
env: DJANGO=2.1
|
||||
- python: 3.7
|
||||
env: DJANGO=2.2
|
||||
- python: 3.7
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.7
|
||||
env: TOXENV=black,flake8
|
||||
|
||||
allow_failures:
|
||||
- env: DJANGO=master
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: syrusakbary
|
||||
|
|
24
Makefile
24
Makefile
|
@ -1,11 +1,29 @@
|
|||
.PHONY: dev-setup ## Install development dependencies
|
||||
dev-setup:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
.PHONY: install-dev
|
||||
install-dev: dev-setup # Alias install-dev -> dev-setup
|
||||
|
||||
.PHONY: tests
|
||||
tests:
|
||||
py.test graphene_django --cov=graphene_django -vv
|
||||
|
||||
format:
|
||||
black graphene_django
|
||||
.PHONY: test
|
||||
test: tests # Alias test -> tests
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
black --exclude "/migrations/" graphene_django examples setup.py
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
flake8 graphene_django
|
||||
flake8 graphene_django examples
|
||||
|
||||
.PHONY: docs ## Generate docs
|
||||
docs: dev-setup
|
||||
cd docs && make install && make html
|
||||
|
||||
.PHONY: docs-live ## Generate docs with live reloading
|
||||
docs-live: dev-setup
|
||||
cd docs && make install && make livehtml
|
||||
|
|
|
@ -38,12 +38,12 @@ GRAPHENE = {
|
|||
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
|
||||
|
||||
```python
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||
path('graphql', GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -100,4 +100,4 @@ To learn more check out the following [examples](examples/):
|
|||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
|
|
@ -48,12 +48,20 @@ help:
|
|||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
.PHONY: install ## to install all documentation related requirements
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: livehtml ## to build and serve live-reloading documentation
|
||||
livehtml:
|
||||
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
|
|
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
|
@ -154,7 +154,8 @@ Adding Login Required
|
|||
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
|
||||
|
||||
.. code:: python
|
||||
#views.py
|
||||
|
||||
# views.py
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from graphene_django.views import GraphQLView
|
||||
|
|
|
@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
|
|||
``filter_fields``.
|
||||
|
||||
However, you may find this to be insufficient. In these cases you can
|
||||
create your own ``Filterset`` as follows:
|
||||
create your own ``FilterSet``. You can pass it directly as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
|
|||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||
filterset_class=AnimalFilter)
|
||||
|
||||
You can also specify the ``FilterSet`` class using the ``filerset_class``
|
||||
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
||||
in unison with the ``filter_fields`` parameter:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class AnimalFilter(django_filters.FilterSet):
|
||||
# Do case-insensitive lookups on 'name'
|
||||
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
||||
|
||||
class Meta:
|
||||
# Assume you have an Animal model defined with the following fields
|
||||
model = Animal
|
||||
fields = ['name', 'genus', 'is_domesticated']
|
||||
|
||||
|
||||
class AnimalNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Animal
|
||||
filterset_class = AnimalFilter
|
||||
interfaces = (relay.Node, )
|
||||
|
||||
|
||||
class Query(ObjectType):
|
||||
animal = relay.Node.Field(AnimalNode)
|
||||
all_animals = DjangoFilterConnectionField(AnimalNode)
|
||||
|
||||
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
||||
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
||||
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||
|
|
|
@ -35,6 +35,8 @@ Advanced Usage
|
|||
The ``--indent`` option can be used to specify the number of indentation spaces to
|
||||
be used in the output. Defaults to `None` which displays all data on a single line.
|
||||
|
||||
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
|
||||
|
||||
To simplify the command to ``./manage.py graphql_schema``, you can
|
||||
specify the parameters in your settings.py:
|
||||
|
||||
|
|
|
@ -199,7 +199,9 @@ You can use relay with mutations. A Relay mutation must inherit from
|
|||
|
||||
.. code:: python
|
||||
|
||||
import graphene import relay, DjangoObjectType
|
||||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphql_relay import from_global_id
|
||||
|
||||
from .queries import QuestionType
|
||||
|
@ -214,7 +216,7 @@ You can use relay with mutations. A Relay mutation must inherit from
|
|||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, text, id):
|
||||
question = Question.objects.get(pk=from_global_id(id))
|
||||
question = Question.objects.get(pk=from_global_id(id)[1])
|
||||
question.text = text
|
||||
question.save()
|
||||
return QuestionMutation(question=question)
|
||||
|
@ -226,4 +228,4 @@ Relay ClientIDMutation accept a ``clientIDMutation`` argument.
|
|||
This argument is also sent back to the client with the mutation result
|
||||
(you do not have to do anything). For services that manage
|
||||
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
|
||||
allows you to match up a specific mutation with the response.
|
||||
allows you to match up a specific mutation with the response.
|
||||
|
|
110
docs/queries.rst
110
docs/queries.rst
|
@ -41,14 +41,18 @@ Full example
|
|||
return Question.objects.get(pk=question_id)
|
||||
|
||||
|
||||
Fields
|
||||
------
|
||||
Specifying which fields to include
|
||||
----------------------------------
|
||||
|
||||
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
|
||||
If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``.
|
||||
If you only want a subset of fields to be present, you can do so using
|
||||
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
|
||||
all fields that should be exposed using the fields attribute.
|
||||
This will make it less likely to result in unintentionally exposing data when
|
||||
your models change.
|
||||
|
||||
only_fields
|
||||
~~~~~~~~~~~
|
||||
``fields``
|
||||
~~~~~~~~~~
|
||||
|
||||
Show **only** these fields on the model:
|
||||
|
||||
|
@ -57,24 +61,35 @@ Show **only** these fields on the model:
|
|||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
only_fields = ('question_text')
|
||||
fields = ('id', 'question_text')
|
||||
|
||||
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
|
||||
|
||||
exclude_fields
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Show all fields **except** those in ``exclude_fields``:
|
||||
For example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude_fields = ('question_text')
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
Customised fields
|
||||
~~~~~~~~~~~~~~~~~
|
||||
``exclude``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Show all fields **except** those in ``exclude``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude = ('question_text',)
|
||||
|
||||
|
||||
Customising fields
|
||||
------------------
|
||||
|
||||
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
|
||||
|
||||
|
@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
|
|||
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude_fields = ('question_text')
|
||||
fields = ('id', 'question_text')
|
||||
|
||||
extra_field = graphene.String()
|
||||
|
||||
|
@ -92,6 +107,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
|
|||
return 'hello!'
|
||||
|
||||
|
||||
Choices to Enum conversion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default Graphene-Django will convert any Django fields that have `choices`_
|
||||
defined into a GraphQL enum type.
|
||||
|
||||
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
|
||||
|
||||
For example the following ``Model`` and ``DjangoObjectType``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class PetModel(models.Model):
|
||||
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
|
||||
Results in the following GraphQL schema definition:
|
||||
|
||||
.. code::
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
}
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
|
||||
You can disable this automatic conversion by setting
|
||||
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
|
||||
``Meta`` class.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = False
|
||||
|
||||
.. code::
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
}
|
||||
|
||||
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
||||
automatically converted into enums:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = ['kind']
|
||||
|
||||
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
||||
``False``.
|
||||
|
||||
|
||||
Related models
|
||||
--------------
|
||||
|
||||
|
@ -113,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
|
|||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
only_fields = ('category',)
|
||||
fields = ('category',)
|
||||
|
||||
Then all query-able related models must be defined as DjangoObjectType subclass,
|
||||
or they will fail to show if you are trying to query those relation fields. You only
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
sphinx
|
||||
Sphinx==1.5.3
|
||||
sphinx-autobuild==0.7.1
|
||||
# Docs template
|
||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||
|
|
|
@ -30,7 +30,7 @@ Default: ``None``
|
|||
|
||||
|
||||
``SCHEMA_OUTPUT``
|
||||
----------
|
||||
-----------------
|
||||
|
||||
The name of the file where the GraphQL schema output will go.
|
||||
|
||||
|
@ -44,7 +44,7 @@ Default: ``schema.json``
|
|||
|
||||
|
||||
``SCHEMA_INDENT``
|
||||
----------
|
||||
-----------------
|
||||
|
||||
The indentation level of the schema output.
|
||||
|
||||
|
@ -58,7 +58,7 @@ Default: ``2``
|
|||
|
||||
|
||||
``MIDDLEWARE``
|
||||
----------
|
||||
--------------
|
||||
|
||||
A tuple of middleware that will be executed for each GraphQL query.
|
||||
|
||||
|
@ -76,7 +76,7 @@ Default: ``()``
|
|||
|
||||
|
||||
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
|
||||
----------
|
||||
------------------------------------------
|
||||
|
||||
Enforces relay queries to have the ``first`` or ``last`` argument.
|
||||
|
||||
|
@ -90,7 +90,7 @@ Default: ``False``
|
|||
|
||||
|
||||
``RELAY_CONNECTION_MAX_LIMIT``
|
||||
----------
|
||||
------------------------------
|
||||
|
||||
The maximum size of objects that can be requested through a relay connection.
|
||||
|
||||
|
@ -101,3 +101,42 @@ Default: ``100``
|
|||
GRAPHENE = {
|
||||
'RELAY_CONNECTION_MAX_LIMIT': 100,
|
||||
}
|
||||
|
||||
|
||||
``CAMELCASE_ERRORS``
|
||||
------------------------------------
|
||||
|
||||
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||
By default they will be snake case.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': False,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'test_field',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': True,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'testField',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
|
|
@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
|
|||
|
||||
@admin.register(Ingredient)
|
||||
class IngredientAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'category')
|
||||
list_editable = ('name', 'category')
|
||||
list_display = ("id", "name", "category")
|
||||
list_editable = ("name", "category")
|
||||
|
||||
|
||||
admin.site.register(Category)
|
||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class IngredientsConfig(AppConfig):
|
||||
name = 'cookbook.ingredients'
|
||||
label = 'ingredients'
|
||||
verbose_name = 'Ingredients'
|
||||
name = "cookbook.ingredients"
|
||||
label = "ingredients"
|
||||
verbose_name = "Ingredients"
|
||||
|
|
|
@ -3,7 +3,8 @@ from django.db import models
|
|||
|
||||
class Category(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural = 'Categories'
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -13,7 +14,9 @@ class Category(models.Model):
|
|||
class Ingredient(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
notes = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -15,14 +15,12 @@ class IngredientType(DjangoObjectType):
|
|||
|
||||
|
||||
class Query(object):
|
||||
category = graphene.Field(CategoryType,
|
||||
id=graphene.Int(),
|
||||
name=graphene.String())
|
||||
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
|
||||
all_categories = graphene.List(CategoryType)
|
||||
|
||||
ingredient = graphene.Field(IngredientType,
|
||||
id=graphene.Int(),
|
||||
name=graphene.String())
|
||||
ingredient = graphene.Field(
|
||||
IngredientType, id=graphene.Int(), name=graphene.String()
|
||||
)
|
||||
all_ingredients = graphene.List(IngredientType)
|
||||
|
||||
def resolve_all_categories(self, context):
|
||||
|
@ -30,7 +28,7 @@ class Query(object):
|
|||
|
||||
def resolve_all_ingredients(self, context):
|
||||
# We can easily optimize query count in the resolve method
|
||||
return Ingredient.objects.select_related('category').all()
|
||||
return Ingredient.objects.select_related("category").all()
|
||||
|
||||
def resolve_category(self, context, id=None, name=None):
|
||||
if id is not None:
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class RecipesConfig(AppConfig):
|
||||
name = 'cookbook.recipes'
|
||||
label = 'recipes'
|
||||
verbose_name = 'Recipes'
|
||||
name = "cookbook.recipes"
|
||||
label = "recipes"
|
||||
verbose_name = "Recipes"
|
||||
|
|
|
@ -6,17 +6,23 @@ from ..ingredients.models import Ingredient
|
|||
class Recipe(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
instructions = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class RecipeIngredient(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
|
||||
ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
|
||||
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
|
||||
ingredient = models.ForeignKey(
|
||||
Ingredient, related_name="used_by", on_delete=models.CASCADE
|
||||
)
|
||||
amount = models.FloatField()
|
||||
unit = models.CharField(max_length=20, choices=(
|
||||
('unit', 'Units'),
|
||||
('kg', 'Kilograms'),
|
||||
('l', 'Litres'),
|
||||
('st', 'Shots'),
|
||||
))
|
||||
unit = models.CharField(
|
||||
max_length=20,
|
||||
choices=(
|
||||
("unit", "Units"),
|
||||
("kg", "Kilograms"),
|
||||
("l", "Litres"),
|
||||
("st", "Shots"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -15,13 +15,10 @@ class RecipeIngredientType(DjangoObjectType):
|
|||
|
||||
|
||||
class Query(object):
|
||||
recipe = graphene.Field(RecipeType,
|
||||
id=graphene.Int(),
|
||||
title=graphene.String())
|
||||
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
|
||||
all_recipes = graphene.List(RecipeType)
|
||||
|
||||
recipeingredient = graphene.Field(RecipeIngredientType,
|
||||
id=graphene.Int())
|
||||
recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int())
|
||||
all_recipeingredients = graphene.List(RecipeIngredientType)
|
||||
|
||||
def resolve_recipe(self, context, id=None, title=None):
|
||||
|
@ -43,5 +40,5 @@ class Query(object):
|
|||
return Recipe.objects.all()
|
||||
|
||||
def resolve_all_recipeingredients(self, context):
|
||||
related = ['recipe', 'ingredient']
|
||||
related = ["recipe", "ingredient"]
|
||||
return RecipeIngredient.objects.select_related(*related).all()
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -5,10 +5,12 @@ import graphene
|
|||
from graphene_django.debug import DjangoDebug
|
||||
|
||||
|
||||
class Query(cookbook.ingredients.schema.Query,
|
||||
cookbook.recipes.schema.Query,
|
||||
graphene.ObjectType):
|
||||
debug = graphene.Field(DjangoDebug, name='_debug')
|
||||
class Query(
|
||||
cookbook.ingredients.schema.Query,
|
||||
cookbook.recipes.schema.Query,
|
||||
graphene.ObjectType,
|
||||
):
|
||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
|
|
@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
|
||||
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
@ -32,64 +32,61 @@ ALLOWED_HOSTS = []
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'graphene_django',
|
||||
|
||||
'cookbook.ingredients.apps.IngredientsConfig',
|
||||
'cookbook.recipes.apps.RecipesConfig',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"graphene_django",
|
||||
"cookbook.ingredients.apps.IngredientsConfig",
|
||||
"cookbook.recipes.apps.RecipesConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'cookbook.schema.schema',
|
||||
'SCHEMA_INDENT': 2,
|
||||
'MIDDLEWARE': (
|
||||
'graphene_django.debug.DjangoDebugMiddleware',
|
||||
)
|
||||
"SCHEMA": "cookbook.schema.schema",
|
||||
"SCHEMA_INDENT": 2,
|
||||
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'cookbook.urls'
|
||||
ROOT_URLCONF = "cookbook.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'cookbook.wsgi.application'
|
||||
WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,26 +96,20 @@ DATABASES = {
|
|||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
@ -130,4 +121,4 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('graphql/', GraphQLView.as_view(graphiql=True)),
|
||||
path("admin/", admin.site.urls),
|
||||
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==2.1.6
|
||||
django==2.1.10
|
||||
|
|
|
@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
|
|||
|
||||
@admin.register(Ingredient)
|
||||
class IngredientAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'category')
|
||||
list_editable = ('name', 'category')
|
||||
list_display = ("id", "name", "category")
|
||||
list_editable = ("name", "category")
|
||||
|
||||
|
||||
admin.site.register(Category)
|
||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class IngredientsConfig(AppConfig):
|
||||
name = 'cookbook.ingredients'
|
||||
label = 'ingredients'
|
||||
verbose_name = 'Ingredients'
|
||||
name = "cookbook.ingredients"
|
||||
label = "ingredients"
|
||||
verbose_name = "Ingredients"
|
||||
|
|
|
@ -11,7 +11,9 @@ class Category(models.Model):
|
|||
class Ingredient(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
notes = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(Category, related_name='ingredients')
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -7,24 +7,22 @@ from graphene_django.types import DjangoObjectType
|
|||
# Graphene will automatically map the Category model's fields onto the CategoryNode.
|
||||
# This is configured in the CategoryNode's Meta class (as you can see below)
|
||||
class CategoryNode(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
interfaces = (Node, )
|
||||
filter_fields = ['name', 'ingredients']
|
||||
interfaces = (Node,)
|
||||
filter_fields = ["name", "ingredients"]
|
||||
|
||||
|
||||
class IngredientNode(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
# Allow for some more advanced filtering here
|
||||
interfaces = (Node, )
|
||||
interfaces = (Node,)
|
||||
filter_fields = {
|
||||
'name': ['exact', 'icontains', 'istartswith'],
|
||||
'notes': ['exact', 'icontains'],
|
||||
'category': ['exact'],
|
||||
'category__name': ['exact'],
|
||||
"name": ["exact", "icontains", "istartswith"],
|
||||
"notes": ["exact", "icontains"],
|
||||
"category": ["exact"],
|
||||
"category__name": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class RecipesConfig(AppConfig):
|
||||
name = 'cookbook.recipes'
|
||||
label = 'recipes'
|
||||
verbose_name = 'Recipes'
|
||||
name = "cookbook.recipes"
|
||||
label = "recipes"
|
||||
verbose_name = "Recipes"
|
||||
|
|
|
@ -10,12 +10,17 @@ class Recipe(models.Model):
|
|||
|
||||
|
||||
class RecipeIngredient(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, related_name='amounts')
|
||||
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
|
||||
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
|
||||
ingredient = models.ForeignKey(
|
||||
Ingredient, related_name="used_by", on_delete=models.CASCADE
|
||||
)
|
||||
amount = models.FloatField()
|
||||
unit = models.CharField(max_length=20, choices=(
|
||||
('unit', 'Units'),
|
||||
('kg', 'Kilograms'),
|
||||
('l', 'Litres'),
|
||||
('st', 'Shots'),
|
||||
))
|
||||
unit = models.CharField(
|
||||
max_length=20,
|
||||
choices=(
|
||||
("unit", "Units"),
|
||||
("kg", "Kilograms"),
|
||||
("l", "Litres"),
|
||||
("st", "Shots"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -3,24 +3,23 @@ from graphene import Node
|
|||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
from graphene_django.types import DjangoObjectType
|
||||
|
||||
class RecipeNode(DjangoObjectType):
|
||||
|
||||
class RecipeNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Recipe
|
||||
interfaces = (Node, )
|
||||
filter_fields = ['title','amounts']
|
||||
interfaces = (Node,)
|
||||
filter_fields = ["title", "amounts"]
|
||||
|
||||
|
||||
class RecipeIngredientNode(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = RecipeIngredient
|
||||
# Allow for some more advanced filtering here
|
||||
interfaces = (Node, )
|
||||
interfaces = (Node,)
|
||||
filter_fields = {
|
||||
'ingredient__name': ['exact', 'icontains', 'istartswith'],
|
||||
'recipe': ['exact'],
|
||||
'recipe__title': ['icontains'],
|
||||
"ingredient__name": ["exact", "icontains", "istartswith"],
|
||||
"recipe": ["exact"],
|
||||
"recipe__title": ["icontains"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -5,10 +5,12 @@ import graphene
|
|||
from graphene_django.debug import DjangoDebug
|
||||
|
||||
|
||||
class Query(cookbook.ingredients.schema.Query,
|
||||
cookbook.recipes.schema.Query,
|
||||
graphene.ObjectType):
|
||||
debug = graphene.Field(DjangoDebug, name='_debug')
|
||||
class Query(
|
||||
cookbook.ingredients.schema.Query,
|
||||
cookbook.recipes.schema.Query,
|
||||
graphene.ObjectType,
|
||||
):
|
||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
|
|
@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
|
||||
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
@ -32,65 +32,61 @@ ALLOWED_HOSTS = []
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'graphene_django',
|
||||
|
||||
'cookbook.ingredients.apps.IngredientsConfig',
|
||||
'cookbook.recipes.apps.RecipesConfig',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"graphene_django",
|
||||
"cookbook.ingredients.apps.IngredientsConfig",
|
||||
"cookbook.recipes.apps.RecipesConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'cookbook.schema.schema',
|
||||
'SCHEMA_INDENT': 2,
|
||||
'MIDDLEWARE': (
|
||||
'graphene_django.debug.DjangoDebugMiddleware',
|
||||
)
|
||||
"SCHEMA": "cookbook.schema.schema",
|
||||
"SCHEMA_INDENT": 2,
|
||||
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'cookbook.urls'
|
||||
ROOT_URLCONF = "cookbook.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'cookbook.wsgi.application'
|
||||
WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,26 +96,20 @@ DATABASES = {
|
|||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
@ -131,4 +121,4 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||
url(r"^admin/", admin.site.urls),
|
||||
url(r"^graphql$", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==1.11.19
|
||||
django==2.2.3
|
||||
django-filter>=2
|
||||
|
|
|
@ -2,97 +2,50 @@ from .models import Character, Faction, Ship
|
|||
|
||||
|
||||
def initialize():
|
||||
human = Character(
|
||||
name='Human'
|
||||
)
|
||||
human = Character(name="Human")
|
||||
human.save()
|
||||
|
||||
droid = Character(
|
||||
name='Droid'
|
||||
)
|
||||
droid = Character(name="Droid")
|
||||
droid.save()
|
||||
|
||||
rebels = Faction(
|
||||
id='1',
|
||||
name='Alliance to Restore the Republic',
|
||||
hero=human
|
||||
)
|
||||
rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human)
|
||||
rebels.save()
|
||||
|
||||
empire = Faction(
|
||||
id='2',
|
||||
name='Galactic Empire',
|
||||
hero=droid
|
||||
)
|
||||
empire = Faction(id="2", name="Galactic Empire", hero=droid)
|
||||
empire.save()
|
||||
|
||||
xwing = Ship(
|
||||
id='1',
|
||||
name='X-Wing',
|
||||
faction=rebels,
|
||||
)
|
||||
xwing = Ship(id="1", name="X-Wing", faction=rebels)
|
||||
xwing.save()
|
||||
|
||||
human.ship = xwing
|
||||
human.save()
|
||||
|
||||
ywing = Ship(
|
||||
id='2',
|
||||
name='Y-Wing',
|
||||
faction=rebels,
|
||||
)
|
||||
ywing = Ship(id="2", name="Y-Wing", faction=rebels)
|
||||
ywing.save()
|
||||
|
||||
awing = Ship(
|
||||
id='3',
|
||||
name='A-Wing',
|
||||
faction=rebels,
|
||||
)
|
||||
awing = Ship(id="3", name="A-Wing", faction=rebels)
|
||||
awing.save()
|
||||
|
||||
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
|
||||
# so for the purposes of this demo it's a rebel ship.
|
||||
falcon = Ship(
|
||||
id='4',
|
||||
name='Millenium Falcon',
|
||||
faction=rebels,
|
||||
)
|
||||
falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
|
||||
falcon.save()
|
||||
|
||||
homeOne = Ship(
|
||||
id='5',
|
||||
name='Home One',
|
||||
faction=rebels,
|
||||
)
|
||||
homeOne = Ship(id="5", name="Home One", faction=rebels)
|
||||
homeOne.save()
|
||||
|
||||
tieFighter = Ship(
|
||||
id='6',
|
||||
name='TIE Fighter',
|
||||
faction=empire,
|
||||
)
|
||||
tieFighter = Ship(id="6", name="TIE Fighter", faction=empire)
|
||||
tieFighter.save()
|
||||
|
||||
tieInterceptor = Ship(
|
||||
id='7',
|
||||
name='TIE Interceptor',
|
||||
faction=empire,
|
||||
)
|
||||
tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire)
|
||||
tieInterceptor.save()
|
||||
|
||||
executor = Ship(
|
||||
id='8',
|
||||
name='Executor',
|
||||
faction=empire,
|
||||
)
|
||||
executor = Ship(id="8", name="Executor", faction=empire)
|
||||
executor.save()
|
||||
|
||||
|
||||
def create_ship(ship_name, faction_id):
|
||||
new_ship = Ship(
|
||||
name=ship_name,
|
||||
faction_id=faction_id
|
||||
)
|
||||
new_ship = Ship(name=ship_name, faction_id=faction_id)
|
||||
new_ship.save()
|
||||
return new_ship
|
||||
|
||||
|
|
|
@ -5,7 +5,13 @@ from django.db import models
|
|||
|
||||
class Character(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters')
|
||||
ship = models.ForeignKey(
|
||||
"Ship",
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="characters",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -21,7 +27,7 @@ class Faction(models.Model):
|
|||
|
||||
class Ship(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships')
|
||||
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -2,18 +2,16 @@ import graphene
|
|||
from graphene import Schema, relay, resolve_only_args
|
||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||
|
||||
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
|
||||
get_ships)
|
||||
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
|
||||
from .models import Character as CharacterModel
|
||||
from .models import Faction as FactionModel
|
||||
from .models import Ship as ShipModel
|
||||
|
||||
|
||||
class Ship(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = ShipModel
|
||||
interfaces = (relay.Node, )
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
@classmethod
|
||||
def get_node(cls, info, id):
|
||||
|
@ -22,16 +20,14 @@ class Ship(DjangoObjectType):
|
|||
|
||||
|
||||
class Character(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = CharacterModel
|
||||
|
||||
|
||||
class Faction(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = FactionModel
|
||||
interfaces = (relay.Node, )
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
@classmethod
|
||||
def get_node(cls, info, id):
|
||||
|
@ -39,7 +35,6 @@ class Faction(DjangoObjectType):
|
|||
|
||||
|
||||
class IntroduceShip(relay.ClientIDMutation):
|
||||
|
||||
class Input:
|
||||
ship_name = graphene.String(required=True)
|
||||
faction_id = graphene.String(required=True)
|
||||
|
@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation):
|
|||
faction = graphene.Field(Faction)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
|
||||
def mutate_and_get_payload(
|
||||
cls, root, info, ship_name, faction_id, client_mutation_id=None
|
||||
):
|
||||
ship = create_ship(ship_name, faction_id)
|
||||
faction = get_faction(faction_id)
|
||||
return IntroduceShip(ship=ship, faction=faction)
|
||||
|
@ -58,7 +55,7 @@ class Query(graphene.ObjectType):
|
|||
rebels = graphene.Field(Faction)
|
||||
empire = graphene.Field(Faction)
|
||||
node = relay.Node.Field()
|
||||
ships = DjangoConnectionField(Ship, description='All the ships.')
|
||||
ships = DjangoConnectionField(Ship, description="All the ships.")
|
||||
|
||||
@resolve_only_args
|
||||
def resolve_ships(self):
|
||||
|
|
|
@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db
|
|||
|
||||
def test_correct_fetch_first_ship_rebels():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query RebelsShipsQuery {
|
||||
rebels {
|
||||
name,
|
||||
|
@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
"""
|
||||
expected = {
|
||||
'rebels': {
|
||||
'name': 'Alliance to Restore the Republic',
|
||||
'hero': {
|
||||
'name': 'Human'
|
||||
},
|
||||
'ships': {
|
||||
'edges': [
|
||||
{
|
||||
'node': {
|
||||
'name': 'X-Wing'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"rebels": {
|
||||
"name": "Alliance to Restore the Republic",
|
||||
"hero": {"name": "Human"},
|
||||
"ships": {"edges": [{"node": {"name": "X-Wing"}}]},
|
||||
}
|
||||
}
|
||||
result = schema.execute(query)
|
||||
|
@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels():
|
|||
|
||||
def test_correct_list_characters():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query RebelsShipsQuery {
|
||||
node(id: "U2hpcDox") {
|
||||
... on Ship {
|
||||
|
@ -60,15 +50,8 @@ def test_correct_list_characters():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
expected = {
|
||||
'node': {
|
||||
'name': 'X-Wing',
|
||||
'characters': [{
|
||||
'name': 'Human'
|
||||
}],
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
|
|
@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
|
|||
def test_mutations():
|
||||
initialize()
|
||||
|
||||
query = '''
|
||||
query = """
|
||||
mutation MyMutation {
|
||||
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
|
||||
ship {
|
||||
|
@ -29,49 +29,23 @@ def test_mutations():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
"""
|
||||
expected = {
|
||||
'introduceShip': {
|
||||
'ship': {
|
||||
'id': 'U2hpcDo5',
|
||||
'name': 'Peter'
|
||||
},
|
||||
'faction': {
|
||||
'name': 'Alliance to Restore the Republic',
|
||||
'ships': {
|
||||
'edges': [{
|
||||
'node': {
|
||||
'id': 'U2hpcDox',
|
||||
'name': 'X-Wing'
|
||||
}
|
||||
}, {
|
||||
'node': {
|
||||
'id': 'U2hpcDoy',
|
||||
'name': 'Y-Wing'
|
||||
}
|
||||
}, {
|
||||
'node': {
|
||||
'id': 'U2hpcDoz',
|
||||
'name': 'A-Wing'
|
||||
}
|
||||
}, {
|
||||
'node': {
|
||||
'id': 'U2hpcDo0',
|
||||
'name': 'Millenium Falcon'
|
||||
}
|
||||
}, {
|
||||
'node': {
|
||||
'id': 'U2hpcDo1',
|
||||
'name': 'Home One'
|
||||
}
|
||||
}, {
|
||||
'node': {
|
||||
'id': 'U2hpcDo5',
|
||||
'name': 'Peter'
|
||||
}
|
||||
}]
|
||||
"introduceShip": {
|
||||
"ship": {"id": "U2hpcDo5", "name": "Peter"},
|
||||
"faction": {
|
||||
"name": "Alliance to Restore the Republic",
|
||||
"ships": {
|
||||
"edges": [
|
||||
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
|
||||
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
|
||||
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
|
||||
{"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
|
||||
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
|
||||
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
result = schema.execute(query)
|
||||
|
|
|
@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db
|
|||
|
||||
def test_correctly_fetches_id_name_rebels():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query RebelsQuery {
|
||||
rebels {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
'''
|
||||
"""
|
||||
expected = {
|
||||
'rebels': {
|
||||
'id': 'RmFjdGlvbjox',
|
||||
'name': 'Alliance to Restore the Republic'
|
||||
}
|
||||
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
|
||||
}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
|
@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels():
|
|||
|
||||
def test_correctly_refetches_rebels():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query RebelsRefetchQuery {
|
||||
node(id: "RmFjdGlvbjox") {
|
||||
id
|
||||
|
@ -38,12 +35,9 @@ def test_correctly_refetches_rebels():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
"""
|
||||
expected = {
|
||||
'node': {
|
||||
'id': 'RmFjdGlvbjox',
|
||||
'name': 'Alliance to Restore the Republic'
|
||||
}
|
||||
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
|
||||
}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
|
@ -52,20 +46,15 @@ def test_correctly_refetches_rebels():
|
|||
|
||||
def test_correctly_fetches_id_name_empire():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query EmpireQuery {
|
||||
empire {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
'''
|
||||
expected = {
|
||||
'empire': {
|
||||
'id': 'RmFjdGlvbjoy',
|
||||
'name': 'Galactic Empire'
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire():
|
|||
|
||||
def test_correctly_refetches_empire():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query EmpireRefetchQuery {
|
||||
node(id: "RmFjdGlvbjoy") {
|
||||
id
|
||||
|
@ -82,13 +71,8 @@ def test_correctly_refetches_empire():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
expected = {
|
||||
'node': {
|
||||
'id': 'RmFjdGlvbjoy',
|
||||
'name': 'Galactic Empire'
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
@ -96,7 +80,7 @@ def test_correctly_refetches_empire():
|
|||
|
||||
def test_correctly_refetches_xwing():
|
||||
initialize()
|
||||
query = '''
|
||||
query = """
|
||||
query XWingRefetchQuery {
|
||||
node(id: "U2hpcDox") {
|
||||
id
|
||||
|
@ -105,13 +89,8 @@ def test_correctly_refetches_xwing():
|
|||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
expected = {
|
||||
'node': {
|
||||
'id': 'U2hpcDox',
|
||||
'name': 'X-Wing'
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .types import DjangoObjectType
|
||||
from .fields import DjangoConnectionField
|
||||
|
||||
__version__ = "2.2.0"
|
||||
__version__ = "2.4.0"
|
||||
|
||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
||||
|
|
|
@ -68,17 +68,20 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
|
|||
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||
|
||||
|
||||
def convert_django_field_with_choices(field, registry=None):
|
||||
def convert_django_field_with_choices(
|
||||
field, registry=None, convert_choices_to_enum=True
|
||||
):
|
||||
if registry is not None:
|
||||
converted = registry.get_converted_field(field)
|
||||
if converted:
|
||||
return converted
|
||||
choices = getattr(field, "choices", None)
|
||||
if choices:
|
||||
if choices and convert_choices_to_enum:
|
||||
meta = field.model._meta
|
||||
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
|
||||
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
|
||||
converted = enum(description=field.help_text, required=not field.null)
|
||||
required = not (field.blank or field.null)
|
||||
converted = enum(description=field.help_text, required=required)
|
||||
else:
|
||||
converted = convert_django_field(field, registry)
|
||||
if registry is not None:
|
||||
|
@ -184,19 +187,32 @@ def convert_field_to_list_or_connection(field, registry=None):
|
|||
if not _type:
|
||||
return
|
||||
|
||||
description = (
|
||||
field.help_text
|
||||
if isinstance(field, models.ManyToManyField)
|
||||
else field.field.help_text
|
||||
)
|
||||
|
||||
# If there is a connection, we should transform the field
|
||||
# into a DjangoConnectionField
|
||||
if _type._meta.connection:
|
||||
# Use a DjangoFilterConnectionField if there are
|
||||
# defined filter_fields in the DjangoObjectType Meta
|
||||
if _type._meta.filter_fields:
|
||||
# defined filter_fields or a filterset_class in the
|
||||
# DjangoObjectType Meta
|
||||
if _type._meta.filter_fields or _type._meta.filterset_class:
|
||||
from .filter.fields import DjangoFilterConnectionField
|
||||
|
||||
return DjangoFilterConnectionField(_type)
|
||||
return DjangoFilterConnectionField(
|
||||
_type, required=True, description=description
|
||||
)
|
||||
|
||||
return DjangoConnectionField(_type)
|
||||
return DjangoConnectionField(_type, required=True, description=description)
|
||||
|
||||
return DjangoListField(_type)
|
||||
return DjangoListField(
|
||||
_type,
|
||||
required=True, # A Set is always returned, never None.
|
||||
description=description,
|
||||
)
|
||||
|
||||
return Dynamic(dynamic_type)
|
||||
|
||||
|
|
|
@ -50,9 +50,7 @@ def test_should_query_field():
|
|||
"""
|
||||
expected = {
|
||||
"reporter": {"lastName": "ABA"},
|
||||
"_debug": {
|
||||
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
|
||||
},
|
||||
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
|
||||
}
|
||||
schema = graphene.Schema(query=Query)
|
||||
result = schema.execute(
|
||||
|
|
|
@ -15,7 +15,8 @@ from .utils import maybe_queryset
|
|||
|
||||
class DjangoListField(Field):
|
||||
def __init__(self, _type, *args, **kwargs):
|
||||
super(DjangoListField, self).__init__(List(_type), *args, **kwargs)
|
||||
# Django would never return a Set of None vvvvvvv
|
||||
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
|
@ -100,7 +101,7 @@ class DjangoConnectionField(ConnectionField):
|
|||
iterable = default_manager
|
||||
iterable = maybe_queryset(iterable)
|
||||
if isinstance(iterable, QuerySet):
|
||||
if iterable is not default_manager:
|
||||
if iterable.model.objects is not default_manager:
|
||||
default_queryset = maybe_queryset(default_manager)
|
||||
iterable = cls.merge_querysets(default_queryset, iterable)
|
||||
_len = iterable.count()
|
||||
|
|
|
@ -40,9 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
if self._extra_filter_meta:
|
||||
meta.update(self._extra_filter_meta)
|
||||
|
||||
self._filterset_class = get_filterset_class(
|
||||
self._provided_filterset_class, **meta
|
||||
filterset_class = self._provided_filterset_class or (
|
||||
self.node_type._meta.filterset_class
|
||||
)
|
||||
self._filterset_class = get_filterset_class(filterset_class, **meta)
|
||||
|
||||
return self._filterset_class
|
||||
|
||||
|
@ -110,7 +111,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
return partial(
|
||||
self.connection_resolver,
|
||||
parent_resolver,
|
||||
self.type,
|
||||
self.connection_type,
|
||||
self.get_manager(),
|
||||
self.max_limit,
|
||||
self.enforce_first_or_last,
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
from datetime import datetime
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from django.db.models import TextField, Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
|
||||
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String
|
||||
from graphene.relay import Node
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||
from graphene_django.tests.models import Article, Pet, Reporter
|
||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||
|
||||
# for annotation test
|
||||
from django.db.models import TextField, Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
pytestmark = []
|
||||
|
||||
if DJANGO_FILTER_INSTALLED:
|
||||
|
@ -183,7 +182,7 @@ def test_filter_shortcut_filterset_context():
|
|||
}
|
||||
"""
|
||||
schema = Schema(query=Query)
|
||||
result = schema.execute(query, context_value=context())
|
||||
result = schema.execute(query, context=context())
|
||||
assert not result.errors
|
||||
|
||||
assert len(result.data["contextArticles"]["edges"]) == 1
|
||||
|
@ -227,6 +226,74 @@ def test_filter_filterset_information_on_meta_related():
|
|||
assert_not_orderable(articles_field)
|
||||
|
||||
|
||||
def test_filter_filterset_class_filter_fields_exception():
|
||||
with pytest.raises(Exception):
|
||||
|
||||
class ReporterFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = ["first_name", "articles"]
|
||||
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
filterset_class = ReporterFilter
|
||||
filter_fields = ["first_name", "articles"]
|
||||
|
||||
|
||||
def test_filter_filterset_class_information_on_meta():
|
||||
class ReporterFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = ["first_name", "articles"]
|
||||
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
filterset_class = ReporterFilter
|
||||
|
||||
field = DjangoFilterConnectionField(ReporterFilterNode)
|
||||
assert_arguments(field, "first_name", "articles")
|
||||
assert_not_orderable(field)
|
||||
|
||||
|
||||
def test_filter_filterset_class_information_on_meta_related():
|
||||
class ReporterFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
fields = ["first_name", "articles"]
|
||||
|
||||
class ArticleFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ["headline", "reporter"]
|
||||
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
filterset_class = ReporterFilter
|
||||
|
||||
class ArticleFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Article
|
||||
interfaces = (Node,)
|
||||
filterset_class = ArticleFilter
|
||||
|
||||
class Query(ObjectType):
|
||||
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
|
||||
reporter = Field(ReporterFilterNode)
|
||||
article = Field(ArticleFilterNode)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
||||
assert_arguments(articles_field, "headline", "reporter")
|
||||
assert_not_orderable(articles_field)
|
||||
|
||||
|
||||
def test_filter_filterset_related_results():
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -253,12 +320,14 @@ def test_filter_filterset_related_results():
|
|||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
reporter=r1,
|
||||
editor=r1,
|
||||
)
|
||||
Article.objects.create(
|
||||
headline="a2",
|
||||
pub_date=datetime.now(),
|
||||
pub_date_time=datetime.now(),
|
||||
reporter=r2,
|
||||
editor=r2,
|
||||
)
|
||||
|
||||
query = """
|
||||
|
@ -382,7 +451,7 @@ def test_global_id_multiple_field_explicit_reverse():
|
|||
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
|
||||
|
||||
|
||||
def test_filter_filterset_related_results():
|
||||
def test_filter_filterset_related_results_with_filter():
|
||||
class ReporterFilterNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
|
@ -392,15 +461,15 @@ def test_filter_filterset_related_results():
|
|||
class Query(ObjectType):
|
||||
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||
|
||||
r1 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="A test user", last_name="Last Name", email="test1@test.com"
|
||||
)
|
||||
r2 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="Other test user",
|
||||
last_name="Other Last Name",
|
||||
email="test2@test.com",
|
||||
)
|
||||
r3 = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="Random", last_name="RandomLast", email="random@test.com"
|
||||
)
|
||||
|
||||
|
@ -568,7 +637,7 @@ def test_should_query_filter_node_double_limit_raises():
|
|||
Reporter.objects.create(
|
||||
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
|
||||
)
|
||||
r = Reporter.objects.create(
|
||||
Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
||||
|
@ -614,7 +683,7 @@ def test_order_by_is_perserved():
|
|||
return reporters
|
||||
|
||||
Reporter.objects.create(first_name="b")
|
||||
r = Reporter.objects.create(first_name="a")
|
||||
Reporter.objects.create(first_name="a")
|
||||
|
||||
schema = Schema(query=Query)
|
||||
query = """
|
||||
|
@ -697,3 +766,55 @@ def test_annotation_is_perserved():
|
|||
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
||||
|
||||
def test_integer_field_filter_type():
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Pet
|
||||
interfaces = (Node,)
|
||||
filter_fields = {"age": ["exact"]}
|
||||
fields = ("age",)
|
||||
|
||||
class Query(ObjectType):
|
||||
pets = DjangoFilterConnectionField(PetType)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type PetType implements Node {
|
||||
age: Int!
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type PetTypeConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [PetTypeEdge]!
|
||||
}
|
||||
|
||||
type PetTypeEdge {
|
||||
node: PetType
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -11,8 +11,25 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
|||
from ..forms.converter import convert_form_field
|
||||
|
||||
args = {}
|
||||
model = filterset_class._meta.model
|
||||
for name, filter_field in six.iteritems(filterset_class.base_filters):
|
||||
field_type = convert_form_field(filter_field.field).Argument()
|
||||
if name in filterset_class.declared_filters:
|
||||
form_field = filter_field.field
|
||||
else:
|
||||
field_name = name.split("__", 1)[0]
|
||||
model_field = model._meta.get_field(field_name)
|
||||
|
||||
if hasattr(model_field, "formfield"):
|
||||
form_field = model_field.formfield(
|
||||
required=filter_field.extra.get("required", False)
|
||||
)
|
||||
|
||||
# Fallback to field defined on filter if we can't get it from the
|
||||
# model field
|
||||
if not form_field:
|
||||
form_field = filter_field.field
|
||||
|
||||
field_type = convert_form_field(form_field).Argument()
|
||||
field_type.description = filter_field.label
|
||||
args[name] = field_type
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ from graphene.types.mutation import MutationOptions
|
|||
from graphene.types.utils import yank_fields_from_attrs
|
||||
from graphene_django.registry import get_global_registry
|
||||
|
||||
from .converter import convert_form_field
|
||||
from ..types import ErrorType
|
||||
from .converter import convert_form_field
|
||||
|
||||
|
||||
def fields_for_form(form, only_fields, exclude_fields):
|
||||
|
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
|||
if form.is_valid():
|
||||
return cls.perform_mutate(form, info)
|
||||
else:
|
||||
errors = [
|
||||
ErrorType(field=key, messages=value)
|
||||
for key, value in form.errors.items()
|
||||
]
|
||||
errors = ErrorType.from_errors(form.errors)
|
||||
|
||||
return cls(errors=errors)
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ from django import forms
|
|||
from django.test import TestCase
|
||||
from py.test import raises
|
||||
|
||||
from graphene_django.tests.models import Pet, Film, FilmDetails
|
||||
from graphene_django.tests.models import Film, FilmDetails, Pet
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||
|
||||
|
||||
|
@ -41,6 +43,22 @@ def test_has_input_fields():
|
|||
assert "text" in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
class ExtraPetForm(PetForm):
|
||||
test_field = forms.CharField(required=True)
|
||||
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = ExtraPetForm
|
||||
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
|
||||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = PetMutation.mutate_and_get_payload(None, None)
|
||||
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
class ModelFormMutationTests(TestCase):
|
||||
def test_default_meta_fields(self):
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import graphene
|
||||
|
||||
|
||||
class ErrorType(graphene.ObjectType):
|
||||
field = graphene.String()
|
||||
messages = graphene.List(graphene.String)
|
||||
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import importlib
|
||||
import json
|
||||
import functools
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import autoreload
|
||||
|
||||
from graphene_django.settings import graphene_settings
|
||||
|
||||
|
@ -32,6 +34,14 @@ class CommandArguments(BaseCommand):
|
|||
help="Output file indent (default: None)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--watch",
|
||||
dest="watch",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Updates the schema on file changes (default: False)",
|
||||
)
|
||||
|
||||
|
||||
class Command(CommandArguments):
|
||||
help = "Dump Graphene schema JSON to file"
|
||||
|
@ -41,6 +51,18 @@ class Command(CommandArguments):
|
|||
with open(out, "w") as outfile:
|
||||
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
||||
|
||||
def get_schema(self, schema, out, indent):
|
||||
schema_dict = {"data": schema.introspect()}
|
||||
if out == "-":
|
||||
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
||||
else:
|
||||
self.save_file(out, schema_dict, indent)
|
||||
|
||||
style = getattr(self, "style", None)
|
||||
success = getattr(style, "SUCCESS", lambda x: x)
|
||||
|
||||
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
||||
|
||||
def handle(self, *args, **options):
|
||||
options_schema = options.get("schema")
|
||||
|
||||
|
@ -63,13 +85,10 @@ class Command(CommandArguments):
|
|||
)
|
||||
|
||||
indent = options.get("indent")
|
||||
schema_dict = {"data": schema.introspect()}
|
||||
if out == "-":
|
||||
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
||||
watch = options.get("watch")
|
||||
if watch:
|
||||
autoreload.run_with_reloader(
|
||||
functools.partial(self.get_schema, schema, out, indent)
|
||||
)
|
||||
else:
|
||||
self.save_file(out, schema_dict, indent)
|
||||
|
||||
style = getattr(self, "style", None)
|
||||
success = getattr(style, "SUCCESS", lambda x: x)
|
||||
|
||||
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
||||
self.get_schema(schema, out, indent)
|
||||
|
|
|
@ -4,3 +4,8 @@ from django.db import models
|
|||
class MyFakeModel(models.Model):
|
||||
cool_name = models.CharField(max_length=50)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class MyFakeModelWithPassword(models.Model):
|
||||
cool_name = models.CharField(max_length=50)
|
||||
password = models.CharField(max_length=50)
|
||||
|
|
|
@ -3,13 +3,13 @@ from collections import OrderedDict
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
import graphene
|
||||
from graphene.relay.mutation import ClientIDMutation
|
||||
from graphene.types import Field, InputField
|
||||
from graphene.types.mutation import MutationOptions
|
||||
from graphene.relay.mutation import ClientIDMutation
|
||||
from graphene.types.objecttype import yank_fields_from_attrs
|
||||
|
||||
from .serializer_converter import convert_serializer_field
|
||||
from ..types import ErrorType
|
||||
from .serializer_converter import convert_serializer_field
|
||||
|
||||
|
||||
class SerializerMutationOptions(MutationOptions):
|
||||
|
@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
|
|||
name
|
||||
in exclude_fields # or
|
||||
# name in already_created_fields
|
||||
) or (
|
||||
field.write_only and not is_input # don't show write_only fields in Query
|
||||
)
|
||||
|
||||
if is_not_in_only or is_excluded:
|
||||
|
@ -50,7 +52,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
lookup_field=None,
|
||||
serializer_class=None,
|
||||
model_class=None,
|
||||
model_operations=["create", "update"],
|
||||
model_operations=("create", "update"),
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
**options
|
||||
|
@ -125,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
if serializer.is_valid():
|
||||
return cls.perform_mutate(serializer, info)
|
||||
else:
|
||||
errors = [
|
||||
ErrorType(field=key, messages=value)
|
||||
for key, value in serializer.errors.items()
|
||||
]
|
||||
errors = ErrorType.from_errors(serializer.errors)
|
||||
|
||||
return cls(errors=errors)
|
||||
|
||||
|
@ -138,6 +137,7 @@ class SerializerMutation(ClientIDMutation):
|
|||
|
||||
kwargs = {}
|
||||
for f, field in serializer.fields.items():
|
||||
kwargs[f] = field.get_attribute(obj)
|
||||
if not field.write_only:
|
||||
kwargs[f] = field.get_attribute(obj)
|
||||
|
||||
return cls(errors=None, **kwargs)
|
||||
|
|
|
@ -58,18 +58,27 @@ def convert_serializer_field(field, is_input=True):
|
|||
|
||||
|
||||
def convert_serializer_to_input_type(serializer_class):
|
||||
cached_type = convert_serializer_to_input_type.cache.get(
|
||||
serializer_class.__name__, None
|
||||
)
|
||||
if cached_type:
|
||||
return cached_type
|
||||
serializer = serializer_class()
|
||||
|
||||
items = {
|
||||
name: convert_serializer_field(field)
|
||||
for name, field in serializer.fields.items()
|
||||
}
|
||||
|
||||
return type(
|
||||
ret_type = type(
|
||||
"{}Input".format(serializer.__class__.__name__),
|
||||
(graphene.InputObjectType,),
|
||||
items,
|
||||
)
|
||||
convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type
|
||||
return ret_type
|
||||
|
||||
|
||||
convert_serializer_to_input_type.cache = {}
|
||||
|
||||
|
||||
@get_graphene_type_from_serializer_field.register(serializers.Field)
|
||||
|
|
|
@ -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
|
|
@ -1,13 +1,14 @@
|
|||
import datetime
|
||||
|
||||
from py.test import mark, raises
|
||||
from rest_framework import serializers
|
||||
|
||||
from graphene import Field, ResolveInfo
|
||||
from graphene.types.inputobjecttype import InputObjectType
|
||||
from py.test import raises
|
||||
from py.test import mark
|
||||
from rest_framework import serializers
|
||||
|
||||
from ...settings import graphene_settings
|
||||
from ...types import DjangoObjectType
|
||||
from ..models import MyFakeModel
|
||||
from ..models import MyFakeModel, MyFakeModelWithPassword
|
||||
from ..mutation import SerializerMutation
|
||||
|
||||
|
||||
|
@ -86,6 +87,51 @@ def test_exclude_fields():
|
|||
assert "created" not in MyMutation.Input._meta.fields
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_write_only_field():
|
||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MyFakeModelWithPassword
|
||||
fields = ["cool_name", "password"]
|
||||
|
||||
class MyMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = WriteOnlyFieldModelSerializer
|
||||
|
||||
result = MyMutation.mutate_and_get_payload(
|
||||
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
|
||||
)
|
||||
|
||||
assert hasattr(result, "cool_name")
|
||||
assert not hasattr(
|
||||
result, "password"
|
||||
), "'password' is write_only field and shouldn't be visible"
|
||||
|
||||
|
||||
@mark.django_db
|
||||
def test_write_only_field_using_extra_kwargs():
|
||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MyFakeModelWithPassword
|
||||
fields = ["cool_name", "password"]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
class MyMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = WriteOnlyFieldModelSerializer
|
||||
|
||||
result = MyMutation.mutate_and_get_payload(
|
||||
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
|
||||
)
|
||||
|
||||
assert hasattr(result, "cool_name")
|
||||
assert not hasattr(
|
||||
result, "password"
|
||||
), "'password' is write_only field and shouldn't be visible"
|
||||
|
||||
|
||||
def test_nested_model():
|
||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -168,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
|
|||
assert len(result.errors) > 0
|
||||
|
||||
|
||||
def test_mutation_error_camelcased():
|
||||
graphene_settings.CAMELCASE_ERRORS = True
|
||||
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||
assert result.errors[0].field == "coolName"
|
||||
graphene_settings.CAMELCASE_ERRORS = False
|
||||
|
||||
|
||||
def test_invalid_serializer_operations():
|
||||
with raises(Exception) as exc:
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ DEFAULTS = {
|
|||
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
|
||||
# Max items returned in ConnectionFields / FilterConnectionFields
|
||||
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
||||
"CAMELCASE_ERRORS": False,
|
||||
}
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
@ -38,7 +38,7 @@ class Reporter(models.Model):
|
|||
last_name = models.CharField(max_length=30)
|
||||
email = models.EmailField()
|
||||
pets = models.ManyToManyField("self")
|
||||
a_choice = models.CharField(max_length=30, choices=CHOICES)
|
||||
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
|
||||
objects = models.Manager()
|
||||
doe_objects = DoeReporterManager()
|
||||
|
||||
|
@ -65,6 +65,11 @@ class Reporter(models.Model):
|
|||
self.__class__ = CNNReporter
|
||||
|
||||
|
||||
class CNNReporterManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
|
||||
|
||||
|
||||
class CNNReporter(Reporter):
|
||||
"""
|
||||
This class is a proxy model for Reporter, used for testing
|
||||
|
@ -74,6 +79,8 @@ class CNNReporter(Reporter):
|
|||
class Meta:
|
||||
proxy = True
|
||||
|
||||
objects = CNNReporterManager()
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
headline = models.CharField(max_length=100)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from graphene import NonNull
|
||||
from py.test import raises
|
||||
|
||||
import graphene
|
||||
|
@ -196,6 +197,23 @@ def test_field_with_choices_collision():
|
|||
convert_django_field_with_choices(field)
|
||||
|
||||
|
||||
def test_field_with_choices_convert_enum_false():
|
||||
field = models.CharField(
|
||||
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
|
||||
)
|
||||
|
||||
class TranslatedModel(models.Model):
|
||||
language = field
|
||||
|
||||
class Meta:
|
||||
app_label = "test"
|
||||
|
||||
graphene_type = convert_django_field_with_choices(
|
||||
field, convert_choices_to_enum=False
|
||||
)
|
||||
assert isinstance(graphene_type, graphene.String)
|
||||
|
||||
|
||||
def test_should_float_convert_float():
|
||||
assert_conversion(models.FloatField, graphene.Float)
|
||||
|
||||
|
@ -217,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, graphene.Field)
|
||||
assert isinstance(dynamic_field.type, graphene.List)
|
||||
assert dynamic_field.type.of_type == A
|
||||
# A NonNull List of NonNull A ([A!]!)
|
||||
# https://github.com/graphql-python/graphene-django/issues/448
|
||||
assert isinstance(dynamic_field.type, NonNull)
|
||||
assert isinstance(dynamic_field.type.of_type, graphene.List)
|
||||
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
|
||||
assert dynamic_field.type.of_type.of_type.of_type == A
|
||||
|
||||
|
||||
def test_should_manytomany_convert_connectionorlist_connection():
|
||||
|
@ -233,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, ConnectionField)
|
||||
assert dynamic_field.type == A._meta.connection
|
||||
assert dynamic_field.type.of_type == A._meta.connection
|
||||
|
||||
|
||||
def test_should_manytoone_convert_connectionorlist():
|
||||
|
@ -245,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist():
|
|||
assert isinstance(graphene_field, graphene.Dynamic)
|
||||
dynamic_field = graphene_field.get_type()
|
||||
assert isinstance(dynamic_field, graphene.Field)
|
||||
assert isinstance(dynamic_field.type, graphene.List)
|
||||
assert dynamic_field.type.of_type == A
|
||||
# a NonNull List of NonNull A ([A!]!)
|
||||
assert isinstance(dynamic_field.type, NonNull)
|
||||
assert isinstance(dynamic_field.type.of_type, graphene.List)
|
||||
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
|
||||
assert dynamic_field.type.of_type.of_type.of_type == A
|
||||
|
||||
|
||||
def test_should_onetoone_reverse_convert_model():
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
@ -7,6 +8,7 @@ from py.test import raises
|
|||
|
||||
from django.db.models import Q
|
||||
|
||||
from graphql_relay import to_global_id
|
||||
import graphene
|
||||
from graphene.relay import Node
|
||||
|
||||
|
@ -26,7 +28,7 @@ def test_should_query_only_fields():
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
schema = graphene.Schema(query=ReporterType)
|
||||
query = """
|
||||
|
@ -42,7 +44,7 @@ def test_should_query_simplelazy_objects():
|
|||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("id",)
|
||||
fields = ("id",)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
reporter = graphene.Field(ReporterType)
|
||||
|
@ -226,12 +228,68 @@ def test_should_node():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_query_onetoone_fields():
|
||||
film = Film(id=1)
|
||||
film_details = FilmDetails(id=1, film=film)
|
||||
|
||||
class FilmNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Film
|
||||
interfaces = (Node,)
|
||||
|
||||
class FilmDetailsNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = FilmDetails
|
||||
interfaces = (Node,)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
film = graphene.Field(FilmNode)
|
||||
film_details = graphene.Field(FilmDetailsNode)
|
||||
|
||||
def resolve_film(root, info):
|
||||
return film
|
||||
|
||||
def resolve_film_details(root, info):
|
||||
return film_details
|
||||
|
||||
query = """
|
||||
query FilmQuery {
|
||||
filmDetails {
|
||||
id
|
||||
film {
|
||||
id
|
||||
}
|
||||
}
|
||||
film {
|
||||
id
|
||||
details {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
expected = {
|
||||
"filmDetails": {
|
||||
"id": "RmlsbURldGFpbHNOb2RlOjE=",
|
||||
"film": {"id": "RmlsbU5vZGU6MQ=="},
|
||||
},
|
||||
"film": {
|
||||
"id": "RmlsbU5vZGU6MQ==",
|
||||
"details": {"id": "RmlsbURldGFpbHNOb2RlOjE="},
|
||||
},
|
||||
}
|
||||
schema = graphene.Schema(query=Query)
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_query_connectionfields():
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
all_reporters = DjangoConnectionField(ReporterType)
|
||||
|
@ -271,7 +329,7 @@ def test_should_keep_annotations():
|
|||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (Node,)
|
||||
only_fields = ("articles",)
|
||||
fields = ("articles",)
|
||||
|
||||
class ArticleType(DjangoObjectType):
|
||||
class Meta:
|
||||
|
@ -895,8 +953,7 @@ def test_should_handle_inherited_choices():
|
|||
|
||||
def test_proxy_model_support():
|
||||
"""
|
||||
This test asserts that we can query for all Reporters,
|
||||
even if some are of a proxy model type at runtime.
|
||||
This test asserts that we can query for all Reporters and proxied Reporters.
|
||||
"""
|
||||
|
||||
class ReporterType(DjangoObjectType):
|
||||
|
@ -905,11 +962,17 @@ def test_proxy_model_support():
|
|||
interfaces = (Node,)
|
||||
use_connection = True
|
||||
|
||||
reporter_1 = Reporter.objects.create(
|
||||
class CNNReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = CNNReporter
|
||||
interfaces = (Node,)
|
||||
use_connection = True
|
||||
|
||||
reporter = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
||||
reporter_2 = CNNReporter.objects.create(
|
||||
cnn_reporter = CNNReporter.objects.create(
|
||||
first_name="Some",
|
||||
last_name="Guy",
|
||||
email="someguy@cnn.com",
|
||||
|
@ -919,6 +982,7 @@ def test_proxy_model_support():
|
|||
|
||||
class Query(graphene.ObjectType):
|
||||
all_reporters = DjangoConnectionField(ReporterType)
|
||||
cnn_reporters = DjangoConnectionField(CNNReporterType)
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
query = """
|
||||
|
@ -930,16 +994,28 @@ def test_proxy_model_support():
|
|||
}
|
||||
}
|
||||
}
|
||||
cnnReporters {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
expected = {
|
||||
"allReporters": {
|
||||
"edges": [
|
||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
|
||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
|
||||
{"node": {"id": to_global_id("ReporterType", reporter.id)}},
|
||||
{"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
|
||||
]
|
||||
}
|
||||
},
|
||||
"cnnReporters": {
|
||||
"edges": [
|
||||
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = schema.execute(query)
|
||||
|
@ -947,68 +1023,6 @@ def test_proxy_model_support():
|
|||
assert result.data == expected
|
||||
|
||||
|
||||
def test_proxy_model_fails():
|
||||
"""
|
||||
This test asserts that if you try to query for a proxy model,
|
||||
that query will fail with:
|
||||
GraphQLError('Expected value of type "CNNReporterType" but got:
|
||||
CNNReporter.',)
|
||||
|
||||
This is because a proxy model has the identical model definition
|
||||
to its superclass, and defines its behavior at runtime, rather than
|
||||
at the database level. Currently, filtering objects of the proxy models'
|
||||
type isn't supported. It would require a field on the model that would
|
||||
represent the type, and it doesn't seem like there is a clear way to
|
||||
enforce this pattern across all projects
|
||||
"""
|
||||
|
||||
class CNNReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = CNNReporter
|
||||
interfaces = (Node,)
|
||||
use_connection = True
|
||||
|
||||
reporter_1 = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
)
|
||||
|
||||
reporter_2 = CNNReporter.objects.create(
|
||||
first_name="Some",
|
||||
last_name="Guy",
|
||||
email="someguy@cnn.com",
|
||||
a_choice=1,
|
||||
reporter_type=2, # set this guy to be CNN
|
||||
)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
all_reporters = DjangoConnectionField(CNNReporterType)
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
query = """
|
||||
query ProxyModelQuery {
|
||||
allReporters {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
expected = {
|
||||
"allReporters": {
|
||||
"edges": [
|
||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
|
||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
result = schema.execute(query)
|
||||
assert result.errors
|
||||
|
||||
|
||||
def test_should_resolve_get_queryset_connectionfields():
|
||||
reporter_1 = Reporter.objects.create(
|
||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||
|
@ -1051,3 +1065,54 @@ def test_should_resolve_get_queryset_connectionfields():
|
|||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
assert result.data == expected
|
||||
|
||||
|
||||
def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||
class ReporterType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
class FilmType(DjangoObjectType):
|
||||
reporters = DjangoConnectionField(ReporterType)
|
||||
|
||||
class Meta:
|
||||
model = Film
|
||||
interfaces = (graphene.relay.Node,)
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
films = DjangoConnectionField(FilmType)
|
||||
|
||||
def resolve_films(root, info):
|
||||
qs = Film.objects.prefetch_related("reporters")
|
||||
return qs
|
||||
|
||||
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
|
||||
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
|
||||
|
||||
f1 = Film.objects.create()
|
||||
f1.reporters.set([r1, r2])
|
||||
f2 = Film.objects.create()
|
||||
f2.reporters.set([r2])
|
||||
|
||||
query = """
|
||||
query {
|
||||
films {
|
||||
edges {
|
||||
node {
|
||||
reporters {
|
||||
edges {
|
||||
node {
|
||||
firstName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
schema = graphene.Schema(query=Query)
|
||||
with django_assert_num_queries(3) as captured:
|
||||
result = schema.execute(query)
|
||||
assert not result.errors
|
||||
|
|
|
@ -48,6 +48,6 @@ def test_should_map_only_few_fields():
|
|||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Reporter
|
||||
only_fields = ("id", "email")
|
||||
fields = ("id", "email")
|
||||
|
||||
assert list(Reporter2._meta.fields.keys()) == ["id", "email"]
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
from collections import OrderedDict, defaultdict
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
from mock import patch
|
||||
|
||||
from graphene import Interface, ObjectType, Schema, Connection, String
|
||||
from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
||||
from graphene.relay import Node
|
||||
|
||||
from .. import registry
|
||||
|
@ -165,10 +170,10 @@ type Reporter {
|
|||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
pets: [Reporter]
|
||||
aChoice: ReporterAChoice!
|
||||
pets: [Reporter!]!
|
||||
aChoice: ReporterAChoice
|
||||
reporterType: ReporterReporterType
|
||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
|
||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||
}
|
||||
|
||||
enum ReporterAChoice {
|
||||
|
@ -206,21 +211,216 @@ def with_local_registry(func):
|
|||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_only_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
with pytest.warns(PendingDeprecationWarning):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert fields == ["id", "email", "films"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields():
|
||||
def test_django_objecttype_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude_fields = "email"
|
||||
fields = ("id", "email", "films")
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert fields == ["id", "email", "films"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_only_fields_and_fields():
|
||||
with pytest.raises(Exception):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
only_fields = ("id", "email", "films")
|
||||
fields = ("id", "email", "films")
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_all_fields():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "__all__"
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert len(fields) == len(ReporterModel._meta.get_fields())
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields():
|
||||
with pytest.warns(PendingDeprecationWarning):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude_fields = ["email"]
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert "email" not in fields
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude():
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
|
||||
fields = list(Reporter._meta.fields.keys())
|
||||
assert "email" not in fields
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_fields_and_exclude():
|
||||
with pytest.raises(Exception):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
exclude_fields = ["email"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_exclude_and_only():
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
exclude = ["email"]
|
||||
fields = ["id"]
|
||||
|
||||
|
||||
@with_local_registry
|
||||
def test_django_objecttype_fields_exclude_type_checking():
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
class Reporter(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "foo"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
|
||||
class Reporter2(DjangoObjectType):
|
||||
class Meta:
|
||||
model = ReporterModel
|
||||
fields = "foo"
|
||||
|
||||
|
||||
class TestDjangoObjectType:
|
||||
@pytest.fixture
|
||||
def PetModel(self):
|
||||
class PetModel(models.Model):
|
||||
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
|
||||
cuteness = models.IntegerField(
|
||||
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
|
||||
)
|
||||
|
||||
yield PetModel
|
||||
|
||||
# Clear Django model cache so we don't get warnings when creating the
|
||||
# model multiple times
|
||||
PetModel._meta.apps.all_models = defaultdict(OrderedDict)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_false(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = False
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = ["kind"]
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
convert_choices_to_enum = []
|
||||
|
||||
class Query(ObjectType):
|
||||
pet = Field(Pet)
|
||||
|
||||
schema = Schema(query=Query)
|
||||
|
||||
assert str(schema) == dedent(
|
||||
"""\
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
cuteness: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
pet: Pet
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from ..utils import get_model_fields
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from ..utils import camelize, get_model_fields
|
||||
from .models import Film, Reporter
|
||||
|
||||
|
||||
|
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
|
|||
film_fields = get_model_fields(Film)
|
||||
film_name_set = set([field[0] for field in film_fields])
|
||||
assert len(film_fields) == len(film_name_set)
|
||||
|
||||
|
||||
def test_camelize():
|
||||
assert camelize({}) == {}
|
||||
assert camelize("value_a") == "value_a"
|
||||
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
|
||||
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
|
||||
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
|
||||
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
|
||||
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
|
||||
}
|
||||
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
|
||||
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
|
||||
assert camelize(gettext_lazy("value_a")) == "value_a"
|
||||
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
|
||||
"valueA": "value_b"
|
||||
}
|
||||
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import six
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
from django.db.models import Model
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
|
||||
import graphene
|
||||
from graphene import Field
|
||||
from graphene.relay import Connection, Node
|
||||
|
@ -11,14 +13,24 @@ from graphene.types.utils import yank_fields_from_attrs
|
|||
|
||||
from .converter import convert_django_field_with_choices
|
||||
from .registry import Registry, get_global_registry
|
||||
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
|
||||
|
||||
from .settings import graphene_settings
|
||||
from .utils import (
|
||||
DJANGO_FILTER_INSTALLED,
|
||||
camelize,
|
||||
get_model_fields,
|
||||
is_valid_django_model,
|
||||
)
|
||||
|
||||
if six.PY3:
|
||||
from typing import Type
|
||||
|
||||
|
||||
def construct_fields(model, registry, only_fields, exclude_fields):
|
||||
ALL_FIELDS = "__all__"
|
||||
|
||||
|
||||
def construct_fields(
|
||||
model, registry, only_fields, exclude_fields, convert_choices_to_enum
|
||||
):
|
||||
_model_fields = get_model_fields(model)
|
||||
|
||||
fields = OrderedDict()
|
||||
|
@ -33,7 +45,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
|
|||
# in there. Or when we exclude this field in exclude_fields.
|
||||
# Or when there is no back reference.
|
||||
continue
|
||||
converted = convert_django_field_with_choices(field, registry)
|
||||
|
||||
_convert_choices_to_enum = convert_choices_to_enum
|
||||
if not isinstance(_convert_choices_to_enum, bool):
|
||||
# then `convert_choices_to_enum` is a list of field names to convert
|
||||
if name in _convert_choices_to_enum:
|
||||
_convert_choices_to_enum = True
|
||||
else:
|
||||
_convert_choices_to_enum = False
|
||||
|
||||
converted = convert_django_field_with_choices(
|
||||
field, registry, convert_choices_to_enum=_convert_choices_to_enum
|
||||
)
|
||||
fields[name] = converted
|
||||
|
||||
return fields
|
||||
|
@ -45,6 +68,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
|
|||
connection = None # type: Type[Connection]
|
||||
|
||||
filter_fields = ()
|
||||
filterset_class = None
|
||||
|
||||
|
||||
class DjangoObjectType(ObjectType):
|
||||
|
@ -54,13 +78,17 @@ class DjangoObjectType(ObjectType):
|
|||
model=None,
|
||||
registry=None,
|
||||
skip_registry=False,
|
||||
only_fields=(),
|
||||
exclude_fields=(),
|
||||
only_fields=(), # deprecated in favour of `fields`
|
||||
fields=(),
|
||||
exclude_fields=(), # deprecated in favour of `exclude`
|
||||
exclude=(),
|
||||
filter_fields=None,
|
||||
filterset_class=None,
|
||||
connection=None,
|
||||
connection_class=None,
|
||||
use_connection=None,
|
||||
interfaces=(),
|
||||
convert_choices_to_enum=True,
|
||||
_meta=None,
|
||||
**options
|
||||
):
|
||||
|
@ -76,11 +104,60 @@ class DjangoObjectType(ObjectType):
|
|||
'Registry, received "{}".'
|
||||
).format(cls.__name__, registry)
|
||||
|
||||
if not DJANGO_FILTER_INSTALLED and filter_fields:
|
||||
raise Exception("Can only set filter_fields if Django-Filter is installed")
|
||||
if filter_fields and filterset_class:
|
||||
raise Exception("Can't set both filter_fields and filterset_class")
|
||||
|
||||
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
|
||||
raise Exception(
|
||||
(
|
||||
"Can only set filter_fields or filterset_class if "
|
||||
"Django-Filter is installed"
|
||||
)
|
||||
)
|
||||
|
||||
assert not (fields and exclude), (
|
||||
"Cannot set both 'fields' and 'exclude' options on "
|
||||
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
|
||||
)
|
||||
|
||||
# Alias only_fields -> fields
|
||||
if only_fields and fields:
|
||||
raise Exception("Can't set both only_fields and fields")
|
||||
if only_fields:
|
||||
warnings.warn(
|
||||
"Defining `only_fields` is deprecated in favour of `fields`.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fields = only_fields
|
||||
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
|
||||
raise TypeError(
|
||||
'The `fields` option must be a list or tuple or "__all__". '
|
||||
"Got %s." % type(fields).__name__
|
||||
)
|
||||
|
||||
if fields == ALL_FIELDS:
|
||||
fields = None
|
||||
|
||||
# Alias exclude_fields -> exclude
|
||||
if exclude_fields and exclude:
|
||||
raise Exception("Can't set both exclude_fields and exclude")
|
||||
if exclude_fields:
|
||||
warnings.warn(
|
||||
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
|
||||
PendingDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
exclude = exclude_fields
|
||||
if exclude and not isinstance(exclude, (list, tuple)):
|
||||
raise TypeError(
|
||||
"The `exclude` option must be a list or tuple. Got %s."
|
||||
% type(exclude).__name__
|
||||
)
|
||||
|
||||
django_fields = yank_fields_from_attrs(
|
||||
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
|
||||
construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
|
||||
_as=Field,
|
||||
)
|
||||
|
||||
if use_connection is None and interfaces:
|
||||
|
@ -108,6 +185,7 @@ class DjangoObjectType(ObjectType):
|
|||
_meta.model = model
|
||||
_meta.registry = registry
|
||||
_meta.filter_fields = filter_fields
|
||||
_meta.filterset_class = filterset_class
|
||||
_meta.fields = django_fields
|
||||
_meta.connection = connection
|
||||
|
||||
|
@ -131,7 +209,11 @@ class DjangoObjectType(ObjectType):
|
|||
if not is_valid_django_model(type(root)):
|
||||
raise Exception(('Received incompatible instance "{}".').format(root))
|
||||
|
||||
model = root._meta.model._meta.concrete_model
|
||||
if cls._meta.model._meta.proxy:
|
||||
model = root._meta.model
|
||||
else:
|
||||
model = root._meta.model._meta.concrete_model
|
||||
|
||||
return model == cls._meta.model
|
||||
|
||||
@classmethod
|
||||
|
@ -150,3 +232,8 @@ class DjangoObjectType(ObjectType):
|
|||
class ErrorType(ObjectType):
|
||||
field = graphene.String(required=True)
|
||||
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
|
||||
|
||||
@classmethod
|
||||
def from_errors(cls, errors):
|
||||
data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors
|
||||
return [cls(field=key, messages=value) for key, value in data.items()]
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
from .testing import GraphQLTestCase
|
||||
from .utils import (
|
||||
DJANGO_FILTER_INSTALLED,
|
||||
get_reverse_fields,
|
||||
maybe_queryset,
|
||||
camelize,
|
||||
get_model_fields,
|
||||
is_valid_django_model,
|
||||
get_reverse_fields,
|
||||
import_single_dispatch,
|
||||
is_valid_django_model,
|
||||
maybe_queryset,
|
||||
)
|
||||
from .testing import GraphQLTestCase
|
||||
|
||||
__all__ = [
|
||||
"DJANGO_FILTER_INSTALLED",
|
||||
"get_reverse_fields",
|
||||
"maybe_queryset",
|
||||
"get_model_fields",
|
||||
"camelize",
|
||||
"is_valid_django_model",
|
||||
"import_single_dispatch",
|
||||
"GraphQLTestCase",
|
||||
|
|
|
@ -22,7 +22,7 @@ class GraphQLTestCase(TestCase):
|
|||
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
|
||||
)
|
||||
|
||||
cls._client = Client(cls.GRAPHQL_SCHEMA)
|
||||
cls._client = Client()
|
||||
|
||||
def query(self, query, op_name=None, input_data=None):
|
||||
"""
|
||||
|
@ -54,8 +54,8 @@ class GraphQLTestCase(TestCase):
|
|||
the call was fine.
|
||||
:resp HttpResponse: Response
|
||||
"""
|
||||
content = json.loads(resp.content)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
content = json.loads(resp.content)
|
||||
self.assertNotIn("errors", list(content.keys()))
|
||||
|
||||
def assertResponseHasErrors(self, resp):
|
||||
|
|
|
@ -2,7 +2,11 @@ import inspect
|
|||
|
||||
from django.db import models
|
||||
from django.db.models.manager import Manager
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
|
||||
from graphene.utils.str_converters import to_camel_case
|
||||
|
||||
try:
|
||||
import django_filters # noqa
|
||||
|
@ -12,13 +16,36 @@ except ImportError:
|
|||
DJANGO_FILTER_INSTALLED = False
|
||||
|
||||
|
||||
def isiterable(value):
|
||||
try:
|
||||
iter(value)
|
||||
except TypeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _camelize_django_str(s):
|
||||
if isinstance(s, Promise):
|
||||
s = force_text(s)
|
||||
return to_camel_case(s) if isinstance(s, six.string_types) else s
|
||||
|
||||
|
||||
def camelize(data):
|
||||
if isinstance(data, dict):
|
||||
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
|
||||
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
|
||||
return [camelize(d) for d in data]
|
||||
return data
|
||||
|
||||
|
||||
def get_reverse_fields(model, local_field_names):
|
||||
for name, attr in model.__dict__.items():
|
||||
# Don't duplicate any local fields
|
||||
if name in local_field_names:
|
||||
continue
|
||||
|
||||
related = getattr(attr, "rel", None)
|
||||
# "rel" for FK and M2M relations and "related" for O2O Relations
|
||||
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
|
||||
if isinstance(related, models.ManyToOneRel):
|
||||
yield (name, related)
|
||||
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
||||
|
|
32
setup.cfg
32
setup.cfg
|
@ -5,11 +5,41 @@ test=pytest
|
|||
universal=1
|
||||
|
||||
[flake8]
|
||||
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/*
|
||||
exclude = docs,graphene_django/debug/sql/*,migrations
|
||||
max-line-length = 120
|
||||
select =
|
||||
# Dictionary key repeated
|
||||
F601,
|
||||
# Ensure use of ==/!= to compare with str, bytes and int literals
|
||||
F632,
|
||||
# Redefinition of unused name
|
||||
F811,
|
||||
# Using an undefined variable
|
||||
F821,
|
||||
# Defining an undefined variable in __all__
|
||||
F822,
|
||||
# Using a variable before it is assigned
|
||||
F823,
|
||||
# Duplicate argument in function declaration
|
||||
F831,
|
||||
# Black would format this line
|
||||
BLK,
|
||||
# Do not use bare except
|
||||
B001,
|
||||
# Don't allow ++n. You probably meant n += 1
|
||||
B002,
|
||||
# Do not use mutable structures for argument defaults
|
||||
B006,
|
||||
# Do not perform calls in argument defaults
|
||||
B008
|
||||
|
||||
[coverage:run]
|
||||
omit = */tests/*
|
||||
|
||||
[isort]
|
||||
known_first_party=graphene,graphene_django
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
|
|
8
setup.py
8
setup.py
|
@ -28,6 +28,8 @@ tests_require = [
|
|||
dev_requires = [
|
||||
"black==19.3b0",
|
||||
"flake8==3.7.7",
|
||||
"flake8-black==0.1.0",
|
||||
"flake8-bugbear==19.3.0",
|
||||
] + tests_require
|
||||
|
||||
setup(
|
||||
|
@ -64,7 +66,11 @@ setup(
|
|||
setup_requires=["pytest-runner"],
|
||||
tests_require=tests_require,
|
||||
rest_framework_require=rest_framework_require,
|
||||
extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires},
|
||||
extras_require={
|
||||
"test": tests_require,
|
||||
"rest_framework": rest_framework_require,
|
||||
"dev": dev_requires,
|
||||
},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
platforms="any",
|
||||
|
|
39
tox.ini
Normal file
39
tox.ini
Normal 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
|
Loading…
Reference in New Issue
Block a user