Merge branch with 'master'

This commit is contained in:
NateScarlet 2019-06-12 23:04:36 +08:00
commit 2909928363
No known key found for this signature in database
GPG Key ID: 5C242793B070309C
97 changed files with 2289 additions and 995 deletions

17
.github/stale.yml vendored Normal file
View File

@ -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

2
.gitignore vendored
View File

@ -78,3 +78,5 @@ Session.vim
*~
# auto-generated tag files
tags
.tox/
.pytest_cache/

View File

@ -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

76
CODE_OF_CONDUCT.md Normal file
View File

@ -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

62
CONTRIBUTING.md Normal file
View File

@ -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
```

View File

@ -1,2 +1,3 @@
include README.md LICENSE
recursive-include graphene_django/templates *
recursive-include graphene_django/static *

11
Makefile Normal file
View File

@ -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

View File

@ -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)

View File

@ -10,8 +10,14 @@ to learn how to upgrade to Graphene ``2.0``.
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
@ -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 <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 <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:
.. 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 <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

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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 <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
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
}
}
}

View File

@ -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.

View File

@ -1,16 +1,34 @@
Graphene-Django
===============
Contents:
Welcome to the Graphene-Django docs.
Graphene-Django is built on top of `Graphene <https://docs.graphene-python.org/en/latest/>`__.
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 <https://docs.graphene-python.org/en/latest/>`__ 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

69
docs/installation.rst Normal file
View File

@ -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.

View File

@ -1,17 +1,15 @@
Introspection Schema
====================
Relay uses `Babel Relay
Plugin <https://facebook.github.io/relay/docs/guides-babel-plugin.html>`__
that requires you to provide your GraphQL schema data.
Relay Modern uses `Babel Relay Plugin <https://facebook.github.io/relay/docs/en/installation-and-setup>`__ 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:

231
docs/mutations.rst Normal file
View File

@ -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 <https://docs.graphene-python.org/en/latest/types/mutations/>`__
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.

332
docs/queries.rst Normal file
View File

@ -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 <https://docs.graphene-python.org/en/latest/types/objecttypes/>`__.
Relay
-----
`Relay <http://docs.graphene-python.org/en/latest/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 <https://docs.graphene-python.org/en/latest/relay/nodes/>`__ 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 <https://graphql.org/learn/pagination/>`__ on the GraphQL community site.

View File

@ -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}

50
docs/schema.rst Normal file
View File

@ -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 <https://docs.graphene-python.org/en/latest/types/schema/>`__

103
docs/settings.rst Normal file
View File

@ -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 <https://docs.graphene-python.org/en/latest/execution/middleware/>`__ 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,
}

60
docs/testing.rst Normal file
View File

@ -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
...

View File

@ -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 <http://docs.graphene-python.org/en/latest/>`__ 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 <http://docs.graphene-python.org/en/latest/>`__
documentation but it is not essential to understand and use Graphene-Django in your project.

View File

@ -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 <https://django-filter.readthedocs.org>`__. See the
`usage
documentation <https://django-filter.readthedocs.org/en/latest/usage.html#the-filter>`__
documentation <https://django-filter.readthedocs.org/en/latest/guide/usage.html#the-filter>`__
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 <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.

View File

@ -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)

View File

@ -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"

View File

@ -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'},
),
]

View File

@ -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

View File

@ -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)

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -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"

View File

@ -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),
),
]

View File

@ -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"),
),
)

View File

@ -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()

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -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)

View File

@ -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/"

View File

@ -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)),
]

View File

@ -1,4 +1,4 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==1.9
django==2.1.6

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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"],
}

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -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"

View File

@ -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"),
),
)

View File

@ -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"],
}

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -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)

View File

@ -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/"

View File

@ -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)),
]

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==1.9
django==1.11.20
django-filter>=2

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -1,6 +1,6 @@
from .types import DjangoObjectType
from .fields import DjangoConnectionField
__version__ = "2.2.0"
__version__ = "2.3.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.")

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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 <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, options),
document.body
);
})();

View File

@ -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 %}
<!DOCTYPE html>
<html>
<head>
@ -16,108 +17,20 @@ add "&raw" to the end of the URL within a browser.
width: 100%;
}
</style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
rel="stylesheet"
crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react@{{react_version}}/umd/react.production.min.js"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@{{react_version}}/umd/react-dom.production.min.js"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
crossorigin="anonymous"></script>
</head>
<body>
<script>
// 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.search.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 query 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));
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: '{{ query|escapejs }}',
response: '{{ result|escapejs }}',
{% if variables %}
variables: '{{ variables|escapejs }}',
{% endif %}
{% if operation_name %}
operationName: '{{ operation_name|escapejs }}',
{% endif %}
}),
document.body
);
</script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
</body>
</html>

View File

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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",
]

View File

@ -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()))

View File

@ -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)

View File

@ -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"
)

View File

@ -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",

39
tox.ini Normal file
View File

@ -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