Skip to content

Commit 332d7b0

Browse files
committed
Merge pull request #140 from graphql-python/features/django-fields
Improved support for Django fields
2 parents ae23a11 + 91cc0ce commit 332d7b0

File tree

22 files changed

+268
-84
lines changed

22 files changed

+268
-84
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ install:
2424
- |
2525
if [ "$TEST_TYPE" = build ]; then
2626
pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter sqlalchemy_utils
27+
pip install --download-cache $HOME/.cache/pip psycopg2 > /dev/null 2>&1
2728
pip install --download-cache $HOME/.cache/pip/ -e .[django]
2829
pip install --download-cache $HOME/.cache/pip/ -e .[sqlalchemy]
2930
pip install django==$DJANGO_VERSION

docs/pages/docs/basic-types.md

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Also the following Types are available:
1616
- `graphene.List`
1717
- `graphene.NonNull`
1818

19+
Graphene also provides custom scalars for Dates and JSON:
20+
- `graphene.core.types.custom_scalars.DateTime`
21+
- `graphene.core.types.custom_scalars.JSONString`
22+
1923
## Shortcuts
2024

2125
There are some shortcuts for building schemas more easily.

examples/flask_sqlalchemy/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from flask import Flask
2-
from database import db_session, init_db
32

4-
from schema import schema
3+
from database import db_session, init_db
54
from flask_graphql import GraphQL
5+
from schema import schema
66

77
app = Flask(__name__)
88
app.debug = True

examples/flask_sqlalchemy/database.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from sqlalchemy import create_engine
2-
from sqlalchemy.orm import scoped_session, sessionmaker
32
from sqlalchemy.ext.declarative import declarative_base
3+
from sqlalchemy.orm import scoped_session, sessionmaker
44

