diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..dc90e5a
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,17 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 60
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - pinned
+ - security
+# Label to use when marking an issue as stale
+staleLabel: wontfix
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.gitignore b/.gitignore
index 0b25625..150025a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,3 +78,5 @@ Session.vim
*~
# auto-generated tag files
tags
+.tox/
+.pytest_cache/
diff --git a/.travis.yml b/.travis.yml
index a8375ee..871d4e3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,54 +1,60 @@
language: python
-sudo: false
-python:
-- 2.7
-- 3.4
-- 3.5
-- 3.6
+cache: pip
+dist: xenial
+
install:
-- |
- if [ "$TEST_TYPE" = build ]; then
- pip install -e .[test]
- pip install psycopg2 # Required for Django postgres fields testing
- pip install django==$DJANGO_VERSION
- python setup.py develop
- elif [ "$TEST_TYPE" = lint ]; then
- pip install flake8
- fi
-script:
-- |
- if [ "$TEST_TYPE" = lint ]; then
- echo "Checking Python code lint."
- flake8 graphene_django
- exit
- elif [ "$TEST_TYPE" = build ]; then
- py.test --cov=graphene_django graphene_django examples
- fi
-after_success:
-- |
- if [ "$TEST_TYPE" = build ]; then
- coveralls
- fi
-env:
- matrix:
- - TEST_TYPE=build DJANGO_VERSION=1.11
+ - pip install tox tox-travis
+
+script:
+ - tox
+
+after_success:
+ - pip install coveralls
+ - coveralls
+
matrix:
fast_finish: true
include:
- - python: '3.4'
- env: TEST_TYPE=build DJANGO_VERSION=2.0
- - python: '3.5'
- env: TEST_TYPE=build DJANGO_VERSION=2.0
- - python: '3.6'
- env: TEST_TYPE=build DJANGO_VERSION=2.0
- - python: '3.5'
- env: TEST_TYPE=build DJANGO_VERSION=2.1
- - python: '3.6'
- env: TEST_TYPE=build DJANGO_VERSION=2.1
- - python: '2.7'
- env: TEST_TYPE=lint
- - python: '3.6'
- env: TEST_TYPE=lint
+ - python: 2.7
+ env: DJANGO=1.11
+
+ - python: 3.5
+ env: DJANGO=1.11
+ - python: 3.5
+ env: DJANGO=2.0
+ - python: 3.5
+ env: DJANGO=2.1
+ - python: 3.5
+ env: DJANGO=2.2
+
+ - python: 3.6
+ env: DJANGO=1.11
+ - python: 3.6
+ env: DJANGO=2.0
+ - python: 3.6
+ env: DJANGO=2.1
+ - python: 3.6
+ env: DJANGO=2.2
+ - python: 3.6
+ env: DJANGO=master
+
+ - python: 3.7
+ env: DJANGO=1.11
+ - python: 3.7
+ env: DJANGO=2.0
+ - python: 3.7
+ env: DJANGO=2.1
+ - python: 3.7
+ env: DJANGO=2.2
+ - python: 3.7
+ env: DJANGO=master
+
+ - python: 3.7
+ env: TOXENV=black,flake8
+
+ allow_failures:
+ - env: DJANGO=master
+
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..70badcb
--- /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 --exclude "/migrations/" graphene_django examples
+
+lint:
+ flake8 graphene_django examples
diff --git a/README.md b/README.md
index 4e0b01d..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
@@ -20,6 +24,7 @@ pip install "graphene-django>=2.0"
```python
INSTALLED_APPS = (
# ...
+ 'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django',
)
@@ -38,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)),
]
```
@@ -95,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 7a08481..3d0bb8a 100644
--- a/docs/authorization.rst
+++ b/docs/authorization.rst
@@ -96,6 +96,29 @@ schema is simple.
result = schema.execute(query, context_value=request)
+
+Global Filtering
+----------------
+
+If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
+
+.. code:: python
+
+ from graphene import relay
+ from graphene_django.types import DjangoObjectType
+ from .models import Post
+
+ class PostNode(DjangoObjectType):
+ class Meta:
+ model = Post
+
+ @classmethod
+ def get_queryset(cls, queryset, info):
+ if info.context.user.is_anonymous:
+ return queryset.filter(published=True)
+ return queryset
+
+
Filtering ID-based Node Access
------------------------------
@@ -114,7 +137,7 @@ method to your ``DjangoObjectType``.
interfaces = (relay.Node, )
@classmethod
- def get_node(cls, id, info):
+ def get_node(cls, info, id):
try:
post = cls._meta.model.objects.get(id=id)
except cls._meta.model.DoesNotExist:
@@ -132,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
@@ -148,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..7661928 100644
--- a/docs/filtering.rst
+++ b/docs/filtering.rst
@@ -100,7 +100,7 @@ features of ``django-filter``. This is done by transparently creating a
``filter_fields``.
However, you may find this to be insufficient. In these cases you can
-create your own ``Filterset`` as follows:
+create your own ``FilterSet``. You can pass it directly as follows:
.. code:: python
@@ -127,6 +127,33 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
+You can also specify the ``FilterSet`` class using the ``filerset_class``
+parameter when defining your ``DjangoObjectType``, however, this can't be used
+in unison with the ``filter_fields`` parameter:
+
+.. code:: python
+
+ class AnimalFilter(django_filters.FilterSet):
+ # Do case-insensitive lookups on 'name'
+ name = django_filters.CharFilter(lookup_expr=['iexact'])
+
+ class Meta:
+ # Assume you have an Animal model defined with the following fields
+ model = Animal
+ fields = ['name', 'genus', 'is_domesticated']
+
+
+ class AnimalNode(DjangoObjectType):
+ class Meta:
+ model = Animal
+ filterset_class = AnimalFilter
+ interfaces = (relay.Node, )
+
+
+ class Query(ObjectType):
+ animal = relay.Node.Field(AnimalNode)
+ all_animals = DjangoFilterConnectionField(AnimalNode)
+
The context argument is passed on as the `request argument `__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
@@ -136,7 +163,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 +173,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 e721a78..0000000
--- a/docs/form-mutations.rst
+++ /dev/null
@@ -1,68 +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.*
-
-FormMutation
-------------
-
-.. code:: python
-
- class MyForm(forms.Form):
- name = forms.CharField()
-
- class MyMutation(FormMutation):
- 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.
-
-ModelFormMutation
------------------
-
-``ModelFormMutation`` will pull the fields from a ``ModelForm``.
-
-.. code:: python
-
- 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 ``form_valid(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 0d30ee4..c1d6ede 100644
--- a/docs/introspection.rst
+++ b/docs/introspection.rst
@@ -1,17 +1,15 @@
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
-----
-Include ``graphene_django`` to ``INSTALLED_APPS`` in you project
+Include ``graphene_django`` to ``INSTALLED_APPS`` in your project
settings:
.. code:: python
@@ -29,12 +27,16 @@ It dumps your full introspection schema to ``schema.json`` inside your
project root directory. Point ``babel-relay-plugin`` to this file and
you're ready to use Relay with Graphene GraphQL implementation.
+The schema file is sorted to create a reproducible canonical representation.
+
Advanced Usage
--------------
The ``--indent`` option can be used to specify the number of indentation spaces to
be used in the output. Defaults to `None` which displays all data on a single line.
+The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
+
To simplify the command to ``./manage.py graphql_schema``, you can
specify the parameters in your settings.py:
diff --git a/docs/mutations.rst b/docs/mutations.rst
new file mode 100644
index 0000000..6610151
--- /dev/null
+++ b/docs/mutations.rst
@@ -0,0 +1,231 @@
+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
+ from graphene import relay
+ from graphene_django import 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)[1])
+ 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.
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 f2502d7..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
@@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, )
- class Query(object):
+ class Query(graphene.ObjectType):
category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)
@@ -158,7 +158,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
The filtering functionality is provided by
`django-filter `__. See the
`usage
-documentation `__
+documentation `__
for details on the format for ``filter_fields``. While optional, this
tutorial makes use of this functionality so you will need to install
``django-filter`` for this tutorial to work:
@@ -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)),
]
@@ -345,3 +345,10 @@ Or you can get only 'meat' ingredients containing the letter 'e':
}
}
}
+
+
+
+Final Steps
+^^^^^^^^^^^
+
+We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema `__ part of this guide.
diff --git a/examples/cookbook-plain/cookbook/ingredients/admin.py b/examples/cookbook-plain/cookbook/ingredients/admin.py
index b57cbc3..042682f 100644
--- a/examples/cookbook-plain/cookbook/ingredients/admin.py
+++ b/examples/cookbook-plain/cookbook/ingredients/admin.py
@@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
- list_display = ('id', 'name', 'category')
- list_editable = ('name', 'category')
+ list_display = ("id", "name", "category")
+ list_editable = ("name", "category")
admin.site.register(Category)
diff --git a/examples/cookbook-plain/cookbook/ingredients/apps.py b/examples/cookbook-plain/cookbook/ingredients/apps.py
index 21b4b08..3ad0143 100644
--- a/examples/cookbook-plain/cookbook/ingredients/apps.py
+++ b/examples/cookbook-plain/cookbook/ingredients/apps.py
@@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
- name = 'cookbook.ingredients'
- label = 'ingredients'
- verbose_name = 'Ingredients'
+ name = "cookbook.ingredients"
+ label = "ingredients"
+ verbose_name = "Ingredients"
diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py
new file mode 100644
index 0000000..184e79e
--- /dev/null
+++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0 on 2018-10-18 17:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ingredients', '0002_auto_20161104_0050'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='category',
+ options={'verbose_name_plural': 'Categories'},
+ ),
+ ]
diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py
index 2f0eba3..5d88785 100644
--- a/examples/cookbook-plain/cookbook/ingredients/models.py
+++ b/examples/cookbook-plain/cookbook/ingredients/models.py
@@ -2,6 +2,9 @@ from django.db import models
class Category(models.Model):
+ class Meta:
+ verbose_name_plural = "Categories"
+
name = models.CharField(max_length=100)
def __str__(self):
@@ -11,7 +14,9 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
- category = models.ForeignKey(Category, related_name='ingredients')
+ category = models.ForeignKey(
+ Category, related_name="ingredients", on_delete=models.CASCADE
+ )
def __str__(self):
return self.name
diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py
index 1f3bb18..1a54c4b 100644
--- a/examples/cookbook-plain/cookbook/ingredients/schema.py
+++ b/examples/cookbook-plain/cookbook/ingredients/schema.py
@@ -1,7 +1,7 @@
import graphene
from graphene_django.types import DjangoObjectType
-from cookbook.ingredients.models import Category, Ingredient
+from .models import Category, Ingredient
class CategoryType(DjangoObjectType):
@@ -15,27 +15,22 @@ class IngredientType(DjangoObjectType):
class Query(object):
- category = graphene.Field(CategoryType,
- id=graphene.Int(),
- name=graphene.String())
+ category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
- ingredient = graphene.Field(IngredientType,
- id=graphene.Int(),
- name=graphene.String())
+ ingredient = graphene.Field(
+ IngredientType, id=graphene.Int(), name=graphene.String()
+ )
all_ingredients = graphene.List(IngredientType)
- def resolve_all_categories(self, args, context, info):
+ def resolve_all_categories(self, context):
return Category.objects.all()
- def resolve_all_ingredients(self, args, context, info):
+ def resolve_all_ingredients(self, context):
# We can easily optimize query count in the resolve method
- return Ingredient.objects.select_related('category').all()
-
- def resolve_category(self, args, context, info):
- id = args.get('id')
- name = args.get('name')
+ return Ingredient.objects.select_related("category").all()
+ def resolve_category(self, context, id=None, name=None):
if id is not None:
return Category.objects.get(pk=id)
@@ -44,10 +39,7 @@ class Query(object):
return None
- def resolve_ingredient(self, args, context, info):
- id = args.get('id')
- name = args.get('name')
-
+ def resolve_ingredient(self, context, id=None, name=None):
if id is not None:
return Ingredient.objects.get(pk=id)
diff --git a/examples/cookbook-plain/cookbook/ingredients/tests.py b/examples/cookbook-plain/cookbook/ingredients/tests.py
index 4929020..a39b155 100644
--- a/examples/cookbook-plain/cookbook/ingredients/tests.py
+++ b/examples/cookbook-plain/cookbook/ingredients/tests.py
@@ -1,2 +1 @@
-
# Create your tests here.
diff --git a/examples/cookbook-plain/cookbook/ingredients/views.py b/examples/cookbook-plain/cookbook/ingredients/views.py
index b8e4ee0..60f00ef 100644
--- a/examples/cookbook-plain/cookbook/ingredients/views.py
+++ b/examples/cookbook-plain/cookbook/ingredients/views.py
@@ -1,2 +1 @@
-
# Create your views here.
diff --git a/examples/cookbook-plain/cookbook/recipes/apps.py b/examples/cookbook-plain/cookbook/recipes/apps.py
index 1f24f13..f1e4dde 100644
--- a/examples/cookbook-plain/cookbook/recipes/apps.py
+++ b/examples/cookbook-plain/cookbook/recipes/apps.py
@@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
- name = 'cookbook.recipes'
- label = 'recipes'
- verbose_name = 'Recipes'
+ name = "cookbook.recipes"
+ label = "recipes"
+ verbose_name = "Recipes"
diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py
new file mode 100644
index 0000000..7a8df49
--- /dev/null
+++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0 on 2018-10-18 17:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('recipes', '0002_auto_20161104_0106'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='recipeingredient',
+ name='unit',
+ field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20),
+ ),
+ ]
diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py
index ca12fac..f6e955e 100644
--- a/examples/cookbook-plain/cookbook/recipes/models.py
+++ b/examples/cookbook-plain/cookbook/recipes/models.py
@@ -1,21 +1,28 @@
from django.db import models
-from cookbook.ingredients.models import Ingredient
+from ..ingredients.models import Ingredient
class Recipe(models.Model):
title = models.CharField(max_length=100)
instructions = models.TextField()
- __unicode__ = lambda self: self.title
+
+ def __str__(self):
+ return self.title
class RecipeIngredient(models.Model):
- recipe = models.ForeignKey(Recipe, related_name='amounts')
- ingredient = models.ForeignKey(Ingredient, related_name='used_by')
+ recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
+ ingredient = models.ForeignKey(
+ Ingredient, related_name="used_by", on_delete=models.CASCADE
+ )
amount = models.FloatField()
- unit = models.CharField(max_length=20, choices=(
- ('unit', 'Units'),
- ('kg', 'Kilograms'),
- ('l', 'Litres'),
- ('st', 'Shots'),
- ))
+ unit = models.CharField(
+ max_length=20,
+ choices=(
+ ("unit", "Units"),
+ ("kg", "Kilograms"),
+ ("l", "Litres"),
+ ("st", "Shots"),
+ ),
+ )
diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py
index 040c985..b029570 100644
--- a/examples/cookbook-plain/cookbook/recipes/schema.py
+++ b/examples/cookbook-plain/cookbook/recipes/schema.py
@@ -1,7 +1,7 @@
import graphene
from graphene_django.types import DjangoObjectType
-from cookbook.recipes.models import Recipe, RecipeIngredient
+from .models import Recipe, RecipeIngredient
class RecipeType(DjangoObjectType):
@@ -15,19 +15,13 @@ class RecipeIngredientType(DjangoObjectType):
class Query(object):
- recipe = graphene.Field(RecipeType,
- id=graphene.Int(),
- title=graphene.String())
+ recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType)
- recipeingredient = graphene.Field(RecipeIngredientType,
- id=graphene.Int())
+ recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int())
all_recipeingredients = graphene.List(RecipeIngredientType)
- def resolve_recipe(self, args, context, info):
- id = args.get('id')
- title = args.get('title')
-
+ def resolve_recipe(self, context, id=None, title=None):
if id is not None:
return Recipe.objects.get(pk=id)
@@ -36,17 +30,15 @@ class Query(object):
return None
- def resolve_recipeingredient(self, args, context, info):
- id = args.get('id')
-
+ def resolve_recipeingredient(self, context, id=None):
if id is not None:
return RecipeIngredient.objects.get(pk=id)
return None
- def resolve_all_recipes(self, args, context, info):
+ def resolve_all_recipes(self, context):
return Recipe.objects.all()
- def resolve_all_recipeingredients(self, args, context, info):
- related = ['recipe', 'ingredient']
+ def resolve_all_recipeingredients(self, context):
+ related = ["recipe", "ingredient"]
return RecipeIngredient.objects.select_related(*related).all()
diff --git a/examples/cookbook-plain/cookbook/recipes/tests.py b/examples/cookbook-plain/cookbook/recipes/tests.py
index 4929020..a39b155 100644
--- a/examples/cookbook-plain/cookbook/recipes/tests.py
+++ b/examples/cookbook-plain/cookbook/recipes/tests.py
@@ -1,2 +1 @@
-
# Create your tests here.
diff --git a/examples/cookbook-plain/cookbook/recipes/views.py b/examples/cookbook-plain/cookbook/recipes/views.py
index b8e4ee0..60f00ef 100644
--- a/examples/cookbook-plain/cookbook/recipes/views.py
+++ b/examples/cookbook-plain/cookbook/recipes/views.py
@@ -1,2 +1 @@
-
# Create your views here.
diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py
index f8606a7..bde9372 100644
--- a/examples/cookbook-plain/cookbook/schema.py
+++ b/examples/cookbook-plain/cookbook/schema.py
@@ -5,10 +5,12 @@ import graphene
from graphene_django.debug import DjangoDebug
-class Query(cookbook.ingredients.schema.Query,
- cookbook.recipes.schema.Query,
- graphene.ObjectType):
- debug = graphene.Field(DjangoDebug, name='__debug')
+class Query(
+ cookbook.ingredients.schema.Query,
+ cookbook.recipes.schema.Query,
+ graphene.ObjectType,
+):
+ 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 948292d..7eb9d56 100644
--- a/examples/cookbook-plain/cookbook/settings.py
+++ b/examples/cookbook-plain/cookbook/settings.py
@@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
+SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -32,64 +32,61 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'graphene_django',
-
- 'cookbook.ingredients.apps.IngredientsConfig',
- 'cookbook.recipes.apps.RecipesConfig',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "graphene_django",
+ "cookbook.ingredients.apps.IngredientsConfig",
+ "cookbook.recipes.apps.RecipesConfig",
]
-MIDDLEWARE_CLASSES = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'MIDDLEWARE': (
- 'graphene_django.debug.DjangoDebugMiddleware',
- )
+ "SCHEMA": "cookbook.schema.schema",
+ "SCHEMA_INDENT": 2,
+ "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
-ROOT_URLCONF = 'cookbook.urls'
+ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ]
},
- },
+ }
]
-WSGI_APPLICATION = 'cookbook.wsgi.application'
+WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@@ -99,26 +96,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
USE_I18N = True
@@ -130,9 +121,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
-STATIC_URL = '/static/'
-
-GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'SCHEMA_INDENT': 2,
-}
+STATIC_URL = "/static/"
diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py
index 9f8755b..a64a875 100644
--- a/examples/cookbook-plain/cookbook/urls.py
+++ b/examples/cookbook-plain/cookbook/urls.py
@@ -1,10 +1,10 @@
-from django.conf.urls import url
+from django.urls import path
from django.contrib import admin
from graphene_django.views import GraphQLView
urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ path("admin/", admin.site.urls),
+ path("graphql/", GraphQLView.as_view(graphiql=True)),
]
diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt
index 362a39a..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==1.9
+django==2.1.6
diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook/cookbook/ingredients/admin.py
index b57cbc3..042682f 100644
--- a/examples/cookbook/cookbook/ingredients/admin.py
+++ b/examples/cookbook/cookbook/ingredients/admin.py
@@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
- list_display = ('id', 'name', 'category')
- list_editable = ('name', 'category')
+ list_display = ("id", "name", "category")
+ list_editable = ("name", "category")
admin.site.register(Category)
diff --git a/examples/cookbook/cookbook/ingredients/apps.py b/examples/cookbook/cookbook/ingredients/apps.py
index 21b4b08..3ad0143 100644
--- a/examples/cookbook/cookbook/ingredients/apps.py
+++ b/examples/cookbook/cookbook/ingredients/apps.py
@@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
- name = 'cookbook.ingredients'
- label = 'ingredients'
- verbose_name = 'Ingredients'
+ name = "cookbook.ingredients"
+ label = "ingredients"
+ verbose_name = "Ingredients"
diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook/cookbook/ingredients/models.py
index 2f0eba3..6426dab 100644
--- a/examples/cookbook/cookbook/ingredients/models.py
+++ b/examples/cookbook/cookbook/ingredients/models.py
@@ -11,7 +11,7 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
- category = models.ForeignKey(Category, related_name='ingredients')
+ category = models.ForeignKey(Category, related_name="ingredients")
def __str__(self):
return self.name
diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py
index 5ad92e8..5e5da80 100644
--- a/examples/cookbook/cookbook/ingredients/schema.py
+++ b/examples/cookbook/cookbook/ingredients/schema.py
@@ -7,24 +7,22 @@ from graphene_django.types import DjangoObjectType
# Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below)
class CategoryNode(DjangoObjectType):
-
class Meta:
model = Category
- interfaces = (Node, )
- filter_fields = ['name', 'ingredients']
+ interfaces = (Node,)
+ filter_fields = ["name", "ingredients"]
class IngredientNode(DjangoObjectType):
-
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
- interfaces = (Node, )
+ interfaces = (Node,)
filter_fields = {
- 'name': ['exact', 'icontains', 'istartswith'],
- 'notes': ['exact', 'icontains'],
- 'category': ['exact'],
- 'category__name': ['exact'],
+ "name": ["exact", "icontains", "istartswith"],
+ "notes": ["exact", "icontains"],
+ "category": ["exact"],
+ "category__name": ["exact"],
}
diff --git a/examples/cookbook/cookbook/ingredients/tests.py b/examples/cookbook/cookbook/ingredients/tests.py
index 4929020..a39b155 100644
--- a/examples/cookbook/cookbook/ingredients/tests.py
+++ b/examples/cookbook/cookbook/ingredients/tests.py
@@ -1,2 +1 @@
-
# Create your tests here.
diff --git a/examples/cookbook/cookbook/ingredients/views.py b/examples/cookbook/cookbook/ingredients/views.py
index b8e4ee0..60f00ef 100644
--- a/examples/cookbook/cookbook/ingredients/views.py
+++ b/examples/cookbook/cookbook/ingredients/views.py
@@ -1,2 +1 @@
-
# Create your views here.
diff --git a/examples/cookbook/cookbook/recipes/apps.py b/examples/cookbook/cookbook/recipes/apps.py
index 1f24f13..f1e4dde 100644
--- a/examples/cookbook/cookbook/recipes/apps.py
+++ b/examples/cookbook/cookbook/recipes/apps.py
@@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
- name = 'cookbook.recipes'
- label = 'recipes'
- verbose_name = 'Recipes'
+ name = "cookbook.recipes"
+ label = "recipes"
+ verbose_name = "Recipes"
diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook/cookbook/recipes/models.py
index ca12fac..b98664c 100644
--- a/examples/cookbook/cookbook/recipes/models.py
+++ b/examples/cookbook/cookbook/recipes/models.py
@@ -10,12 +10,15 @@ class Recipe(models.Model):
class RecipeIngredient(models.Model):
- recipe = models.ForeignKey(Recipe, related_name='amounts')
- ingredient = models.ForeignKey(Ingredient, related_name='used_by')
+ recipe = models.ForeignKey(Recipe, related_name="amounts")
+ ingredient = models.ForeignKey(Ingredient, related_name="used_by")
amount = models.FloatField()
- unit = models.CharField(max_length=20, choices=(
- ('unit', 'Units'),
- ('kg', 'Kilograms'),
- ('l', 'Litres'),
- ('st', 'Shots'),
- ))
+ unit = models.CharField(
+ max_length=20,
+ choices=(
+ ("unit", "Units"),
+ ("kg", "Kilograms"),
+ ("l", "Litres"),
+ ("st", "Shots"),
+ ),
+ )
diff --git a/examples/cookbook/cookbook/recipes/schema.py b/examples/cookbook/cookbook/recipes/schema.py
index 8018322..fbbedd8 100644
--- a/examples/cookbook/cookbook/recipes/schema.py
+++ b/examples/cookbook/cookbook/recipes/schema.py
@@ -3,24 +3,23 @@ from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
-class RecipeNode(DjangoObjectType):
+class RecipeNode(DjangoObjectType):
class Meta:
model = Recipe
- interfaces = (Node, )
- filter_fields = ['title','amounts']
+ interfaces = (Node,)
+ filter_fields = ["title", "amounts"]
class RecipeIngredientNode(DjangoObjectType):
-
class Meta:
model = RecipeIngredient
# Allow for some more advanced filtering here
- interfaces = (Node, )
+ interfaces = (Node,)
filter_fields = {
- 'ingredient__name': ['exact', 'icontains', 'istartswith'],
- 'recipe': ['exact'],
- 'recipe__title': ['icontains'],
+ "ingredient__name": ["exact", "icontains", "istartswith"],
+ "recipe": ["exact"],
+ "recipe__title": ["icontains"],
}
diff --git a/examples/cookbook/cookbook/recipes/tests.py b/examples/cookbook/cookbook/recipes/tests.py
index 4929020..a39b155 100644
--- a/examples/cookbook/cookbook/recipes/tests.py
+++ b/examples/cookbook/cookbook/recipes/tests.py
@@ -1,2 +1 @@
-
# Create your tests here.
diff --git a/examples/cookbook/cookbook/recipes/views.py b/examples/cookbook/cookbook/recipes/views.py
index b8e4ee0..60f00ef 100644
--- a/examples/cookbook/cookbook/recipes/views.py
+++ b/examples/cookbook/cookbook/recipes/views.py
@@ -1,2 +1 @@
-
# Create your views here.
diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py
index f8606a7..bde9372 100644
--- a/examples/cookbook/cookbook/schema.py
+++ b/examples/cookbook/cookbook/schema.py
@@ -5,10 +5,12 @@ import graphene
from graphene_django.debug import DjangoDebug
-class Query(cookbook.ingredients.schema.Query,
- cookbook.recipes.schema.Query,
- graphene.ObjectType):
- debug = graphene.Field(DjangoDebug, name='__debug')
+class Query(
+ cookbook.ingredients.schema.Query,
+ cookbook.recipes.schema.Query,
+ graphene.ObjectType,
+):
+ 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..ed41a65 100644
--- a/examples/cookbook/cookbook/settings.py
+++ b/examples/cookbook/cookbook/settings.py
@@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
+SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -32,64 +32,62 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'graphene_django',
-
- 'cookbook.ingredients.apps.IngredientsConfig',
- 'cookbook.recipes.apps.RecipesConfig',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "graphene_django",
+ "cookbook.ingredients.apps.IngredientsConfig",
+ "cookbook.recipes.apps.RecipesConfig",
]
MIDDLEWARE_CLASSES = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.auth.middleware.SessionAuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'MIDDLEWARE': (
- 'graphene_django.debug.DjangoDebugMiddleware',
- )
+ "SCHEMA": "cookbook.schema.schema",
+ "SCHEMA_INDENT": 2,
+ "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
-ROOT_URLCONF = 'cookbook.urls'
+ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ],
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ]
},
- },
+ }
]
-WSGI_APPLICATION = 'cookbook.wsgi.application'
+WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@@ -99,26 +97,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
USE_I18N = True
@@ -130,9 +122,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
-STATIC_URL = '/static/'
-
-GRAPHENE = {
- 'SCHEMA': 'cookbook.schema.schema',
- 'SCHEMA_INDENT': 2,
-}
+STATIC_URL = "/static/"
diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py
index 9f8755b..6f8a302 100644
--- a/examples/cookbook/cookbook/urls.py
+++ b/examples/cookbook/cookbook/urls.py
@@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
urlpatterns = [
- url(r'^admin/', admin.site.urls),
- url(r'^graphql', GraphQLView.as_view(graphiql=True)),
+ url(r"^admin/", admin.site.urls),
+ url(r"^graphql$", GraphQLView.as_view(graphiql=True)),
]
diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt
index b2ace1f..fe0527a 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.20
django-filter>=2
diff --git a/examples/starwars/data.py b/examples/starwars/data.py
index 9b52006..6bdbf57 100644
--- a/examples/starwars/data.py
+++ b/examples/starwars/data.py
@@ -2,97 +2,50 @@ from .models import Character, Faction, Ship
def initialize():
- human = Character(
- name='Human'
- )
+ human = Character(name="Human")
human.save()
- droid = Character(
- name='Droid'
- )
+ droid = Character(name="Droid")
droid.save()
- rebels = Faction(
- id='1',
- name='Alliance to Restore the Republic',
- hero=human
- )
+ rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human)
rebels.save()
- empire = Faction(
- id='2',
- name='Galactic Empire',
- hero=droid
- )
+ empire = Faction(id="2", name="Galactic Empire", hero=droid)
empire.save()
- xwing = Ship(
- id='1',
- name='X-Wing',
- faction=rebels,
- )
+ xwing = Ship(id="1", name="X-Wing", faction=rebels)
xwing.save()
human.ship = xwing
human.save()
- ywing = Ship(
- id='2',
- name='Y-Wing',
- faction=rebels,
- )
+ ywing = Ship(id="2", name="Y-Wing", faction=rebels)
ywing.save()
- awing = Ship(
- id='3',
- name='A-Wing',
- faction=rebels,
- )
+ awing = Ship(id="3", name="A-Wing", faction=rebels)
awing.save()
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship.
- falcon = Ship(
- id='4',
- name='Millenium Falcon',
- faction=rebels,
- )
+ falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
falcon.save()
- homeOne = Ship(
- id='5',
- name='Home One',
- faction=rebels,
- )
+ homeOne = Ship(id="5", name="Home One", faction=rebels)
homeOne.save()
- tieFighter = Ship(
- id='6',
- name='TIE Fighter',
- faction=empire,
- )
+ tieFighter = Ship(id="6", name="TIE Fighter", faction=empire)
tieFighter.save()
- tieInterceptor = Ship(
- id='7',
- name='TIE Interceptor',
- faction=empire,
- )
+ tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire)
tieInterceptor.save()
- executor = Ship(
- id='8',
- name='Executor',
- faction=empire,
- )
+ executor = Ship(id="8", name="Executor", faction=empire)
executor.save()
def create_ship(ship_name, faction_id):
- new_ship = Ship(
- name=ship_name,
- faction_id=faction_id
- )
+ new_ship = Ship(name=ship_name, faction_id=faction_id)
new_ship.save()
return new_ship
diff --git a/examples/starwars/models.py b/examples/starwars/models.py
index 45741da..03e06a2 100644
--- a/examples/starwars/models.py
+++ b/examples/starwars/models.py
@@ -5,7 +5,13 @@ from django.db import models
class Character(models.Model):
name = models.CharField(max_length=50)
- ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters')
+ ship = models.ForeignKey(
+ "Ship",
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ related_name="characters",
+ )
def __str__(self):
return self.name
@@ -21,7 +27,7 @@ class Faction(models.Model):
class Ship(models.Model):
name = models.CharField(max_length=50)
- faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships')
+ faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
def __str__(self):
return self.name
diff --git a/examples/starwars/schema.py b/examples/starwars/schema.py
index 492918e..fb22840 100644
--- a/examples/starwars/schema.py
+++ b/examples/starwars/schema.py
@@ -2,18 +2,16 @@ import graphene
from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType
-from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
- get_ships)
+from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
from .models import Character as CharacterModel
from .models import Faction as FactionModel
from .models import Ship as ShipModel
class Ship(DjangoObjectType):
-
class Meta:
model = ShipModel
- interfaces = (relay.Node, )
+ interfaces = (relay.Node,)
@classmethod
def get_node(cls, info, id):
@@ -22,16 +20,14 @@ class Ship(DjangoObjectType):
class Character(DjangoObjectType):
-
class Meta:
model = CharacterModel
class Faction(DjangoObjectType):
-
class Meta:
model = FactionModel
- interfaces = (relay.Node, )
+ interfaces = (relay.Node,)
@classmethod
def get_node(cls, info, id):
@@ -39,7 +35,6 @@ class Faction(DjangoObjectType):
class IntroduceShip(relay.ClientIDMutation):
-
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
@@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction)
@classmethod
- def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
+ def mutate_and_get_payload(
+ cls, root, info, ship_name, faction_id, client_mutation_id=None
+ ):
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@@ -58,7 +55,7 @@ class Query(graphene.ObjectType):
rebels = graphene.Field(Faction)
empire = graphene.Field(Faction)
node = relay.Node.Field()
- ships = DjangoConnectionField(Ship, description='All the ships.')
+ ships = DjangoConnectionField(Ship, description="All the ships.")
@resolve_only_args
def resolve_ships(self):
diff --git a/examples/starwars/tests/test_connections.py b/examples/starwars/tests/test_connections.py
index d266df3..425dce5 100644
--- a/examples/starwars/tests/test_connections.py
+++ b/examples/starwars/tests/test_connections.py
@@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db
def test_correct_fetch_first_ship_rebels():
initialize()
- query = '''
+ query = """
query RebelsShipsQuery {
rebels {
name,
@@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels():
}
}
}
- '''
+ """
expected = {
- 'rebels': {
- 'name': 'Alliance to Restore the Republic',
- 'hero': {
- 'name': 'Human'
- },
- 'ships': {
- 'edges': [
- {
- 'node': {
- 'name': 'X-Wing'
- }
- }
- ]
- }
+ "rebels": {
+ "name": "Alliance to Restore the Republic",
+ "hero": {"name": "Human"},
+ "ships": {"edges": [{"node": {"name": "X-Wing"}}]},
}
}
result = schema.execute(query)
@@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels():
def test_correct_list_characters():
initialize()
- query = '''
+ query = """
query RebelsShipsQuery {
node(id: "U2hpcDox") {
... on Ship {
@@ -60,15 +50,8 @@ def test_correct_list_characters():
}
}
}
- '''
- expected = {
- 'node': {
- 'name': 'X-Wing',
- 'characters': [{
- 'name': 'Human'
- }],
- }
- }
+ """
+ expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
diff --git a/examples/starwars/tests/test_mutation.py b/examples/starwars/tests/test_mutation.py
index aa312ff..e24bf8a 100644
--- a/examples/starwars/tests/test_mutation.py
+++ b/examples/starwars/tests/test_mutation.py
@@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
def test_mutations():
initialize()
- query = '''
+ query = """
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
@@ -29,49 +29,23 @@ def test_mutations():
}
}
}
- '''
+ """
expected = {
- 'introduceShip': {
- 'ship': {
- 'id': 'U2hpcDo5',
- 'name': 'Peter'
- },
- 'faction': {
- 'name': 'Alliance to Restore the Republic',
- 'ships': {
- 'edges': [{
- 'node': {
- 'id': 'U2hpcDox',
- 'name': 'X-Wing'
- }
- }, {
- 'node': {
- 'id': 'U2hpcDoy',
- 'name': 'Y-Wing'
- }
- }, {
- 'node': {
- 'id': 'U2hpcDoz',
- 'name': 'A-Wing'
- }
- }, {
- 'node': {
- 'id': 'U2hpcDo0',
- 'name': 'Millenium Falcon'
- }
- }, {
- 'node': {
- 'id': 'U2hpcDo1',
- 'name': 'Home One'
- }
- }, {
- 'node': {
- 'id': 'U2hpcDo5',
- 'name': 'Peter'
- }
- }]
+ "introduceShip": {
+ "ship": {"id": "U2hpcDo5", "name": "Peter"},
+ "faction": {
+ "name": "Alliance to Restore the Republic",
+ "ships": {
+ "edges": [
+ {"node": {"id": "U2hpcDox", "name": "X-Wing"}},
+ {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
+ {"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
+ {"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
+ {"node": {"id": "U2hpcDo1", "name": "Home One"}},
+ {"node": {"id": "U2hpcDo5", "name": "Peter"}},
+ ]
},
- }
+ },
}
}
result = schema.execute(query)
diff --git a/examples/starwars/tests/test_objectidentification.py b/examples/starwars/tests/test_objectidentification.py
index fad1958..6e04a7b 100644
--- a/examples/starwars/tests/test_objectidentification.py
+++ b/examples/starwars/tests/test_objectidentification.py
@@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db
def test_correctly_fetches_id_name_rebels():
initialize()
- query = '''
+ query = """
query RebelsQuery {
rebels {
id
name
}
}
- '''
+ """
expected = {
- 'rebels': {
- 'id': 'RmFjdGlvbjox',
- 'name': 'Alliance to Restore the Republic'
- }
+ "rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels():
def test_correctly_refetches_rebels():
initialize()
- query = '''
+ query = """
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
@@ -38,12 +35,9 @@ def test_correctly_refetches_rebels():
}
}
}
- '''
+ """
expected = {
- 'node': {
- 'id': 'RmFjdGlvbjox',
- 'name': 'Alliance to Restore the Republic'
- }
+ "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@@ -52,20 +46,15 @@ def test_correctly_refetches_rebels():
def test_correctly_fetches_id_name_empire():
initialize()
- query = '''
+ query = """
query EmpireQuery {
empire {
id
name
}
}
- '''
- expected = {
- 'empire': {
- 'id': 'RmFjdGlvbjoy',
- 'name': 'Galactic Empire'
- }
- }
+ """
+ expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire():
def test_correctly_refetches_empire():
initialize()
- query = '''
+ query = """
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
@@ -82,13 +71,8 @@ def test_correctly_refetches_empire():
}
}
}
- '''
- expected = {
- 'node': {
- 'id': 'RmFjdGlvbjoy',
- 'name': 'Galactic Empire'
- }
- }
+ """
+ expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@@ -96,7 +80,7 @@ def test_correctly_refetches_empire():
def test_correctly_refetches_xwing():
initialize()
- query = '''
+ query = """
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
@@ -105,13 +89,8 @@ def test_correctly_refetches_xwing():
}
}
}
- '''
- expected = {
- 'node': {
- 'id': 'U2hpcDox',
- 'name': 'X-Wing'
- }
- }
+ """
+ expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py
index 4538cb3..51acfd2 100644
--- a/graphene_django/__init__.py
+++ b/graphene_django/__init__.py
@@ -1,6 +1,6 @@
from .types import DjangoObjectType
from .fields import DjangoConnectionField
-__version__ = "2.2.0"
+__version__ = "2.3.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
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/converter.py b/graphene_django/converter.py
index c40313d..1bb16f4 100644
--- a/graphene_django/converter.py
+++ b/graphene_django/converter.py
@@ -177,19 +177,26 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type:
return
+ description = (
+ field.help_text
+ if isinstance(field, models.ManyToManyField)
+ else field.field.help_text
+ )
+
# If there is a connection, we should transform the field
# into a DjangoConnectionField
if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are
- # defined filter_fields in the DjangoObjectType Meta
- if _type._meta.filter_fields:
+ # defined filter_fields or a filterset_class in the
+ # DjangoObjectType Meta
+ if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField
- return DjangoFilterConnectionField(_type)
+ return DjangoFilterConnectionField(_type, description=description)
- return DjangoConnectionField(_type)
+ return DjangoConnectionField(_type, description=description)
- return DjangoListField(_type)
+ return DjangoListField(_type, description=description)
return Dynamic(dynamic_type)
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..af69715 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,9 +50,7 @@ def test_should_query_field():
"""
expected = {
"reporter": {"lastName": "ABA"},
- "__debug": {
- "sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
- },
+ "_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
}
schema = graphene.Schema(query=Query)
result = schema.execute(
@@ -75,7 +73,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 +83,7 @@ def test_should_query_list():
allReporters {
lastName
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -94,7 +92,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 +115,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 +129,7 @@ def test_should_query_connection():
}
}
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -145,9 +143,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 +164,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 +178,7 @@ def test_should_query_connectionfilter():
}
}
}
- __debug {
+ _debug {
sql {
rawSql
}
@@ -194,6 +192,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 7261baf..948a520 100644
--- a/graphene_django/fields.py
+++ b/graphene_django/fields.py
@@ -1,10 +1,12 @@
from functools import partial
from django.db.models.query import QuerySet
-from graphene.relay import ConnectionField, PageInfo
-from graphene.types import Field, List
+from graphene import NonNull
+
from promise import Promise
+from graphene.types import Field, List
+from graphene.relay import ConnectionField, PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from .settings import graphene_settings
@@ -44,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):
@@ -66,6 +82,10 @@ class DjangoConnectionField(ConnectionField):
else:
return self.model._default_manager
+ @classmethod
+ def resolve_queryset(cls, connection, queryset, info, args):
+ return connection._meta.node.get_queryset(queryset, info)
+
@classmethod
def merge_querysets(cls, default_queryset, queryset):
if default_queryset.query.distinct and not queryset.query.distinct:
@@ -138,8 +158,8 @@ class DjangoConnectionField(ConnectionField):
"exceeds the limit of {max_limit} records.").format(**locals()))
iterable = resolver(root, info, **kwargs)
- on_resolve = partial(cls.resolve_connection,
- connection, default_manager, kwargs)
+ queryset = cls.resolve_queryset(connection, default_manager, info, kwargs)
+ on_resolve = partial(cls.resolve_connection, connection, queryset, kwargs)
if Promise.is_thenable(iterable):
return Promise.resolve(iterable).then(on_resolve)
@@ -150,7 +170,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/fields.py b/graphene_django/filter/fields.py
index cb42543..62f4b1a 100644
--- a/graphene_django/filter/fields.py
+++ b/graphene_django/filter/fields.py
@@ -40,9 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
if self._extra_filter_meta:
meta.update(self._extra_filter_meta)
- self._filterset_class = get_filterset_class(
- self._provided_filterset_class, **meta
+ filterset_class = self._provided_filterset_class or (
+ self.node_type._meta.filterset_class
)
+ self._filterset_class = get_filterset_class(filterset_class, **meta)
return self._filterset_class
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/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py
index f5d2fb7..0dbfd26 100644
--- a/graphene_django/filter/tests/test_fields.py
+++ b/graphene_django/filter/tests/test_fields.py
@@ -228,6 +228,74 @@ def test_filter_filterset_information_on_meta_related():
assert_not_orderable(articles_field)
+def test_filter_filterset_class_filter_fields_exception():
+ with pytest.raises(Exception):
+
+ class ReporterFilter(FilterSet):
+ class Meta:
+ model = Reporter
+ fields = ["first_name", "articles"]
+
+ class ReporterFilterNode(DjangoObjectType):
+ class Meta:
+ model = Reporter
+ interfaces = (Node,)
+ filterset_class = ReporterFilter
+ filter_fields = ["first_name", "articles"]
+
+
+def test_filter_filterset_class_information_on_meta():
+ class ReporterFilter(FilterSet):
+ class Meta:
+ model = Reporter
+ fields = ["first_name", "articles"]
+
+ class ReporterFilterNode(DjangoObjectType):
+ class Meta:
+ model = Reporter
+ interfaces = (Node,)
+ filterset_class = ReporterFilter
+
+ field = DjangoFilterConnectionField(ReporterFilterNode)
+ assert_arguments(field, "first_name", "articles")
+ assert_not_orderable(field)
+
+
+def test_filter_filterset_class_information_on_meta_related():
+ class ReporterFilter(FilterSet):
+ class Meta:
+ model = Reporter
+ fields = ["first_name", "articles"]
+
+ class ArticleFilter(FilterSet):
+ class Meta:
+ model = Article
+ fields = ["headline", "reporter"]
+
+ class ReporterFilterNode(DjangoObjectType):
+ class Meta:
+ model = Reporter
+ interfaces = (Node,)
+ filterset_class = ReporterFilter
+
+ class ArticleFilterNode(DjangoObjectType):
+ class Meta:
+ model = Article
+ interfaces = (Node,)
+ filterset_class = ArticleFilter
+
+ class Query(ObjectType):
+ all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
+ all_articles = DjangoFilterConnectionField(ArticleFilterNode)
+ reporter = Field(ReporterFilterNode)
+ article = Field(ArticleFilterNode)
+
+ schema = Schema(query=Query)
+ articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
+ assert_arguments(articles_field, "headline", "reporter")
+ assert_not_orderable(articles_field)
+
+
def test_filter_filterset_related_results():
class ReporterFilterNode(DjangoObjectType):
class Meta:
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/mutation.py b/graphene_django/forms/mutation.py
index 63ea089..0851a75 100644
--- a/graphene_django/forms/mutation.py
+++ b/graphene_django/forms/mutation.py
@@ -14,7 +14,7 @@ from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry
from .converter import convert_form_field
-from .types import ErrorType
+from ..types import ErrorType
def fields_for_form(form, only_fields, exclude_fields):
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 4e526ec..1e8baf6 100644
--- a/graphene_django/management/commands/graphql_schema.py
+++ b/graphene_django/management/commands/graphql_schema.py
@@ -1,7 +1,9 @@
import importlib
import json
+import functools
from django.core.management.base import BaseCommand, CommandError
+from django.utils import autoreload
from graphene_django.settings import graphene_settings
@@ -32,6 +34,14 @@ class CommandArguments(BaseCommand):
help="Output file indent (default: None)",
)
+ parser.add_argument(
+ "--watch",
+ dest="watch",
+ default=False,
+ action="store_true",
+ help="Updates the schema on file changes (default: False)",
+ )
+
class Command(CommandArguments):
help = "Dump Graphene schema JSON to file"
@@ -39,7 +49,19 @@ class Command(CommandArguments):
def save_file(self, out, schema_dict, indent):
with open(out, "w") as outfile:
- json.dump(schema_dict, outfile, indent=indent)
+ json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
+
+ def get_schema(self, schema, out, indent):
+ schema_dict = {"data": schema.introspect()}
+ if out == "-":
+ self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
+ else:
+ self.save_file(out, schema_dict, indent)
+
+ style = getattr(self, "style", None)
+ success = getattr(style, "SUCCESS", lambda x: x)
+
+ self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
def handle(self, *args, **options):
options_schema = options.get("schema")
@@ -63,13 +85,10 @@ class Command(CommandArguments):
)
indent = options.get("indent")
- schema_dict = {"data": schema.introspect()}
- if out == '-':
- self.stdout.write(json.dumps(schema_dict, indent=indent))
+ watch = options.get("watch")
+ if watch:
+ autoreload.run_with_reloader(
+ functools.partial(self.get_schema, schema, out, indent)
+ )
else:
- self.save_file(out, schema_dict, indent)
-
- style = getattr(self, "style", None)
- success = getattr(style, "SUCCESS", lambda x: x)
-
- self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
+ self.get_schema(schema, out, indent)
diff --git a/graphene_django/rest_framework/models.py b/graphene_django/rest_framework/models.py
index 848837b..06d9b60 100644
--- a/graphene_django/rest_framework/models.py
+++ b/graphene_django/rest_framework/models.py
@@ -4,3 +4,8 @@ from django.db import models
class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
+
+
+class MyFakeModelWithPassword(models.Model):
+ cool_name = models.CharField(max_length=50)
+ password = models.CharField(max_length=50)
diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py
index 5e343aa..0fe9a02 100644
--- a/graphene_django/rest_framework/mutation.py
+++ b/graphene_django/rest_framework/mutation.py
@@ -9,7 +9,7 @@ from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs
from .serializer_converter import convert_serializer_field
-from .types import ErrorType
+from ..types import ErrorType
class SerializerMutationOptions(MutationOptions):
@@ -27,6 +27,8 @@ def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=Fals
name
in exclude_fields # or
# name in already_created_fields
+ ) or (
+ field.write_only and not is_input # don't show write_only fields in Query
)
if is_not_in_only or is_excluded:
@@ -138,6 +140,7 @@ class SerializerMutation(ClientIDMutation):
kwargs = {}
for f, field in serializer.fields.items():
- kwargs[f] = field.get_attribute(obj)
+ if not field.write_only:
+ kwargs[f] = field.get_attribute(obj)
return cls(errors=None, **kwargs)
diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py
index 4dccc18..9621ee3 100644
--- a/graphene_django/rest_framework/tests/test_mutation.py
+++ b/graphene_django/rest_framework/tests/test_mutation.py
@@ -7,7 +7,7 @@ from py.test import mark
from rest_framework import serializers
from ...types import DjangoObjectType
-from ..models import MyFakeModel
+from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation
@@ -86,6 +86,51 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.fields
+@mark.django_db
+def test_write_only_field():
+ class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
+ password = serializers.CharField(write_only=True)
+
+ class Meta:
+ model = MyFakeModelWithPassword
+ fields = ["cool_name", "password"]
+
+ class MyMutation(SerializerMutation):
+ class Meta:
+ serializer_class = WriteOnlyFieldModelSerializer
+
+ result = MyMutation.mutate_and_get_payload(
+ None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
+ )
+
+ assert hasattr(result, "cool_name")
+ assert not hasattr(
+ result, "password"
+ ), "'password' is write_only field and shouldn't be visible"
+
+
+@mark.django_db
+def test_write_only_field_using_extra_kwargs():
+ class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = MyFakeModelWithPassword
+ fields = ["cool_name", "password"]
+ extra_kwargs = {"password": {"write_only": True}}
+
+ class MyMutation(SerializerMutation):
+ class Meta:
+ serializer_class = WriteOnlyFieldModelSerializer
+
+ result = MyMutation.mutate_and_get_payload(
+ None, mock_info(), **{"cool_name": "New Narf", "password": "admin"}
+ )
+
+ assert hasattr(result, "cool_name")
+ assert not hasattr(
+ result, "password"
+ ), "'password' is write_only field and shouldn't be visible"
+
+
def test_nested_model():
class MyFakeModelGrapheneType(DjangoObjectType):
class Meta:
diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py
index 4c84c69..2472c32 100644
--- a/graphene_django/rest_framework/types.py
+++ b/graphene_django/rest_framework/types.py
@@ -2,11 +2,6 @@ import graphene
from graphene.types.unmountedtype import UnmountedType
-class ErrorType(graphene.ObjectType):
- field = graphene.String(required=True)
- messages = graphene.List(graphene.NonNull(graphene.String), required=True)
-
-
class DictType(UnmountedType):
key = graphene.String()
value = graphene.String()
diff --git a/graphene_django/settings.py b/graphene_django/settings.py
index 7cd750a..e5fad78 100644
--- a/graphene_django/settings.py
+++ b/graphene_django/settings.py
@@ -28,7 +28,7 @@ except ImportError:
DEFAULTS = {
"SCHEMA": None,
"SCHEMA_OUTPUT": "schema.json",
- "SCHEMA_INDENT": None,
+ "SCHEMA_INDENT": 2,
"MIDDLEWARE": (),
# Set to True if the connection fields must have
# either the first or last argument
diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js
new file mode 100644
index 0000000..2be7e3c
--- /dev/null
+++ b/graphene_django/static/graphene_django/graphiql.js
@@ -0,0 +1,99 @@
+(function() {
+
+ // Parse the cookie value for a CSRF token
+ var csrftoken;
+ var cookies = ('; ' + document.cookie).split('; csrftoken=');
+ if (cookies.length == 2)
+ csrftoken = cookies.pop().split(';').shift();
+
+ // Collect the URL parameters
+ var parameters = {};
+ window.location.hash.substr(1).split('&').forEach(function (entry) {
+ var eq = entry.indexOf('=');
+ if (eq >= 0) {
+ parameters[decodeURIComponent(entry.slice(0, eq))] =
+ decodeURIComponent(entry.slice(eq + 1));
+ }
+ });
+ // Produce a Location fragment string from a parameter object.
+ function locationQuery(params) {
+ return '#' + Object.keys(params).map(function (key) {
+ return encodeURIComponent(key) + '=' +
+ encodeURIComponent(params[key]);
+ }).join('&');
+ }
+ // Derive a fetch URL from the current URL, sans the GraphQL parameters.
+ var graphqlParamNames = {
+ query: true,
+ variables: true,
+ operationName: true
+ };
+ var otherParams = {};
+ for (var k in parameters) {
+ if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
+ otherParams[k] = parameters[k];
+ }
+ }
+
+ var fetchURL = locationQuery(otherParams);
+
+ // Defines a GraphQL fetcher using the fetch API.
+ function graphQLFetcher(graphQLParams) {
+ var headers = {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ };
+ if (csrftoken) {
+ headers['X-CSRFToken'] = csrftoken;
+ }
+ return fetch(fetchURL, {
+ method: 'post',
+ headers: headers,
+ body: JSON.stringify(graphQLParams),
+ credentials: 'include',
+ }).then(function (response) {
+ return response.text();
+ }).then(function (responseBody) {
+ try {
+ return JSON.parse(responseBody);
+ } catch (error) {
+ return responseBody;
+ }
+ });
+ }
+ // When the query and variables string is edited, update the URL bar so
+ // that it can be easily shared.
+ function onEditQuery(newQuery) {
+ parameters.query = newQuery;
+ updateURL();
+ }
+ function onEditVariables(newVariables) {
+ parameters.variables = newVariables;
+ updateURL();
+ }
+ function onEditOperationName(newOperationName) {
+ parameters.operationName = newOperationName;
+ updateURL();
+ }
+ function updateURL() {
+ history.replaceState(null, null, locationQuery(parameters));
+ }
+ var options = {
+ fetcher: graphQLFetcher,
+ onEditQuery: onEditQuery,
+ onEditVariables: onEditVariables,
+ onEditOperationName: onEditOperationName,
+ query: parameters.query,
+ }
+ if (parameters.variables) {
+ options.variables = parameters.variables;
+ }
+ if (parameters.operation_name) {
+ options.operationName = parameters.operation_name;
+ }
+ // Render into the body.
+ ReactDOM.render(
+ React.createElement(GraphiQL, options),
+ document.body
+ );
+})();
diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html
index 1ba0613..d0fb5a8 100644
--- a/graphene_django/templates/graphene/graphiql.html
+++ b/graphene_django/templates/graphene/graphiql.html
@@ -5,6 +5,7 @@ exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
+{% load static %}
@@ -16,108 +17,20 @@ add "&raw" to the end of the URL within a browser.
width: 100%;
}
-
-
-
-
-
+
+
+
+
+
-
+
diff --git a/graphene_django/tests/issues/__init__.py b/graphene_django/tests/issues/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/graphene_django/tests/issues/test_520.py b/graphene_django/tests/issues/test_520.py
new file mode 100644
index 0000000..60c5b54
--- /dev/null
+++ b/graphene_django/tests/issues/test_520.py
@@ -0,0 +1,44 @@
+# https://github.com/graphql-python/graphene-django/issues/520
+
+import datetime
+
+from django import forms
+
+import graphene
+
+from graphene import Field, ResolveInfo
+from graphene.types.inputobjecttype import InputObjectType
+from py.test import raises
+from py.test import mark
+from rest_framework import serializers
+
+from ...types import DjangoObjectType
+from ...rest_framework.models import MyFakeModel
+from ...rest_framework.mutation import SerializerMutation
+from ...forms.mutation import DjangoFormMutation
+
+
+class MyModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = MyFakeModel
+ fields = "__all__"
+
+
+class MyForm(forms.Form):
+ text = forms.CharField()
+
+
+def test_can_use_form_and_serializer_mutations():
+ class MyMutation(SerializerMutation):
+ class Meta:
+ serializer_class = MyModelSerializer
+
+ class MyFormMutation(DjangoFormMutation):
+ class Meta:
+ form_class = MyForm
+
+ class Mutation(graphene.ObjectType):
+ my_mutation = MyMutation.Field()
+ my_form_mutation = MyFormMutation.Field()
+
+ graphene.Schema(mutation=Mutation)
diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py
index 4fe546d..b4eb3ce 100644
--- a/graphene_django/tests/models.py
+++ b/graphene_django/tests/models.py
@@ -65,6 +65,11 @@ class Reporter(models.Model):
self.__class__ = CNNReporter
+class CNNReporterManager(models.Manager):
+ def get_queryset(self):
+ return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
+
+
class CNNReporter(Reporter):
"""
This class is a proxy model for Reporter, used for testing
@@ -74,6 +79,8 @@ class CNNReporter(Reporter):
class Meta:
proxy = True
+ objects = CNNReporterManager()
+
class Article(models.Model):
headline = models.CharField(max_length=100)
diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py
index ff6e6e1..dbabafa 100644
--- a/graphene_django/tests/test_command.py
+++ b/graphene_django/tests/test_command.py
@@ -1,5 +1,5 @@
from django.core import management
-from mock import patch
+from mock import patch, mock_open
from six import StringIO
@@ -8,3 +8,20 @@ def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
out = StringIO()
management.call_command("graphql_schema", schema="", stdout=out)
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
+
+
+@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="")
+
+ 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"
diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py
index 5dc0184..bb176b3 100644
--- a/graphene_django/tests/test_converter.py
+++ b/graphene_django/tests/test_converter.py
@@ -83,7 +83,7 @@ def test_should_image_convert_string():
assert_conversion(models.ImageField, graphene.String)
-def test_should_url_convert_string():
+def test_should_file_path_field_convert_string():
assert_conversion(models.FilePathField, graphene.String)
@@ -91,7 +91,7 @@ def test_should_auto_convert_id():
assert_conversion(models.AutoField, graphene.ID, primary_key=True)
-def test_should_auto_convert_id():
+def test_should_uuid_convert_id():
assert_conversion(models.UUIDField, graphene.UUID)
@@ -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/tests/test_query.py b/graphene_django/tests/test_query.py
index 172946c..f4f48f9 100644
--- a/graphene_django/tests/test_query.py
+++ b/graphene_django/tests/test_query.py
@@ -1,19 +1,23 @@
+import base64
import datetime
-import graphene
import pytest
from django.db import models
-from django.db.models import Q
from django.utils.functional import SimpleLazyObject
-from graphene.relay import Node
from py.test import raises
-from ..compat import JSONField, MissingType
-from ..fields import DjangoConnectionField
-from ..settings import graphene_settings
-from ..types import DjangoObjectType
+from django.db.models import Q
+
+from graphql_relay import to_global_id
+import graphene
+from graphene.relay import Node
+
from ..utils import DJANGO_FILTER_INSTALLED
-from .models import Article, CNNReporter, Film, FilmDetails, Reporter
+from ..compat import MissingType, JSONField
+from ..fields import DjangoConnectionField
+from ..types import DjangoObjectType
+from ..settings import graphene_settings
+from .models import Article, CNNReporter, Reporter, Film, FilmDetails
pytestmark = pytest.mark.django_db
@@ -224,6 +228,62 @@ def test_should_node():
assert result.data == expected
+def test_should_query_onetoone_fields():
+ film = Film(id=1)
+ film_details = FilmDetails(id=1, film=film)
+
+ class FilmNode(DjangoObjectType):
+ class Meta:
+ model = Film
+ interfaces = (Node,)
+
+ class FilmDetailsNode(DjangoObjectType):
+ class Meta:
+ model = FilmDetails
+ interfaces = (Node,)
+
+ class Query(graphene.ObjectType):
+ film = graphene.Field(FilmNode)
+ film_details = graphene.Field(FilmDetailsNode)
+
+ def resolve_film(root, info):
+ return film
+
+ def resolve_film_details(root, info):
+ return film_details
+
+ query = """
+ query FilmQuery {
+ filmDetails {
+ id
+ film {
+ id
+ }
+ }
+ film {
+ id
+ details {
+ id
+ }
+ }
+ }
+ """
+ expected = {
+ "filmDetails": {
+ "id": "RmlsbURldGFpbHNOb2RlOjE=",
+ "film": {"id": "RmlsbU5vZGU6MQ=="},
+ },
+ "film": {
+ "id": "RmlsbU5vZGU6MQ==",
+ "details": {"id": "RmlsbURldGFpbHNOb2RlOjE="},
+ },
+ }
+ schema = graphene.Schema(query=Query)
+ result = schema.execute(query)
+ assert not result.errors
+ assert result.data == expected
+
+
def test_should_query_connectionfields():
class ReporterType(DjangoObjectType):
class Meta:
@@ -1061,8 +1121,7 @@ def test_should_handle_inherited_choices():
def test_proxy_model_support():
"""
- This test asserts that we can query for all Reporters,
- even if some are of a proxy model type at runtime.
+ This test asserts that we can query for all Reporters and proxied Reporters.
"""
class ReporterType(DjangoObjectType):
@@ -1071,11 +1130,17 @@ def test_proxy_model_support():
interfaces = (Node,)
use_connection = True
- reporter_1 = Reporter.objects.create(
+ class CNNReporterType(DjangoObjectType):
+ class Meta:
+ model = CNNReporter
+ interfaces = (Node,)
+ use_connection = True
+
+ reporter = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
- reporter_2 = CNNReporter.objects.create(
+ cnn_reporter = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
email="someguy@cnn.com",
@@ -1085,6 +1150,7 @@ def test_proxy_model_support():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
+ cnn_reporters = DjangoConnectionField(CNNReporterType)
schema = graphene.Schema(query=Query)
query = """
@@ -1096,16 +1162,28 @@ def test_proxy_model_support():
}
}
}
+ cnnReporters {
+ edges {
+ node {
+ id
+ }
+ }
+ }
}
"""
expected = {
"allReporters": {
"edges": [
- {"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
- {"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
+ {"node": {"id": to_global_id("ReporterType", reporter.id)}},
+ {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}},
]
- }
+ },
+ "cnnReporters": {
+ "edges": [
+ {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}}
+ ]
+ },
}
result = schema.execute(query)
@@ -1113,31 +1191,10 @@ def test_proxy_model_support():
assert result.data == expected
-def test_proxy_model_fails():
- """
- This test asserts that if you try to query for a proxy model,
- that query will fail with:
- GraphQLError('Expected value of type "CNNReporterType" but got:
- CNNReporter.',)
-
- This is because a proxy model has the identical model definition
- to its superclass, and defines its behavior at runtime, rather than
- at the database level. Currently, filtering objects of the proxy models'
- type isn't supported. It would require a field on the model that would
- represent the type, and it doesn't seem like there is a clear way to
- enforce this pattern across all projects
- """
-
- class CNNReporterType(DjangoObjectType):
- class Meta:
- model = CNNReporter
- interfaces = (Node,)
- use_connection = True
-
+def test_should_resolve_get_queryset_connectionfields():
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
-
reporter_2 = CNNReporter.objects.create(
first_name="Some",
last_name="Guy",
@@ -1146,13 +1203,22 @@ def test_proxy_model_fails():
reporter_type=2, # set this guy to be CNN
)
+ class ReporterType(DjangoObjectType):
+ class Meta:
+ model = Reporter
+ interfaces = (Node,)
+
+ @classmethod
+ def get_queryset(cls, queryset, info):
+ return queryset.filter(reporter_type=2)
+
class Query(graphene.ObjectType):
- all_reporters = DjangoConnectionField(CNNReporterType)
+ all_reporters = DjangoConnectionField(ReporterType)
schema = graphene.Schema(query=Query)
query = """
- query ProxyModelQuery {
- allReporters {
+ query ReporterPromiseConnectionQuery {
+ allReporters(first: 1) {
edges {
node {
id
@@ -1162,14 +1228,8 @@ def test_proxy_model_fails():
}
"""
- expected = {
- "allReporters": {
- "edges": [
- {"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}},
- {"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}},
- ]
- }
- }
+ expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}]}}
result = schema.execute(query)
- assert result.errors
+ assert not result.errors
+ assert result.data == expected
diff --git a/graphene_django/types.py b/graphene_django/types.py
index aa8b5a3..a1e17b3 100644
--- a/graphene_django/types.py
+++ b/graphene_django/types.py
@@ -1,6 +1,9 @@
+import six
from collections import OrderedDict
+from django.db.models import Model
from django.utils.functional import SimpleLazyObject
+import graphene
from graphene import Field
from graphene.relay import Connection, Node
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
@@ -11,6 +14,10 @@ from .registry import Registry, get_global_registry
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
+if six.PY3:
+ from typing import Type
+
+
def construct_fields(model, registry, only_fields, exclude_fields):
_model_fields = get_model_fields(model)
@@ -38,6 +45,7 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
connection = None # type: Type[Connection]
filter_fields = ()
+ filterset_class = None
class DjangoObjectType(ObjectType):
@@ -50,6 +58,7 @@ class DjangoObjectType(ObjectType):
only_fields=(),
exclude_fields=(),
filter_fields=None,
+ filterset_class=None,
connection=None,
connection_class=None,
use_connection=None,
@@ -69,8 +78,16 @@ class DjangoObjectType(ObjectType):
'Registry, received "{}".'
).format(cls.__name__, registry)
- if not DJANGO_FILTER_INSTALLED and filter_fields:
- raise Exception("Can only set filter_fields if Django-Filter is installed")
+ if filter_fields and filterset_class:
+ raise Exception("Can't set both filter_fields and filterset_class")
+
+ if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
+ raise Exception(
+ (
+ "Can only set filter_fields or filterset_class if "
+ "Django-Filter is installed"
+ )
+ )
django_fields = yank_fields_from_attrs(
construct_fields(model, registry, only_fields, exclude_fields), _as=Field
@@ -101,6 +118,7 @@ class DjangoObjectType(ObjectType):
_meta.model = model
_meta.registry = registry
_meta.filter_fields = filter_fields
+ _meta.filterset_class = filterset_class
_meta.fields = django_fields
_meta.connection = connection
@@ -124,12 +142,26 @@ class DjangoObjectType(ObjectType):
if not is_valid_django_model(type(root)):
raise Exception(('Received incompatible instance "{}".').format(root))
- model = root._meta.model._meta.concrete_model
+ if cls._meta.model._meta.proxy:
+ model = root._meta.model
+ else:
+ model = root._meta.model._meta.concrete_model
+
return model == cls._meta.model
+ @classmethod
+ def get_queryset(cls, queryset, info):
+ return queryset
+
@classmethod
def get_node(cls, info, id):
+ queryset = cls.get_queryset(cls._meta.model.objects, info)
try:
- return cls._meta.model.objects.get(pk=id)
+ return queryset.get(pk=id)
except cls._meta.model.DoesNotExist:
return None
+
+
+class ErrorType(ObjectType):
+ field = graphene.String(required=True)
+ messages = graphene.List(graphene.NonNull(graphene.String), required=True)
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..db3e9f4
--- /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()
+
+ 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 93%
rename from graphene_django/utils.py
rename to graphene_django/utils/utils.py
index 560f604..b8aaba0 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,7 +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'
+ # "rel" for FK and M2M relations and "related" for O2O Relations
related = getattr(attr, "rel", None) or getattr(attr, "related", None)
if isinstance(related, models.ManyToOneRel):
yield (name, related)
diff --git a/graphene_django/views.py b/graphene_django/views.py
index be7ccf9..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
@@ -124,6 +125,13 @@ class GraphQLView(View):
data = self.parse_body(request)
show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
+ if show_graphiql:
+ return self.render_graphiql(
+ request,
+ graphiql_version=self.graphiql_version,
+ react_version=self.react_version,
+ )
+
if self.batch:
responses = [self.get_response(request, entry) for entry in data]
result = "[{}]".format(
@@ -137,19 +145,6 @@ class GraphQLView(View):
else:
result, status_code = self.get_response(request, data, show_graphiql)
- if show_graphiql:
- query, variables, operation_name, id = self.get_graphql_params(
- request, data
- )
- return self.render_graphiql(
- request,
- graphiql_version=self.graphiql_version,
- query=query or "",
- variables=json.dumps(variables) or "",
- operation_name=operation_name or "",
- result=result or "",
- )
-
return HttpResponse(
status=status_code, content=result, content_type="application/json"
)
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",
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..58f283a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+envlist =
+ py{27,35,36,37}-django{111,20,21,22,master},
+ black,flake8
+
+[travis:env]
+DJANGO =
+ 1.11: django111
+ 2.0: django20
+ 2.1: django21
+ 2.2: django22
+ master: djangomaster
+
+[testenv]
+passenv = *
+usedevelop = True
+setenv =
+ DJANGO_SETTINGS_MODULE=django_test_settings
+deps =
+ -e.[test]
+ psycopg2
+ django111: Django>=1.11,<2.0
+ django20: Django>=2.0,<2.1
+ django21: Django>=2.1,<2.2
+ django22: Django>=2.2,<3.0
+ djangomaster: https://github.com/django/django/archive/master.zip
+commands = {posargs:py.test --cov=graphene_django graphene_django examples}
+
+[testenv:black]
+basepython = python3.7
+deps = black
+commands =
+ black --exclude "/migrations/" graphene_django examples --check
+
+[testenv:flake8]
+basepython = python3.7
+deps = flake8
+commands =
+ flake8 graphene_django examples