diff --git a/graphene/contrib/__init__.py b/graphene/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py new file mode 100644 index 00000000..fab7622f --- /dev/null +++ b/graphene/contrib/django/__init__.py @@ -0,0 +1,4 @@ +from graphene.contrib.django.types import ( + DjangoObjectType, + DjangoNode +) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py new file mode 100644 index 00000000..590d9064 --- /dev/null +++ b/graphene/contrib/django/converter.py @@ -0,0 +1,40 @@ +from singledispatch import singledispatch +from django.db import models + +from graphene.core.fields import ( + StringField, + IDField, + IntField, + BooleanField, + FloatField, +) + +@singledispatch +def convert_django_field(field): + raise Exception("Don't know how to convert the Django field %s"%field) + + +@convert_django_field.register(models.CharField) +def _(field): + return StringField(description=field.help_text) + + +@convert_django_field.register(models.AutoField) +def _(field): + return IDField(description=field.help_text) + + +@convert_django_field.register(models.BigIntegerField) +@convert_django_field.register(models.IntegerField) +def _(field): + return IntField(description=field.help_text) + + +@convert_django_field.register(models.BooleanField) +def _(field): + return BooleanField(description=field.help_text) + + +@convert_django_field.register(models.FloatField) +def _(field): + return FloatField(description=field.help_text) diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py new file mode 100644 index 00000000..381914f0 --- /dev/null +++ b/graphene/contrib/django/options.py @@ -0,0 +1,22 @@ +import inspect +from django.db import models + +from graphene.core.options import Options + +VALID_ATTRS = ('model', 'only_fields') + +class DjangoOptions(Options): + def __init__(self, *args, **kwargs): + self.model = None + super(DjangoOptions, self).__init__(*args, **kwargs) + self.valid_attrs += VALID_ATTRS + self.only_fields = None + + def contribute_to_class(self, cls, name): + super(DjangoOptions, self).contribute_to_class(cls, name) + if self.proxy: + return + if not self.model: + raise Exception('Django ObjectType %s must have a model in the Meta attr' % cls) + elif not inspect.isclass(self.model) or not issubclass(self.model, models.Model): + raise Exception('Provided model in %s is not a Django model' % cls) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py new file mode 100644 index 00000000..daa7c6b7 --- /dev/null +++ b/graphene/contrib/django/types.py @@ -0,0 +1,31 @@ +import six + +from graphene.core.types import ObjectTypeMeta, ObjectType +from graphene.contrib.django.options import DjangoOptions +from graphene.contrib.django.converter import convert_django_field + +from graphene.relay import Node + +class DjangoObjectTypeMeta(ObjectTypeMeta): + options_cls = DjangoOptions + def add_extra_fields(cls): + if not cls._meta.model: + return + + only_fields = cls._meta.only_fields + # print cls._meta.model._meta._get_fields(forward=False, reverse=True, include_hidden=True) + for field in cls._meta.model._meta.fields: + if only_fields and field.name not in only_fields: + continue + converted_field = convert_django_field(field) + cls.add_to_class(field.name, converted_field) + + +class DjangoObjectType(six.with_metaclass(DjangoObjectTypeMeta, ObjectType)): + class Meta: + proxy = True + + +class DjangoNode(six.with_metaclass(DjangoObjectTypeMeta, Node)): + class Meta: + proxy = True diff --git a/graphene/core/fields.py b/graphene/core/fields.py index 383138d6..12258c6a 100644 --- a/graphene/core/fields.py +++ b/graphene/core/fields.py @@ -8,8 +8,10 @@ from graphql.core.type import ( GraphQLBoolean, GraphQLID, GraphQLArgument, + GraphQLFloat, ) from graphene.utils import cached_property +from graphene.core.types import ObjectType class Field(object): def __init__(self, field_type, resolve=None, null=True, args=None, description='', **extra_args): @@ -44,7 +46,6 @@ class Field(object): return resolve_fn(instance, args, info) def get_object_type(self): - from graphene.core.types import ObjectType field_type = self.field_type _is_class = inspect.isclass(field_type) if _is_class and issubclass(field_type, ObjectType): @@ -143,6 +144,10 @@ class IDField(TypeField): field_type = GraphQLID +class FloatField(TypeField): + field_type = GraphQLFloat + + class ListField(Field): def type_wrapper(self, field_type): return GraphQLList(field_type) diff --git a/graphene/core/options.py b/graphene/core/options.py index 5d0263f0..c0d48fc1 100644 --- a/graphene/core/options.py +++ b/graphene/core/options.py @@ -2,7 +2,7 @@ from graphene.env import get_global_schema from graphene.utils import cached_property DEFAULT_NAMES = ('description', 'name', 'interface', 'schema', - 'type_name', 'interfaces', 'proxy') + 'type_name', 'interfaces', 'proxy') class Options(object): @@ -14,6 +14,7 @@ class Options(object): self.schema = schema self.interfaces = [] self.parents = [] + self.valid_attrs = DEFAULT_NAMES def contribute_to_class(self, cls, name): cls._meta = self @@ -36,7 +37,7 @@ class Options(object): # over it, so we loop over the *original* dictionary instead. if name.startswith('_'): del meta_attrs[name] - for attr_name in DEFAULT_NAMES: + for attr_name in self.valid_attrs: if attr_name in meta_attrs: setattr(self, attr_name, meta_attrs.pop(attr_name)) self.original_attrs[attr_name] = getattr(self, attr_name) @@ -44,9 +45,13 @@ class Options(object): setattr(self, attr_name, getattr(self.meta, attr_name)) self.original_attrs[attr_name] = getattr(self, attr_name) + del self.valid_attrs + # Any leftover attributes must be invalid. if meta_attrs != {}: raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())) + else: + self.proxy = False if self.interfaces != [] and self.interface: raise Exception("A interface cannot inherit from interfaces") diff --git a/graphene/core/types.py b/graphene/core/types.py index 6c098a57..fcafa0d6 100644 --- a/graphene/core/types.py +++ b/graphene/core/types.py @@ -11,9 +11,11 @@ from graphene.core.options import Options class ObjectTypeMeta(type): + options_cls = Options + def __new__(cls, name, bases, attrs): super_new = super(ObjectTypeMeta, cls).__new__ - parents = [b for b in bases if isinstance(b, ObjectTypeMeta)] + parents = [b for b in bases if isinstance(b, cls)] if not parents: # If this isn't a subclass of Model, don't do anything special. return super_new(cls, name, bases, attrs) @@ -25,20 +27,26 @@ class ObjectTypeMeta(type): '__doc__': doc }) attr_meta = attrs.pop('Meta', None) + proxy = None if not attr_meta: - meta = getattr(new_class, 'Meta', None) + meta = None + # meta = getattr(new_class, 'Meta', None) else: meta = attr_meta + base_meta = getattr(new_class, '_meta', None) schema = (base_meta and base_meta.schema) - new_class.add_to_class('_meta', Options(meta, schema)) + new_class.add_to_class('_meta', new_class.options_cls(meta, schema)) + if base_meta and base_meta.proxy: new_class._meta.interface = base_meta.interface + # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) + new_class.add_extra_fields() new_fields = new_class._meta.local_fields field_names = {f.field_name for f in new_fields} @@ -71,6 +79,9 @@ class ObjectTypeMeta(type): new_class._prepare() return new_class + def add_extra_fields(cls): + pass + def _prepare(cls): signals.class_prepared.send(cls) diff --git a/graphene/relay/connections.py b/graphene/relay/connections.py index 24f2a61d..83d73af5 100644 --- a/graphene/relay/connections.py +++ b/graphene/relay/connections.py @@ -1,5 +1,3 @@ -import collections - from graphql_relay.node.node import ( globalIdField ) @@ -8,7 +6,6 @@ from graphql_relay.connection.connection import ( ) from graphene import signals - from graphene.core.fields import NativeField from graphene.relay.utils import get_relay from graphene.relay.relay import Relay @@ -18,10 +15,9 @@ from graphene.relay.relay import Relay def object_type_created(object_type): relay = get_relay(object_type._meta.schema) if relay and issubclass(object_type, relay.Node): + if object_type._meta.proxy: + return type_name = object_type._meta.type_name - # def getId(*args, **kwargs): - # print '**GET ID', args, kwargs - # return 2 field = NativeField(globalIdField(type_name)) object_type.add_to_class('id', field) assert hasattr(object_type, 'get_node'), 'get_node classmethod not found in %s Node' % type_name diff --git a/graphene/relay/nodes.py b/graphene/relay/nodes.py index 643d148f..11e1b6da 100644 --- a/graphene/relay/nodes.py +++ b/graphene/relay/nodes.py @@ -25,7 +25,7 @@ def create_node_definitions(getNode=None, getNodeType=getNodeType, schema=None): getNode = getNode or getSchemaNode(schema) _nodeDefinitions = nodeDefinitions(getNode, getNodeType) - _Interface = getattr(schema,'Interface', Interface) + _Interface = getattr(schema, 'Interface', Interface) class Node(_Interface): @classmethod diff --git a/tests/contrib_django/__init__.py b/tests/contrib_django/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/contrib_django/data.py b/tests/contrib_django/data.py new file mode 100644 index 00000000..107b146c --- /dev/null +++ b/tests/contrib_django/data.py @@ -0,0 +1,17 @@ +from datetime import date + +from .models import Reporter, Article + +r = Reporter(first_name='John', last_name='Smith', email='john@example.com') +r.save() + +r2 = Reporter(first_name='Paul', last_name='Jones', email='paul@example.com') +r2.save() + +a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r) +a.save() + +new_article = r.articles.create(headline="John's second story", pub_date=date(2005, 7, 29)) + +new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17)) +r.articles.add(new_article2) diff --git a/tests/contrib_django/models.py b/tests/contrib_django/models.py new file mode 100644 index 00000000..ab37eaf8 --- /dev/null +++ b/tests/contrib_django/models.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +import django +from django.conf import settings + +settings.configure( + DATABASES={ + 'INSTALLED_APPS': [ + 'graphql.contrib.django', + ], + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'db_test.sqlite', + } + } +) + +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField() + + def __str__(self): # __unicode__ on Python 2 + return "%s %s" % (self.first_name, self.last_name) + + class Meta: + app_label = 'graphql' + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter, related_name='articles') + + def __str__(self): # __unicode__ on Python 2 + return self.headline + + class Meta: + ordering = ('headline',) + app_label = 'graphql' + + +django.setup() diff --git a/tests/contrib_django/test_schema.py b/tests/contrib_django/test_schema.py new file mode 100644 index 00000000..cbfea539 --- /dev/null +++ b/tests/contrib_django/test_schema.py @@ -0,0 +1,115 @@ +from py.test import raises +from collections import namedtuple +from pytest import raises +import graphene +from graphene import relay +from graphene.contrib.django import ( + DjangoObjectType, + DjangoNode +) +from .models import Reporter, Article + + +def test_should_raise_if_no_model(): + with raises(Exception) as excinfo: + class Character1(DjangoObjectType): + pass + assert 'model in the Meta' in str(excinfo.value) + + +def test_should_raise_if_model_is_invalid(): + with raises(Exception) as excinfo: + class Character2(DjangoObjectType): + class Meta: + model = 1 + assert 'not a Django model' in str(excinfo.value) + + +def test_should_map_fields(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, *args, **kwargs): + return ReporterType(Reporter(first_name='ABA', last_name='X')) + + query = ''' + query ReporterQuery { + reporter { + first_name, + last_name, + email + } + } + ''' + expected = { + 'reporter': { + 'first_name': 'ABA', + 'last_name': 'X', + 'email': '' + } + } + Schema = graphene.Schema(query=Query) + result = Schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_map_only_few_fields(): + class Reporter2(DjangoObjectType): + class Meta: + model = Reporter + only_fields = ('id', 'email') + assert Reporter2._meta.fields_map.keys() == ['id', 'email'] + +def test_should_node(): + class ReporterNodeType(DjangoNode): + class Meta: + model = Reporter + + @classmethod + def get_node(cls, id): + return ReporterNodeType(Reporter(id=2, first_name='Cookie Monster')) + + class Query(graphene.ObjectType): + node = relay.NodeField() + reporter = graphene.Field(ReporterNodeType) + + def resolve_reporter(self, *args, **kwargs): + return ReporterNodeType(Reporter(id=1, first_name='ABA', last_name='X')) + + query = ''' + query ReporterQuery { + reporter { + id, + first_name, + last_name, + email + } + aCustomNode: node(id:"UmVwb3J0ZXJOb2RlVHlwZToy") { + id + ... on ReporterNodeType { + first_name + } + } + } + ''' + expected = { + 'reporter': { + 'id': 'UmVwb3J0ZXJOb2RlVHlwZTox', + 'first_name': 'ABA', + 'last_name': 'X', + 'email': '' + }, + 'aCustomNode': { + 'id': 'UmVwb3J0ZXJOb2RlVHlwZToy', + 'first_name': 'Cookie Monster' + } + } + Schema = graphene.Schema(query=Query) + result = Schema.execute(query) + assert not result.errors + assert result.data == expected