mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 17:52:19 +03:00
Merge branch 'original-master' into elasticsearch-filterset
This commit is contained in:
commit
054ebaa57d
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -78,3 +78,5 @@ Session.vim
|
||||||
*~
|
*~
|
||||||
# auto-generated tag files
|
# auto-generated tag files
|
||||||
tags
|
tags
|
||||||
|
.tox/
|
||||||
|
.pytest_cache/
|
||||||
|
|
98
.travis.yml
98
.travis.yml
|
@ -1,62 +1,60 @@
|
||||||
language: python
|
language: python
|
||||||
sudo: required
|
sudo: required
|
||||||
dist: xenial
|
dist: xenial
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.4
|
- 3.4
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.6
|
- 3.6
|
||||||
- 3.7
|
- 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:
|
env:
|
||||||
matrix:
|
matrix:
|
||||||
- TEST_TYPE=build DJANGO_VERSION=1.11
|
- DJANGO=1.11
|
||||||
|
- DJANGO=2.1
|
||||||
|
- DJANGO=2.2
|
||||||
|
- DJANGO=master
|
||||||
|
|
||||||
|
install:
|
||||||
|
- TOX_ENV=py${TRAVIS_PYTHON_VERSION}-django${DJANGO}
|
||||||
|
- pip install tox
|
||||||
|
- tox -e $TOX_ENV --notest
|
||||||
|
script:
|
||||||
|
- tox -e $TOX_ENV
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- tox -e $TOX_ENV -- pip install coveralls
|
||||||
|
- tox -e $TOX_ENV -- coveralls $COVERALLS_OPTION
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- python: '3.4'
|
- python: 3.5
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
script: tox -e lint
|
||||||
- python: '3.5'
|
exclude:
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
- python: 2.7
|
||||||
- python: '3.6'
|
env: DJANGO=2.1
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
- python: 2.7
|
||||||
- python: '3.5'
|
env: DJANGO=2.2
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
- python: 2.7
|
||||||
- python: '3.6'
|
env: DJANGO=master
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
- python: 3.4
|
||||||
- python: '3.6'
|
env: DJANGO=2.1
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.2
|
- python: 3.4
|
||||||
- python: '3.7'
|
env: DJANGO=2.2
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.2
|
- python: 3.4
|
||||||
- python: '2.7'
|
env: DJANGO=master
|
||||||
env: TEST_TYPE=lint
|
- python: 3.5
|
||||||
- python: '3.6'
|
env: DJANGO=master
|
||||||
env: TEST_TYPE=lint
|
- python: 3.7
|
||||||
- python: '3.7'
|
env: DJANGO=1.10
|
||||||
env: TEST_TYPE=lint
|
- python: 3.7
|
||||||
|
env: DJANGO=1.11
|
||||||
|
allow_failures:
|
||||||
|
- python: 3.7
|
||||||
|
- env: DJANGO=master
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: syrusakbary
|
user: syrusakbary
|
||||||
|
|
|
@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
|
||||||
``filter_fields``.
|
``filter_fields``.
|
||||||
|
|
||||||
However, you may find this to be insufficient. In these cases you can
|
However, you may find this to be insufficient. In these cases you can
|
||||||
create your own ``Filterset`` as follows:
|
create your own ``FilterSet``. You can pass it directly as follows:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
|
||||||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||||
filterset_class=AnimalFilter)
|
filterset_class=AnimalFilter)
|
||||||
|
|
||||||
|
You can also specify the ``FilterSet`` class using the ``filerset_class``
|
||||||
|
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
||||||
|
in unison with the ``filter_fields`` parameter:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class AnimalFilter(django_filters.FilterSet):
|
||||||
|
# Do case-insensitive lookups on 'name'
|
||||||
|
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
# Assume you have an Animal model defined with the following fields
|
||||||
|
model = Animal
|
||||||
|
fields = ['name', 'genus', 'is_domesticated']
|
||||||
|
|
||||||
|
|
||||||
|
class AnimalNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Animal
|
||||||
|
filterset_class = AnimalFilter
|
||||||
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
animal = relay.Node.Field(AnimalNode)
|
||||||
|
all_animals = DjangoFilterConnectionField(AnimalNode)
|
||||||
|
|
||||||
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
||||||
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
||||||
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||||
|
|
|
@ -35,6 +35,8 @@ Advanced Usage
|
||||||
The ``--indent`` option can be used to specify the number of indentation spaces to
|
The ``--indent`` option can be used to specify the number of indentation spaces to
|
||||||
be used in the output. Defaults to `None` which displays all data on a single line.
|
be used in the output. Defaults to `None` which displays all data on a single line.
|
||||||
|
|
||||||
|
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
|
||||||
|
|
||||||
To simplify the command to ``./manage.py graphql_schema``, you can
|
To simplify the command to ``./manage.py graphql_schema``, you can
|
||||||
specify the parameters in your settings.py:
|
specify the parameters in your settings.py:
|
||||||
|
|
||||||
|
|
|
@ -199,7 +199,9 @@ You can use relay with mutations. A Relay mutation must inherit from
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
import graphene import relay, DjangoObjectType
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
from graphql_relay import from_global_id
|
from graphql_relay import from_global_id
|
||||||
|
|
||||||
from .queries import QuestionType
|
from .queries import QuestionType
|
||||||
|
@ -214,7 +216,7 @@ You can use relay with mutations. A Relay mutation must inherit from
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mutate_and_get_payload(cls, root, info, text, id):
|
def mutate_and_get_payload(cls, root, info, text, id):
|
||||||
question = Question.objects.get(pk=from_global_id(id))
|
question = Question.objects.get(pk=from_global_id(id)[1])
|
||||||
question.text = text
|
question.text = text
|
||||||
question.save()
|
question.save()
|
||||||
return QuestionMutation(question=question)
|
return QuestionMutation(question=question)
|
||||||
|
@ -226,4 +228,4 @@ Relay ClientIDMutation accept a ``clientIDMutation`` argument.
|
||||||
This argument is also sent back to the client with the mutation result
|
This argument is also sent back to the client with the mutation result
|
||||||
(you do not have to do anything). For services that manage
|
(you do not have to do anything). For services that manage
|
||||||
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
|
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.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
graphene
|
graphene
|
||||||
graphene-django
|
graphene-django
|
||||||
graphql-core>=2.1rc1
|
graphql-core>=2.1rc1
|
||||||
django==1.11.19
|
django==1.11.20
|
||||||
django-filter>=2
|
django-filter>=2
|
||||||
|
|
|
@ -177,19 +177,22 @@ def convert_field_to_list_or_connection(field, registry=None):
|
||||||
if not _type:
|
if not _type:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
description = field.help_text if isinstance(field, models.ManyToManyField) else field.field.help_text
|
||||||
|
|
||||||
# If there is a connection, we should transform the field
|
# If there is a connection, we should transform the field
|
||||||
# into a DjangoConnectionField
|
# into a DjangoConnectionField
|
||||||
if _type._meta.connection:
|
if _type._meta.connection:
|
||||||
# Use a DjangoFilterConnectionField if there are
|
# Use a DjangoFilterConnectionField if there are
|
||||||
# defined filter_fields in the DjangoObjectType Meta
|
# defined filter_fields or a filterset_class in the
|
||||||
if _type._meta.filter_fields:
|
# DjangoObjectType Meta
|
||||||
|
if _type._meta.filter_fields or _type._meta.filterset_class:
|
||||||
from .filter.fields import DjangoFilterConnectionField
|
from .filter.fields import DjangoFilterConnectionField
|
||||||
|
|
||||||
return DjangoFilterConnectionField(_type)
|
return DjangoFilterConnectionField(_type, description=description)
|
||||||
|
|
||||||
return DjangoConnectionField(_type)
|
return DjangoConnectionField(_type, description=description)
|
||||||
|
|
||||||
return DjangoListField(_type)
|
return DjangoListField(_type, description=description)
|
||||||
|
|
||||||
return Dynamic(dynamic_type)
|
return Dynamic(dynamic_type)
|
||||||
|
|
||||||
|
|
|
@ -50,9 +50,7 @@ def test_should_query_field():
|
||||||
"""
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
"reporter": {"lastName": "ABA"},
|
"reporter": {"lastName": "ABA"},
|
||||||
"_debug": {
|
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
|
||||||
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
|
|
|
@ -40,8 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
if self._extra_filter_meta:
|
if self._extra_filter_meta:
|
||||||
meta.update(self._extra_filter_meta)
|
meta.update(self._extra_filter_meta)
|
||||||
|
|
||||||
|
filterset_class = self._provided_filterset_class or (
|
||||||
|
self.node_type._meta.filterset_class)
|
||||||
self._filterset_class = get_filterset_class(
|
self._filterset_class = get_filterset_class(
|
||||||
self._provided_filterset_class, **meta
|
filterset_class, **meta
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._filterset_class
|
return self._filterset_class
|
||||||
|
|
|
@ -227,6 +227,73 @@ def test_filter_filterset_information_on_meta_related():
|
||||||
assert_not_orderable(articles_field)
|
assert_not_orderable(articles_field)
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_filterset_class_filter_fields_exception():
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
class ReporterFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filterset_class = ReporterFilter
|
||||||
|
filter_fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_filterset_class_information_on_meta():
|
||||||
|
class ReporterFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filterset_class = ReporterFilter
|
||||||
|
|
||||||
|
field = DjangoFilterConnectionField(ReporterFilterNode)
|
||||||
|
assert_arguments(field, "first_name", "articles")
|
||||||
|
assert_not_orderable(field)
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_filterset_class_information_on_meta_related():
|
||||||
|
class ReporterFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
class ArticleFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ["headline", "reporter"]
|
||||||
|
|
||||||
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filterset_class = ReporterFilter
|
||||||
|
|
||||||
|
class ArticleFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
interfaces = (Node,)
|
||||||
|
filterset_class = ArticleFilter
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||||
|
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
|
||||||
|
reporter = Field(ReporterFilterNode)
|
||||||
|
article = Field(ArticleFilterNode)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
|
||||||
|
assert_arguments(articles_field, "headline", "reporter")
|
||||||
|
assert_not_orderable(articles_field)
|
||||||
|
|
||||||
|
|
||||||
def test_filter_filterset_related_results():
|
def test_filter_filterset_related_results():
|
||||||
class ReporterFilterNode(DjangoObjectType):
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import functools
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import autoreload
|
||||||
|
|
||||||
from graphene_django.settings import graphene_settings
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
|
@ -32,6 +34,14 @@ class CommandArguments(BaseCommand):
|
||||||
help="Output file indent (default: None)",
|
help="Output file indent (default: None)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--watch",
|
||||||
|
dest="watch",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Updates the schema on file changes (default: False)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Command(CommandArguments):
|
class Command(CommandArguments):
|
||||||
help = "Dump Graphene schema JSON to file"
|
help = "Dump Graphene schema JSON to file"
|
||||||
|
@ -41,6 +51,18 @@ class Command(CommandArguments):
|
||||||
with open(out, "w") as outfile:
|
with open(out, "w") as outfile:
|
||||||
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
||||||
|
|
||||||
|
def get_schema(self, schema, out, indent):
|
||||||
|
schema_dict = {"data": schema.introspect()}
|
||||||
|
if out == "-":
|
||||||
|
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
||||||
|
else:
|
||||||
|
self.save_file(out, schema_dict, indent)
|
||||||
|
|
||||||
|
style = getattr(self, "style", None)
|
||||||
|
success = getattr(style, "SUCCESS", lambda x: x)
|
||||||
|
|
||||||
|
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
options_schema = options.get("schema")
|
options_schema = options.get("schema")
|
||||||
|
|
||||||
|
@ -63,13 +85,10 @@ class Command(CommandArguments):
|
||||||
)
|
)
|
||||||
|
|
||||||
indent = options.get("indent")
|
indent = options.get("indent")
|
||||||
schema_dict = {"data": schema.introspect()}
|
watch = options.get("watch")
|
||||||
if out == "-":
|
if watch:
|
||||||
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
autoreload.run_with_reloader(
|
||||||
|
functools.partial(self.get_schema, schema, out, indent)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.save_file(out, schema_dict, indent)
|
self.get_schema(schema, out, indent)
|
||||||
|
|
||||||
style = getattr(self, "style", None)
|
|
||||||
success = getattr(style, "SUCCESS", lambda x: x)
|
|
||||||
|
|
||||||
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
|
||||||
|
|
|
@ -4,3 +4,8 @@ from django.db import models
|
||||||
class MyFakeModel(models.Model):
|
class MyFakeModel(models.Model):
|
||||||
cool_name = models.CharField(max_length=50)
|
cool_name = models.CharField(max_length=50)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModelWithPassword(models.Model):
|
||||||
|
cool_name = models.CharField(max_length=50)
|
||||||
|
password = models.CharField(max_length=50)
|
||||||
|
|
|
@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
|
||||||
name
|
name
|
||||||
in exclude_fields # or
|
in exclude_fields # or
|
||||||
# name in already_created_fields
|
# name in already_created_fields
|
||||||
|
) or (
|
||||||
|
field.write_only and not is_input # don't show write_only fields in Query
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_not_in_only or is_excluded:
|
if is_not_in_only or is_excluded:
|
||||||
|
@ -138,6 +140,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for f, field in serializer.fields.items():
|
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)
|
return cls(errors=None, **kwargs)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from py.test import mark
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ...types import DjangoObjectType
|
from ...types import DjangoObjectType
|
||||||
from ..models import MyFakeModel
|
from ..models import MyFakeModel, MyFakeModelWithPassword
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,6 +86,47 @@ def test_exclude_fields():
|
||||||
assert "created" not in MyMutation.Input._meta.fields
|
assert "created" not in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_write_only_field():
|
||||||
|
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
password = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithPassword
|
||||||
|
fields = ["cool_name", "password"]
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = WriteOnlyFieldModelSerializer
|
||||||
|
|
||||||
|
result = MyMutation.mutate_and_get_payload(
|
||||||
|
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(result, "cool_name")
|
||||||
|
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible"
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_write_only_field_using_extra_kwargs():
|
||||||
|
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithPassword
|
||||||
|
fields = ["cool_name", "password"]
|
||||||
|
extra_kwargs = {"password": {"write_only": True}}
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = WriteOnlyFieldModelSerializer
|
||||||
|
|
||||||
|
result = MyMutation.mutate_and_get_payload(
|
||||||
|
None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(result, "cool_name")
|
||||||
|
assert not hasattr(result, "password"), "'password' is write_only field and shouldn't be visible"
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -65,6 +65,11 @@ class Reporter(models.Model):
|
||||||
self.__class__ = CNNReporter
|
self.__class__ = CNNReporter
|
||||||
|
|
||||||
|
|
||||||
|
class CNNReporterManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
|
||||||
|
|
||||||
|
|
||||||
class CNNReporter(Reporter):
|
class CNNReporter(Reporter):
|
||||||
"""
|
"""
|
||||||
This class is a proxy model for Reporter, used for testing
|
This class is a proxy model for Reporter, used for testing
|
||||||
|
@ -74,6 +79,8 @@ class CNNReporter(Reporter):
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
|
objects = CNNReporterManager()
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
headline = models.CharField(max_length=100)
|
headline = models.CharField(max_length=100)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -7,6 +8,7 @@ from py.test import raises
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from graphql_relay import to_global_id
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
|
@ -226,6 +228,62 @@ def test_should_node():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_onetoone_fields():
|
||||||
|
film = Film(id=1)
|
||||||
|
film_details = FilmDetails(id=1, film=film)
|
||||||
|
|
||||||
|
class FilmNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class FilmDetailsNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = FilmDetails
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
film = graphene.Field(FilmNode)
|
||||||
|
film_details = graphene.Field(FilmDetailsNode)
|
||||||
|
|
||||||
|
def resolve_film(root, info):
|
||||||
|
return film
|
||||||
|
|
||||||
|
def resolve_film_details(root, info):
|
||||||
|
return film_details
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query FilmQuery {
|
||||||
|
filmDetails {
|
||||||
|
id
|
||||||
|
film {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
film {
|
||||||
|
id
|
||||||
|
details {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {
|
||||||
|
"filmDetails": {
|
||||||
|
"id": "RmlsbURldGFpbHNOb2RlOjE=",
|
||||||
|
"film": {"id": "RmlsbU5vZGU6MQ=="},
|
||||||
|
},
|
||||||
|
"film": {
|
||||||
|
"id": "RmlsbU5vZGU6MQ==",
|
||||||
|
"details": {"id": "RmlsbURldGFpbHNOb2RlOjE="},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_connectionfields():
|
def test_should_query_connectionfields():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -895,8 +953,7 @@ def test_should_handle_inherited_choices():
|
||||||
|
|
||||||
def test_proxy_model_support():
|
def test_proxy_model_support():
|
||||||
"""
|
"""
|
||||||
This test asserts that we can query for all Reporters,
|
This test asserts that we can query for all Reporters and proxied Reporters.
|
||||||
even if some are of a proxy model type at runtime.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
|
@ -905,11 +962,17 @@ def test_proxy_model_support():
|
||||||
interfaces = (Node,)
|
interfaces = (Node,)
|
||||||
use_connection = True
|
use_connection = True
|
||||||
|
|
||||||
reporter_1 = Reporter.objects.create(
|
class CNNReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = CNNReporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
use_connection = True
|
||||||
|
|
||||||
|
reporter = Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
reporter_2 = CNNReporter.objects.create(
|
cnn_reporter = CNNReporter.objects.create(
|
||||||
first_name="Some",
|
first_name="Some",
|
||||||
last_name="Guy",
|
last_name="Guy",
|
||||||
email="someguy@cnn.com",
|
email="someguy@cnn.com",
|
||||||
|
@ -919,6 +982,7 @@ def test_proxy_model_support():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
cnn_reporters = DjangoConnectionField(CNNReporterType)
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
query = """
|
query = """
|
||||||
|
@ -930,14 +994,26 @@ def test_proxy_model_support():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cnnReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": {
|
"allReporters": {
|
||||||
"edges": [
|
"edges": [
|
||||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
|
{"node": {"id": to_global_id("ReporterType", reporter.id)}},
|
||||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
|
{"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cnnReporters": {
|
||||||
|
"edges": [
|
||||||
|
{"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -945,69 +1021,7 @@ def test_proxy_model_support():
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_proxy_model_fails():
|
|
||||||
"""
|
|
||||||
This test asserts that if you try to query for a proxy model,
|
|
||||||
that query will fail with:
|
|
||||||
GraphQLError('Expected value of type "CNNReporterType" but got:
|
|
||||||
CNNReporter.',)
|
|
||||||
|
|
||||||
This is because a proxy model has the identical model definition
|
|
||||||
to its superclass, and defines its behavior at runtime, rather than
|
|
||||||
at the database level. Currently, filtering objects of the proxy models'
|
|
||||||
type isn't supported. It would require a field on the model that would
|
|
||||||
represent the type, and it doesn't seem like there is a clear way to
|
|
||||||
enforce this pattern across all projects
|
|
||||||
"""
|
|
||||||
|
|
||||||
class CNNReporterType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = CNNReporter
|
|
||||||
interfaces = (Node,)
|
|
||||||
use_connection = True
|
|
||||||
|
|
||||||
reporter_1 = Reporter.objects.create(
|
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
|
||||||
)
|
|
||||||
|
|
||||||
reporter_2 = CNNReporter.objects.create(
|
|
||||||
first_name="Some",
|
|
||||||
last_name="Guy",
|
|
||||||
email="someguy@cnn.com",
|
|
||||||
a_choice=1,
|
|
||||||
reporter_type=2, # set this guy to be CNN
|
|
||||||
)
|
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
|
||||||
all_reporters = DjangoConnectionField(CNNReporterType)
|
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
|
||||||
query = """
|
|
||||||
query ProxyModelQuery {
|
|
||||||
allReporters {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
expected = {
|
|
||||||
"allReporters": {
|
|
||||||
"edges": [
|
|
||||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
|
|
||||||
{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert result.errors
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_resolve_get_queryset_connectionfields():
|
def test_should_resolve_get_queryset_connectionfields():
|
||||||
reporter_1 = Reporter.objects.create(
|
reporter_1 = Reporter.objects.create(
|
||||||
|
|
|
@ -45,6 +45,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
|
||||||
connection = None # type: Type[Connection]
|
connection = None # type: Type[Connection]
|
||||||
|
|
||||||
filter_fields = ()
|
filter_fields = ()
|
||||||
|
filterset_class = None
|
||||||
|
|
||||||
|
|
||||||
class DjangoObjectType(ObjectType):
|
class DjangoObjectType(ObjectType):
|
||||||
|
@ -57,6 +58,7 @@ class DjangoObjectType(ObjectType):
|
||||||
only_fields=(),
|
only_fields=(),
|
||||||
exclude_fields=(),
|
exclude_fields=(),
|
||||||
filter_fields=None,
|
filter_fields=None,
|
||||||
|
filterset_class=None,
|
||||||
connection=None,
|
connection=None,
|
||||||
connection_class=None,
|
connection_class=None,
|
||||||
use_connection=None,
|
use_connection=None,
|
||||||
|
@ -76,8 +78,14 @@ class DjangoObjectType(ObjectType):
|
||||||
'Registry, received "{}".'
|
'Registry, received "{}".'
|
||||||
).format(cls.__name__, registry)
|
).format(cls.__name__, registry)
|
||||||
|
|
||||||
if not DJANGO_FILTER_INSTALLED and filter_fields:
|
if filter_fields and filterset_class:
|
||||||
raise Exception("Can only set filter_fields if Django-Filter is installed")
|
raise Exception("Can't set both filter_fields and filterset_class")
|
||||||
|
|
||||||
|
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
|
||||||
|
raise Exception((
|
||||||
|
"Can only set filter_fields or filterset_class if "
|
||||||
|
"Django-Filter is installed"
|
||||||
|
))
|
||||||
|
|
||||||
django_fields = yank_fields_from_attrs(
|
django_fields = yank_fields_from_attrs(
|
||||||
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
|
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
|
||||||
|
@ -108,6 +116,7 @@ class DjangoObjectType(ObjectType):
|
||||||
_meta.model = model
|
_meta.model = model
|
||||||
_meta.registry = registry
|
_meta.registry = registry
|
||||||
_meta.filter_fields = filter_fields
|
_meta.filter_fields = filter_fields
|
||||||
|
_meta.filterset_class = filterset_class
|
||||||
_meta.fields = django_fields
|
_meta.fields = django_fields
|
||||||
_meta.connection = connection
|
_meta.connection = connection
|
||||||
|
|
||||||
|
@ -131,7 +140,11 @@ class DjangoObjectType(ObjectType):
|
||||||
if not is_valid_django_model(type(root)):
|
if not is_valid_django_model(type(root)):
|
||||||
raise Exception(('Received incompatible instance "{}".').format(root))
|
raise Exception(('Received incompatible instance "{}".').format(root))
|
||||||
|
|
||||||
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
|
return model == cls._meta.model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -22,7 +22,7 @@ class GraphQLTestCase(TestCase):
|
||||||
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
|
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
|
||||||
)
|
)
|
||||||
|
|
||||||
cls._client = Client(cls.GRAPHQL_SCHEMA)
|
cls._client = Client()
|
||||||
|
|
||||||
def query(self, query, op_name=None, input_data=None):
|
def query(self, query, op_name=None, input_data=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -26,7 +26,8 @@ def get_reverse_fields(model, local_field_names):
|
||||||
if name in local_field_names:
|
if name in local_field_names:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
related = getattr(attr, "rel", None)
|
# "rel" for FK and M2M relations and "related" for O2O Relations
|
||||||
|
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
|
||||||
if isinstance(related, models.ManyToOneRel):
|
if isinstance(related, models.ManyToOneRel):
|
||||||
yield (name, related)
|
yield (name, related)
|
||||||
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
||||||
|
|
31
tox.ini
Normal file
31
tox.ini
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
[tox]
|
||||||
|
envlist = py{2.7,3.4,3.5,3.6,3.7,pypy,pypy3}-django{1.10,1.11,2.0,2.1,2.2,master},lint
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
passenv = *
|
||||||
|
usedevelop = True
|
||||||
|
setenv =
|
||||||
|
DJANGO_SETTINGS_MODULE=django_test_settings
|
||||||
|
basepython =
|
||||||
|
py2.7: python2.7
|
||||||
|
py3.4: python3.4
|
||||||
|
py3.5: python3.5
|
||||||
|
py3.6: python3.6
|
||||||
|
py3.7: python3.7
|
||||||
|
pypypy: pypy
|
||||||
|
pypypy3: pypy3
|
||||||
|
deps =
|
||||||
|
-e.[test]
|
||||||
|
psycopg2
|
||||||
|
django1.10: Django>=1.10,<1.11
|
||||||
|
django1.11: Django>=1.11,<1.12
|
||||||
|
django2.0: Django>=2.0
|
||||||
|
django2.1: Django>=2.1
|
||||||
|
djangomaster: https://github.com/django/django/archive/master.zip
|
||||||
|
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
basepython = python
|
||||||
|
deps =
|
||||||
|
prospector
|
||||||
|
commands = prospector graphene_django -0
|
Loading…
Reference in New Issue
Block a user