mirror of
https://github.com/graphql-python/graphene.git
synced 2025-02-02 20:54:16 +03:00
Merge pull request #79 from syrusakbary/feature/django
Improved Django integration
This commit is contained in:
commit
b18fbb5ace
|
@ -58,6 +58,7 @@ def convert_field_to_float(field):
|
||||||
|
|
||||||
@convert_django_field.register(models.ManyToManyField)
|
@convert_django_field.register(models.ManyToManyField)
|
||||||
@convert_django_field.register(models.ManyToOneRel)
|
@convert_django_field.register(models.ManyToOneRel)
|
||||||
|
@convert_django_field.register(models.ManyToManyRel)
|
||||||
def convert_field_to_list_or_connection(field):
|
def convert_field_to_list_or_connection(field):
|
||||||
from .fields import DjangoModelField, ConnectionOrListField
|
from .fields import DjangoModelField, ConnectionOrListField
|
||||||
model_field = DjangoModelField(get_related_model(field))
|
model_field = DjangoModelField(get_related_model(field))
|
||||||
|
|
|
@ -4,7 +4,7 @@ from ...core.types.base import FieldType
|
||||||
from ...core.types.definitions import List
|
from ...core.types.definitions import List
|
||||||
from ...relay import ConnectionField
|
from ...relay import ConnectionField
|
||||||
from ...relay.utils import is_node
|
from ...relay.utils import is_node
|
||||||
from .utils import get_type_for_model, maybe_queryset
|
from .utils import DJANGO_FILTER_INSTALLED, get_type_for_model, maybe_queryset
|
||||||
|
|
||||||
|
|
||||||
class DjangoConnectionField(ConnectionField):
|
class DjangoConnectionField(ConnectionField):
|
||||||
|
@ -37,7 +37,8 @@ class DjangoConnectionField(ConnectionField):
|
||||||
class ConnectionOrListField(Field):
|
class ConnectionOrListField(Field):
|
||||||
|
|
||||||
def internal_type(self, schema):
|
def internal_type(self, schema):
|
||||||
from .filter.fields import DjangoFilterConnectionField
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
from .filter.fields import DjangoFilterConnectionField
|
||||||
|
|
||||||
model_field = self.type
|
model_field = self.type
|
||||||
field_object_type = model_field.get_object_type(schema)
|
field_object_type = model_field.get_object_type(schema)
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
import warnings
|
||||||
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
|
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
if not DJANGO_FILTER_INSTALLED:
|
if not DJANGO_FILTER_INSTALLED:
|
||||||
raise Exception(
|
warnings.warn(
|
||||||
"Use of django filtering requires the django-filter package "
|
"Use of django filtering requires the django-filter package "
|
||||||
"be installed. You can do so using `pip install django-filter`"
|
"be installed. You can do so using `pip install django-filter`", ImportWarning
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
from .fields import DjangoFilterConnectionField
|
||||||
|
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
|
||||||
|
|
||||||
from .fields import DjangoFilterConnectionField
|
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
|
||||||
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
|
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']
|
||||||
|
|
||||||
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
|
|
||||||
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import six
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django_filters import Filter, MultipleChoiceFilter
|
|
||||||
from django_filters.filterset import FilterSet, FilterSetMetaclass
|
|
||||||
from graphql_relay.node.node import from_global_id
|
from graphql_relay.node.node import from_global_id
|
||||||
|
|
||||||
|
from django_filters import Filter, MultipleChoiceFilter
|
||||||
|
from django_filters.filterset import FilterSet, FilterSetMetaclass
|
||||||
from graphene.contrib.django.forms import (GlobalIDFormField,
|
from graphene.contrib.django.forms import (GlobalIDFormField,
|
||||||
GlobalIDMultipleChoiceField)
|
GlobalIDMultipleChoiceField)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from graphene.contrib.django.tests.models import Article, Pet, Reporter
|
from graphene.contrib.django.tests.models import Article, Pet, Reporter
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -69,8 +69,8 @@ def assert_not_orderable(field):
|
||||||
def test_filter_explicit_filterset_arguments():
|
def test_filter_explicit_filterset_arguments():
|
||||||
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
|
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
|
||||||
assert_arguments(field,
|
assert_arguments(field,
|
||||||
'headline', 'headlineIcontains',
|
'headline', 'headline_Icontains',
|
||||||
'pubDate', 'pubDateGt', 'pubDateLt',
|
'pubDate', 'pubDate_Gt', 'pubDate_Lt',
|
||||||
'reporter',
|
'reporter',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ def test_filter_shortcut_filterset_arguments_dict():
|
||||||
'reporter': ['exact'],
|
'reporter': ['exact'],
|
||||||
})
|
})
|
||||||
assert_arguments(field,
|
assert_arguments(field,
|
||||||
'headline', 'headlineIcontains',
|
'headline', 'headline_Icontains',
|
||||||
'reporter',
|
'reporter',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,72 @@
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
from optparse import make_option
|
||||||
|
|
||||||
|
from django import get_version as get_django_version
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8')
|
||||||
|
|
||||||
class Command(BaseCommand):
|
if LT_DJANGO_1_8:
|
||||||
|
class CommandArguments(BaseCommand):
|
||||||
|
option_list = BaseCommand.option_list + (
|
||||||
|
make_option(
|
||||||
|
'--schema',
|
||||||
|
type=str,
|
||||||
|
dest='schema',
|
||||||
|
default='',
|
||||||
|
help='Django app containing schema to dump, e.g. myproject.core.schema',
|
||||||
|
),
|
||||||
|
make_option(
|
||||||
|
'--out',
|
||||||
|
type=str,
|
||||||
|
dest='out',
|
||||||
|
default='',
|
||||||
|
help='Output file (default: schema.json)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
class CommandArguments(BaseCommand):
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
from django.conf import settings
|
||||||
|
parser.add_argument(
|
||||||
|
'--schema',
|
||||||
|
type=str,
|
||||||
|
dest='schema',
|
||||||
|
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
|
||||||
|
help='Django app containing schema to dump, e.g. myproject.core.schema')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--out',
|
||||||
|
type=str,
|
||||||
|
dest='out',
|
||||||
|
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
|
||||||
|
help='Output file (default: schema.json)')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(CommandArguments):
|
||||||
help = 'Dump Graphene schema JSON to file'
|
help = 'Dump Graphene schema JSON to file'
|
||||||
can_import_settings = True
|
can_import_settings = True
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def save_file(self, out, schema_dict):
|
||||||
from django.conf import settings
|
with open(out, 'w') as outfile:
|
||||||
parser.add_argument(
|
|
||||||
'--schema',
|
|
||||||
type=str,
|
|
||||||
dest='schema',
|
|
||||||
default=getattr(settings, 'GRAPHENE_SCHEMA', ''),
|
|
||||||
help='Django app containing schema to dump, e.g. myproject.core.schema')
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--out',
|
|
||||||
type=str,
|
|
||||||
dest='out',
|
|
||||||
default=getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json'),
|
|
||||||
help='Output file (default: schema.json)')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
schema_module = options['schema']
|
|
||||||
if schema_module == '':
|
|
||||||
raise CommandError('Specify schema on GRAPHENE_SCHEMA setting or by using --schema')
|
|
||||||
i = importlib.import_module(schema_module)
|
|
||||||
|
|
||||||
schema_dict = {'data': i.schema.introspect()}
|
|
||||||
|
|
||||||
with open(options['out'], 'w') as outfile:
|
|
||||||
json.dump(schema_dict, outfile)
|
json.dump(schema_dict, outfile)
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS('Successfully dumped GraphQL schema to %s' % options['out']))
|
def handle(self, *args, **options):
|
||||||
|
from django.conf import settings
|
||||||
|
schema = options.get('schema') or getattr(settings, 'GRAPHENE_SCHEMA', '')
|
||||||
|
out = options.get('out') or getattr(settings, 'GRAPHENE_SCHEMA_OUTPUT', 'schema.json')
|
||||||
|
|
||||||
|
if schema == '':
|
||||||
|
raise CommandError('Specify schema on GRAPHENE_SCHEMA setting or by using --schema')
|
||||||
|
i = importlib.import_module(schema)
|
||||||
|
|
||||||
|
schema_dict = {'data': i.schema.introspect()}
|
||||||
|
self.save_file(out, schema_dict)
|
||||||
|
|
||||||
|
style = getattr(self, 'style', None)
|
||||||
|
SUCCESS = getattr(style, 'SUCCESS', lambda x: x)
|
||||||
|
|
||||||
|
self.stdout.write(SUCCESS('Successfully dumped GraphQL schema to %s' % out))
|
||||||
|
|
|
@ -7,6 +7,11 @@ class Pet(models.Model):
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
class Film(models.Model):
|
||||||
|
reporters = models.ManyToManyField('Reporter',
|
||||||
|
related_name='films')
|
||||||
|
|
||||||
|
|
||||||
class Reporter(models.Model):
|
class Reporter(models.Model):
|
||||||
first_name = models.CharField(max_length=30)
|
first_name = models.CharField(max_length=30)
|
||||||
last_name = models.CharField(max_length=30)
|
last_name = models.CharField(max_length=30)
|
||||||
|
|
11
graphene/contrib/django/tests/test_command.py
Normal file
11
graphene/contrib/django/tests/test_command.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.core import management
|
||||||
|
from mock import patch
|
||||||
|
from six import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
@patch('graphene.contrib.django.management.commands.graphql_schema.Command.save_file')
|
||||||
|
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
|
||||||
|
settings.GRAPHENE_SCHEMA = 'graphene.contrib.django.tests.test_urls'
|
||||||
|
out = StringIO()
|
||||||
|
management.call_command('graphql_schema', schema='', stdout=out)
|
||||||
|
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
|
|
@ -1,7 +1,7 @@
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
from tests.utils import assert_equal_lists
|
|
||||||
|
|
||||||
from graphene.contrib.django import DjangoObjectType
|
from graphene.contrib.django import DjangoObjectType
|
||||||
|
from tests.utils import assert_equal_lists
|
||||||
|
|
||||||
from .models import Reporter
|
from .models import Reporter
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ def test_should_map_fields_correctly():
|
||||||
model = Reporter
|
model = Reporter
|
||||||
assert_equal_lists(
|
assert_equal_lists(
|
||||||
ReporterType2._meta.fields_map.keys(),
|
ReporterType2._meta.fields_map.keys(),
|
||||||
['articles', 'first_name', 'last_name', 'email', 'pets', 'id']
|
['articles', 'first_name', 'last_name', 'email', 'pets', 'id', 'films']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from graphql.core.type import GraphQLObjectType
|
from graphql.core.type import GraphQLObjectType
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from tests.utils import assert_equal_lists
|
|
||||||
|
|
||||||
from graphene import Schema
|
from graphene import Schema
|
||||||
from graphene.contrib.django.types import DjangoNode, DjangoObjectType
|
from graphene.contrib.django.types import DjangoNode, DjangoObjectType
|
||||||
from graphene.core.fields import Field
|
from graphene.core.fields import Field
|
||||||
from graphene.core.types.scalars import Int
|
from graphene.core.types.scalars import Int
|
||||||
from graphene.relay.fields import GlobalIDField
|
from graphene.relay.fields import GlobalIDField
|
||||||
|
from tests.utils import assert_equal_lists
|
||||||
|
|
||||||
from .models import Article, Reporter
|
from .models import Article, Reporter
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ from .compat import RelatedObject
|
||||||
try:
|
try:
|
||||||
import django_filters # noqa
|
import django_filters # noqa
|
||||||
DJANGO_FILTER_INSTALLED = True
|
DJANGO_FILTER_INSTALLED = True
|
||||||
except ImportError:
|
except (ImportError, AttributeError):
|
||||||
|
# AtributeError raised if DjangoFilters installed with a incompatible Django Version
|
||||||
DJANGO_FILTER_INSTALLED = False
|
DJANGO_FILTER_INSTALLED = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ def get_reverse_fields(model):
|
||||||
yield new_related
|
yield new_related
|
||||||
elif isinstance(related, models.ManyToOneRel):
|
elif isinstance(related, models.ManyToOneRel):
|
||||||
yield related
|
yield related
|
||||||
|
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
|
||||||
|
yield related
|
||||||
|
|
||||||
|
|
||||||
class WrappedQueryset(LazyList):
|
class WrappedQueryset(LazyList):
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
from graphql.core import graphql
|
from graphql.core import graphql
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
from tests.utils import assert_equal_lists
|
|
||||||
|
|
||||||
from graphene import Interface, List, ObjectType, Schema, String
|
from graphene import Interface, List, ObjectType, Schema, String
|
||||||
from graphene.core.fields import Field
|
from graphene.core.fields import Field
|
||||||
from graphene.core.types.base import LazyType
|
from graphene.core.types.base import LazyType
|
||||||
|
from tests.utils import assert_equal_lists
|
||||||
|
|
||||||
schema = Schema(name='My own schema')
|
schema = Schema(name='My own schema')
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ def test_to_arguments_wrong_type():
|
||||||
|
|
||||||
|
|
||||||
def test_snake_case_args():
|
def test_snake_case_args():
|
||||||
resolver = lambda instance, args, info: args['my_arg']['inner_arg']
|
def resolver(instance, args, info):
|
||||||
|
return args['my_arg']['inner_arg']
|
||||||
r = snake_case_args(resolver)
|
r = snake_case_args(resolver)
|
||||||
assert r(None, {'myArg': {'innerArg': 3}}, None) == 3
|
assert r(None, {'myArg': {'innerArg': 3}}, None) == 3
|
||||||
|
|
|
@ -25,7 +25,9 @@ def test_orderedtype_different():
|
||||||
|
|
||||||
@patch('graphene.core.types.field.Field')
|
@patch('graphene.core.types.field.Field')
|
||||||
def test_type_as_field_called(Field):
|
def test_type_as_field_called(Field):
|
||||||
resolver = lambda x: x
|
def resolver(x):
|
||||||
|
return x
|
||||||
|
|
||||||
a = MountedType(2, description='A', resolver=resolver)
|
a = MountedType(2, description='A', resolver=resolver)
|
||||||
a.as_field()
|
a.as_field()
|
||||||
Field.assert_called_with(
|
Field.assert_called_with(
|
||||||
|
@ -45,7 +47,8 @@ def test_type_as_argument_called(Argument):
|
||||||
|
|
||||||
|
|
||||||
def test_type_as_field():
|
def test_type_as_field():
|
||||||
resolver = lambda x: x
|
def resolver(x):
|
||||||
|
return x
|
||||||
|
|
||||||
class MyObjectType(ObjectType):
|
class MyObjectType(ObjectType):
|
||||||
t = MountedType(description='A', resolver=resolver)
|
t = MountedType(description='A', resolver=resolver)
|
||||||
|
|
|
@ -11,7 +11,8 @@ from ..scalars import String
|
||||||
|
|
||||||
|
|
||||||
def test_field_internal_type():
|
def test_field_internal_type():
|
||||||
resolver = lambda *args: 'RESOLVED'
|
def resolver(*args):
|
||||||
|
return 'RESOLVED'
|
||||||
|
|
||||||
field = Field(String(), description='My argument', resolver=resolver)
|
field = Field(String(), description='My argument', resolver=resolver)
|
||||||
|
|
||||||
|
@ -132,7 +133,8 @@ def test_inputfield_internal_type():
|
||||||
|
|
||||||
|
|
||||||
def test_field_resolve_argument():
|
def test_field_resolve_argument():
|
||||||
resolver = lambda instance, args, info: args.get('first_name')
|
def resolver(instance, args, info):
|
||||||
|
return args.get('first_name')
|
||||||
|
|
||||||
field = Field(String(), first_name=String(), description='My argument', resolver=resolver)
|
field = Field(String(), first_name=String(), description='My argument', resolver=resolver)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ def to_camel_case(snake_str):
|
||||||
components = snake_str.split('_')
|
components = snake_str.split('_')
|
||||||
# We capitalize the first letter of each component except the first one
|
# We capitalize the first letter of each component except the first one
|
||||||
# with the 'title' method and join them together.
|
# with the 'title' method and join them together.
|
||||||
return components[0] + "".join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() if x else '_' for x in components[1:])
|
||||||
|
|
||||||
|
|
||||||
# From this response in Stackoverflow
|
# From this response in Stackoverflow
|
||||||
|
|
|
@ -2,7 +2,11 @@ from ..resolve_only_args import resolve_only_args
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_only_args():
|
def test_resolve_only_args():
|
||||||
|
|
||||||
|
def resolver(*args, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
my_data = {'one': 1, 'two': 2}
|
my_data = {'one': 1, 'two': 2}
|
||||||
resolver = lambda *args, **kwargs: kwargs
|
|
||||||
wrapped = resolve_only_args(resolver)
|
wrapped = resolve_only_args(resolver)
|
||||||
assert wrapped(None, my_data, None) == my_data
|
assert wrapped(None, my_data, None) == my_data
|
||||||
|
|
|
@ -4,11 +4,14 @@ from ..str_converters import to_camel_case, to_snake_case
|
||||||
def test_snake_case():
|
def test_snake_case():
|
||||||
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
|
assert to_snake_case('snakesOnAPlane') == 'snakes_on_a_plane'
|
||||||
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
|
assert to_snake_case('SnakesOnAPlane') == 'snakes_on_a_plane'
|
||||||
|
assert to_snake_case('SnakesOnA_Plane') == 'snakes_on_a__plane'
|
||||||
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
|
assert to_snake_case('snakes_on_a_plane') == 'snakes_on_a_plane'
|
||||||
|
assert to_snake_case('snakes_on_a__plane') == 'snakes_on_a__plane'
|
||||||
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
|
assert to_snake_case('IPhoneHysteria') == 'i_phone_hysteria'
|
||||||
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
|
assert to_snake_case('iPhoneHysteria') == 'i_phone_hysteria'
|
||||||
|
|
||||||
|
|
||||||
def test_camel_case():
|
def test_camel_case():
|
||||||
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
|
assert to_camel_case('snakes_on_a_plane') == 'snakesOnAPlane'
|
||||||
|
assert to_camel_case('snakes_on_a__plane') == 'snakesOnA_Plane'
|
||||||
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
|
assert to_camel_case('i_phone_hysteria') == 'iPhoneHysteria'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
SECRET_KEY = 1
|
SECRET_KEY = 1
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'graphene.contrib.django',
|
||||||
'graphene.contrib.django.tests',
|
'graphene.contrib.django.tests',
|
||||||
'examples.starwars_django',
|
'examples.starwars_django',
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user