55
engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True)
66
db_session = scoped_session(sessionmaker(autocommit=False,

examples/flask_sqlalchemy/models.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func
2+
from sqlalchemy.orm import backref, relationship
3+
14
from database import Base
2-
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func
3-
from sqlalchemy.orm import relationship, backref
45

56

67
class Department(Base):

examples/flask_sqlalchemy/schema.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import graphene
22
from graphene import relay
3-
from graphene.contrib.sqlalchemy import SQLAlchemyNode, SQLAlchemyConnectionField
4-
from models import Department as DepartmentModel, Employee as EmployeeModel
3+
from graphene.contrib.sqlalchemy import (SQLAlchemyConnectionField,
4+
SQLAlchemyNode)
5+
from models import Department as DepartmentModel
6+
from models import Employee as EmployeeModel
57

68
schema = graphene.Schema()
79

810

911
@schema.register
1012
class Department(SQLAlchemyNode):
13+
1114
class Meta:
1215
model = DepartmentModel
1316

1417

1518
@schema.register
1619
class Employee(SQLAlchemyNode):
20+
1721
class Meta:
1822
model = EmployeeModel
1923

graphene/contrib/django/compat.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
from django.db import models
22

3+
4+
class MissingType(object):
5+
pass
6+
37
try:
48
UUIDField = models.UUIDField
59
except AttributeError:
610
# Improved compatibility for Django 1.6
7-
class UUIDField(object):
8-
pass
11+
UUIDField = MissingType
912

1013
try:
1114
from django.db.models.related import RelatedObject
1215
except:
1316
# Improved compatibility for Django 1.6
14-
class RelatedObject(object):
15-
pass
17+
RelatedObject = MissingType
18+
19+
20+
try:
21+
# Postgres fields are only available in Django 1.8+
22+
from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField, RangeField
23+
except ImportError:
24+
ArrayField, HStoreField, JSONField, RangeField = (MissingType, ) * 4

graphene/contrib/django/converter.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from django.db import models
22

3-
from ...core.types.scalars import ID, Boolean, Float, Int, String
43
from ...core.classtypes.enum import Enum
4+
from ...core.types.custom_scalars import DateTime, JSONString
5+
from ...core.types.definitions import List
6+
from ...core.types.scalars import ID, Boolean, Float, Int, String
57
from ...utils import to_const
6-
from .compat import RelatedObject, UUIDField
8+
from .compat import (ArrayField, HStoreField, JSONField, RangeField,
9+
RelatedObject, UUIDField)
710
from .utils import get_related_model, import_single_dispatch
811

912
singledispatch = import_single_dispatch()
@@ -30,13 +33,13 @@ def convert_django_field(field):
3033
(field, field.__class__))
3134

3235

33-
@convert_django_field.register(models.DateField)
3436
@convert_django_field.register(models.CharField)
3537
@convert_django_field.register(models.TextField)
3638
@convert_django_field.register(models.EmailField)
3739
@convert_django_field.register(models.SlugField)
3840
@convert_django_field.register(models.URLField)
3941
@convert_django_field.register(models.GenericIPAddressField)
42+
@convert_django_field.register(models.FileField)
4043
@convert_django_field.register(UUIDField)
4144
def convert_field_to_string(field):
4245
return String(description=field.help_text)
@@ -72,6 +75,11 @@ def convert_field_to_float(field):
7275
return Float(description=field.help_text)
7376

7477

78+
@convert_django_field.register(models.DateField)
79+
def convert_date_to_string(field):
80+
return DateTime(description=field.help_text)
81+
82+
7583
@convert_django_field.register(models.ManyToManyField)
7684
@convert_django_field.register(models.ManyToOneRel)
7785
@convert_django_field.register(models.ManyToManyRel)
@@ -94,3 +102,21 @@ def convert_relatedfield_to_djangomodel(field):
94102
def convert_field_to_djangomodel(field):
95103
from .fields import DjangoModelField
96104
return DjangoModelField(get_related_model(field), description=field.help_text)
105+
106+
107+
@convert_django_field.register(ArrayField)
108+
def convert_postgres_array_to_list(field):
109+
base_type = convert_django_field(field.base_field)
110+
return List(base_type, description=field.help_text)
111+
112+
113+
@convert_django_field.register(HStoreField)
114+
@convert_django_field.register(JSONField)
115+
def convert_posgres_field_to_string(field):
116+
return JSONString(description=field.help_text)
117+
118+
119+
@convert_django_field.register(RangeField)
120+
def convert_posgres_range_to_string(field):
121+
inner_type = convert_django_field(field.base_field)
122+
return List(inner_type, description=field.help_text)

graphene/contrib/django/debug/tests/test_query.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
import graphene
4-
from graphene.contrib.django import DjangoNode, DjangoConnectionField
4+
from graphene.contrib.django import DjangoConnectionField, DjangoNode
55
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
66

77
from ...tests.models import Reporter
@@ -19,6 +19,7 @@ def test_should_query_field():
1919
r2.save()
2020

2121
class ReporterType(DjangoNode):
22+
2223
class Meta:
2324
model = Reporter
2425

graphene/contrib/django/filter/filterset.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from django.conf import settings
33
from django.db import models
44
from django.utils.text import capfirst
5-
from graphql_relay.node.node import from_global_id
6-
75
from django_filters import Filter, MultipleChoiceFilter
86
from django_filters.filterset import FilterSet, FilterSetMetaclass
7+
from graphql_relay.node.node import from_global_id
8+
99
from graphene.contrib.django.forms import (GlobalIDFormField,
1010
GlobalIDMultipleChoiceField)
1111

graphene/contrib/django/filter/tests/filters.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import django_filters
2+
23
from graphene.contrib.django.tests.models import Article, Pet, Reporter
34

45

graphene/contrib/django/tests/test_converter.py

+52-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import pytest
12
from django.db import models
23
from py.test import raises
34

45
import graphene
5-
from graphene.contrib.django.converter import (
6-
convert_django_field, convert_django_field_with_choices)
7-
from graphene.contrib.django.fields import (ConnectionOrListField,
8-
DjangoModelField)
6+
from graphene.core.types.custom_scalars import DateTime, JSONString
97

8+
from ..compat import (ArrayField, HStoreField, JSONField, MissingType,
9+
RangeField)
10+
from ..converter import convert_django_field, convert_django_field_with_choices
11+
from ..fields import ConnectionOrListField, DjangoModelField
1012
from .models import Article, Reporter
1113

1214

@@ -26,7 +28,7 @@ def test_should_unknown_django_field_raise_exception():
2628

2729

2830
def test_should_date_convert_string():
29-
assert_conversion(models.DateField, graphene.String)
31+
assert_conversion(models.DateField, DateTime)
3032

3133

3234
def test_should_char_convert_string():
@@ -53,6 +55,14 @@ def test_should_ipaddress_convert_string():
5355
assert_conversion(models.GenericIPAddressField, graphene.String)
5456

5557

58+
def test_should_file_convert_string():
59+
assert_conversion(models.FileField, graphene.String)
60+
61+
62+
def test_should_image_convert_string():
63+
assert_conversion(models.ImageField, graphene.String)
64+
65+
5666
def test_should_auto_convert_id():
5767
assert_conversion(models.AutoField, graphene.ID, primary_key=True)
5868

@@ -136,3 +146,40 @@ def test_should_onetoone_convert_model():
136146
def test_should_foreignkey_convert_model():
137147
field = assert_conversion(models.ForeignKey, DjangoModelField, Article)
138148
assert field.type.model == Article
149+
150+
151+
@pytest.mark.skipif(ArrayField is MissingType,
152+
reason="ArrayField should exist")
153+
def test_should_postgres_array_convert_list():
154+
field = assert_conversion(ArrayField, graphene.List, models.CharField(max_length=100))
155+
assert isinstance(field.type, graphene.List)
156+
assert isinstance(field.type.of_type, graphene.String)
157+
158+
159+
@pytest.mark.skipif(ArrayField is MissingType,
160+
reason="ArrayField should exist")
161+
def test_should_postgres_array_multiple_convert_list():
162+
field = assert_conversion(ArrayField, graphene.List, ArrayField(models.CharField(max_length=100)))
163+
assert isinstance(field.type, graphene.List)
164+
assert isinstance(field.type.of_type, graphene.List)
165+
assert isinstance(field.type.of_type.of_type, graphene.String)
166+
167+
168+
@pytest.mark.skipif(HStoreField is MissingType,
169+
reason="HStoreField should exist")
170+
def test_should_postgres_hstore_convert_string():
171+
assert_conversion(HStoreField, JSONString)
172+
173+
174+
@pytest.mark.skipif(JSONField is MissingType,
175+
reason="JSONField should exist")
176+
def test_should_postgres_json_convert_string():
177+
assert_conversion(JSONField, JSONString)
178+
179+
180+
@pytest.mark.skipif(RangeField is MissingType,
181+
reason="RangeField should exist")
182+
def test_should_postgres_range_convert_list():
183+
from django.contrib.postgres.fields import IntegerRangeField
184+
field = assert_conversion(IntegerRangeField, graphene.List)
185+
assert isinstance(field.type.of_type, graphene.Int)

graphene/contrib/django/tests/test_query.py

+60-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import datetime
2+
13
import pytest
4+
from django.db import models
25
from py.test import raises
36

47
import graphene
58
from graphene import relay
6-
from graphene.contrib.django import DjangoNode, DjangoObjectType
79

10+
from ..compat import MissingType, RangeField
11+
from ..types import DjangoNode, DjangoObjectType
812
from .models import Article, Reporter
913

1014
pytestmark = pytest.mark.django_db
@@ -62,6 +66,57 @@ def resolve_reporter(self, *args, **kwargs):
6266
assert result.data == expected
6367

6468

69+
@pytest.mark.skipif(RangeField is MissingType,
70+
reason="RangeField should exist")
71+
def test_should_query_postgres_fields():
72+
from django.contrib.postgres.fields import IntegerRangeField, ArrayField, JSONField, HStoreField
73+
74+
class Event(models.Model):
75+
ages = IntegerRangeField(help_text='The age ranges')
76+
data = JSONField(help_text='Data')
77+
store = HStoreField()
78+
tags = ArrayField(models.CharField(max_length=50))
79+
80+
class EventType(DjangoObjectType):
81+
82+
class Meta:
83+
model = Event
84+
85+
class Query(graphene.ObjectType):
86+
event = graphene.Field(EventType)
87+
88+
def resolve_event(self, *args, **kwargs):
89+
return Event(
90+
ages=(0, 10),
91+
data={'angry_babies': True},
92+
store={'h': 'store'},
93+
tags=['child', 'angry', 'babies']
94+
)
95+
96+
schema = graphene.Schema(query=Query)
97+
query = '''
98+
query myQuery {
99+
event {
100+
ages
101+
tags
102+
data
103+
store
104+
}
105+
}
106+
'''
107+
expected = {
108+
'event': {
109+
'ages': [0, 10],
110+
'tags': ['child', 'angry', 'babies'],
111+
'data': '{"angry_babies": true}',
112+
'store': '{"h": "store"}',
113+
},
114+
}
115+
result = schema.execute(query)
116+
assert not result.errors
117+
assert result.data == expected
118+
119+
65120
def test_should_node():
66121
class ReporterNode(DjangoNode):
67122

@@ -82,7 +137,7 @@ class Meta:
82137

83138
@classmethod
84139
def get_node(cls, id, info):
85-
return ArticleNode(Article(id=1, headline='Article node'))
140+
return ArticleNode(Article(id=1, headline='Article node', pub_date=datetime.date(2002, 3, 11)))
86141

87142
class Query(graphene.ObjectType):
88143
node = relay.NodeField()
@@ -115,6 +170,7 @@ def resolve_reporter(self, *args, **kwargs):
115170
}
116171
... on ArticleNode {
117172
headline
173+
pubDate
118174
}
119175
}
120176
}
@@ -135,7 +191,8 @@ def resolve_reporter(self, *args, **kwargs):
135191
},
136192
'myArticle': {
137193
'id': 'QXJ0aWNsZU5vZGU6MQ==',
138-
'headline': 'Article node'
194+
'headline': 'Article node',
195+
'pubDate': '2002-03-11',
139196
}
140197
}
141198
schema = graphene.Schema(query=Query)

0 commit comments

Comments
 (0)