Merge pull request #1 from nickhudkins/recursive-nodes

Bring up to date with upstream master
This commit is contained in:
Tony Angerilli 2017-01-13 08:39:08 -08:00 committed by GitHub
commit e77962bb0a
19 changed files with 92 additions and 157 deletions

View File

@ -6,19 +6,19 @@ python:
- 3.5
- pypy
before_install:
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
install:
- |
if [ "$TEST_TYPE" = build ]; then
@ -45,7 +45,7 @@ after_success:
fi
env:
matrix:
- TEST_TYPE=build
- TEST_TYPE=build DJANGO_VERSION=1.10
matrix:
fast_finish: true
include:
@ -59,3 +59,10 @@ matrix:
env: TEST_TYPE=build DJANGO_VERSION=1.9
- python: '2.7'
env: TEST_TYPE=lint
deploy:
provider: pypi
user: syrusakbary
on:
tags: true
password:
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=

View File

@ -12,7 +12,7 @@ Let's use a simple example model.
from django.db import models
class Post(models.Model):
name = models.CharField(max_length=100)
title = models.CharField(max_length=100)
content = models.TextField()
published = models.BooleanField(default=False)
owner = models.ForeignKey('auth.User')

View File

@ -91,50 +91,13 @@ Which you could query as follows:
}
}
Orderable fields
----------------
Ordering can also be specified using ``filter_order_by``. Like
``filter_fields``, this value is also passed directly to
``django-filter`` as the ``order_by`` field. For full details see the
`order\_by
documentation <https://django-filter.readthedocs.org/en/latest/usage.html#ordering-using-order-by>`__.
For example:
.. code:: python
class AnimalNode(DjangoObjectType):
class Meta:
model = Animal
filter_fields = ['name', 'genus', 'is_domesticated']
# Either a tuple/list of fields upon which ordering is allowed, or
# True to allow filtering on all fields specified in filter_fields
filter_order_by = True
interfaces = (relay.Node, )
You can then control the ordering via the ``orderBy`` argument:
.. code::
query {
allAnimals(orderBy: "name") {
edges {
node {
id,
name
}
}
}
}
Custom Filtersets
-----------------
By default Graphene provides easy access to the most commonly used
features of ``django-filter``. This is done by transparently creating a
``django_filters.FilterSet`` class for you and passing in the values for
``filter_fields`` and ``filter_order_by``.
``filter_fields``.
However, you may find this to be insufficient. In these cases you can
create your own ``Filterset`` as follows:

View File

@ -31,6 +31,7 @@ We will setup the project, create the following:
# Set up a new project with a single application
django-admin.py startproject cookbook . # Note the trailing '.' character
cd cookbook
django-admin.py startapp ingredients
Now sync your database for the first time:
@ -98,7 +99,6 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
class Meta:
model = Category
filter_fields = ['name', 'ingredients']
filter_order_by = ['name']
interfaces = (relay.Node, )
@ -112,7 +112,6 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
'category': ['exact'],
'category__name': ['exact'],
}
filter_order_by = ['name', 'category__name']
interfaces = (relay.Node, )

View File

@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
and run some queries!
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial.html#testing-our-graphql-schema)
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema)
for some example queries)

View File

@ -12,7 +12,6 @@ class CategoryNode(DjangoObjectType):
model = Category
interfaces = (Node, )
filter_fields = ['name', 'ingredients']
filter_order_by = ['name']
class IngredientNode(DjangoObjectType):
@ -27,7 +26,6 @@ class IngredientNode(DjangoObjectType):
'category': ['exact'],
'category__name': ['exact'],
}
filter_order_by = ['name', 'category__name']
class Query(AbstractType):

View File

@ -9,7 +9,7 @@ class RecipeNode(DjangoObjectType):
model = Recipe
interfaces = (Node, )
filter_fields = ['title','amounts']
filter_order_by = ['title']
class RecipeIngredientNode(DjangoObjectType):
@ -22,7 +22,7 @@ class RecipeIngredientNode(DjangoObjectType):
'recipe': ['exact'],
'recipe__title': ['icontains'],
}
filter_order_by = ['ingredient__name', 'recipe__title',]
class Query(AbstractType):
recipe = Node.Field(RecipeNode)

