diff --git a/.travis.yml b/.travis.yml
index a8375ee..07ee59f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,21 @@
language: python
-sudo: false
+sudo: required
+dist: xenial
python:
- 2.7
- 3.4
- 3.5
- 3.6
+- 3.7
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install -e .[test]
- pip install psycopg2 # Required for Django postgres fields testing
+ pip install psycopg2==2.8.2 # Required for Django postgres fields testing
pip install django==$DJANGO_VERSION
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
- pip install flake8
+ pip install flake8==3.7.7
fi
script:
- |
@@ -45,10 +47,16 @@ matrix:
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.1
+ - python: '3.6'
+ env: TEST_TYPE=build DJANGO_VERSION=2.2
+ - python: '3.7'
+ env: TEST_TYPE=build DJANGO_VERSION=2.2
- python: '2.7'
env: TEST_TYPE=lint
- python: '3.6'
env: TEST_TYPE=lint
+ - python: '3.7'
+ env: TEST_TYPE=lint
deploy:
provider: pypi
user: syrusakbary
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..5560ba2
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at me@syrusakbary.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..f9428e9
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,62 @@
+# Contributing
+
+Thanks for helping to make graphene-django great!
+
+We welcome all kinds of contributions:
+
+- Bug fixes
+- Documentation improvements
+- New features
+- Refactoring & tidying
+
+
+## Getting started
+
+If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/graphene-django/issues) and [projects](https://github.com/graphql-python/graphene-django/projects) in progress - someone could already be working on something similar and you can help out.
+
+
+## Project setup
+
+After cloning this repo, ensure dependencies are installed by running:
+
+```sh
+make dev-setup
+```
+
+## Running tests
+
+After developing, the full test suite can be evaluated by running:
+
+```sh
+make tests
+```
+
+## Opening Pull Requests
+
+Please fork the project and open a pull request against the master branch.
+
+This will trigger a series of test and lint checks.
+
+We advise that you format and run lint locally before doing this to save time:
+
+```sh
+make format
+make lint
+```
+
+## Documentation
+
+The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
+
+The documentation dependencies are installed by running:
+
+```sh
+cd docs
+pip install -r requirements.txt
+```
+
+Then to produce a HTML version of the documentation:
+
+```sh
+make html
+```
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index 3c3d4f9..4677330 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,3 @@
include README.md LICENSE
recursive-include graphene_django/templates *
+recursive-include graphene_django/static *
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..061ad4e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+dev-setup:
+ pip install -e ".[dev]"
+
+tests:
+ py.test graphene_django --cov=graphene_django -vv
+
+format:
+ black graphene_django
+
+lint:
+ flake8 graphene_django
diff --git a/README.md b/README.md
index ef3f40c..159a592 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,11 @@ Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/ma
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
-## Installation
+## Documentation
+
+[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/)
+
+## Quickstart
For installing graphene, just run this command in your shell
@@ -39,7 +43,7 @@ from graphene_django.views import GraphQLView
urlpatterns = [
# ...
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
```
@@ -96,32 +100,4 @@ To learn more check out the following [examples](examples/):
## Contributing
-After cloning this repo, ensure dependencies are installed by running:
-
-```sh
-pip install -e ".[test]"
-```
-
-After developing, the full test suite can be evaluated by running:
-
-```sh
-py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode
-```
-
-
-### Documentation
-
-The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
-
-The documentation dependencies are installed by running:
-
-```sh
-cd docs
-pip install -r requirements.txt
-```
-
-Then to produce a HTML version of the documentation:
-
-```sh
-make html
-```
+See [CONTRIBUTING.md](CONTRIBUTING.md)
\ No newline at end of file
diff --git a/README.rst b/README.rst
index a96e60f..44feaee 100644
--- a/README.rst
+++ b/README.rst
@@ -10,8 +10,14 @@ to learn how to upgrade to Graphene ``2.0``.
A `Django `__ integration for
`Graphene `__.
-Installation
-------------
+
+Documentation
+-------------
+
+`Visit the documentation to get started! `__
+
+Quickstart
+----------
For installing graphene, just run this command in your shell
@@ -46,7 +52,7 @@ serve the queries.
urlpatterns = [
# ...
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
Examples
@@ -105,36 +111,7 @@ To learn more check out the following `examples `__:
Contributing
------------
-After cloning this repo, ensure dependencies are installed by running:
-
-.. code:: sh
-
- pip install -e ".[test]"
-
-After developing, the full test suite can be evaluated by running:
-
-.. code:: sh
-
- py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode
-
-Documentation
-~~~~~~~~~~~~~
-
-The `documentation `__ is generated using the excellent
-`Sphinx `__ and a custom theme.
-
-The documentation dependencies are installed by running:
-
-.. code:: sh
-
- cd docs
- pip install -r requirements.txt
-
-Then to produce a HTML version of the documentation:
-
-.. code:: sh
-
- make html
+See `CONTRIBUTING.md `__.
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
diff --git a/docs/authorization.rst b/docs/authorization.rst
index 3b34326..3d0bb8a 100644
--- a/docs/authorization.rst
+++ b/docs/authorization.rst
@@ -155,7 +155,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
.. code:: python
#views.py
-
+
from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView
@@ -171,9 +171,9 @@ For Django 1.9 and below:
urlpatterns = [
# some other urls
- url(r'^graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
+ url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
-
+
For Django 2.0 and above:
.. code:: python
diff --git a/docs/conf.py b/docs/conf.py
index 2ea2d55..a485d5b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,6 +1,6 @@
import os
-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
# -*- coding: utf-8 -*-
#
@@ -34,46 +34,44 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.intersphinx',
- 'sphinx.ext.todo',
- 'sphinx.ext.coverage',
- 'sphinx.ext.viewcode',
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.todo",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
]
if not on_rtd:
- extensions += [
- 'sphinx.ext.githubpages',
- ]
+ extensions += ["sphinx.ext.githubpages"]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = u'Graphene Django'
-copyright = u'Graphene 2017'
-author = u'Syrus Akbary'
+project = u"Graphene Django"
+copyright = u"Graphene 2017"
+author = u"Syrus Akbary"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = u'1.0'
+version = u"1.0"
# The full version, including alpha/beta/rc tags.
-release = u'1.0.dev'
+release = u"1.0.dev"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -94,7 +92,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -116,7 +114,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -175,7 +173,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@@ -255,34 +253,30 @@ html_static_path = ['_static']
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
-htmlhelp_basename = 'Graphenedoc'
+htmlhelp_basename = "Graphenedoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
- # The paper size ('letterpaper' or 'a4paper').
- #
- # 'papersize': 'letterpaper',
-
- # The font size ('10pt', '11pt' or '12pt').
- #
- # 'pointsize': '10pt',
-
- # Additional stuff for the LaTeX preamble.
- #
- # 'preamble': '',
-
- # Latex figure (float) alignment
- #
- # 'figure_align': 'htbp',
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'Graphene.tex', u'Graphene Documentation',
- u'Syrus Akbary', 'manual'),
+ (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
]
# The name of an image file (relative to this directory) to place at the top of
@@ -323,8 +317,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- (master_doc, 'graphene_django', u'Graphene Django Documentation',
- [author], 1)
+ (master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
]
# If true, show URL addresses after external links.
@@ -338,9 +331,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'Graphene-Django', u'Graphene Django Documentation',
- author, 'Graphene Django', 'One line description of project.',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "Graphene-Django",
+ u"Graphene Django Documentation",
+ author,
+ "Graphene Django",
+ "One line description of project.",
+ "Miscellaneous",
+ )
]
# Documents to append as an appendix to all manuals.
@@ -414,7 +413,7 @@ epub_copyright = copyright
# epub_post_files = []
# A list of files that should not be packed into the epub file.
-epub_exclude_files = ['search.html']
+epub_exclude_files = ["search.html"]
# The depth of the table of contents in toc.ncx.
#
@@ -446,4 +445,4 @@ epub_exclude_files = ['search.html']
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None}
+intersphinx_mapping = {"https://docs.python.org/": None}
diff --git a/docs/debug.rst b/docs/debug.rst
index 8ef2e86..d1cbb21 100644
--- a/docs/debug.rst
+++ b/docs/debug.rst
@@ -15,7 +15,7 @@ For use the Django Debug plugin in Graphene:
* Add ``graphene_django.debug.DjangoDebugMiddleware`` into ``MIDDLEWARE`` in the ``GRAPHENE`` settings.
-* Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='__debug')``.
+* Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='_debug')``.
.. code:: python
@@ -24,7 +24,7 @@ For use the Django Debug plugin in Graphene:
class Query(graphene.ObjectType):
# ...
- debug = graphene.Field(DjangoDebug, name='__debug')
+ debug = graphene.Field(DjangoDebug, name='_debug')
schema = graphene.Schema(query=Query)
@@ -34,6 +34,7 @@ And in your ``settings.py``:
.. code:: python
GRAPHENE = {
+ ...
'MIDDLEWARE': [
'graphene_django.debug.DjangoDebugMiddleware',
]
@@ -58,11 +59,11 @@ the GraphQL request, like:
}
}
# Here is the debug field that will output the SQL queries
- __debug {
+ _debug {
sql {
rawSql
}
}
}
-Note that the ``__debug`` field must be the last field in your query.
+Note that the ``_debug`` field must be the last field in your query.
diff --git a/docs/filtering.rst b/docs/filtering.rst
index feafd40..d02366f 100644
--- a/docs/filtering.rst
+++ b/docs/filtering.rst
@@ -136,7 +136,7 @@ pre-filter animals owned by the authenticated user (set in ``context.user``).
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
- name = django_filters.CharFilter(lookup_type='iexact')
+ name = django_filters.CharFilter(lookup_type=['iexact'])
class Meta:
model = Animal
@@ -146,3 +146,49 @@ pre-filter animals owned by the authenticated user (set in ``context.user``).
def qs(self):
# The query context can be found in self.request.
return super(AnimalFilter, self).qs.filter(owner=self.request.user)
+
+
+Ordering
+--------
+
+You can use ``OrderFilter`` to define how you want your returned results to be ordered.
+
+Extend the tuple of fields if you want to order by more than one field.
+
+.. code:: python
+
+ from django_filters import FilterSet, OrderingFilter
+
+ class UserFilter(FilterSet):
+ class Meta:
+ model = UserModel
+
+ order_by = OrderingFilter(
+ fields=(
+ ('created_at', 'created_at'),
+ )
+ )
+
+ class Group(DjangoObjectType):
+ users = DjangoFilterConnectionField(Ticket, filterset_class=UserFilter)
+
+ class Meta:
+ name = 'Group'
+ model = GroupModel
+ interfaces = (relay.Node,)
+
+ def resolve_users(self, info, **kwargs):
+ return UserFilter(kwargs).qs
+
+
+with this set up, you can now order the users under group:
+
+.. code::
+
+ query {
+ group(id: "xxx") {
+ users(orderBy: "-created_at") {
+ xxx
+ }
+ }
+ }
\ No newline at end of file
diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst
deleted file mode 100644
index bbaadb1..0000000
--- a/docs/form-mutations.rst
+++ /dev/null
@@ -1,72 +0,0 @@
-Integration with Django forms
-=============================
-
-Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
-*Note: the API is experimental and will likely change in the future.*
-
-DjangoFormMutation
-------------------
-
-.. code:: python
-
- from graphene_django.forms.mutation import DjangoFormMutation
-
- class MyForm(forms.Form):
- name = forms.CharField()
-
- class MyMutation(DjangoFormMutation):
- class Meta:
- form_class = MyForm
-
-``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
-
-DjangoModelFormMutation
------------------------
-
-``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``.
-
-.. code:: python
-
- from graphene_django.forms.mutation import DjangoModelFormMutation
-
- class Pet(models.Model):
- name = models.CharField()
-
- class PetForm(forms.ModelForm):
- class Meta:
- model = Pet
- fields = ('name',)
-
- # This will get returned when the mutation completes successfully
- class PetType(DjangoObjectType):
- class Meta:
- model = Pet
-
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
-
-``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation
-will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will
-return a list of errors.
-
-You can change the input name (default is ``input``) and the return field name (default is the model name lowercase).
-
-.. code:: python
-
- class PetMutation(DjangoModelFormMutation):
- class Meta:
- form_class = PetForm
- input_field_name = 'data'
- return_field_name = 'my_pet'
-
-Form validation
----------------
-
-Form mutations will call ``is_valid()`` on your forms.
-
-If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
-to change how the form is saved or to return a different Graphene object type.
-
-If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
-containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
diff --git a/docs/index.rst b/docs/index.rst
index 7c64ae7..602f8dd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,16 +1,34 @@
Graphene-Django
===============
-Contents:
+Welcome to the Graphene-Django docs.
+
+Graphene-Django is built on top of `Graphene `__.
+Graphene-Django provides some additional abstractions that make it easy to add GraphQL functionality to your Django project.
+
+First time? We recommend you start with the installation guide to get set up and the basic tutorial.
+It is worth reading the `core graphene docs `__ to familiarize yourself with the basic utilities.
+
+Core tenants
+------------
+
+If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section.
+
+
+For more advanced use, check out the Relay tutorial.
.. toctree::
- :maxdepth: 0
+ :maxdepth: 1
+ installation
tutorial-plain
tutorial-relay
+ schema
+ queries
+ mutations
filtering
authorization
debug
- rest-framework
- form-mutations
introspection
+ testing
+ settings
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..a2dc665
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,69 @@
+Installation
+============
+
+Graphene-Django takes a few seconds to install and set up.
+
+Requirements
+------------
+
+Graphene-Django currently supports the following versions of Django:
+
+* Django 2.X
+
+Installation
+------------
+
+.. code:: bash
+
+ pip install graphene-django
+
+**We strongly recommend pinning against a specific version of Graphene-Django because new versions could introduce breaking changes to your project.**
+
+Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project:
+
+.. code:: python
+
+ INSTALLED_APPS = [
+ ...
+ 'django.contrib.staticfiles', # Required for GraphiQL
+ 'graphene_django'
+ ]
+
+
+We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
+
+.. code:: python
+
+ from django.urls import path
+ from graphene_django.views import GraphQLView
+
+ urlpatterns = [
+ # ...
+ path("graphql", GraphQLView.as_view(graphiql=True)),
+ ]
+
+(Change ``graphiql=True`` to ``graphiql=False`` if you do not want to use the GraphiQL API browser.)
+
+Finally, define the schema location for Graphene in the ``settings.py`` file of your Django project:
+
+.. code:: python
+
+ GRAPHENE = {
+ 'SCHEMA': 'django_root.schema.schema'
+ }
+
+Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
+
+The most basic ``schema.py`` looks like this:
+
+.. code:: python
+
+ import graphene
+
+ class Query(graphene.ObjectType):
+ pass
+
+ schema = graphene.Schema(query=Query)
+
+
+To learn how to extend the schema object for your project, read the basic tutorial.
\ No newline at end of file
diff --git a/docs/introspection.rst b/docs/introspection.rst
index bd80f26..0fc6776 100644
--- a/docs/introspection.rst
+++ b/docs/introspection.rst
@@ -1,12 +1,10 @@
Introspection Schema
====================
-Relay uses `Babel Relay
-Plugin `__
-that requires you to provide your GraphQL schema data.
+Relay Modern uses `Babel Relay Plugin `__ which requires you to provide your GraphQL schema data.
-Graphene comes with a management command for Django to dump your schema
-data to ``schema.json`` that is compatible with babel-relay-plugin.
+Graphene comes with a Django management command to dump your schema
+data to ``schema.json`` which is compatible with babel-relay-plugin.
Usage
-----
diff --git a/docs/mutations.rst b/docs/mutations.rst
new file mode 100644
index 0000000..f6c6f14
--- /dev/null
+++ b/docs/mutations.rst
@@ -0,0 +1,229 @@
+Mutations
+=========
+
+Introduction
+------------
+
+Graphene-Django makes it easy to perform mutations.
+
+With Graphene-Django we can take advantage of pre-existing Django features to
+quickly build CRUD functionality, while still using the core `graphene mutation `__
+features to add custom mutations to a Django project.
+
+Simple example
+--------------
+
+.. code:: python
+
+ import graphene
+
+ from graphene_django import DjangoObjectType
+
+ from .models import Question
+
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+
+
+ class QuestionMutation(graphene.Mutation):
+ class Arguments:
+ # The input arguments for this mutation
+ text = graphene.String(required=True)
+ id = graphene.ID()
+
+ # The class attributes define the response of the mutation
+ question = graphene.Field(QuestionType)
+
+ def mutate(self, info, text, id):
+ question = Question.objects.get(pk=id)
+ question.text = text
+ question.save()
+ # Notice we return an instance of this mutation
+ return QuestionMutation(question=question)
+
+
+ class Mutation:
+ update_question = QuestionMutation.Field()
+
+
+Django Forms
+------------
+
+Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
+
+DjangoFormMutation
+~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+ from graphene_django.forms.mutation import DjangoFormMutation
+
+ class MyForm(forms.Form):
+ name = forms.CharField()
+
+ class MyMutation(DjangoFormMutation):
+ class Meta:
+ form_class = MyForm
+
+``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
+
+DjangoModelFormMutation
+~~~~~~~~~~~~~~~~~~~~~~~
+
+``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``.
+
+.. code:: python
+
+ from graphene_django.forms.mutation import DjangoModelFormMutation
+
+ class Pet(models.Model):
+ name = models.CharField()
+
+ class PetForm(forms.ModelForm):
+ class Meta:
+ model = Pet
+ fields = ('name',)
+
+ # This will get returned when the mutation completes successfully
+ class PetType(DjangoObjectType):
+ class Meta:
+ model = Pet
+
+ class PetMutation(DjangoModelFormMutation):
+ pet = Field(PetType)
+
+ class Meta:
+ form_class = PetForm
+
+``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation
+will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will
+return a list of errors.
+
+You can change the input name (default is ``input``) and the return field name (default is the model name lowercase).
+
+.. code:: python
+
+ class PetMutation(DjangoModelFormMutation):
+ class Meta:
+ form_class = PetForm
+ input_field_name = 'data'
+ return_field_name = 'my_pet'
+
+Form validation
+~~~~~~~~~~~~~~~
+
+Form mutations will call ``is_valid()`` on your forms.
+
+If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
+to change how the form is saved or to return a different Graphene object type.
+
+If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
+containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
+
+
+Django REST Framework
+---------------------
+
+You can re-use your Django Rest Framework serializer with Graphene Django mutations.
+
+You can create a Mutation based on a serializer by using the `SerializerMutation` base class:
+
+.. code:: python
+
+ from graphene_django.rest_framework.mutation import SerializerMutation
+
+ class MyAwesomeMutation(SerializerMutation):
+ class Meta:
+ serializer_class = MySerializer
+
+
+Create/Update Operations
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default ModelSerializers accept create and update operations. To
+customize this use the `model_operations` attribute on the ``SerializerMutation`` class.
+
+The update operation looks up models by the primary key by default. You can
+customize the look up with the ``lookup_field`` attribute on the ``SerializerMutation`` class.
+
+.. code:: python
+
+ from graphene_django.rest_framework.mutation import SerializerMutation
+ from .serializers imoprt MyModelSerializer
+
+
+ class AwesomeModelMutation(SerializerMutation):
+ class Meta:
+ serializer_class = MyModelSerializer
+ model_operations = ['create', 'update']
+ lookup_field = 'id'
+
+Overriding Update Queries
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Use the method ``get_serializer_kwargs`` to override how updates are applied.
+
+.. code:: python
+
+ from graphene_django.rest_framework.mutation import SerializerMutation
+ from .serializers imoprt MyModelSerializer
+
+
+ class AwesomeModelMutation(SerializerMutation):
+ class Meta:
+ serializer_class = MyModelSerializer
+
+ @classmethod
+ def get_serializer_kwargs(cls, root, info, **input):
+ if 'id' in input:
+ instance = Post.objects.filter(
+ id=input['id'], owner=info.context.user
+ ).first()
+ if instance:
+ return {'instance': instance, 'data': input, 'partial': True}
+
+ else:
+ raise http.Http404
+
+ return {'data': input, 'partial': True}
+
+
+
+Relay
+-----
+
+You can use relay with mutations. A Relay mutation must inherit from
+``ClientIDMutation`` and implement the ``mutate_and_get_payload`` method:
+
+.. code:: python
+
+ import graphene import relay, DjangoObjectType
+ from graphql_relay import from_global_id
+
+ from .queries import QuestionType
+
+
+ class QuestionMutation(relay.ClientIDMutation):
+ class Input:
+ text = graphene.String(required=True)
+ id = graphene.ID()
+
+ question = graphene.Field(QuestionType)
+
+ @classmethod
+ def mutate_and_get_payload(cls, root, info, text, id):
+ question = Question.objects.get(pk=from_global_id(id))
+ question.text = text
+ question.save()
+ return QuestionMutation(question=question)
+
+Notice that the ``class Arguments`` is renamed to ``class Input`` with relay.
+This is due to a deprecation of ``class Arguments`` in graphene 2.0.
+
+Relay ClientIDMutation accept a ``clientIDMutation`` argument.
+This argument is also sent back to the client with the mutation result
+(you do not have to do anything). For services that manage
+a pool of many GraphQL requests in bulk, the ``clientIDMutation``
+allows you to match up a specific mutation with the response.
\ No newline at end of file
diff --git a/docs/queries.rst b/docs/queries.rst
new file mode 100644
index 0000000..0edd1dd
--- /dev/null
+++ b/docs/queries.rst
@@ -0,0 +1,332 @@
+Queries & ObjectTypes
+=====================
+
+Introduction
+------------
+
+Graphene-Django offers a host of features for performing GraphQL queries.
+
+Graphene-Django ships with a special ``DjangoObjectType`` that automatically transforms a Django Model
+into a ``ObjectType`` for you.
+
+
+Full example
+~~~~~~~~~~~~
+
+.. code:: python
+
+ # my_app/schema.py
+
+ import graphene
+
+ from graphene_django.types import DjangoObjectType
+ from .models import Question
+
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+
+
+ class Query:
+ questions = graphene.List(QuestionType)
+ question = graphene.Field(QuestionType, question_id=graphene.String())
+
+ def resolve_questions(self, info, **kwargs):
+ # Querying a list
+ return Question.objects.all()
+
+ def resolve_question(self, info, question_id):
+ # Querying a single question
+ return Question.objects.get(pk=question_id)
+
+
+Fields
+------
+
+By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
+If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``.
+
+only_fields
+~~~~~~~~~~~
+
+Show **only** these fields on the model:
+
+.. code:: python
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+ only_fields = ('question_text')
+
+
+exclude_fields
+~~~~~~~~~~~~~~
+
+Show all fields **except** those in ``exclude_fields``:
+
+.. code:: python
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+ exclude_fields = ('question_text')
+
+
+Customised fields
+~~~~~~~~~~~~~~~~~
+
+You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
+
+.. code:: python
+
+ class QuestionType(DjangoObjectType):
+
+ class Meta:
+ model = Question
+ exclude_fields = ('question_text')
+
+ extra_field = graphene.String()
+
+ def resolve_extra_field(self, info):
+ return 'hello!'
+
+
+Related models
+--------------
+
+Say you have the following models:
+
+.. code:: python
+
+ class Category(models.Model):
+ foo = models.CharField(max_length=256)
+
+ class Question(models.Model):
+ category = models.ForeignKey(Category, on_delete=models.CASCADE)
+
+
+When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``Category`` as a query-able field like so:
+
+.. code:: python
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+ only_fields = ('category',)
+
+Then all query-able related models must be defined as DjangoObjectType subclass,
+or they will fail to show if you are trying to query those relation fields. You only
+need to create the most basic class for this to work:
+
+.. code:: python
+
+ class CategoryType(DjangoObjectType):
+ class Meta:
+ model = Category
+
+Default QuerySet
+-----------------
+
+If you are using ``DjangoObjectType`` you can define a custom `get_queryset` method.
+Use this to control filtering on the ObjectType level instead of the Query object level.
+
+.. code:: python
+
+ from graphene_django.types import DjangoObjectType
+ from .models import Question
+
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+
+ @classmethod
+ def get_queryset(cls, queryset, info):
+ if info.context.user.is_anonymous:
+ return queryset.filter(published=True)
+ return queryset
+
+Resolvers
+---------
+
+When a GraphQL query is received by the ``Schema`` object, it will map it to a "Resolver" related to it.
+
+This resolve method should follow this format:
+
+.. code:: python
+
+ def resolve_foo(self, info, **kwargs):
+
+Where "foo" is the name of the field declared in the ``Query`` object.
+
+.. code:: python
+
+ class Query:
+ foo = graphene.List(QuestionType)
+
+ def resolve_foo(self, info, **kwargs):
+ id = kwargs.get('id')
+ return QuestionModel.objects.get(id)
+
+Arguments
+~~~~~~~~~
+
+Additionally, Resolvers will receive **any arguments declared in the field definition**. This allows you to provide input arguments in your GraphQL server and can be useful for custom queries.
+
+.. code:: python
+
+ class Query:
+ question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int())
+
+ def resolve_question(self, info, foo, bar):
+ # If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
+ return Question.objects.filter(foo=foo, bar=bar).first()
+
+
+Info
+~~~~
+
+The ``info`` argument passed to all resolve methods holds some useful information.
+For Graphene-Django, the ``info.context`` attribute is the ``HTTPRequest`` object
+that would be familiar to any Django developer. This gives you the full functionality
+of Django's ``HTTPRequest`` in your resolve methods, such as checking for authenticated users:
+
+.. code:: python
+
+ def resolve_questions(self, info, **kwargs):
+ # See if a user is authenticated
+ if info.context.user.is_authenticated():
+ return Question.objects.all()
+ else:
+ return Question.objects.none()
+
+
+Plain ObjectTypes
+-----------------
+
+With Graphene-Django you are not limited to just Django Models - you can use the standard
+``ObjectType`` to create custom fields or to provide an abstraction between your internal
+Django models and your external API.
+
+.. code:: python
+
+ import graphene
+ from .models import Question
+
+
+ class MyQuestion(graphene.ObjectType):
+ text = graphene.String()
+
+
+ class Query:
+ question = graphene.Field(MyQuestion, question_id=graphene.String())
+
+ def resolve_question(self, info, question_id):
+ question = Question.objects.get(pk=question_id)
+ return MyQuestion(
+ text=question.question_text
+ )
+
+For more information and more examples, please see the `core object type documentation `__.
+
+
+Relay
+-----
+
+`Relay `__ with Graphene-Django gives us some additional features:
+
+- Pagination and slicing.
+- An abstract ``id`` value which contains enough info for the server to know its type and its id.
+
+There is one additional import and a single line of code needed to adopt this:
+
+Full example
+~~~~~~~~~~~~
+See the `Relay documentation `__ on
+the core graphene pages for more information on customizing the Relay experience.
+
+.. code:: python
+
+ from graphene import relay
+ from graphene_django import DjangoObjectType
+ from .models import Question
+
+
+ class QuestionType(DjangoObjectType):
+ class Meta:
+ model = Question
+ interfaces = (relay.Node,)
+
+
+ class QuestionConnection(relay.Connection):
+ class Meta:
+ node = QuestionType
+
+
+ class Query:
+ questions = relay.ConnectionField(QuestionConnection)
+
+ def resolve_questions(root, info, **kwargs):
+ return Question.objects.all()
+
+
+You can now execute queries like:
+
+
+.. code:: python
+
+ {
+ questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ cursor
+ node {
+ id
+ question_text
+ }
+ }
+ }
+ }
+
+Which returns:
+
+.. code:: python
+
+ {
+ "data": {
+ "questions": {
+ "pageInfo": {
+ "startCursor": "YXJyYXljb25uZWN0aW9uOjEwNg==",
+ "endCursor": "YXJyYXljb25uZWN0aW9uOjEwNw==",
+ "hasNextPage": true,
+ "hasPreviousPage": false
+ },
+ "edges": [
+ {
+ "cursor": "YXJyYXljb25uZWN0aW9uOjEwNg==",
+ "node": {
+ "id": "UGxhY2VUeXBlOjEwNw==",
+ "question_text": "How did we get here?"
+ }
+ },
+ {
+ "cursor": "YXJyYXljb25uZWN0aW9uOjEwNw==",
+ "node": {
+ "id": "UGxhY2VUeXBlOjEwOA==",
+ "name": "Where are we?"
+ }
+ }
+ ]
+ }
+ }
+ }
+
+Note that relay implements :code:`pagination` capabilities automatically, adding a :code:`pageInfo` element, and including :code:`cursor` on nodes. These elements are included in the above example for illustration.
+
+To learn more about Pagination in general, take a look at `Pagination `__ on the GraphQL community site.
diff --git a/docs/rest-framework.rst b/docs/rest-framework.rst
deleted file mode 100644
index ce666de..0000000
--- a/docs/rest-framework.rst
+++ /dev/null
@@ -1,64 +0,0 @@
-Integration with Django Rest Framework
-======================================
-
-You can re-use your Django Rest Framework serializer with
-graphene django.
-
-
-Mutation
---------
-
-You can create a Mutation based on a serializer by using the
-`SerializerMutation` base class:
-
-.. code:: python
-
- from graphene_django.rest_framework.mutation import SerializerMutation
-
- class MyAwesomeMutation(SerializerMutation):
- class Meta:
- serializer_class = MySerializer
-
-Create/Update Operations
----------------------
-
-By default ModelSerializers accept create and update operations. To
-customize this use the `model_operations` attribute. The update
-operation looks up models by the primary key by default. You can
-customize the look up with the lookup attribute.
-
-.. code:: python
-
- from graphene_django.rest_framework.mutation import SerializerMutation
-
- class AwesomeModelMutation(SerializerMutation):
- class Meta:
- serializer_class = MyModelSerializer
- model_operations = ['create', 'update']
- lookup_field = 'id'
-
-Overriding Update Queries
--------------------------
-
-Use the method `get_serializer_kwargs` to override how
-updates are applied.
-
-.. code:: python
-
- from graphene_django.rest_framework.mutation import SerializerMutation
-
- class AwesomeModelMutation(SerializerMutation):
- class Meta:
- serializer_class = MyModelSerializer
-
- @classmethod
- def get_serializer_kwargs(cls, root, info, **input):
- if 'id' in input:
- instance = Post.objects.filter(id=input['id'], owner=info.context.user).first()
- if instance:
- return {'instance': instance, 'data': input, 'partial': True}
-
- else:
- raise http.Http404
-
- return {'data': input, 'partial': True}
diff --git a/docs/schema.rst b/docs/schema.rst
new file mode 100644
index 0000000..9f0c283
--- /dev/null
+++ b/docs/schema.rst
@@ -0,0 +1,50 @@
+Schema
+======
+
+The ``graphene.Schema`` object describes your data model and provides a GraphQL server with an associated set of resolve methods that know how to fetch data. The most basic schema you can create looks like this:
+
+.. code:: python
+
+ import graphene
+
+ class Query(graphene.ObjectType):
+ pass
+
+ class Mutation(graphene.ObjectType):
+ pass
+
+ schema = graphene.Schema(query=Query, mutation=Mutation)
+
+
+This schema doesn't do anything yet, but it is ready to accept new Query or Mutation fields.
+
+
+Adding to the schema
+--------------------
+
+If you have defined a ``Query`` or ``Mutation``, you can register them with the schema:
+
+.. code:: python
+
+ import graphene
+
+ import my_app.schema.Query
+ import my_app.schema.Mutation
+
+ class Query(
+ my_app.schema.Query, # Add your Query objects here
+ graphene.ObjectType
+ ):
+ pass
+
+ class Mutation(
+ my_app.schema.Mutation, # Add your Mutation objects here
+ graphene.ObjectType
+ ):
+ pass
+
+ schema = graphene.Schema(query=Query, mutation=Mutation)
+
+You can add as many mixins to the base ``Query`` and ``Mutation`` objects as you like.
+
+Read more about Schema on the `core graphene docs `__
\ No newline at end of file
diff --git a/docs/settings.rst b/docs/settings.rst
new file mode 100644
index 0000000..547e77f
--- /dev/null
+++ b/docs/settings.rst
@@ -0,0 +1,103 @@
+Settings
+========
+
+Graphene-Django can be customised using settings. This page explains each setting and their defaults.
+
+Usage
+-----
+
+Add settings to your Django project by creating a Dictonary with name ``GRAPHENE`` in the project's ``settings.py``:
+
+.. code:: python
+
+ GRAPHENE = {
+ ...
+ }
+
+
+``SCHEMA``
+----------
+
+The location of the top-level ``Schema`` class.
+
+Default: ``None``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'SCHEMA': 'path.to.schema.schema',
+ }
+
+
+``SCHEMA_OUTPUT``
+----------
+
+The name of the file where the GraphQL schema output will go.
+
+Default: ``schema.json``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'SCHEMA_OUTPUT': 'schema.json',
+ }
+
+
+``SCHEMA_INDENT``
+----------
+
+The indentation level of the schema output.
+
+Default: ``2``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'SCHEMA_INDENT': 2,
+ }
+
+
+``MIDDLEWARE``
+----------
+
+A tuple of middleware that will be executed for each GraphQL query.
+
+See the `middleware documentation `__ for more information.
+
+Default: ``()``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'MIDDLEWARE': (
+ 'path.to.my.middleware.class',
+ ),
+ }
+
+
+``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
+----------
+
+Enforces relay queries to have the ``first`` or ``last`` argument.
+
+Default: ``False``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False,
+ }
+
+
+``RELAY_CONNECTION_MAX_LIMIT``
+----------
+
+The maximum size of objects that can be requested through a relay connection.
+
+Default: ``100``
+
+.. code:: python
+
+ GRAPHENE = {
+ 'RELAY_CONNECTION_MAX_LIMIT': 100,
+ }
diff --git a/docs/testing.rst b/docs/testing.rst
new file mode 100644
index 0000000..b111642
--- /dev/null
+++ b/docs/testing.rst
@@ -0,0 +1,60 @@
+Testing API calls with django
+=============================
+
+If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
+
+Usage:
+
+.. code:: python
+
+ import json
+
+ from graphene_django.utils.testing import GraphQLTestCase
+ from my_project.config.schema import schema
+
+ class MyFancyTestCase(GraphQLTestCase):
+ # Here you need to inject your test case's schema
+ GRAPHQL_SCHEMA = schema
+
+ def test_some_query(self):
+ response = self.query(
+ '''
+ query {
+ myModel {
+ id
+ name
+ }
+ }
+ ''',
+ op_name='myModel'
+ )
+
+ content = json.loads(response.content)
+
+ # This validates the status code and if you get errors
+ self.assertResponseNoErrors(response)
+
+ # Add some more asserts if you like
+ ...
+
+ def test_some_mutation(self):
+ response = self.query(
+ '''
+ mutation myMutation($input: MyMutationInput!) {
+ myMutation(input: $input) {
+ my-model {
+ id
+ name
+ }
+ }
+ }
+ ''',
+ op_name='myMutation',
+ input_data={'my_field': 'foo', 'other_field': 'bar'}
+ )
+
+ # This validates the status code and if you get errors
+ self.assertResponseNoErrors(response)
+
+ # Add some more asserts if you like
+ ...
diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst
index a87b011..29df56e 100644
--- a/docs/tutorial-plain.rst
+++ b/docs/tutorial-plain.rst
@@ -1,12 +1,9 @@
-Introduction tutorial - Graphene and Django
+Basic Tutorial
===========================================
-Graphene has a number of additional features that are designed to make
-working with Django *really simple*.
-
-Our primary focus here is to give a good understanding of how to connect models from Django ORM to graphene object types.
-
-A good idea is to check the `graphene `__ documentation first.
+Graphene Django has a number of additional features that are designed to make
+working with Django easy. Our primary focus in this tutorial is to give a good
+understanding of how to connect models from Django ORM to graphene object types.
Set up the Django project
-------------------------
@@ -91,7 +88,7 @@ Don't forget to create & run migrations:
python manage.py makemigrations
python manage.py migrate
-
+
Load some test data
^^^^^^^^^^^^^^^^^^^
@@ -108,7 +105,7 @@ following:
$ python ./manage.py loaddata ingredients
Installed 6 object(s) from 1 fixture(s)
-
+
Alternatively you can use the Django admin interface to create some data
yourself. You'll need to run the development server (see below), and
create a login for yourself too (``./manage.py createsuperuser``).
@@ -255,7 +252,7 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``
urlpatterns = [
url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
@@ -273,7 +270,7 @@ as explained above, we can do so here using:
urlpatterns = [
url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
]
@@ -487,7 +484,7 @@ Now, with the code in place, we can query for single objects.
For example, lets query ``category``:
-.. code::
+.. code::
query {
category(id: 1) {
@@ -536,3 +533,6 @@ Summary
As you can see, GraphQL is very powerful but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``.
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.**
+
+A good idea is to check the `graphene `__
+documentation but it is not essential to understand and use Graphene-Django in your project.
\ No newline at end of file
diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst
index 630898e..5f8bd64 100644
--- a/docs/tutorial-relay.rst
+++ b/docs/tutorial-relay.rst
@@ -1,4 +1,4 @@
-Graphene and Django Tutorial using Relay
+Relay tutorial
========================================
Graphene has a number of additional features that are designed to make
@@ -244,7 +244,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``.
urlpatterns = [
url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
@@ -262,7 +262,7 @@ as explained above, we can do so here using:
urlpatterns = [
url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
]
diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py
index f8606a7..f91d62c 100644
--- a/examples/cookbook-plain/cookbook/schema.py
+++ b/examples/cookbook-plain/cookbook/schema.py
@@ -8,7 +8,7 @@ from graphene_django.debug import DjangoDebug
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
- debug = graphene.Field(DjangoDebug, name='__debug')
+ debug = graphene.Field(DjangoDebug, name='_debug')
schema = graphene.Schema(query=Query)
diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py
index d846db4..bce2bab 100644
--- a/examples/cookbook-plain/cookbook/settings.py
+++ b/examples/cookbook-plain/cookbook/settings.py
@@ -56,6 +56,7 @@ MIDDLEWARE = [
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
+ 'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
@@ -130,8 +131,3 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
-
-GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'SCHEMA_INDENT': 2,
-}
diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt
index 539fd67..2154fd8 100644
--- a/examples/cookbook-plain/requirements.txt
+++ b/examples/cookbook-plain/requirements.txt
@@ -1,4 +1,4 @@
graphene
graphene-django
graphql-core>=2.1rc1
-django==2.1.2
+django==2.1.6
diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py
index f8606a7..f91d62c 100644
--- a/examples/cookbook/cookbook/schema.py
+++ b/examples/cookbook/cookbook/schema.py
@@ -8,7 +8,7 @@ from graphene_django.debug import DjangoDebug
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
- debug = graphene.Field(DjangoDebug, name='__debug')
+ debug = graphene.Field(DjangoDebug, name='_debug')
schema = graphene.Schema(query=Query)
diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py
index 948292d..0b3207e 100644
--- a/examples/cookbook/cookbook/settings.py
+++ b/examples/cookbook/cookbook/settings.py
@@ -57,6 +57,7 @@ MIDDLEWARE_CLASSES = [
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
+ 'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
@@ -131,8 +132,3 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
-
-GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'SCHEMA_INDENT': 2,
-}
diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py
index 9f8755b..4bf6003 100644
--- a/examples/cookbook/cookbook/urls.py
+++ b/examples/cookbook/cookbook/urls.py
@@ -6,5 +6,5 @@ from graphene_django.views import GraphQLView
urlpatterns = [
url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt
index b2ace1f..3fed30f1 100644
--- a/examples/cookbook/requirements.txt
+++ b/examples/cookbook/requirements.txt
@@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
-django==1.9
+django==1.11.19
django-filter>=2
diff --git a/graphene_django/compat.py b/graphene_django/compat.py
index 4a51de8..59fab30 100644
--- a/graphene_django/compat.py
+++ b/graphene_django/compat.py
@@ -5,7 +5,11 @@ class MissingType(object):
try:
# Postgres fields are only available in Django with psycopg2 installed
# and we cannot have psycopg2 on PyPy
- from django.contrib.postgres.fields import (ArrayField, HStoreField,
- JSONField, RangeField)
+ from django.contrib.postgres.fields import (
+ ArrayField,
+ HStoreField,
+ JSONField,
+ RangeField,
+ )
except ImportError:
ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
diff --git a/graphene_django/debug/sql/types.py b/graphene_django/debug/sql/types.py
index 850ced4..eeef482 100644
--- a/graphene_django/debug/sql/types.py
+++ b/graphene_django/debug/sql/types.py
@@ -3,9 +3,7 @@ from graphene import Boolean, Float, ObjectType, String
class DjangoDebugSQL(ObjectType):
class Meta:
- description = (
- "Represents a single database query made to a Django managed DB."
- )
+ description = "Represents a single database query made to a Django managed DB."
vendor = String(
required=True,
@@ -14,37 +12,26 @@ class DjangoDebugSQL(ObjectType):
),
)
alias = String(
- required=True,
- description="The Django database alias (e.g. 'default').",
+ required=True, description="The Django database alias (e.g. 'default')."
)
sql = String(description="The actual SQL sent to this database.")
duration = Float(
- required=True,
- description="Duration of this database query in seconds.",
+ required=True, description="Duration of this database query in seconds."
)
raw_sql = String(
- required=True,
- description="The raw SQL of this query, without params.",
+ required=True, description="The raw SQL of this query, without params."
)
params = String(
- required=True,
- description="JSON encoded database query parameters.",
- )
- start_time = Float(
- required=True,
- description="Start time of this database query.",
- )
- stop_time = Float(
- required=True,
- description="Stop time of this database query.",
+ required=True, description="JSON encoded database query parameters."
)
+ start_time = Float(required=True, description="Start time of this database query.")
+ stop_time = Float(required=True, description="Stop time of this database query.")
is_slow = Boolean(
required=True,
description="Whether this database query took more than 10 seconds.",
)
is_select = Boolean(
- required=True,
- description="Whether this database query was a SELECT.",
+ required=True, description="Whether this database query was a SELECT."
)
# Postgres
diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py
index f2ef096..592899b 100644
--- a/graphene_django/debug/tests/test_query.py
+++ b/graphene_django/debug/tests/test_query.py
@@ -31,7 +31,7 @@ def test_should_query_field():
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
- debug = graphene.Field(DjangoDebug, name="__debug")
+ debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_reporter(self, info, **args):
return Reporter.objects.first()
@@ -41,7 +41,7 @@ def test_should_query_field():
reporter {
lastName
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -50,7 +50,7 @@ def test_should_query_field():
"""
expected = {
"reporter": {"lastName": "ABA"},
- "__debug": {
+ "_debug": {
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
},
}
@@ -75,7 +75,7 @@ def test_should_query_list():
class Query(graphene.ObjectType):
all_reporters = graphene.List(ReporterType)
- debug = graphene.Field(DjangoDebug, name="__debug")
+ debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_all_reporters(self, info, **args):
return Reporter.objects.all()
@@ -85,7 +85,7 @@ def test_should_query_list():
allReporters {
lastName
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -94,7 +94,7 @@ def test_should_query_list():
"""
expected = {
"allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}],
- "__debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]},
+ "_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]},
}
schema = graphene.Schema(query=Query)
result = schema.execute(
@@ -117,7 +117,7 @@ def test_should_query_connection():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
- debug = graphene.Field(DjangoDebug, name="__debug")
+ debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_all_reporters(self, info, **args):
return Reporter.objects.all()
@@ -131,7 +131,7 @@ def test_should_query_connection():
}
}
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -145,9 +145,9 @@ def test_should_query_connection():
)
assert not result.errors
assert result.data["allReporters"] == expected["allReporters"]
- assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"]
+ assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
query = str(Reporter.objects.all()[:1].query)
- assert result.data["__debug"]["sql"][1]["rawSql"] == query
+ assert result.data["_debug"]["sql"][1]["rawSql"] == query
def test_should_query_connectionfilter():
@@ -166,7 +166,7 @@ def test_should_query_connectionfilter():
class Query(graphene.ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
s = graphene.String(resolver=lambda *_: "S")
- debug = graphene.Field(DjangoDebug, name="__debug")
+ debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_all_reporters(self, info, **args):
return Reporter.objects.all()
@@ -180,7 +180,7 @@ def test_should_query_connectionfilter():
}
}
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -194,6 +194,6 @@ def test_should_query_connectionfilter():
)
assert not result.errors
assert result.data["allReporters"] == expected["allReporters"]
- assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"]
+ assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
query = str(Reporter.objects.all()[:1].query)
- assert result.data["__debug"]["sql"][1]["rawSql"] == query
+ assert result.data["_debug"]["sql"][1]["rawSql"] == query
diff --git a/graphene_django/debug/types.py b/graphene_django/debug/types.py
index cda5725..1cd816d 100644
--- a/graphene_django/debug/types.py
+++ b/graphene_django/debug/types.py
@@ -7,7 +7,4 @@ class DjangoDebug(ObjectType):
class Meta:
description = "Debugging information for the current query."
- sql = List(
- DjangoDebugSQL,
- description="Executed SQL queries for this API query.",
- )
+ sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.")
diff --git a/graphene_django/fields.py b/graphene_django/fields.py
index 9b27f70..791e785 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -1,6 +1,7 @@
from functools import partial
from django.db.models.query import QuerySet
+from graphene import NonNull
from promise import Promise
@@ -45,17 +46,31 @@ class DjangoConnectionField(ConnectionField):
from .types import DjangoObjectType
_type = super(ConnectionField, self).type
+ non_null = False
+ if isinstance(_type, NonNull):
+ _type = _type.of_type
+ non_null = True
assert issubclass(
_type, DjangoObjectType
), "DjangoConnectionField only accepts DjangoObjectType types"
assert _type._meta.connection, "The type {} doesn't have a connection".format(
_type.__name__
)
- return _type._meta.connection
+ connection_type = _type._meta.connection
+ if non_null:
+ return NonNull(connection_type)
+ return connection_type
+
+ @property
+ def connection_type(self):
+ type = self.type
+ if isinstance(type, NonNull):
+ return type.of_type
+ return type
@property
def node_type(self):
- return self.type._meta.node
+ return self.connection_type._meta.node
@property
def model(self):
@@ -151,7 +166,7 @@ class DjangoConnectionField(ConnectionField):
return partial(
self.connection_resolver,
parent_resolver,
- self.type,
+ self.connection_type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,
diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py
index 4059083..7676ea8 100644
--- a/graphene_django/filter/filterset.py
+++ b/graphene_django/filter/filterset.py
@@ -45,8 +45,7 @@ class GrapheneFilterSetMixin(BaseFilterSet):
FILTER_DEFAULTS = dict(
itertools.chain(
- FILTER_FOR_DBFIELD_DEFAULTS.items(),
- GRAPHENE_FILTER_SET_OVERRIDES.items()
+ FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items()
)
)
@@ -59,7 +58,6 @@ if VERSION[0] < 2:
from django.utils.text import capfirst
class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
-
@classmethod
def filter_for_reverse_field(cls, f, name):
"""Handles retrieving filters for reverse relationships
diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py
index 87180b2..8916456 100644
--- a/graphene_django/forms/converter.py
+++ b/graphene_django/forms/converter.py
@@ -43,7 +43,7 @@ def convert_form_field_to_int(field):
@convert_form_field.register(forms.BooleanField)
def convert_form_field_to_boolean(field):
- return Boolean(description=field.help_text, required=True)
+ return Boolean(description=field.help_text, required=field.required)
@convert_form_field.register(forms.NullBooleanField)
diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py
index df0ffd5..543e89e 100644
--- a/graphene_django/forms/tests/test_mutation.py
+++ b/graphene_django/forms/tests/test_mutation.py
@@ -13,7 +13,7 @@ class MyForm(forms.Form):
class PetForm(forms.ModelForm):
class Meta:
model = Pet
- fields = '__all__'
+ fields = "__all__"
def test_needs_form_class():
@@ -66,7 +66,7 @@ class ModelFormMutationTests(TestCase):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
- exclude_fields = ['id']
+ exclude_fields = ["id"]
self.assertEqual(PetMutation._meta.model, Pet)
self.assertEqual(PetMutation._meta.return_field_name, "pet")
@@ -102,7 +102,9 @@ class ModelFormMutationTests(TestCase):
pet = Pet.objects.create(name="Axel", age=10)
- result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name="Mia", age=10)
+ result = PetMutation.mutate_and_get_payload(
+ None, None, id=pet.pk, name="Mia", age=10
+ )
self.assertEqual(Pet.objects.count(), 1)
pet.refresh_from_db()
@@ -132,7 +134,6 @@ class ModelFormMutationTests(TestCase):
# A pet was not created
self.assertEqual(Pet.objects.count(), 0)
-
fields_w_error = [e.field for e in result.errors]
self.assertEqual(len(result.errors), 2)
self.assertIn("name", fields_w_error)
diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py
index d7f83da..9f8689e 100644
--- a/graphene_django/management/commands/graphql_schema.py
+++ b/graphene_django/management/commands/graphql_schema.py
@@ -64,7 +64,7 @@ class Command(CommandArguments):
indent = options.get("indent")
schema_dict = {"data": schema.introspect()}
- if out == '-':
+ if out == "-":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
else:
self.save_file(out, schema_dict, indent)
diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html
index c0c9af1..d0fb5a8 100644
--- a/graphene_django/templates/graphene/graphiql.html
+++ b/graphene_django/templates/graphene/graphiql.html
@@ -23,11 +23,9 @@ add "&raw" to the end of the URL within a browser.
-
-
diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py
index fa78aec..dbabafa 100644
--- a/graphene_django/tests/test_command.py
+++ b/graphene_django/tests/test_command.py
@@ -10,14 +10,18 @@ def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
-@patch('json.dump')
+@patch("json.dump")
def test_files_are_canonical(dump_mock):
open_mock = mock_open()
- with patch('graphene_django.management.commands.graphql_schema.open', open_mock):
- management.call_command('graphql_schema', schema='')
+ with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
+ management.call_command("graphql_schema", schema="")
open_mock.assert_called_once()
dump_mock.assert_called_once()
- assert dump_mock.call_args[1]["sort_keys"], "json.mock() should be used to sort the output"
- assert dump_mock.call_args[1]["indent"] > 0, "output should be pretty-printed by default"
+ assert dump_mock.call_args[1][
+ "sort_keys"
+ ], "json.mock() should be used to sort the output"
+ assert (
+ dump_mock.call_args[1]["indent"] > 0
+ ), "output should be pretty-printed by default"
diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py
index eac5851..bb176b3 100644
--- a/graphene_django/tests/test_converter.py
+++ b/graphene_django/tests/test_converter.py
@@ -241,8 +241,7 @@ def test_should_manytoone_convert_connectionorlist():
class Meta:
model = Article
- graphene_field = convert_django_field(Reporter.articles.rel,
- A._meta.registry)
+ graphene_field = convert_django_field(Reporter.articles.rel, A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
@@ -255,8 +254,7 @@ def test_should_onetoone_reverse_convert_model():
class Meta:
model = FilmDetails
- graphene_field = convert_django_field(Film.details.related,
- A._meta.registry)
+ graphene_field = convert_django_field(Film.details.related, A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field)
diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py
new file mode 100644
index 0000000..f9c388d
--- /dev/null
+++ b/graphene_django/utils/__init__.py
@@ -0,0 +1,19 @@
+from .utils import (
+ DJANGO_FILTER_INSTALLED,
+ get_reverse_fields,
+ maybe_queryset,
+ get_model_fields,
+ is_valid_django_model,
+ import_single_dispatch,
+)
+from .testing import GraphQLTestCase
+
+__all__ = [
+ "DJANGO_FILTER_INSTALLED",
+ "get_reverse_fields",
+ "maybe_queryset",
+ "get_model_fields",
+ "is_valid_django_model",
+ "import_single_dispatch",
+ "GraphQLTestCase",
+]
diff --git a/graphene_django/utils/testing.py b/graphene_django/utils/testing.py
new file mode 100644
index 0000000..47f8d04
--- /dev/null
+++ b/graphene_django/utils/testing.py
@@ -0,0 +1,67 @@
+import json
+
+from django.test import TestCase, Client
+
+
+class GraphQLTestCase(TestCase):
+ """
+ Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/
+ """
+
+ # URL to graphql endpoint
+ GRAPHQL_URL = "/graphql/"
+ # Here you need to set your graphql schema for the tests
+ GRAPHQL_SCHEMA = None
+
+ @classmethod
+ def setUpClass(cls):
+ super(GraphQLTestCase, cls).setUpClass()
+
+ if not cls.GRAPHQL_SCHEMA:
+ raise AttributeError(
+ "Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
+ )
+
+ cls._client = Client(cls.GRAPHQL_SCHEMA)
+
+ def query(self, query, op_name=None, input_data=None):
+ """
+ Args:
+ query (string) - GraphQL query to run
+ op_name (string) - If the query is a mutation or named query, you must
+ supply the op_name. For annon queries ("{ ... }"),
+ should be None (default).
+ input_data (dict) - If provided, the $input variable in GraphQL will be set
+ to this value
+
+ Returns:
+ Response object from client
+ """
+ body = {"query": query}
+ if op_name:
+ body["operation_name"] = op_name
+ if input_data:
+ body["variables"] = {"input": input_data}
+
+ resp = self._client.post(
+ self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
+ )
+ return resp
+
+ def assertResponseNoErrors(self, resp):
+ """
+ Assert that the call went through correctly. 200 means the syntax is ok, if there are no `errors`,
+ the call was fine.
+ :resp HttpResponse: Response
+ """
+ content = json.loads(resp.content)
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotIn("errors", list(content.keys()))
+
+ def assertResponseHasErrors(self, resp):
+ """
+ Assert that the call was failing. Take care: Even with errors, GraphQL returns status 200!
+ :resp HttpResponse: Response
+ """
+ content = json.loads(resp.content)
+ self.assertIn("errors", list(content.keys()))
diff --git a/graphene_django/utils.py b/graphene_django/utils/utils.py
similarity index 89%
rename from graphene_django/utils.py
rename to graphene_django/utils/utils.py
index 560f604..02c47ee 100644
--- a/graphene_django/utils.py
+++ b/graphene_django/utils/utils.py
@@ -4,13 +4,6 @@ from django.db import models
from django.db.models.manager import Manager
-# from graphene.utils import LazyList
-
-
-class LazyList(object):
- pass
-
-
try:
import django_filters # noqa
@@ -25,8 +18,7 @@ def get_reverse_fields(model, local_field_names):
if name in local_field_names:
continue
- # Django =>1.9 uses 'rel', django <1.9 uses 'related'
- related = getattr(attr, "rel", None) or getattr(attr, "related", None)
+ related = getattr(attr, "rel", None)
if isinstance(related, models.ManyToOneRel):
yield (name, related)
elif isinstance(related, models.ManyToManyRel) and not related.symmetrical:
diff --git a/graphene_django/views.py b/graphene_django/views.py
index 9a530de..aefe114 100644
--- a/graphene_django/views.py
+++ b/graphene_django/views.py
@@ -51,8 +51,9 @@ def instantiate_middleware(middlewares):
class GraphQLView(View):
- graphiql_version = "0.11.10"
+ graphiql_version = "0.13.0"
graphiql_template = "graphene/graphiql.html"
+ react_version = "16.8.6"
schema = None
graphiql = False
@@ -128,6 +129,7 @@ class GraphQLView(View):
return self.render_graphiql(
request,
graphiql_version=self.graphiql_version,
+ react_version=self.react_version,
)
if self.batch:
diff --git a/setup.py b/setup.py
index 3431cd5..e622a71 100644
--- a/setup.py
+++ b/setup.py
@@ -24,6 +24,12 @@ tests_require = [
"pytest-django>=3.3.2",
] + rest_framework_require
+
+dev_requires = [
+ "black==19.3b0",
+ "flake8==3.7.7",
+] + tests_require
+
setup(
name="graphene-django",
version=version,
@@ -58,7 +64,7 @@ setup(
setup_requires=["pytest-runner"],
tests_require=tests_require,
rest_framework_require=rest_framework_require,
- extras_require={"test": tests_require, "rest_framework": rest_framework_require},
+ extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires},
include_package_data=True,
zip_safe=False,
platforms="any",