mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-08 22:33:13 +03:00
Merge branch with 'master'
This commit is contained in:
commit
2909928363
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal 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
2
.gitignore
vendored
|
@ -78,3 +78,5 @@ Session.vim
|
|||
*~
|
||||
# auto-generated tag files
|
||||
tags
|
||||
.tox/
|
||||
.pytest_cache/
|
||||
|
|
98
.travis.yml
98
.travis.yml
|
@ -1,54 +1,60 @@
|
|||
language: python
|
||||
sudo: false
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
cache: pip
|
||||
dist: xenial
|
||||
|
||||
install:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = build ]; then
|
||||
pip install -e .[test]
|
||||
pip install psycopg2 # Required for Django postgres fields testing
|
||||
pip install django==$DJANGO_VERSION
|
||||
python setup.py develop
|
||||
elif [ "$TEST_TYPE" = lint ]; then
|
||||
pip install flake8
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = lint ]; then
|
||||
echo "Checking Python code lint."
|
||||
flake8 graphene_django
|
||||
exit
|
||||
elif [ "$TEST_TYPE" = build ]; then
|
||||
py.test --cov=graphene_django graphene_django examples
|
||||
fi
|
||||
after_success:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = build ]; then
|
||||
coveralls
|
||||
fi
|
||||
env:
|
||||
matrix:
|
||||
- TEST_TYPE=build DJANGO_VERSION=1.11
|
||||
- pip install tox tox-travis
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- pip install coveralls
|
||||
- coveralls
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: '3.4'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.5'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
||||
- python: '3.5'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
||||
- python: '2.7'
|
||||
env: TEST_TYPE=lint
|
||||
- python: '3.6'
|
||||
env: TEST_TYPE=lint
|
||||
- python: 2.7
|
||||
env: DJANGO=1.11
|
||||
|
||||
- python: 3.5
|
||||
env: DJANGO=1.11
|
||||
- python: 3.5
|
||||
env: DJANGO=2.0
|
||||
- python: 3.5
|
||||
env: DJANGO=2.1
|
||||
- python: 3.5
|
||||
env: DJANGO=2.2
|
||||
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11
|
||||
- python: 3.6
|
||||
env: DJANGO=2.0
|
||||
- python: 3.6
|
||||
env: DJANGO=2.1
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2
|
||||
- python: 3.6
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.7
|
||||
env: DJANGO=1.11
|
||||
- python: 3.7
|
||||
env: DJANGO=2.0
|
||||
- python: 3.7
|
||||
env: DJANGO=2.1
|
||||
- python: 3.7
|
||||
env: DJANGO=2.2
|
||||
- python: 3.7
|
||||
env: DJANGO=master
|
||||
|
||||
- python: 3.7
|
||||
env: TOXENV=black,flake8
|
||||
|
||||
allow_failures:
|
||||
- env: DJANGO=master
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: syrusakbary
|
||||
|
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal 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
62
CONTRIBUTING.md
Normal 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
|
||||
```
|
|
@ -1,2 +1,3 @@
|
|||
include README.md LICENSE
|
||||
recursive-include graphene_django/templates *
|
||||
recursive-include graphene_django/static *
|
||||
|
|
11
Makefile
Normal file
11
Makefile
Normal 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
|
39
README.md
39
README.md
|
@ -7,7 +7,11 @@ Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/ma
|
|||
|
||||
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
||||
|
||||
## Installation
|
||||
## Documentation
|
||||
|
||||
[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/)
|
||||
|
||||
## Quickstart
|
||||
|
||||
For installing graphene, just run this command in your shell
|
||||
|
||||
|
@ -20,6 +24,7 @@ pip install "graphene-django>=2.0"
|
|||
```python
|
||||
INSTALLED_APPS = (
|
||||
# ...
|
||||
'django.contrib.staticfiles', # Required for GraphiQL
|
||||
'graphene_django',
|
||||
)
|
||||
|
||||
|
@ -38,7 +43,7 @@ from graphene_django.views import GraphQLView
|
|||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -95,32 +100,4 @@ To learn more check out the following [examples](examples/):
|
|||
|
||||
## Contributing
|
||||
|
||||
After cloning this repo, ensure dependencies are installed by running:
|
||||
|
||||
```sh
|
||||
pip install -e ".[test]"
|
||||
```
|
||||
|
||||
After developing, the full test suite can be evaluated by running:
|
||||
|
||||
```sh
|
||||
py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode
|
||||
```
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
|
||||
|
||||
The documentation dependencies are installed by running:
|
||||
|
||||
```sh
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Then to produce a HTML version of the documentation:
|
||||
|
||||
```sh
|
||||
make html
|
||||
```
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
43
README.rst
43
README.rst
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
89
docs/conf.py
89
docs/conf.py
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
@ -34,46 +34,44 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
|||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
]
|
||||
if not on_rtd:
|
||||
extensions += [
|
||||
'sphinx.ext.githubpages',
|
||||
]
|
||||
extensions += ["sphinx.ext.githubpages"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Graphene Django'
|
||||
copyright = u'Graphene 2017'
|
||||
author = u'Syrus Akbary'
|
||||
project = u"Graphene Django"
|
||||
copyright = u"Graphene 2017"
|
||||
author = u"Syrus Akbary"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'1.0'
|
||||
version = u"1.0"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'1.0.dev'
|
||||
release = u"1.0.dev"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -94,7 +92,7 @@ language = None
|
|||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
|
@ -116,7 +114,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
@ -175,7 +173,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()]
|
|||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
|
@ -255,34 +253,30 @@ html_static_path = ['_static']
|
|||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Graphenedoc'
|
||||
htmlhelp_basename = "Graphenedoc"
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Graphene.tex', u'Graphene Documentation',
|
||||
u'Syrus Akbary', 'manual'),
|
||||
(master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
@ -323,8 +317,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'graphene_django', u'Graphene Django Documentation',
|
||||
[author], 1)
|
||||
(master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
|
@ -338,9 +331,15 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Graphene-Django', u'Graphene Django Documentation',
|
||||
author, 'Graphene Django', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"Graphene-Django",
|
||||
u"Graphene Django Documentation",
|
||||
author,
|
||||
"Graphene Django",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
@ -414,7 +413,7 @@ epub_copyright = copyright
|
|||
# epub_post_files = []
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
# The depth of the table of contents in toc.ncx.
|
||||
#
|
||||
|
@ -446,4 +445,4 @@ epub_exclude_files = ['search.html']
|
|||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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
69
docs/installation.rst
Normal 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.
|
|
@ -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
231
docs/mutations.rst
Normal 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
332
docs/queries.rst
Normal 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.
|
|
@ -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
50
docs/schema.rst
Normal 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
103
docs/settings.rst
Normal 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
60
docs/testing.rst
Normal 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
|
||||
...
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==1.9
|
||||
django==2.1.6
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==1.9
|
||||
django==1.11.20
|
||||
django-filter>=2
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .types import DjangoObjectType
|
||||
from .fields import DjangoConnectionField
|
||||
|
||||
__version__ = "2.2.0"
|
||||
__version__ = "2.3.0"
|
||||
|
||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
99
graphene_django/static/graphene_django/graphiql.js
Normal file
99
graphene_django/static/graphene_django/graphiql.js
Normal 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
|
||||
);
|
||||
})();
|
|
@ -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>
|
||||
|
|
0
graphene_django/tests/issues/__init__.py
Normal file
0
graphene_django/tests/issues/__init__.py
Normal file
44
graphene_django/tests/issues/test_520.py
Normal file
44
graphene_django/tests/issues/test_520.py
Normal 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)
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
19
graphene_django/utils/__init__.py
Normal file
19
graphene_django/utils/__init__.py
Normal 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",
|
||||
]
|
67
graphene_django/utils/testing.py
Normal file
67
graphene_django/utils/testing.py
Normal 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()))
|
|
@ -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)
|
|
@ -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"
|
||||
)
|
||||
|
|
8
setup.py
8
setup.py
|
@ -24,6 +24,12 @@ tests_require = [
|
|||
"pytest-django>=3.3.2",
|
||||
] + rest_framework_require
|
||||
|
||||
|
||||
dev_requires = [
|
||||
"black==19.3b0",
|
||||
"flake8==3.7.7",
|
||||
] + tests_require
|
||||
|
||||
setup(
|
||||
name="graphene-django",
|
||||
version=version,
|
||||
|
@ -58,7 +64,7 @@ setup(
|
|||
setup_requires=["pytest-runner"],
|
||||
tests_require=tests_require,
|
||||
rest_framework_require=rest_framework_require,
|
||||
extras_require={"test": tests_require, "rest_framework": rest_framework_require},
|
||||
extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
platforms="any",
|
||||
|
|
39
tox.ini
Normal file
39
tox.ini
Normal 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
|
Loading…
Reference in New Issue
Block a user