View File

@ -4,6 +4,7 @@ from django.db import models
class MissingType(object):
pass
try:
DurationField = models.DurationField
UUIDField = models.UUIDField
@ -21,6 +22,13 @@ except:
try:
# Postgres fields are only available in Django 1.8+
from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField, RangeField
from django.contrib.postgres.fields import ArrayField, HStoreField, RangeField
except ImportError:
ArrayField, HStoreField, JSONField, RangeField = (MissingType, ) * 4
try:
# Postgres fields are only available in Django 1.9+
from django.contrib.postgres.fields import JSONField
except ImportError:
JSONField = MissingType

View File

@ -4,9 +4,9 @@ from django.utils.encoding import force_text
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
NonNull, String)
from graphene.relay import is_node
from graphene.types.datetime import DateTime
from graphene.types.datetime import DateTime, Time
from graphene.types.json import JSONString
from graphene.utils.str_converters import to_const
from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name
from .compat import (ArrayField, HStoreField, JSONField, RangeField,
@ -41,7 +41,7 @@ def convert_django_field_with_choices(field, registry=None):
choices = getattr(field, 'choices', None)
if choices:
meta = field.model._meta
name = '{}{}'.format(meta.object_name, field.name.capitalize())
name = to_camel_case('{}_{}'.format(meta.object_name, field.name))
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
@ -112,6 +112,11 @@ def convert_date_to_string(field, registry=None):
return DateTime(description=field.help_text, required=not field.null)
@convert_django_field.register(models.TimeField)
def convert_time_to_string(field, registry=None):
return Time(description=field.help_text, required=not field.null)
@convert_django_field.register(models.OneToOneRel)
def convert_onetoone_field_to_djangomodel(field, registry=None):
model = get_related_model(field)

View File

@ -8,7 +8,7 @@ if not DJANGO_FILTER_INSTALLED:
)
else:
from .fields import DjangoFilterConnectionField
from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter
from .filterset import GlobalIDFilter, GlobalIDMultipleChoiceFilter
__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet',
__all__ = ['DjangoFilterConnectionField',
'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter']

View File

@ -13,7 +13,6 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None,
*args, **kwargs):
self._order_by = order_by
self._fields = fields
self._type = type
self._filterset_class = filterset_class
@ -27,15 +26,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
return self._type()
return self._type
@property
def order_by(self):
return self._order_by or self.node_type._meta.filter_order_by
@property
def meta(self):
meta = dict(model=self.node_type._meta.model,
fields=self.fields,
order_by=self.order_by)
fields=self.fields)
if self._extra_filter_meta:
meta.update(self._extra_filter_meta)
return meta
@ -64,12 +58,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def connection_resolver(resolver, connection, default_manager, filterset_class, filtering_args,
root, args, context, info):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
order = args.get('order_by', None)
qs = default_manager.get_queryset()
if order:
qs = qs.order_by(order)
qs = filterset_class(data=filter_kwargs, queryset=qs)
qs = filterset_class(data=filter_kwargs, queryset=qs).qs
return DjangoConnectionField.connection_resolver(resolver, connection, qs, root, args, context, info)
def get_resolver(self, parent_resolver):

View File

