Merge pull request #79 from syrusakbary/feature/django

Improved Django integration
This commit is contained in:
Syrus Akbary 2016-01-13 18:51:02 -08:00
commit b18fbb5ace
20 changed files with 123 additions and 54 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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']

View File

@ -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)

View File

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

View File

@ -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',
) )

View File

@ -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))

View File

@ -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)

View 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()

View File

@ -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']
) )

View File

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

View File

@ -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):

View File

@ -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')

View File

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

View File

@ -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)

View File

@ -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)

View File

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

View File

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

View File

@ -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'

View File

@ -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',
] ]