@ -1,11 +1,9 @@
import itertools
import six
from django.conf import settings
from django.db import models
from django.utils.text import capfirst
from django_filters import Filter, MultipleChoiceFilter
from django_filters.filterset import FilterSet, FilterSetMetaclass
from django_filters.filterset import BaseFilterSet, FilterSet
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
from graphql_relay.node.node import from_global_id
@ -29,9 +27,6 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by')
GRAPHENE_FILTER_SET_OVERRIDES = {
models.AutoField: {
'filter_class': GlobalIDFilter,
@ -48,25 +43,7 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
}
# Only useful for Django-filter 0.14-, not necessary in latest version 0.15+
class GrapheneFilterSetMetaclass(FilterSetMetaclass):
def __new__(cls, name, bases, attrs):
new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
# Customise the filter_overrides for Graphene
if hasattr(new_class, '_meta') and hasattr(new_class._meta, 'filter_overrides'):
filter_overrides = new_class._meta.filter_overrides
else:
filter_overrides = new_class.filter_overrides
for k, v in GRAPHENE_FILTER_SET_OVERRIDES.items():
filter_overrides.setdefault(k, v)
return new_class
class GrapheneFilterSetMixin(object):
order_by_field = ORDER_BY_FIELD
class GrapheneFilterSetMixin(BaseFilterSet):
FILTER_DEFAULTS = dict(itertools.chain(
FILTER_FOR_DBFIELD_DEFAULTS.items(),
GRAPHENE_FILTER_SET_OVERRIDES.items()
@ -93,26 +70,17 @@ class GrapheneFilterSetMixin(object):
return GlobalIDFilter(**default)
class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, FilterSet)):
""" Base class for FilterSets used by Graphene
You shouldn't usually need to use this class. The
DjangoFilterConnectionField will wrap FilterSets with this class as
necessary
"""
def setup_filterset(filterset_class):
""" Wrap a provided filterset in Graphene-specific functionality
"""
return type(
'Graphene{}'.format(filterset_class.__name__),
(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, filterset_class),),
(filterset_class, GrapheneFilterSetMixin),
{},
)
def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet,
def custom_filterset_factory(model, filterset_base_class=FilterSet,
**meta):
""" Create a filterset for the given model using the provided meta data
"""
@ -122,7 +90,7 @@ def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet,
meta_class = type(str('Meta'), (object,), meta)
filterset = type(
str('%sFilterSet' % model._meta.object_name),
(filterset_base_class,),
(filterset_base_class, GrapheneFilterSetMixin),
{
'Meta': meta_class
}

View File

@ -1,4 +1,5 @@
import django_filters
from django_filters import OrderingFilter
from graphene_django.tests.models import Article, Pet, Reporter
@ -12,7 +13,8 @@ class ArticleFilter(django_filters.FilterSet):
'pub_date': ['gt', 'lt', 'exact'],
'reporter': ['exact'],
}
order_by = False
order_by = OrderingFilter(fields=('pub_date',))
class ReporterFilter(django_filters.FilterSet):
@ -20,7 +22,8 @@ class ReporterFilter(django_filters.FilterSet):
class Meta:
model = Reporter
fields = ['first_name', 'last_name', 'email', 'pets']
order_by = True
order_by = OrderingFilter(fields=('pub_date',))
class PetFilter(django_filters.FilterSet):
@ -28,4 +31,3 @@ class PetFilter(django_filters.FilterSet):
class Meta:
model = Pet
fields = ['name']
order_by = False

View File

@ -11,6 +11,7 @@ from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED
pytestmark = []
if DJANGO_FILTER_INSTALLED:
import django_filters
from graphene_django.filter import (GlobalIDFilter, DjangoFilterConnectionField,
@ -22,27 +23,29 @@ else:
pytestmark.append(pytest.mark.django_db)
class ArticleNode(DjangoObjectType):
if DJANGO_FILTER_INSTALLED:
class ArticleNode(DjangoObjectType):
class Meta:
model = Article
interfaces = (Node, )
class Meta:
model = Article
interfaces = (Node, )
filter_fields = ('headline', )
class ReporterNode(DjangoObjectType):
class ReporterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Meta:
model = Reporter
interfaces = (Node, )
class PetNode(DjangoObjectType):
class PetNode(DjangoObjectType):
class Meta:
model = Pet
interfaces = (Node, )
class Meta:
model = Pet
interfaces = (Node, )
# schema = Schema()
# schema = Schema()
def get_args(field):
@ -110,8 +113,8 @@ def test_filter_explicit_filterset_orderable():
def test_filter_shortcut_filterset_orderable_true():
field = DjangoFilterConnectionField(ReporterNode, order_by=True)
assert_orderable(field)
field = DjangoFilterConnectionField(ReporterNode)
assert_not_orderable(field)
# def test_filter_shortcut_filterset_orderable_headline():
@ -126,9 +129,9 @@ def test_filter_explicit_filterset_not_orderable():
def test_filter_shortcut_filterset_extra_meta():
field = DjangoFilterConnectionField(ArticleNode, extra_filter_meta={
'order_by': True
'exclude': ('headline', )
})
assert_orderable(field)
assert 'headline' not in field.filterset_class.get_fields()
def test_filter_filterset_information_on_meta():
@ -138,11 +141,10 @@ def test_filter_filterset_information_on_meta():
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
field = DjangoFilterConnectionField(ReporterFilterNode)
assert_arguments(field, 'first_name', 'articles')
assert_orderable(field)
assert_not_orderable(field)
def test_filter_filterset_information_on_meta_related():
@ -152,7 +154,6 @@ def test_filter_filterset_information_on_meta_related():
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoObjectType):
@ -160,7 +161,6 @@ def test_filter_filterset_information_on_meta_related():
model = Article
interfaces = (Node, )
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
@ -171,7 +171,7 @@ def test_filter_filterset_information_on_meta_related():
schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields['articles'].get_type()
assert_arguments(articles_field, 'headline', 'reporter')
assert_orderable(articles_field)
assert_not_orderable(articles_field)
def test_filter_filterset_related_results():
@ -181,7 +181,6 @@ def test_filter_filterset_related_results():
model = Reporter
interfaces = (Node, )
filter_fields = ['first_name', 'articles']
filter_order_by = True
class ArticleFilterNode(DjangoObjectType):
@ -189,7 +188,6 @@ def test_filter_filterset_related_results():
interfaces = (Node, )
model = Article
filter_fields = ['headline', 'reporter']
filter_order_by = True
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)

View File

@ -1,7 +1,5 @@
import six
from graphene import String
from .filterset import custom_filterset_factory, setup_filterset
@ -18,10 +16,6 @@ def get_filtering_args_from_filterset(filterset_class, type):
field_type.description = filter_field.label
args[name] = field_type
# Also add the 'order_by' field
if filterset_class._meta.order_by:
args[filterset_class.order_by_field] = String()
return args

View File

@ -5,7 +5,7 @@ from py.test import raises
import graphene
from graphene.relay import ConnectionField, Node
from graphene.types.datetime import DateTime
from graphene.types.datetime import DateTime, Time
from graphene.types.json import JSONString
from ..compat import (ArrayField, HStoreField, JSONField, MissingType,
@ -16,7 +16,7 @@ from ..types import DjangoObjectType
from .models import Article, Film, FilmDetails, Reporter
# from graphene.core.types.custom_scalars import DateTime, JSONString
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
def assert_conversion(django_field, graphene_field, *args, **kwargs):
@ -44,6 +44,10 @@ def test_should_date_convert_string():
assert_conversion(models.DateField, DateTime)
def test_should_time_convert_string():
assert_conversion(models.TimeField, Time)
def test_should_char_convert_string():
assert_conversion(models.CharField, graphene.String)

View File

@ -73,7 +73,7 @@ type Article implements Node {
type ArticleConnection {
pageInfo: PageInfo!
edges: [ArticleEdge]
edges: [ArticleEdge]!
}
type ArticleEdge {
@ -110,11 +110,11 @@ type Reporter {
lastName: String!
email: String!
pets: [Reporter]
aChoice: ReporterA_choice!
aChoice: ReporterAChoice!
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
}
enum ReporterA_choice {
enum ReporterAChoice {
A_1
A_2
}

View File

@ -65,7 +65,6 @@ class DjangoObjectTypeMeta(ObjectTypeMeta):
# we allow more attributes in Meta
defaults.update(
filter_fields=(),
filter_order_by=(),
)
options = Options(

View File

@ -2,7 +2,7 @@ from setuptools import find_packages, setup
setup(
name='graphene-django',
version='1.0',
version='1.2.1',
description='Graphene Django integration',
long_description=open('README.rst').read(),
@ -33,7 +33,7 @@ setup(
install_requires=[
'six>=1.10.0',
'graphene>=1.0',
'graphene>=1.1.3',
'Django>=1.6.0',
'iso8601',
'singledispatch>=3.4.0.3',
@ -42,7 +42,7 @@ setup(
'pytest-runner',
],
tests_require=[
'django-filter>=0.10.0',
'django-filter>=1.0.0',
'pytest',
'pytest-django==2.9.1',
'mock',