mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-05 12:53:12 +03:00
Merge pull request #27 from revolico/upgrade-to-v2-9-1
Upgrade to v2 9 1
This commit is contained in:
commit
ae663474e0
21
.github/stale.yml
vendored
Normal file
21
.github/stale.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 120
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 30
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
- 🐛bug
|
||||||
|
- 📖 documentation
|
||||||
|
- help wanted
|
||||||
|
- ✨enhancement
|
||||||
|
# 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
|
# auto-generated tag files
|
||||||
tags
|
tags
|
||||||
|
.tox/
|
||||||
|
.pytest_cache/
|
||||||
|
|
143
.travis.yml
143
.travis.yml
|
@ -1,59 +1,92 @@
|
||||||
language: python
|
language: python
|
||||||
sudo: false
|
cache: pip
|
||||||
python:
|
dist: xenial
|
||||||
- 2.7
|
|
||||||
- 3.4
|
|
||||||
- 3.5
|
|
||||||
- 3.6
|
|
||||||
install:
|
install:
|
||||||
- |
|
- pip install tox tox-travis
|
||||||
if [ "$TEST_TYPE" = build ]; then
|
|
||||||
pip install -e .[test]
|
script:
|
||||||
pip install psycopg2 # Required for Django postgres fields testing
|
- tox
|
||||||
pip install django==$DJANGO_VERSION
|
|
||||||
python setup.py develop
|
after_success:
|
||||||
elif [ "$TEST_TYPE" = lint ]; then
|
- pip install coveralls
|
||||||
pip install flake8
|
- coveralls
|
||||||
fi
|
|
||||||
script:
|
stages:
|
||||||
- |
|
- test
|
||||||
if [ "$TEST_TYPE" = lint ]; then
|
- name: deploy
|
||||||
echo "Checking Python code lint."
|
if: tag IS present
|
||||||
flake8 graphene_django
|
|
||||||
exit
|
jobs:
|
||||||
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
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|
||||||
|
allow_failures:
|
||||||
|
- env: DJANGO=master
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- python: '3.4'
|
- python: 2.7
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
env: DJANGO=1.11
|
||||||
- python: '3.5'
|
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
- python: 3.5
|
||||||
- python: '3.6'
|
env: DJANGO=1.11
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.0
|
- python: 3.5
|
||||||
- python: '3.5'
|
env: DJANGO=2.0
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
- python: 3.5
|
||||||
- python: '3.6'
|
env: DJANGO=2.1
|
||||||
env: TEST_TYPE=build DJANGO_VERSION=2.1
|
- python: 3.5
|
||||||
- python: '2.7'
|
env: DJANGO=2.2
|
||||||
env: TEST_TYPE=lint
|
|
||||||
- python: '3.6'
|
- python: 3.6
|
||||||
env: TEST_TYPE=lint
|
env: DJANGO=1.11
|
||||||
deploy:
|
- python: 3.6
|
||||||
provider: pypi
|
env: DJANGO=2.0
|
||||||
user: syrusakbary
|
- python: 3.6
|
||||||
on:
|
env: DJANGO=2.1
|
||||||
tags: true
|
- python: 3.6
|
||||||
password:
|
env: DJANGO=2.2
|
||||||
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
|
- python: 3.6
|
||||||
distributions: "sdist bdist_wheel"
|
env: DJANGO=3.0
|
||||||
|
- 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=3.0
|
||||||
|
- python: 3.7
|
||||||
|
env: DJANGO=master
|
||||||
|
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=1.11
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=2.0
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=2.1
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=2.2
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=3.0
|
||||||
|
- python: 3.8
|
||||||
|
env: DJANGO=master
|
||||||
|
|
||||||
|
- python: 3.7
|
||||||
|
env: TOXENV=black,flake8
|
||||||
|
|
||||||
|
- stage: deploy
|
||||||
|
script: skip
|
||||||
|
python: 3.7
|
||||||
|
after_success: true
|
||||||
|
deploy:
|
||||||
|
provider: pypi
|
||||||
|
user: syrusakbary
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
password:
|
||||||
|
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
|
||||||
|
distributions: "sdist bdist_wheel"
|
||||||
|
|
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
|
include README.md LICENSE
|
||||||
recursive-include graphene_django/templates *
|
recursive-include graphene_django/templates *
|
||||||
|
recursive-include graphene_django/static *
|
||||||
|
|
29
Makefile
Normal file
29
Makefile
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.PHONY: dev-setup ## Install development dependencies
|
||||||
|
dev-setup:
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
.PHONY: install-dev
|
||||||
|
install-dev: dev-setup # Alias install-dev -> dev-setup
|
||||||
|
|
||||||
|
.PHONY: tests
|
||||||
|
tests:
|
||||||
|
py.test graphene_django --cov=graphene_django -vv
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: tests # Alias test -> tests
|
||||||
|
|
||||||
|
.PHONY: format
|
||||||
|
format:
|
||||||
|
black --exclude "/migrations/" graphene_django examples setup.py
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
flake8 graphene_django examples
|
||||||
|
|
||||||
|
.PHONY: docs ## Generate docs
|
||||||
|
docs: dev-setup
|
||||||
|
cd docs && make install && make html
|
||||||
|
|
||||||
|
.PHONY: docs-live ## Generate docs with live reloading
|
||||||
|
docs-live: dev-setup
|
||||||
|
cd docs && make install && make livehtml
|
65
README.md
65
README.md
|
@ -1,13 +1,29 @@
|
||||||
Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`.
|
#  Graphene-Django
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#  Graphene-Django [](https://travis-ci.org/graphql-python/graphene-django) [](https://badge.fury.io/py/graphene-django) [](https://coveralls.io/github/graphql-python/graphene-django?branch=master)
|
|
||||||
|
|
||||||
|
|
||||||
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
||||||
|
|
||||||
## Installation
|
[![travis][travis-image]][travis-url]
|
||||||
|
[![pypi][pypi-image]][pypi-url]
|
||||||
|
[![Anaconda-Server Badge][conda-image]][conda-url]
|
||||||
|
[![coveralls][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
[travis-image]: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master&style=flat
|
||||||
|
[travis-url]: https://travis-ci.org/graphql-python/graphene-django
|
||||||
|
[pypi-image]: https://img.shields.io/pypi/v/graphene-django.svg?style=flat
|
||||||
|
[pypi-url]: https://pypi.org/project/graphene-django/
|
||||||
|
[coveralls-image]: https://coveralls.io/repos/github/graphql-python/graphene-django/badge.svg?branch=master
|
||||||
|
[coveralls-url]: https://coveralls.io/github/graphql-python/graphene-django?branch=master
|
||||||
|
[conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg
|
||||||
|
[conda-url]: https://anaconda.org/conda-forge/graphene-django
|
||||||
|
|
||||||
|
[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM)
|
||||||
|
|
||||||
|
## 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
|
For installing graphene, just run this command in your shell
|
||||||
|
|
||||||
|
@ -20,6 +36,7 @@ pip install "graphene-django>=2.0"
|
||||||
```python
|
```python
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
# ...
|
# ...
|
||||||
|
'django.contrib.staticfiles', # Required for GraphiQL
|
||||||
'graphene_django',
|
'graphene_django',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,12 +50,12 @@ GRAPHENE = {
|
||||||
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
|
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# ...
|
# ...
|
||||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
path('graphql', GraphQLView.as_view(graphiql=True)),
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -73,7 +90,7 @@ class Query(graphene.ObjectType):
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can simply query the schema:
|
Then you can query the schema:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
query = '''
|
query = '''
|
||||||
|
@ -95,32 +112,8 @@ To learn more check out the following [examples](examples/):
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
After cloning this repo, ensure dependencies are installed by running:
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
```sh
|
## Release Notes
|
||||||
pip install -e ".[test]"
|
|
||||||
```
|
|
||||||
|
|
||||||
After developing, the full test suite can be evaluated by running:
|
* See [Releases page on github](https://github.com/graphql-python/graphene-django/releases)
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
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
|
A `Django <https://www.djangoproject.com/>`__ integration for
|
||||||
`Graphene <http://graphene-python.org/>`__.
|
`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
|
For installing graphene, just run this command in your shell
|
||||||
|
|
||||||
|
@ -46,7 +52,7 @@ serve the queries.
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# ...
|
# ...
|
||||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||||
]
|
]
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
|
@ -105,36 +111,7 @@ To learn more check out the following `examples <examples/>`__:
|
||||||
Contributing
|
Contributing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
After cloning this repo, ensure dependencies are installed by running:
|
See `CONTRIBUTING.md <CONTRIBUTING.md>`__.
|
||||||
|
|
||||||
.. 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
|
|
||||||
|
|
||||||
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
||||||
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
|
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
|
||||||
|
|
|
@ -48,12 +48,20 @@ help:
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILDDIR)/*
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
.PHONY: install ## to install all documentation related requirements
|
||||||
|
install:
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
.PHONY: html
|
.PHONY: html
|
||||||
html:
|
html:
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
.PHONY: livehtml ## to build and serve live-reloading documentation
|
||||||
|
livehtml:
|
||||||
|
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
|
||||||
.PHONY: dirhtml
|
.PHONY: dirhtml
|
||||||
dirhtml:
|
dirhtml:
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
|
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
|
@ -20,7 +20,7 @@ Let's use a simple example model.
|
||||||
Limiting Field Access
|
Limiting Field Access
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
To limit fields in a GraphQL query simply use the ``only_fields`` meta attribute.
|
To limit fields in a GraphQL query simply use the ``fields`` meta attribute.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ To limit fields in a GraphQL query simply use the ``only_fields`` meta attribute
|
||||||
class PostNode(DjangoObjectType):
|
class PostNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
only_fields = ('title', 'content')
|
fields = ('title', 'content')
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
conversely you can use ``exclude_fields`` meta attribute.
|
conversely you can use ``exclude`` meta attribute.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ conversely you can use ``exclude_fields`` meta attribute.
|
||||||
class PostNode(DjangoObjectType):
|
class PostNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
exclude_fields = ('published', 'owner')
|
exclude = ('published', 'owner')
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
Queryset Filtering On Lists
|
Queryset Filtering On Lists
|
||||||
|
@ -84,7 +84,7 @@ with the context argument.
|
||||||
|
|
||||||
def resolve_my_posts(self, info):
|
def resolve_my_posts(self, info):
|
||||||
# context will reference to the Django request
|
# context will reference to the Django request
|
||||||
if not info.context.user.is_authenticated():
|
if not info.context.user.is_authenticated:
|
||||||
return Post.objects.none()
|
return Post.objects.none()
|
||||||
else:
|
else:
|
||||||
return Post.objects.filter(owner=info.context.user)
|
return Post.objects.filter(owner=info.context.user)
|
||||||
|
@ -96,6 +96,29 @@ schema is simple.
|
||||||
|
|
||||||
result = schema.execute(query, context_value=request)
|
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
|
Filtering ID-based Node Access
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -110,11 +133,11 @@ method to your ``DjangoObjectType``.
|
||||||
class PostNode(DjangoObjectType):
|
class PostNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
only_fields = ('title', 'content')
|
fields = ('title', 'content')
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node(cls, id, info):
|
def get_node(cls, info, id):
|
||||||
try:
|
try:
|
||||||
post = cls._meta.model.objects.get(id=id)
|
post = cls._meta.model.objects.get(id=id)
|
||||||
except cls._meta.model.DoesNotExist:
|
except cls._meta.model.DoesNotExist:
|
||||||
|
@ -131,8 +154,9 @@ Adding Login Required
|
||||||
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
|
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
#views.py
|
|
||||||
|
# views.py
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
@ -142,15 +166,15 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
|
||||||
|
|
||||||
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
||||||
|
|
||||||
For Django 1.9 and below:
|
For Django 1.11:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# some other urls
|
# 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:
|
For Django 2.0 and above:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
89
docs/conf.py
89
docs/conf.py
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.intersphinx',
|
"sphinx.ext.intersphinx",
|
||||||
'sphinx.ext.todo',
|
"sphinx.ext.todo",
|
||||||
'sphinx.ext.coverage',
|
"sphinx.ext.coverage",
|
||||||
'sphinx.ext.viewcode',
|
"sphinx.ext.viewcode",
|
||||||
]
|
]
|
||||||
if not on_rtd:
|
if not on_rtd:
|
||||||
extensions += [
|
extensions += ["sphinx.ext.githubpages"]
|
||||||
'sphinx.ext.githubpages',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
# The suffix(es) of source filenames.
|
||||||
# You can specify multiple suffix as a list of string:
|
# You can specify multiple suffix as a list of string:
|
||||||
#
|
#
|
||||||
# source_suffix = ['.rst', '.md']
|
# source_suffix = ['.rst', '.md']
|
||||||
source_suffix = '.rst'
|
source_suffix = ".rst"
|
||||||
|
|
||||||
# The encoding of source files.
|
# The encoding of source files.
|
||||||
#
|
#
|
||||||
# source_encoding = 'utf-8-sig'
|
# source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Graphene Django'
|
project = u"Graphene Django"
|
||||||
copyright = u'Graphene 2017'
|
copyright = u"Graphene 2017"
|
||||||
author = u'Syrus Akbary'
|
author = u"Syrus Akbary"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u'1.0'
|
version = u"1.0"
|
||||||
# The full version, including alpha/beta/rc tags.
|
# 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
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -94,7 +92,7 @@ language = None
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This patterns also effect to html_static_path and html_extra_path
|
# 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
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
# documents.
|
# documents.
|
||||||
|
@ -116,7 +114,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
# show_authors = False
|
# show_authors = False
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# 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.
|
# A list of ignored prefixes for module index sorting.
|
||||||
# modindex_common_prefix = []
|
# 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,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# 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
|
# Add any extra paths that contain custom files (such as robots.txt or
|
||||||
# .htaccess) here, relative to this directory. These files are copied
|
# .htaccess) here, relative to this directory. These files are copied
|
||||||
|
@ -255,34 +253,30 @@ html_static_path = ['_static']
|
||||||
# html_search_scorer = 'scorer.js'
|
# html_search_scorer = 'scorer.js'
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'Graphenedoc'
|
htmlhelp_basename = "Graphenedoc"
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
latex_elements = {
|
latex_elements = {
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
#
|
#
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
#
|
||||||
#
|
# 'pointsize': '10pt',
|
||||||
# 'pointsize': '10pt',
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
# Additional stuff for the LaTeX preamble.
|
# 'preamble': '',
|
||||||
#
|
# Latex figure (float) alignment
|
||||||
# 'preamble': '',
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
# Latex figure (float) alignment
|
|
||||||
#
|
|
||||||
# 'figure_align': 'htbp',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'Graphene.tex', u'Graphene Documentation',
|
(master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
|
||||||
u'Syrus Akbary', 'manual'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# 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
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
(master_doc, 'graphene_django', u'Graphene Django Documentation',
|
(master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
|
||||||
[author], 1)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
# If true, show URL addresses after external links.
|
||||||
|
@ -338,9 +331,15 @@ man_pages = [
|
||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'Graphene-Django', u'Graphene Django Documentation',
|
(
|
||||||
author, 'Graphene Django', 'One line description of project.',
|
master_doc,
|
||||||
'Miscellaneous'),
|
"Graphene-Django",
|
||||||
|
u"Graphene Django Documentation",
|
||||||
|
author,
|
||||||
|
"Graphene Django",
|
||||||
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
# Documents to append as an appendix to all manuals.
|
||||||
|
@ -414,7 +413,7 @@ epub_copyright = copyright
|
||||||
# epub_post_files = []
|
# epub_post_files = []
|
||||||
|
|
||||||
# A list of files that should not be packed into the epub file.
|
# 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.
|
# 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.
|
# 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 ``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
|
.. code:: python
|
||||||
|
@ -24,7 +24,7 @@ For use the Django Debug plugin in Graphene:
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
# ...
|
# ...
|
||||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
debug = graphene.Field(DjangoDebug, name='_debug')
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ And in your ``settings.py``:
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
|
...
|
||||||
'MIDDLEWARE': [
|
'MIDDLEWARE': [
|
||||||
'graphene_django.debug.DjangoDebugMiddleware',
|
'graphene_django.debug.DjangoDebugMiddleware',
|
||||||
]
|
]
|
||||||
|
@ -58,11 +59,11 @@ the GraphQL request, like:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# Here is the debug field that will output the SQL queries
|
# Here is the debug field that will output the SQL queries
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
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.
|
||||||
|
|
|
@ -14,8 +14,17 @@ You will need to install it manually, which can be done as follows:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
# You'll need to django-filter
|
# You'll need to install django-filter
|
||||||
pip install django-filter>=2
|
pip install django-filter>=2
|
||||||
|
|
||||||
|
After installing ``django-filter`` you'll need to add the application in the ``settings.py`` file:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# ...
|
||||||
|
"django_filters",
|
||||||
|
]
|
||||||
|
|
||||||
Note: The techniques below are demoed in the `cookbook example
|
Note: The techniques below are demoed in the `cookbook example
|
||||||
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
||||||
|
@ -100,7 +109,7 @@ features of ``django-filter``. This is done by transparently creating a
|
||||||
``filter_fields``.
|
``filter_fields``.
|
||||||
|
|
||||||
However, you may find this to be insufficient. In these cases you can
|
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
|
.. code:: python
|
||||||
|
|
||||||
|
@ -127,6 +136,33 @@ create your own ``Filterset`` as follows:
|
||||||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||||
filterset_class=AnimalFilter)
|
filterset_class=AnimalFilter)
|
||||||
|
|
||||||
|
You can also specify the ``FilterSet`` class using the ``filterset_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>`__
|
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
|
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
|
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||||
|
@ -136,7 +172,7 @@ pre-filter animals owned by the authenticated user (set in ``context.user``).
|
||||||
|
|
||||||
class AnimalFilter(django_filters.FilterSet):
|
class AnimalFilter(django_filters.FilterSet):
|
||||||
# Do case-insensitive lookups on 'name'
|
# Do case-insensitive lookups on 'name'
|
||||||
name = django_filters.CharFilter(lookup_type='iexact')
|
name = django_filters.CharFilter(lookup_type=['iexact'])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Animal
|
model = Animal
|
||||||
|
@ -146,3 +182,49 @@ pre-filter animals owned by the authenticated user (set in ``context.user``).
|
||||||
def qs(self):
|
def qs(self):
|
||||||
# The query context can be found in self.request.
|
# The query context can be found in self.request.
|
||||||
return super(AnimalFilter, self).qs.filter(owner=self.request.user)
|
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
|
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::
|
.. toctree::
|
||||||
:maxdepth: 0
|
:maxdepth: 1
|
||||||
|
|
||||||
|
installation
|
||||||
tutorial-plain
|
tutorial-plain
|
||||||
tutorial-relay
|
tutorial-relay
|
||||||
|
schema
|
||||||
|
queries
|
||||||
|
mutations
|
||||||
filtering
|
filtering
|
||||||
authorization
|
authorization
|
||||||
debug
|
debug
|
||||||
rest-framework
|
|
||||||
form-mutations
|
|
||||||
introspection
|
introspection
|
||||||
|
testing
|
||||||
|
settings
|
||||||
|
|
105
docs/installation.rst
Normal file
105
docs/installation.rst
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Graphene-Django takes a few seconds to install and set up.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
Graphene-Django currently supports the following versions of Django:
|
||||||
|
|
||||||
|
* >= Django 1.11
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
For Django 1.11:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# ...
|
||||||
|
url(r"graphql", GraphQLView.as_view(graphiql=True)),
|
||||||
|
]
|
||||||
|
|
||||||
|
For Django 2.0 and above:
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
|
||||||
|
CSRF exempt
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If have enabled `CSRF protection <https://docs.djangoproject.com/en/3.0/ref/csrf/>`_ in your Django app
|
||||||
|
you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either
|
||||||
|
update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt``
|
||||||
|
decorator:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# urls.py
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# ...
|
||||||
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||||
|
]
|
|
@ -1,17 +1,15 @@
|
||||||
Introspection Schema
|
Introspection Schema
|
||||||
====================
|
====================
|
||||||
|
|
||||||
Relay uses `Babel Relay
|
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.
|
||||||
Plugin <https://facebook.github.io/relay/docs/guides-babel-plugin.html>`__
|
|
||||||
that requires you to provide your GraphQL schema data.
|
|
||||||
|
|
||||||
Graphene comes with a management command for Django to dump your schema
|
Graphene comes with a Django management command to dump your schema
|
||||||
data to ``schema.json`` that is compatible with babel-relay-plugin.
|
data to ``schema.json`` which is compatible with babel-relay-plugin.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Include ``graphene_django`` to ``INSTALLED_APPS`` in you project
|
Include ``graphene_django`` to ``INSTALLED_APPS`` in your project
|
||||||
settings:
|
settings:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -29,12 +27,30 @@ It dumps your full introspection schema to ``schema.json`` inside your
|
||||||
project root directory. Point ``babel-relay-plugin`` to this file and
|
project root directory. Point ``babel-relay-plugin`` to this file and
|
||||||
you're ready to use Relay with Graphene GraphQL implementation.
|
you're ready to use Relay with Graphene GraphQL implementation.
|
||||||
|
|
||||||
|
The schema file is sorted to create a reproducible canonical representation.
|
||||||
|
|
||||||
|
GraphQL SDL Representation
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The schema can also be exported as a GraphQL SDL file by changing the file
|
||||||
|
extension :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql
|
||||||
|
|
||||||
|
When exporting the schema as a ``.graphql`` file the ``--indent`` option is
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
|
||||||
Advanced Usage
|
Advanced Usage
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
The ``--indent`` option can be used to specify the number of indentation spaces to
|
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.
|
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
|
To simplify the command to ``./manage.py graphql_schema``, you can
|
||||||
specify the parameters in your settings.py:
|
specify the parameters in your settings.py:
|
||||||
|
|
||||||
|
@ -42,7 +58,8 @@ specify the parameters in your settings.py:
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'tutorial.quickstart.schema',
|
'SCHEMA': 'tutorial.quickstart.schema',
|
||||||
'SCHEMA_OUTPUT': 'data/schema.json' # defaults to schema.json
|
'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json,
|
||||||
|
'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
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(graphene.ObjectType):
|
||||||
|
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 import 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 import 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.
|
419
docs/queries.rst
Normal file
419
docs/queries.rst
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
Specifying which fields to include
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
|
||||||
|
If you only want a subset of fields to be present, you can do so using
|
||||||
|
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
|
||||||
|
all fields that should be exposed using the fields attribute.
|
||||||
|
This will make it less likely to result in unintentionally exposing data when
|
||||||
|
your models change.
|
||||||
|
|
||||||
|
``fields``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Show **only** these fields on the model:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class QuestionType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
fields = ('id', 'question_text')
|
||||||
|
|
||||||
|
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class QuestionType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
``exclude``
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Show all fields **except** those in ``exclude``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class QuestionType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
exclude = ('question_text',)
|
||||||
|
|
||||||
|
|
||||||
|
Customising 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
|
||||||
|
fields = ('id', 'question_text')
|
||||||
|
|
||||||
|
extra_field = graphene.String()
|
||||||
|
|
||||||
|
def resolve_extra_field(self, info):
|
||||||
|
return 'hello!'
|
||||||
|
|
||||||
|
|
||||||
|
Choices to Enum conversion
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
By default Graphene-Django will convert any Django fields that have `choices`_
|
||||||
|
defined into a GraphQL enum type.
|
||||||
|
|
||||||
|
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
|
||||||
|
|
||||||
|
For example the following ``Model`` and ``DjangoObjectType``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class PetModel(models.Model):
|
||||||
|
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
|
||||||
|
|
||||||
|
class Pet(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
|
||||||
|
Results in the following GraphQL schema definition:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
type Pet {
|
||||||
|
id: ID!
|
||||||
|
kind: PetModelKind!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PetModelKind {
|
||||||
|
CAT
|
||||||
|
DOG
|
||||||
|
}
|
||||||
|
|
||||||
|
You can disable this automatic conversion by setting
|
||||||
|
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
|
||||||
|
``Meta`` class.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class Pet(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
convert_choices_to_enum = False
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
type Pet {
|
||||||
|
id: ID!
|
||||||
|
kind: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
||||||
|
automatically converted into enums:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class Pet(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
convert_choices_to_enum = ['kind']
|
||||||
|
|
||||||
|
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
||||||
|
``False``.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
DjangoObjectTypes
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset.
|
||||||
|
Queryset methods like `values` will return dictionaries, use `defer` instead.
|
||||||
|
|
||||||
|
|
||||||
|
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,3 +1,4 @@
|
||||||
sphinx
|
Sphinx==1.5.3
|
||||||
|
sphinx-autobuild==0.7.1
|
||||||
# Docs template
|
# Docs template
|
||||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||||
|
|
|
@ -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}
|
|
58
docs/schema.py
Normal file
58
docs/schema.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(object):
|
||||||
|
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())
|
||||||
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
def resolve_all_categories(self, info, **kwargs):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_ingredients(self, info, **kwargs):
|
||||||
|
return Ingredient.objects.all()
|
||||||
|
|
||||||
|
def resolve_category(self, info, **kwargs):
|
||||||
|
id = kwargs.get('id')
|
||||||
|
name = kwargs.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Category.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Category.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_ingredient(self, info, **kwargs):
|
||||||
|
id = kwargs.get('id')
|
||||||
|
name = kwargs.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Ingredient.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
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/>`__
|
172
docs/settings.rst
Normal file
172
docs/settings.rst
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
``CAMELCASE_ERRORS``
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||||
|
By default they will be snake case.
|
||||||
|
|
||||||
|
Default: ``False``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'CAMELCASE_ERRORS': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# result = schema.execute(...)
|
||||||
|
print(result.errors)
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# 'field': 'test_field',
|
||||||
|
# 'messages': ['This field is required.'],
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'CAMELCASE_ERRORS': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# result = schema.execute(...)
|
||||||
|
print(result.errors)
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# 'field': 'testField',
|
||||||
|
# 'messages': ['This field is required.'],
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING``
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Set to ``True`` to use the new naming format for the auto generated Enum types from Django choice fields. The new format looks like this: ``{app_label}{object_name}{field_name}Choices``
|
||||||
|
|
||||||
|
Default: ``False``
|
||||||
|
|
||||||
|
|
||||||
|
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
|
||||||
|
|
||||||
|
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING`` setting is ignored.
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# myapp.utils
|
||||||
|
def enum_naming(field):
|
||||||
|
if isinstance(field.model, User):
|
||||||
|
return f"CustomUserEnum{field.name.title()}"
|
||||||
|
return f"CustomEnum{field.name.title()}"
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
||||||
|
}
|
84
docs/testing.rst
Normal file
84
docs/testing.rst
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
Testing API calls with django
|
||||||
|
=============================
|
||||||
|
|
||||||
|
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
|
||||||
|
|
||||||
|
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`.
|
||||||
|
|
||||||
|
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_query_with_variables(self):
|
||||||
|
response = self.query(
|
||||||
|
'''
|
||||||
|
query myModel($id: Int!){
|
||||||
|
myModel(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''',
|
||||||
|
op_name='myModel',
|
||||||
|
variables={'id': 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
Graphene Django has a number of additional features that are designed to make
|
||||||
working with Django *really simple*.
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
Set up the Django project
|
Set up the Django project
|
||||||
-------------------------
|
-------------------------
|
||||||
|
@ -91,7 +88,7 @@ Don't forget to create & run migrations:
|
||||||
|
|
||||||
python manage.py makemigrations
|
python manage.py makemigrations
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
|
|
||||||
|
|
||||||
Load some test data
|
Load some test data
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -108,7 +105,7 @@ following:
|
||||||
$ python ./manage.py loaddata ingredients
|
$ python ./manage.py loaddata ingredients
|
||||||
|
|
||||||
Installed 6 object(s) from 1 fixture(s)
|
Installed 6 object(s) from 1 fixture(s)
|
||||||
|
|
||||||
Alternatively you can use the Django admin interface to create some data
|
Alternatively you can use the Django admin interface to create some data
|
||||||
yourself. You'll need to run the development server (see below), and
|
yourself. You'll need to run the development server (see below), and
|
||||||
create a login for yourself too (``./manage.py createsuperuser``).
|
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 = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
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 = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
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)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,7 +286,7 @@ from the command line.
|
||||||
$ python ./manage.py runserver
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.9, using settings 'cookbook.settings'
|
Django version 1.11, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
@ -420,74 +417,17 @@ Getting single objects
|
||||||
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
||||||
|
|
||||||
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
||||||
|
Add the **Highlighted** lines to ``cookbook/ingredients/schema.py``
|
||||||
|
|
||||||
.. code:: python
|
.. literalinclude:: schema.py
|
||||||
|
:emphasize-lines: 19-21,25-27,36-58
|
||||||
import graphene
|
|
||||||
|
|
||||||
from graphene_django.types import DjangoObjectType
|
|
||||||
|
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Category
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Ingredient
|
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
|
||||||
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())
|
|
||||||
all_ingredients = graphene.List(IngredientType)
|
|
||||||
|
|
||||||
def resolve_all_categories(self, info, **kwargs):
|
|
||||||
return Category.objects.all()
|
|
||||||
|
|
||||||
def resolve_all_ingredients(self, info, **kwargs):
|
|
||||||
return Ingredient.objects.all()
|
|
||||||
|
|
||||||
def resolve_category(self, info, **kwargs):
|
|
||||||
id = kwargs.get('id')
|
|
||||||
name = kwargs.get('name')
|
|
||||||
|
|
||||||
if id is not None:
|
|
||||||
return Category.objects.get(pk=id)
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
return Category.objects.get(name=name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def resolve_ingredient(self, info, **kwargs):
|
|
||||||
id = kwargs.get('id')
|
|
||||||
name = kwargs.get('name')
|
|
||||||
|
|
||||||
if id is not None:
|
|
||||||
return Ingredient.objects.get(pk=id)
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
return Ingredient.objects.get(name=name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
Now, with the code in place, we can query for single objects.
|
Now, with the code in place, we can query for single objects.
|
||||||
|
|
||||||
For example, lets query ``category``:
|
For example, lets query ``category``:
|
||||||
|
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
query {
|
query {
|
||||||
category(id: 1) {
|
category(id: 1) {
|
||||||
|
@ -536,3 +476,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``.
|
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.**
|
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
|
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, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
class Query(graphene.ObjectType):
|
||||||
category = relay.Node.Field(CategoryNode)
|
category = relay.Node.Field(CategoryNode)
|
||||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
The filtering functionality is provided by
|
The filtering functionality is provided by
|
||||||
`django-filter <https://django-filter.readthedocs.org>`__. See the
|
`django-filter <https://django-filter.readthedocs.org>`__. See the
|
||||||
`usage
|
`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
|
for details on the format for ``filter_fields``. While optional, this
|
||||||
tutorial makes use of this functionality so you will need to install
|
tutorial makes use of this functionality so you will need to install
|
||||||
``django-filter`` for this tutorial to work:
|
``django-filter`` for this tutorial to work:
|
||||||
|
@ -244,7 +244,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``.
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
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 = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
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)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ from the command line.
|
||||||
$ python ./manage.py runserver
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.9, using settings 'cookbook.settings'
|
Django version 1.11, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
@ -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)
|
@admin.register(Ingredient)
|
||||||
class IngredientAdmin(admin.ModelAdmin):
|
class IngredientAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'category')
|
list_display = ("id", "name", "category")
|
||||||
list_editable = ('name', 'category')
|
list_editable = ("name", "category")
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Category)
|
admin.site.register(Category)
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class IngredientsConfig(AppConfig):
|
class IngredientsConfig(AppConfig):
|
||||||
name = 'cookbook.ingredients'
|
name = "cookbook.ingredients"
|
||||||
label = 'ingredients'
|
label = "ingredients"
|
||||||
verbose_name = '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 Category(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Categories"
|
||||||
|
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -11,7 +14,9 @@ class Category(models.Model):
|
||||||
class Ingredient(models.Model):
|
class Ingredient(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
notes = models.TextField(null=True, blank=True)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
from .models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
class CategoryType(DjangoObjectType):
|
class CategoryType(DjangoObjectType):
|
||||||
|
@ -15,27 +15,22 @@ class IngredientType(DjangoObjectType):
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
class Query(object):
|
||||||
category = graphene.Field(CategoryType,
|
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
|
||||||
id=graphene.Int(),
|
|
||||||
name=graphene.String())
|
|
||||||
all_categories = graphene.List(CategoryType)
|
all_categories = graphene.List(CategoryType)
|
||||||
|
|
||||||
ingredient = graphene.Field(IngredientType,
|
ingredient = graphene.Field(
|
||||||
id=graphene.Int(),
|
IngredientType, id=graphene.Int(), name=graphene.String()
|
||||||
name=graphene.String())
|
)
|
||||||
all_ingredients = graphene.List(IngredientType)
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
def resolve_all_categories(self, args, context, info):
|
def resolve_all_categories(self, context):
|
||||||
return Category.objects.all()
|
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
|
# We can easily optimize query count in the resolve method
|
||||||
return Ingredient.objects.select_related('category').all()
|
return Ingredient.objects.select_related("category").all()
|
||||||
|
|
||||||
def resolve_category(self, args, context, info):
|
|
||||||
id = args.get('id')
|
|
||||||
name = args.get('name')
|
|
||||||
|
|
||||||
|
def resolve_category(self, context, id=None, name=None):
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return Category.objects.get(pk=id)
|
return Category.objects.get(pk=id)
|
||||||
|
|
||||||
|
@ -44,10 +39,7 @@ class Query(object):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_ingredient(self, args, context, info):
|
def resolve_ingredient(self, context, id=None, name=None):
|
||||||
id = args.get('id')
|
|
||||||
name = args.get('name')
|
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return Ingredient.objects.get(pk=id)
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class RecipesConfig(AppConfig):
|
class RecipesConfig(AppConfig):
|
||||||
name = 'cookbook.recipes'
|
name = "cookbook.recipes"
|
||||||
label = 'recipes'
|
label = "recipes"
|
||||||
verbose_name = '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 django.db import models
|
||||||
|
|
||||||
from cookbook.ingredients.models import Ingredient
|
from ..ingredients.models import Ingredient
|
||||||
|
|
||||||
|
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model):
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
instructions = models.TextField()
|
instructions = models.TextField()
|
||||||
__unicode__ = lambda self: self.title
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(models.Model):
|
class RecipeIngredient(models.Model):
|
||||||
recipe = models.ForeignKey(Recipe, related_name='amounts')
|
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
|
||||||
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
|
ingredient = models.ForeignKey(
|
||||||
|
Ingredient, related_name="used_by", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
amount = models.FloatField()
|
amount = models.FloatField()
|
||||||
unit = models.CharField(max_length=20, choices=(
|
unit = models.CharField(
|
||||||
('unit', 'Units'),
|
max_length=20,
|
||||||
('kg', 'Kilograms'),
|
choices=(
|
||||||
('l', 'Litres'),
|
("unit", "Units"),
|
||||||
('st', 'Shots'),
|
("kg", "Kilograms"),
|
||||||
))
|
("l", "Litres"),
|
||||||
|
("st", "Shots"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
from cookbook.recipes.models import Recipe, RecipeIngredient
|
from .models import Recipe, RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
class RecipeType(DjangoObjectType):
|
class RecipeType(DjangoObjectType):
|
||||||
|
@ -15,19 +15,13 @@ class RecipeIngredientType(DjangoObjectType):
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
class Query(object):
|
||||||
recipe = graphene.Field(RecipeType,
|
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
|
||||||
id=graphene.Int(),
|
|
||||||
title=graphene.String())
|
|
||||||
all_recipes = graphene.List(RecipeType)
|
all_recipes = graphene.List(RecipeType)
|
||||||
|
|
||||||
recipeingredient = graphene.Field(RecipeIngredientType,
|
recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int())
|
||||||
id=graphene.Int())
|
|
||||||
all_recipeingredients = graphene.List(RecipeIngredientType)
|
all_recipeingredients = graphene.List(RecipeIngredientType)
|
||||||
|
|
||||||
def resolve_recipe(self, args, context, info):
|
def resolve_recipe(self, context, id=None, title=None):
|
||||||
id = args.get('id')
|
|
||||||
title = args.get('title')
|
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return Recipe.objects.get(pk=id)
|
return Recipe.objects.get(pk=id)
|
||||||
|
|
||||||
|
@ -36,17 +30,15 @@ class Query(object):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_recipeingredient(self, args, context, info):
|
def resolve_recipeingredient(self, context, id=None):
|
||||||
id = args.get('id')
|
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return RecipeIngredient.objects.get(pk=id)
|
return RecipeIngredient.objects.get(pk=id)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_all_recipes(self, args, context, info):
|
def resolve_all_recipes(self, context):
|
||||||
return Recipe.objects.all()
|
return Recipe.objects.all()
|
||||||
|
|
||||||
def resolve_all_recipeingredients(self, args, context, info):
|
def resolve_all_recipeingredients(self, context):
|
||||||
related = ['recipe', 'ingredient']
|
related = ["recipe", "ingredient"]
|
||||||
return RecipeIngredient.objects.select_related(*related).all()
|
return RecipeIngredient.objects.select_related(*related).all()
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
@ -5,10 +5,12 @@ import graphene
|
||||||
from graphene_django.debug import DjangoDebug
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
|
|
||||||
class Query(cookbook.ingredients.schema.Query,
|
class Query(
|
||||||
cookbook.recipes.schema.Query,
|
cookbook.ingredients.schema.Query,
|
||||||
graphene.ObjectType):
|
cookbook.recipes.schema.Query,
|
||||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
graphene.ObjectType,
|
||||||
|
):
|
||||||
|
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||||
|
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
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/
|
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -32,64 +32,61 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'graphene_django',
|
"graphene_django",
|
||||||
|
"cookbook.ingredients.apps.IngredientsConfig",
|
||||||
'cookbook.ingredients.apps.IngredientsConfig',
|
"cookbook.recipes.apps.RecipesConfig",
|
||||||
'cookbook.recipes.apps.RecipesConfig',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'cookbook.schema.schema',
|
"SCHEMA": "cookbook.schema.schema",
|
||||||
'MIDDLEWARE': (
|
"SCHEMA_INDENT": 2,
|
||||||
'graphene_django.debug.DjangoDebugMiddleware',
|
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'cookbook.urls'
|
ROOT_URLCONF = "cookbook.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [],
|
"DIRS": [],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.debug',
|
"django.template.context_processors.debug",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'cookbook.wsgi.application'
|
WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,26 +96,20 @@ DATABASES = {
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"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.MinimumLengthValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
# 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
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -130,9 +121,4 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
GRAPHENE = {
|
|
||||||
'SCHEMA': 'cookbook.schema.schema',
|
|
||||||
'SCHEMA_INDENT': 2,
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
path("graphql/", GraphQLView.as_view(graphiql=True)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
graphene
|
graphene>=2.1,<3
|
||||||
graphene-django
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1rc1
|
graphql-core>=2.1,<3
|
||||||
django==1.9
|
django==3.0.3
|
||||||
|
|
|
@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
@admin.register(Ingredient)
|
@admin.register(Ingredient)
|
||||||
class IngredientAdmin(admin.ModelAdmin):
|
class IngredientAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'category')
|
list_display = ("id", "name", "category")
|
||||||
list_editable = ('name', 'category')
|
list_editable = ("name", "category")
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Category)
|
admin.site.register(Category)
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class IngredientsConfig(AppConfig):
|
class IngredientsConfig(AppConfig):
|
||||||
name = 'cookbook.ingredients'
|
name = "cookbook.ingredients"
|
||||||
label = 'ingredients'
|
label = "ingredients"
|
||||||
verbose_name = 'Ingredients'
|
verbose_name = "Ingredients"
|
||||||
|
|
|
@ -11,7 +11,9 @@ class Category(models.Model):
|
||||||
class Ingredient(models.Model):
|
class Ingredient(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
notes = models.TextField(null=True, blank=True)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
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.
|
# 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)
|
# This is configured in the CategoryNode's Meta class (as you can see below)
|
||||||
class CategoryNode(DjangoObjectType):
|
class CategoryNode(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
interfaces = (Node, )
|
interfaces = (Node,)
|
||||||
filter_fields = ['name', 'ingredients']
|
filter_fields = ["name", "ingredients"]
|
||||||
|
|
||||||
|
|
||||||
class IngredientNode(DjangoObjectType):
|
class IngredientNode(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ingredient
|
model = Ingredient
|
||||||
# Allow for some more advanced filtering here
|
# Allow for some more advanced filtering here
|
||||||
interfaces = (Node, )
|
interfaces = (Node,)
|
||||||
filter_fields = {
|
filter_fields = {
|
||||||
'name': ['exact', 'icontains', 'istartswith'],
|
"name": ["exact", "icontains", "istartswith"],
|
||||||
'notes': ['exact', 'icontains'],
|
"notes": ["exact", "icontains"],
|
||||||
'category': ['exact'],
|
"category": ["exact"],
|
||||||
'category__name': ['exact'],
|
"category__name": ["exact"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class RecipesConfig(AppConfig):
|
class RecipesConfig(AppConfig):
|
||||||
name = 'cookbook.recipes'
|
name = "cookbook.recipes"
|
||||||
label = 'recipes'
|
label = "recipes"
|
||||||
verbose_name = 'Recipes'
|
verbose_name = "Recipes"
|
||||||
|
|
|
@ -10,12 +10,17 @@ class Recipe(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(models.Model):
|
class RecipeIngredient(models.Model):
|
||||||
recipe = models.ForeignKey(Recipe, related_name='amounts')
|
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
|
||||||
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
|
ingredient = models.ForeignKey(
|
||||||
|
Ingredient, related_name="used_by", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
amount = models.FloatField()
|
amount = models.FloatField()
|
||||||
unit = models.CharField(max_length=20, choices=(
|
unit = models.CharField(
|
||||||
('unit', 'Units'),
|
max_length=20,
|
||||||
('kg', 'Kilograms'),
|
choices=(
|
||||||
('l', 'Litres'),
|
("unit", "Units"),
|
||||||
('st', 'Shots'),
|
("kg", "Kilograms"),
|
||||||
))
|
("l", "Litres"),
|
||||||
|
("st", "Shots"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
@ -3,24 +3,23 @@ from graphene import Node
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
class RecipeNode(DjangoObjectType):
|
|
||||||
|
|
||||||
|
class RecipeNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
interfaces = (Node, )
|
interfaces = (Node,)
|
||||||
filter_fields = ['title','amounts']
|
filter_fields = ["title", "amounts"]
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredientNode(DjangoObjectType):
|
class RecipeIngredientNode(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeIngredient
|
model = RecipeIngredient
|
||||||
# Allow for some more advanced filtering here
|
# Allow for some more advanced filtering here
|
||||||
interfaces = (Node, )
|
interfaces = (Node,)
|
||||||
filter_fields = {
|
filter_fields = {
|
||||||
'ingredient__name': ['exact', 'icontains', 'istartswith'],
|
"ingredient__name": ["exact", "icontains", "istartswith"],
|
||||||
'recipe': ['exact'],
|
"recipe": ["exact"],
|
||||||
'recipe__title': ['icontains'],
|
"recipe__title": ["icontains"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
@ -5,10 +5,12 @@ import graphene
|
||||||
from graphene_django.debug import DjangoDebug
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
|
|
||||||
class Query(cookbook.ingredients.schema.Query,
|
class Query(
|
||||||
cookbook.recipes.schema.Query,
|
cookbook.ingredients.schema.Query,
|
||||||
graphene.ObjectType):
|
cookbook.recipes.schema.Query,
|
||||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
graphene.ObjectType,
|
||||||
|
):
|
||||||
|
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||||
|
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
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/
|
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -32,64 +32,62 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'graphene_django',
|
"graphene_django",
|
||||||
|
"cookbook.ingredients.apps.IngredientsConfig",
|
||||||
'cookbook.ingredients.apps.IngredientsConfig',
|
"cookbook.recipes.apps.RecipesConfig",
|
||||||
'cookbook.recipes.apps.RecipesConfig',
|
"django_filters",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'cookbook.schema.schema',
|
"SCHEMA": "cookbook.schema.schema",
|
||||||
'MIDDLEWARE': (
|
"SCHEMA_INDENT": 2,
|
||||||
'graphene_django.debug.DjangoDebugMiddleware',
|
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'cookbook.urls'
|
ROOT_URLCONF = "cookbook.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [],
|
"DIRS": [],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.debug',
|
"django.template.context_processors.debug",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'cookbook.wsgi.application'
|
WSGI_APPLICATION = "cookbook.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,26 +97,20 @@ DATABASES = {
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"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.MinimumLengthValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
# 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
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -130,9 +122,4 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
GRAPHENE = {
|
|
||||||
'SCHEMA': 'cookbook.schema.schema',
|
|
||||||
'SCHEMA_INDENT': 2,
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,6 +5,6 @@ from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r"^admin/", admin.site.urls),
|
||||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
url(r"^graphql$", GraphQLView.as_view(graphiql=True)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
graphene
|
graphene>=2.1,<3
|
||||||
graphene-django
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1rc1
|
graphql-core>=2.1,<3
|
||||||
django==1.9
|
django==3.0.3
|
||||||
django-filter>=2
|
django-filter>=2
|
||||||
|
|
|
@ -2,97 +2,50 @@ from .models import Character, Faction, Ship
|
||||||
|
|
||||||
|
|
||||||
def initialize():
|
def initialize():
|
||||||
human = Character(
|
human = Character(name="Human")
|
||||||
name='Human'
|
|
||||||
)
|
|
||||||
human.save()
|
human.save()
|
||||||
|
|
||||||
droid = Character(
|
droid = Character(name="Droid")
|
||||||
name='Droid'
|
|
||||||
)
|
|
||||||
droid.save()
|
droid.save()
|
||||||
|
|
||||||
rebels = Faction(
|
rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human)
|
||||||
id='1',
|
|
||||||
name='Alliance to Restore the Republic',
|
|
||||||
hero=human
|
|
||||||
)
|
|
||||||
rebels.save()
|
rebels.save()
|
||||||
|
|
||||||
empire = Faction(
|
empire = Faction(id="2", name="Galactic Empire", hero=droid)
|
||||||
id='2',
|
|
||||||
name='Galactic Empire',
|
|
||||||
hero=droid
|
|
||||||
)
|
|
||||||
empire.save()
|
empire.save()
|
||||||
|
|
||||||
xwing = Ship(
|
xwing = Ship(id="1", name="X-Wing", faction=rebels)
|
||||||
id='1',
|
|
||||||
name='X-Wing',
|
|
||||||
faction=rebels,
|
|
||||||
)
|
|
||||||
xwing.save()
|
xwing.save()
|
||||||
|
|
||||||
human.ship = xwing
|
human.ship = xwing
|
||||||
human.save()
|
human.save()
|
||||||
|
|
||||||
ywing = Ship(
|
ywing = Ship(id="2", name="Y-Wing", faction=rebels)
|
||||||
id='2',
|
|
||||||
name='Y-Wing',
|
|
||||||
faction=rebels,
|
|
||||||
)
|
|
||||||
ywing.save()
|
ywing.save()
|
||||||
|
|
||||||
awing = Ship(
|
awing = Ship(id="3", name="A-Wing", faction=rebels)
|
||||||
id='3',
|
|
||||||
name='A-Wing',
|
|
||||||
faction=rebels,
|
|
||||||
)
|
|
||||||
awing.save()
|
awing.save()
|
||||||
|
|
||||||
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
|
# 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.
|
# so for the purposes of this demo it's a rebel ship.
|
||||||
falcon = Ship(
|
falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
|
||||||
id='4',
|
|
||||||
name='Millenium Falcon',
|
|
||||||
faction=rebels,
|
|
||||||
)
|
|
||||||
falcon.save()
|
falcon.save()
|
||||||
|
|
||||||
homeOne = Ship(
|
homeOne = Ship(id="5", name="Home One", faction=rebels)
|
||||||
id='5',
|
|
||||||
name='Home One',
|
|
||||||
faction=rebels,
|
|
||||||
)
|
|
||||||
homeOne.save()
|
homeOne.save()
|
||||||
|
|
||||||
tieFighter = Ship(
|
tieFighter = Ship(id="6", name="TIE Fighter", faction=empire)
|
||||||
id='6',
|
|
||||||
name='TIE Fighter',
|
|
||||||
faction=empire,
|
|
||||||
)
|
|
||||||
tieFighter.save()
|
tieFighter.save()
|
||||||
|
|
||||||
tieInterceptor = Ship(
|
tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire)
|
||||||
id='7',
|
|
||||||
name='TIE Interceptor',
|
|
||||||
faction=empire,
|
|
||||||
)
|
|
||||||
tieInterceptor.save()
|
tieInterceptor.save()
|
||||||
|
|
||||||
executor = Ship(
|
executor = Ship(id="8", name="Executor", faction=empire)
|
||||||
id='8',
|
|
||||||
name='Executor',
|
|
||||||
faction=empire,
|
|
||||||
)
|
|
||||||
executor.save()
|
executor.save()
|
||||||
|
|
||||||
|
|
||||||
def create_ship(ship_name, faction_id):
|
def create_ship(ship_name, faction_id):
|
||||||
new_ship = Ship(
|
new_ship = Ship(name=ship_name, faction_id=faction_id)
|
||||||
name=ship_name,
|
|
||||||
faction_id=faction_id
|
|
||||||
)
|
|
||||||
new_ship.save()
|
new_ship.save()
|
||||||
return new_ship
|
return new_ship
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,13 @@ from django.db import models
|
||||||
|
|
||||||
class Character(models.Model):
|
class Character(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -21,7 +27,7 @@ class Faction(models.Model):
|
||||||
|
|
||||||
class Ship(models.Model):
|
class Ship(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -2,18 +2,16 @@ import graphene
|
||||||
from graphene import Schema, relay, resolve_only_args
|
from graphene import Schema, relay, resolve_only_args
|
||||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
|
|
||||||
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
|
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
|
||||||
get_ships)
|
|
||||||
from .models import Character as CharacterModel
|
from .models import Character as CharacterModel
|
||||||
from .models import Faction as FactionModel
|
from .models import Faction as FactionModel
|
||||||
from .models import Ship as ShipModel
|
from .models import Ship as ShipModel
|
||||||
|
|
||||||
|
|
||||||
class Ship(DjangoObjectType):
|
class Ship(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShipModel
|
model = ShipModel
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node(cls, info, id):
|
def get_node(cls, info, id):
|
||||||
|
@ -22,16 +20,14 @@ class Ship(DjangoObjectType):
|
||||||
|
|
||||||
|
|
||||||
class Character(DjangoObjectType):
|
class Character(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CharacterModel
|
model = CharacterModel
|
||||||
|
|
||||||
|
|
||||||
class Faction(DjangoObjectType):
|
class Faction(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FactionModel
|
model = FactionModel
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node(cls, info, id):
|
def get_node(cls, info, id):
|
||||||
|
@ -39,7 +35,6 @@ class Faction(DjangoObjectType):
|
||||||
|
|
||||||
|
|
||||||
class IntroduceShip(relay.ClientIDMutation):
|
class IntroduceShip(relay.ClientIDMutation):
|
||||||
|
|
||||||
class Input:
|
class Input:
|
||||||
ship_name = graphene.String(required=True)
|
ship_name = graphene.String(required=True)
|
||||||
faction_id = graphene.String(required=True)
|
faction_id = graphene.String(required=True)
|
||||||
|
@ -48,7 +43,9 @@ class IntroduceShip(relay.ClientIDMutation):
|
||||||
faction = graphene.Field(Faction)
|
faction = graphene.Field(Faction)
|
||||||
|
|
||||||
@classmethod
|
@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)
|
ship = create_ship(ship_name, faction_id)
|
||||||
faction = get_faction(faction_id)
|
faction = get_faction(faction_id)
|
||||||
return IntroduceShip(ship=ship, faction=faction)
|
return IntroduceShip(ship=ship, faction=faction)
|
||||||
|
@ -58,7 +55,7 @@ class Query(graphene.ObjectType):
|
||||||
rebels = graphene.Field(Faction)
|
rebels = graphene.Field(Faction)
|
||||||
empire = graphene.Field(Faction)
|
empire = graphene.Field(Faction)
|
||||||
node = relay.Node.Field()
|
node = relay.Node.Field()
|
||||||
ships = DjangoConnectionField(Ship, description='All the ships.')
|
ships = DjangoConnectionField(Ship, description="All the ships.")
|
||||||
|
|
||||||
@resolve_only_args
|
@resolve_only_args
|
||||||
def resolve_ships(self):
|
def resolve_ships(self):
|
||||||
|
|
|
@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
def test_correct_fetch_first_ship_rebels():
|
def test_correct_fetch_first_ship_rebels():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query RebelsShipsQuery {
|
query RebelsShipsQuery {
|
||||||
rebels {
|
rebels {
|
||||||
name,
|
name,
|
||||||
|
@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
'rebels': {
|
"rebels": {
|
||||||
'name': 'Alliance to Restore the Republic',
|
"name": "Alliance to Restore the Republic",
|
||||||
'hero': {
|
"hero": {"name": "Human"},
|
||||||
'name': 'Human'
|
"ships": {"edges": [{"node": {"name": "X-Wing"}}]},
|
||||||
},
|
|
||||||
'ships': {
|
|
||||||
'edges': [
|
|
||||||
{
|
|
||||||
'node': {
|
|
||||||
'name': 'X-Wing'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
|
@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels():
|
||||||
|
|
||||||
def test_correct_list_characters():
|
def test_correct_list_characters():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query RebelsShipsQuery {
|
query RebelsShipsQuery {
|
||||||
node(id: "U2hpcDox") {
|
node(id: "U2hpcDox") {
|
||||||
... on Ship {
|
... on Ship {
|
||||||
|
@ -60,15 +50,8 @@ def test_correct_list_characters():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}}
|
||||||
'node': {
|
|
||||||
'name': 'X-Wing',
|
|
||||||
'characters': [{
|
|
||||||
'name': 'Human'
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
|
@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
|
||||||
def test_mutations():
|
def test_mutations():
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
query = '''
|
query = """
|
||||||
mutation MyMutation {
|
mutation MyMutation {
|
||||||
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
|
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
|
||||||
ship {
|
ship {
|
||||||
|
@ -29,49 +29,23 @@ def test_mutations():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
'introduceShip': {
|
"introduceShip": {
|
||||||
'ship': {
|
"ship": {"id": "U2hpcDo5", "name": "Peter"},
|
||||||
'id': 'U2hpcDo5',
|
"faction": {
|
||||||
'name': 'Peter'
|
"name": "Alliance to Restore the Republic",
|
||||||
},
|
"ships": {
|
||||||
'faction': {
|
"edges": [
|
||||||
'name': 'Alliance to Restore the Republic',
|
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
|
||||||
'ships': {
|
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
|
||||||
'edges': [{
|
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
|
||||||
'node': {
|
{"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
|
||||||
'id': 'U2hpcDox',
|
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
|
||||||
'name': 'X-Wing'
|
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
|
||||||
}
|
]
|
||||||
}, {
|
|
||||||
'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)
|
result = schema.execute(query)
|
||||||
|
|
|
@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
def test_correctly_fetches_id_name_rebels():
|
def test_correctly_fetches_id_name_rebels():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query RebelsQuery {
|
query RebelsQuery {
|
||||||
rebels {
|
rebels {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
'rebels': {
|
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
|
||||||
'id': 'RmFjdGlvbjox',
|
|
||||||
'name': 'Alliance to Restore the Republic'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels():
|
||||||
|
|
||||||
def test_correctly_refetches_rebels():
|
def test_correctly_refetches_rebels():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query RebelsRefetchQuery {
|
query RebelsRefetchQuery {
|
||||||
node(id: "RmFjdGlvbjox") {
|
node(id: "RmFjdGlvbjox") {
|
||||||
id
|
id
|
||||||
|
@ -38,12 +35,9 @@ def test_correctly_refetches_rebels():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
'node': {
|
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
|
||||||
'id': 'RmFjdGlvbjox',
|
|
||||||
'name': 'Alliance to Restore the Republic'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
@ -52,20 +46,15 @@ def test_correctly_refetches_rebels():
|
||||||
|
|
||||||
def test_correctly_fetches_id_name_empire():
|
def test_correctly_fetches_id_name_empire():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query EmpireQuery {
|
query EmpireQuery {
|
||||||
empire {
|
empire {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
|
||||||
'empire': {
|
|
||||||
'id': 'RmFjdGlvbjoy',
|
|
||||||
'name': 'Galactic Empire'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire():
|
||||||
|
|
||||||
def test_correctly_refetches_empire():
|
def test_correctly_refetches_empire():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query EmpireRefetchQuery {
|
query EmpireRefetchQuery {
|
||||||
node(id: "RmFjdGlvbjoy") {
|
node(id: "RmFjdGlvbjoy") {
|
||||||
id
|
id
|
||||||
|
@ -82,13 +71,8 @@ def test_correctly_refetches_empire():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
|
||||||
'node': {
|
|
||||||
'id': 'RmFjdGlvbjoy',
|
|
||||||
'name': 'Galactic Empire'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
@ -96,7 +80,7 @@ def test_correctly_refetches_empire():
|
||||||
|
|
||||||
def test_correctly_refetches_xwing():
|
def test_correctly_refetches_xwing():
|
||||||
initialize()
|
initialize()
|
||||||
query = '''
|
query = """
|
||||||
query XWingRefetchQuery {
|
query XWingRefetchQuery {
|
||||||
node(id: "U2hpcDox") {
|
node(id: "U2hpcDox") {
|
||||||
id
|
id
|
||||||
|
@ -105,13 +89,8 @@ def test_correctly_refetches_xwing():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
expected = {
|
expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
|
||||||
'node': {
|
|
||||||
'id': 'U2hpcDox',
|
|
||||||
'name': 'X-Wing'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
from .fields import DjangoConnectionField
|
from .fields import DjangoConnectionField
|
||||||
|
|
||||||
__version__ = "2.2.0"
|
__version__ = "2.9.1"
|
||||||
|
|
||||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
||||||
|
|
|
@ -5,7 +5,11 @@ class MissingType(object):
|
||||||
try:
|
try:
|
||||||
# Postgres fields are only available in Django with psycopg2 installed
|
# Postgres fields are only available in Django with psycopg2 installed
|
||||||
# and we cannot have psycopg2 on PyPy
|
# and we cannot have psycopg2 on PyPy
|
||||||
from django.contrib.postgres.fields import (ArrayField, HStoreField,
|
from django.contrib.postgres.fields import (
|
||||||
JSONField, RangeField)
|
ArrayField,
|
||||||
|
HStoreField,
|
||||||
|
JSONField,
|
||||||
|
RangeField,
|
||||||
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
|
ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
from collections import OrderedDict
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_str
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
ID,
|
ID,
|
||||||
|
@ -21,6 +23,7 @@ from graphene.types.json import JSONString
|
||||||
from graphene.utils.str_converters import to_camel_case, to_const
|
from graphene.utils.str_converters import to_camel_case, to_const
|
||||||
from graphql import assert_valid_name
|
from graphql import assert_valid_name
|
||||||
|
|
||||||
|
from .settings import graphene_settings
|
||||||
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
||||||
from .fields import DjangoListField, DjangoConnectionField
|
from .fields import DjangoListField, DjangoConnectionField
|
||||||
from .utils import import_single_dispatch
|
from .utils import import_single_dispatch
|
||||||
|
@ -29,7 +32,7 @@ singledispatch = import_single_dispatch()
|
||||||
|
|
||||||
|
|
||||||
def convert_choice_name(name):
|
def convert_choice_name(name):
|
||||||
name = to_const(force_text(name))
|
name = to_const(force_str(name))
|
||||||
try:
|
try:
|
||||||
assert_valid_name(name)
|
assert_valid_name(name)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
|
@ -39,6 +42,8 @@ def convert_choice_name(name):
|
||||||
|
|
||||||
def get_choices(choices):
|
def get_choices(choices):
|
||||||
converted_names = []
|
converted_names = []
|
||||||
|
if isinstance(choices, OrderedDict):
|
||||||
|
choices = choices.items()
|
||||||
for value, help_text in choices:
|
for value, help_text in choices:
|
||||||
if isinstance(help_text, (tuple, list)):
|
if isinstance(help_text, (tuple, list)):
|
||||||
for choice in get_choices(help_text):
|
for choice in get_choices(help_text):
|
||||||
|
@ -52,26 +57,56 @@ def get_choices(choices):
|
||||||
yield name, value, description
|
yield name, value, description
|
||||||
|
|
||||||
|
|
||||||
def convert_django_field_with_choices(field, registry=None):
|
def convert_choices_to_named_enum_with_descriptions(name, choices):
|
||||||
|
choices = list(get_choices(choices))
|
||||||
|
named_choices = [(c[0], c[1]) for c in choices]
|
||||||
|
named_choices_descriptions = {c[0]: c[2] for c in choices}
|
||||||
|
|
||||||
|
class EnumWithDescriptionsType(object):
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return named_choices_descriptions[self.name]
|
||||||
|
|
||||||
|
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_enum_name(django_model_meta, field):
|
||||||
|
if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
|
||||||
|
# Try and import custom function
|
||||||
|
custom_func = import_string(
|
||||||
|
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
|
||||||
|
)
|
||||||
|
name = custom_func(field)
|
||||||
|
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING is True:
|
||||||
|
name = "{app_label}{object_name}{field_name}Choices".format(
|
||||||
|
app_label=to_camel_case(django_model_meta.app_label.title()),
|
||||||
|
object_name=django_model_meta.object_name,
|
||||||
|
field_name=to_camel_case(field.name.title()),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def convert_choice_field_to_enum(field, name=None):
|
||||||
|
if name is None:
|
||||||
|
name = generate_enum_name(field.model._meta, field)
|
||||||
|
choices = field.choices
|
||||||
|
return convert_choices_to_named_enum_with_descriptions(name, choices)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_django_field_with_choices(
|
||||||
|
field, registry=None, convert_choices_to_enum=True
|
||||||
|
):
|
||||||
if registry is not None:
|
if registry is not None:
|
||||||
converted = registry.get_converted_field(field)
|
converted = registry.get_converted_field(field)
|
||||||
if converted:
|
if converted:
|
||||||
return converted
|
return converted
|
||||||
choices = getattr(field, "choices", None)
|
choices = getattr(field, "choices", None)
|
||||||
if choices:
|
if choices and convert_choices_to_enum:
|
||||||
meta = field.model._meta
|
enum = convert_choice_field_to_enum(field)
|
||||||
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
|
required = not (field.blank or field.null)
|
||||||
choices = list(get_choices(choices))
|
converted = enum(description=field.help_text, required=required)
|
||||||
named_choices = [(c[0], c[1]) for c in choices]
|
|
||||||
named_choices_descriptions = {c[0]: c[2] for c in choices}
|
|
||||||
|
|
||||||
class EnumWithDescriptionsType(object):
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return named_choices_descriptions[self.name]
|
|
||||||
|
|
||||||
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
|
||||||
converted = enum(description=field.help_text, required=not field.null)
|
|
||||||
else:
|
else:
|
||||||
converted = convert_django_field(field, registry)
|
converted = convert_django_field(field, registry)
|
||||||
if registry is not None:
|
if registry is not None:
|
||||||
|
@ -177,19 +212,32 @@ def convert_field_to_list_or_connection(field, registry=None):
|
||||||
if not _type:
|
if not _type:
|
||||||
return
|
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
|
# If there is a connection, we should transform the field
|
||||||
# into a DjangoConnectionField
|
# into a DjangoConnectionField
|
||||||
if _type._meta.connection:
|
if _type._meta.connection:
|
||||||
# Use a DjangoFilterConnectionField if there are
|
# Use a DjangoFilterConnectionField if there are
|
||||||
# defined filter_fields in the DjangoObjectType Meta
|
# defined filter_fields or a filterset_class in the
|
||||||
if _type._meta.filter_fields:
|
# DjangoObjectType Meta
|
||||||
|
if _type._meta.filter_fields or _type._meta.filterset_class:
|
||||||
from .filter.fields import DjangoFilterConnectionField
|
from .filter.fields import DjangoFilterConnectionField
|
||||||
|
|
||||||
return DjangoFilterConnectionField(_type)
|
return DjangoFilterConnectionField(
|
||||||
|
_type, required=True, description=description
|
||||||
|
)
|
||||||
|
|
||||||
return DjangoConnectionField(_type)
|
return DjangoConnectionField(_type, required=True, description=description)
|
||||||
|
|
||||||
return DjangoListField(_type)
|
return DjangoListField(
|
||||||
|
_type,
|
||||||
|
required=True, # A Set is always returned, never None.
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
return Dynamic(dynamic_type)
|
return Dynamic(dynamic_type)
|
||||||
|
|
||||||
|
@ -219,12 +267,12 @@ def convert_postgres_array_to_list(field, registry=None):
|
||||||
|
|
||||||
@convert_django_field.register(HStoreField)
|
@convert_django_field.register(HStoreField)
|
||||||
@convert_django_field.register(JSONField)
|
@convert_django_field.register(JSONField)
|
||||||
def convert_posgres_field_to_string(field, registry=None):
|
def convert_postgres_field_to_string(field, registry=None):
|
||||||
return JSONString(description=field.help_text, required=not field.null)
|
return JSONString(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(RangeField)
|
@convert_django_field.register(RangeField)
|
||||||
def convert_posgres_range_to_string(field, registry=None):
|
def convert_postgres_range_to_string(field, registry=None):
|
||||||
inner_type = convert_django_field(field.base_field)
|
inner_type = convert_django_field(field.base_field)
|
||||||
if not isinstance(inner_type, (List, NonNull)):
|
if not isinstance(inner_type, (List, NonNull)):
|
||||||
inner_type = type(inner_type)
|
inner_type = type(inner_type)
|
||||||
|
|
|
@ -16,14 +16,18 @@ class DjangoDebugContext(object):
|
||||||
def get_debug_promise(self):
|
def get_debug_promise(self):
|
||||||
if not self.debug_promise:
|
if not self.debug_promise:
|
||||||
self.debug_promise = Promise.all(self.promises)
|
self.debug_promise = Promise.all(self.promises)
|
||||||
|
self.promises = []
|
||||||
return self.debug_promise.then(self.on_resolve_all_promises)
|
return self.debug_promise.then(self.on_resolve_all_promises)
|
||||||
|
|
||||||
def on_resolve_all_promises(self, values):
|
def on_resolve_all_promises(self, values):
|
||||||
|
if self.promises:
|
||||||
|
self.debug_promise = None
|
||||||
|
return self.get_debug_promise()
|
||||||
self.disable_instrumentation()
|
self.disable_instrumentation()
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
def add_promise(self, promise):
|
def add_promise(self, promise):
|
||||||
if self.debug_promise and not self.debug_promise.is_fulfilled:
|
if self.debug_promise:
|
||||||
self.promises.append(promise)
|
self.promises.append(promise)
|
||||||
|
|
||||||
def enable_instrumentation(self):
|
def enable_instrumentation(self):
|
||||||
|
|
|
@ -5,8 +5,8 @@ import json
|
||||||
from threading import local
|
from threading import local
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from django.utils import six
|
import six
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from .types import DjangoDebugSQL
|
from .types import DjangoDebugSQL
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class NormalCursorWrapper(object):
|
||||||
|
|
||||||
def _quote_expr(self, element):
|
def _quote_expr(self, element):
|
||||||
if isinstance(element, six.string_types):
|
if isinstance(element, six.string_types):
|
||||||
return "'%s'" % force_text(element).replace("'", "''")
|
return "'%s'" % force_str(element).replace("'", "''")
|
||||||
else:
|
else:
|
||||||
return repr(element)
|
return repr(element)
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class NormalCursorWrapper(object):
|
||||||
|
|
||||||
def _decode(self, param):
|
def _decode(self, param):
|
||||||
try:
|
try:
|
||||||
return force_text(param, strings_only=True)
|
return force_str(param, strings_only=True)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return "(encoded string)"
|
return "(encoded string)"
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,7 @@ from graphene import Boolean, Float, ObjectType, String
|
||||||
|
|
||||||
class DjangoDebugSQL(ObjectType):
|
class DjangoDebugSQL(ObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
description = (
|
description = "Represents a single database query made to a Django managed DB."
|
||||||
"Represents a single database query made to a Django managed DB."
|
|
||||||
)
|
|
||||||
|
|
||||||
vendor = String(
|
vendor = String(
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -14,37 +12,26 @@ class DjangoDebugSQL(ObjectType):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
alias = String(
|
alias = String(
|
||||||
required=True,
|
required=True, description="The Django database alias (e.g. 'default')."
|
||||||
description="The Django database alias (e.g. 'default').",
|
|
||||||
)
|
)
|
||||||
sql = String(description="The actual SQL sent to this database.")
|
sql = String(description="The actual SQL sent to this database.")
|
||||||
duration = Float(
|
duration = Float(
|
||||||
required=True,
|
required=True, description="Duration of this database query in seconds."
|
||||||
description="Duration of this database query in seconds.",
|
|
||||||
)
|
)
|
||||||
raw_sql = String(
|
raw_sql = String(
|
||||||
required=True,
|
required=True, description="The raw SQL of this query, without params."
|
||||||
description="The raw SQL of this query, without params.",
|
|
||||||
)
|
)
|
||||||
params = String(
|
params = String(
|
||||||
required=True,
|
required=True, description="JSON encoded database query parameters."
|
||||||
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.",
|
|
||||||
)
|
)
|
||||||
|
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(
|
is_slow = Boolean(
|
||||||
required=True,
|
required=True,
|
||||||
description="Whether this database query took more than 10 seconds.",
|
description="Whether this database query took more than 10 seconds.",
|
||||||
)
|
)
|
||||||
is_select = Boolean(
|
is_select = Boolean(
|
||||||
required=True,
|
required=True, description="Whether this database query was a SELECT."
|
||||||
description="Whether this database query was a SELECT.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Postgres
|
# Postgres
|
||||||
|
|
|
@ -31,7 +31,7 @@ def test_should_query_field():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
reporter = graphene.Field(ReporterType)
|
reporter = graphene.Field(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="__debug")
|
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||||
|
|
||||||
def resolve_reporter(self, info, **args):
|
def resolve_reporter(self, info, **args):
|
||||||
return Reporter.objects.first()
|
return Reporter.objects.first()
|
||||||
|
@ -41,7 +41,7 @@ def test_should_query_field():
|
||||||
reporter {
|
reporter {
|
||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,7 @@ def test_should_query_field():
|
||||||
"""
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
"reporter": {"lastName": "ABA"},
|
"reporter": {"lastName": "ABA"},
|
||||||
"__debug": {
|
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
|
||||||
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
|
@ -62,6 +60,73 @@ def test_should_query_field():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_nested_field():
|
||||||
|
r1 = Reporter(last_name="ABA")
|
||||||
|
r1.save()
|
||||||
|
r2 = Reporter(last_name="Griffin")
|
||||||
|
r2.save()
|
||||||
|
r2.pets.add(r1)
|
||||||
|
r1.pets.add(r2)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||||
|
|
||||||
|
def resolve_reporter(self, info, **args):
|
||||||
|
return Reporter.objects.first()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query ReporterQuery {
|
||||||
|
reporter {
|
||||||
|
lastName
|
||||||
|
pets { edges { node {
|
||||||
|
lastName
|
||||||
|
pets { edges { node { lastName } } }
|
||||||
|
} } }
|
||||||
|
}
|
||||||
|
__debug {
|
||||||
|
sql {
|
||||||
|
rawSql
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {
|
||||||
|
"reporter": {
|
||||||
|
"lastName": "ABA",
|
||||||
|
"pets": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"lastName": "Griffin",
|
||||||
|
"pets": {"edges": [{"node": {"lastName": "ABA"}}]},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
result = schema.execute(
|
||||||
|
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
|
||||||
|
)
|
||||||
|
assert not result.errors
|
||||||
|
query = str(Reporter.objects.order_by("pk")[:1].query)
|
||||||
|
assert result.data["__debug"]["sql"][0]["rawSql"] == query
|
||||||
|
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
|
||||||
|
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
|
||||||
|
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
|
||||||
|
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
|
||||||
|
assert len(result.data["__debug"]["sql"]) == 5
|
||||||
|
|
||||||
|
assert result.data["reporter"] == expected["reporter"]
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_list():
|
def test_should_query_list():
|
||||||
r1 = Reporter(last_name="ABA")
|
r1 = Reporter(last_name="ABA")
|
||||||
r1.save()
|
r1.save()
|
||||||
|
@ -75,7 +140,7 @@ def test_should_query_list():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = graphene.List(ReporterType)
|
all_reporters = graphene.List(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="__debug")
|
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -85,7 +150,7 @@ def test_should_query_list():
|
||||||
allReporters {
|
allReporters {
|
||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -94,7 +159,7 @@ def test_should_query_list():
|
||||||
"""
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}],
|
"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)
|
schema = graphene.Schema(query=Query)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
|
@ -117,7 +182,7 @@ def test_should_query_connection():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="__debug")
|
debug = graphene.Field(DjangoDebug, name="_debug")
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -131,7 +196,7 @@ def test_should_query_connection():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -145,9 +210,9 @@ def test_should_query_connection():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["allReporters"] == expected["allReporters"]
|
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)
|
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():
|
def test_should_query_connectionfilter():
|
||||||
|
@ -166,7 +231,7 @@ def test_should_query_connectionfilter():
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
|
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
|
||||||
s = graphene.String(resolver=lambda *_: "S")
|
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):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -180,7 +245,7 @@ def test_should_query_connectionfilter():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -194,6 +259,6 @@ def test_should_query_connectionfilter():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["allReporters"] == expected["allReporters"]
|
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)
|
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:
|
class Meta:
|
||||||
description = "Debugging information for the current query."
|
description = "Debugging information for the current query."
|
||||||
|
|
||||||
sql = List(
|
sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.")
|
||||||
DjangoDebugSQL,
|
|
||||||
description="Executed SQL queries for this API query.",
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,30 +1,62 @@
|
||||||
from functools import partial, reduce
|
from functools import partial, reduce
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
from graphene import NonNull
|
||||||
from graphene.types import Field, List
|
from graphene.types import Field, List
|
||||||
from graphene.relay import ConnectionField, PageInfo
|
from graphene.relay import ConnectionField, PageInfo
|
||||||
from graphene.utils.get_unbound_function import get_unbound_function
|
from graphene.utils.get_unbound_function import get_unbound_function
|
||||||
from graphql_relay.connection.arrayconnection import connection_from_list_slice
|
from graphql_relay.connection.arrayconnection import connection_from_list_slice
|
||||||
|
from graphene_django.utils.utils import auth_resolver
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
from .utils import maybe_queryset, auth_resolver
|
from .utils import maybe_queryset
|
||||||
|
|
||||||
|
|
||||||
class DjangoListField(Field):
|
class DjangoListField(Field):
|
||||||
def __init__(self, _type, *args, **kwargs):
|
def __init__(self, _type, *args, **kwargs):
|
||||||
super(DjangoListField, self).__init__(List(_type), *args, **kwargs)
|
from .types import DjangoObjectType
|
||||||
|
|
||||||
|
if isinstance(_type, NonNull):
|
||||||
|
_type = _type.of_type
|
||||||
|
|
||||||
|
# Django would never return a Set of None vvvvvvv
|
||||||
|
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
|
||||||
|
|
||||||
|
assert issubclass(
|
||||||
|
self._underlying_type, DjangoObjectType
|
||||||
|
), "DjangoListField only accepts DjangoObjectType types"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _underlying_type(self):
|
||||||
|
_type = self._type
|
||||||
|
while hasattr(_type, "of_type"):
|
||||||
|
_type = _type.of_type
|
||||||
|
return _type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
return self.type.of_type._meta.node._meta.model
|
return self._underlying_type._meta.model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_resolver(resolver, root, info, **args):
|
def list_resolver(django_object_type, resolver, root, info, **args):
|
||||||
return maybe_queryset(resolver(root, info, **args))
|
queryset = maybe_queryset(resolver(root, info, **args))
|
||||||
|
if queryset is None:
|
||||||
|
# Default to Django Model queryset
|
||||||
|
# N.B. This happens if DjangoListField is used in the top level Query object
|
||||||
|
model_manager = django_object_type._meta.model.objects
|
||||||
|
queryset = maybe_queryset(
|
||||||
|
django_object_type.get_queryset(model_manager, info)
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def get_resolver(self, parent_resolver):
|
||||||
return partial(self.list_resolver, parent_resolver)
|
_type = self.type
|
||||||
|
if isinstance(_type, NonNull):
|
||||||
|
_type = _type.of_type
|
||||||
|
django_object_type = _type.of_type.of_type
|
||||||
|
return partial(self.list_resolver, django_object_type, parent_resolver)
|
||||||
|
|
||||||
|
|
||||||
class DjangoConnectionField(ConnectionField):
|
class DjangoConnectionField(ConnectionField):
|
||||||
|
@ -44,17 +76,31 @@ class DjangoConnectionField(ConnectionField):
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
|
|
||||||
_type = super(ConnectionField, self).type
|
_type = super(ConnectionField, self).type
|
||||||
|
non_null = False
|
||||||
|
if isinstance(_type, NonNull):
|
||||||
|
_type = _type.of_type
|
||||||
|
non_null = True
|
||||||
assert issubclass(
|
assert issubclass(
|
||||||
_type, DjangoObjectType
|
_type, DjangoObjectType
|
||||||
), "DjangoConnectionField only accepts DjangoObjectType types"
|
), "DjangoConnectionField only accepts DjangoObjectType types"
|
||||||
assert _type._meta.connection, "The type {} doesn't have a connection".format(
|
assert _type._meta.connection, "The type {} doesn't have a connection".format(
|
||||||
_type.__name__
|
_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
|
@property
|
||||||
def node_type(self):
|
def node_type(self):
|
||||||
return self.type._meta.node
|
return self.connection_type._meta.node
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
|
@ -67,22 +113,18 @@ class DjangoConnectionField(ConnectionField):
|
||||||
return self.model._default_manager
|
return self.model._default_manager
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def merge_querysets(cls, default_queryset, queryset):
|
def resolve_queryset(cls, connection, queryset, info, args):
|
||||||
if default_queryset.query.distinct and not queryset.query.distinct:
|
# queryset is the resolved iterable from ObjectType
|
||||||
queryset = queryset.distinct()
|
return connection._meta.node.get_queryset(queryset, info)
|
||||||
elif queryset.query.distinct and not default_queryset.query.distinct:
|
|
||||||
default_queryset = default_queryset.distinct()
|
|
||||||
return queryset & default_queryset
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_connection(cls, connection, default_manager, args, iterable):
|
def resolve_connection(cls, connection, args, default_manager, iterable):
|
||||||
|
|
||||||
if iterable is None:
|
if iterable is None:
|
||||||
iterable = default_manager
|
iterable = default_manager
|
||||||
|
|
||||||
iterable = maybe_queryset(iterable)
|
iterable = maybe_queryset(iterable)
|
||||||
if isinstance(iterable, QuerySet):
|
if isinstance(iterable, QuerySet):
|
||||||
if iterable is not default_manager:
|
|
||||||
default_queryset = maybe_queryset(default_manager)
|
|
||||||
iterable = cls.merge_querysets(default_queryset, iterable)
|
|
||||||
_len = iterable.count()
|
_len = iterable.count()
|
||||||
else:
|
else:
|
||||||
_len = len(iterable)
|
_len = len(iterable)
|
||||||
|
@ -106,6 +148,7 @@ class DjangoConnectionField(ConnectionField):
|
||||||
resolver,
|
resolver,
|
||||||
connection,
|
connection,
|
||||||
default_manager,
|
default_manager,
|
||||||
|
queryset_resolver,
|
||||||
max_limit,
|
max_limit,
|
||||||
enforce_first_or_last,
|
enforce_first_or_last,
|
||||||
root,
|
root,
|
||||||
|
@ -133,8 +176,15 @@ class DjangoConnectionField(ConnectionField):
|
||||||
).format(last, info.field_name, max_limit)
|
).format(last, info.field_name, max_limit)
|
||||||
args["last"] = min(last, max_limit)
|
args["last"] = min(last, max_limit)
|
||||||
|
|
||||||
|
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
|
||||||
|
# or a resolve_foo (does not accept queryset)
|
||||||
iterable = resolver(root, info, **args)
|
iterable = resolver(root, info, **args)
|
||||||
on_resolve = partial(cls.resolve_connection, connection, default_manager, args)
|
if iterable is None:
|
||||||
|
iterable = default_manager
|
||||||
|
# thus the iterable gets refiltered by resolve_queryset
|
||||||
|
# but iterable might be promise
|
||||||
|
iterable = queryset_resolver(connection, iterable, info, args)
|
||||||
|
on_resolve = partial(cls.resolve_connection, connection, args, default_manager)
|
||||||
|
|
||||||
if Promise.is_thenable(iterable):
|
if Promise.is_thenable(iterable):
|
||||||
return Promise.resolve(iterable).then(on_resolve)
|
return Promise.resolve(iterable).then(on_resolve)
|
||||||
|
@ -145,17 +195,23 @@ class DjangoConnectionField(ConnectionField):
|
||||||
return partial(
|
return partial(
|
||||||
self.connection_resolver,
|
self.connection_resolver,
|
||||||
parent_resolver,
|
parent_resolver,
|
||||||
self.type,
|
self.connection_type,
|
||||||
self.get_manager(),
|
self.get_manager(),
|
||||||
|
self.get_queryset_resolver(),
|
||||||
self.max_limit,
|
self.max_limit,
|
||||||
self.enforce_first_or_last,
|
self.enforce_first_or_last,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_queryset_resolver(self):
|
||||||
|
return self.resolve_queryset
|
||||||
|
|
||||||
|
|
||||||
class DjangoField(Field):
|
class DjangoField(Field):
|
||||||
"""Class to manage permission for fields"""
|
"""Class to manage permission for fields"""
|
||||||
|
|
||||||
def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs):
|
def __init__(
|
||||||
|
self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs
|
||||||
|
):
|
||||||
"""Get permissions to access a field"""
|
"""Get permissions to access a field"""
|
||||||
super(DjangoField, self).__init__(type, *args, **kwargs)
|
super(DjangoField, self).__init__(type, *args, **kwargs)
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
@ -165,15 +221,23 @@ class DjangoField(Field):
|
||||||
"""Intercept resolver to analyse permissions"""
|
"""Intercept resolver to analyse permissions"""
|
||||||
parent_resolver = super(DjangoField, self).get_resolver(parent_resolver)
|
parent_resolver = super(DjangoField, self).get_resolver(parent_resolver)
|
||||||
if self.permissions:
|
if self.permissions:
|
||||||
return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None,
|
return partial(
|
||||||
None, True)
|
get_unbound_function(self.permissions_resolver),
|
||||||
|
parent_resolver,
|
||||||
|
self.permissions,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
)
|
||||||
return parent_resolver
|
return parent_resolver
|
||||||
|
|
||||||
|
|
||||||
class DataLoaderField(DjangoField):
|
class DataLoaderField(DjangoField):
|
||||||
"""Class to manage access to data-loader when resolve the field"""
|
"""Class to manage access to data-loader when resolve the field"""
|
||||||
|
|
||||||
def __init__(self, type, data_loader, source_loader, load_many=False, *args, **kwargs):
|
def __init__(
|
||||||
|
self, type, data_loader, source_loader, load_many=False, *args, **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Initialization of data-loader to resolve field
|
Initialization of data-loader to resolve field
|
||||||
:param data_loader: data-loader to resolve field
|
:param data_loader: data-loader to resolve field
|
||||||
|
@ -193,7 +257,9 @@ class DataLoaderField(DjangoField):
|
||||||
def resolver_data_loader(self, root, info, *args, **kwargs):
|
def resolver_data_loader(self, root, info, *args, **kwargs):
|
||||||
"""Resolve field through dataloader"""
|
"""Resolve field through dataloader"""
|
||||||
if root:
|
if root:
|
||||||
source_loader = reduce(lambda x, y: getattr(x, y), self.source_loader.split('.'), root)
|
source_loader = reduce(
|
||||||
|
lambda x, y: getattr(x, y), self.source_loader.split("."), root
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
source_loader = kwargs.get(self.source_loader)
|
source_loader = kwargs.get(self.source_loader)
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,10 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
if self._extra_filter_meta:
|
if self._extra_filter_meta:
|
||||||
meta.update(self._extra_filter_meta)
|
meta.update(self._extra_filter_meta)
|
||||||
|
|
||||||
self._filterset_class = get_filterset_class(
|
filterset_class = self._provided_filterset_class or (
|
||||||
self._provided_filterset_class, **meta
|
self.node_type._meta.filterset_class
|
||||||
)
|
)
|
||||||
|
self._filterset_class = get_filterset_class(filterset_class, **meta)
|
||||||
|
|
||||||
return self._filterset_class
|
return self._filterset_class
|
||||||
|
|
||||||
|
@ -51,69 +52,18 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def merge_querysets(cls, default_queryset, queryset):
|
def resolve_queryset(
|
||||||
# There could be the case where the default queryset (returned from the filterclass)
|
cls, connection, iterable, info, args, filtering_args, filterset_class
|
||||||
# and the resolver queryset have some limits on it.
|
|
||||||
# We only would be able to apply one of those, but not both
|
|
||||||
# at the same time.
|
|
||||||
|
|
||||||
# See related PR: https://github.com/graphql-python/graphene-django/pull/126
|
|
||||||
|
|
||||||
assert not (
|
|
||||||
default_queryset.query.low_mark and queryset.query.low_mark
|
|
||||||
), "Received two sliced querysets (low mark) in the connection, please slice only in one."
|
|
||||||
assert not (
|
|
||||||
default_queryset.query.high_mark and queryset.query.high_mark
|
|
||||||
), "Received two sliced querysets (high mark) in the connection, please slice only in one."
|
|
||||||
low = default_queryset.query.low_mark or queryset.query.low_mark
|
|
||||||
high = default_queryset.query.high_mark or queryset.query.high_mark
|
|
||||||
default_queryset.query.clear_limits()
|
|
||||||
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(
|
|
||||||
default_queryset, queryset
|
|
||||||
)
|
|
||||||
queryset.query.set_limits(low, high)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def connection_resolver(
|
|
||||||
cls,
|
|
||||||
resolver,
|
|
||||||
connection,
|
|
||||||
default_manager,
|
|
||||||
max_limit,
|
|
||||||
enforce_first_or_last,
|
|
||||||
filterset_class,
|
|
||||||
filtering_args,
|
|
||||||
root,
|
|
||||||
info,
|
|
||||||
**args
|
|
||||||
):
|
):
|
||||||
|
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
|
||||||
|
connection, iterable, info, args
|
||||||
|
)
|
||||||
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
||||||
qs = filterset_class(
|
return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs
|
||||||
data=filter_kwargs,
|
|
||||||
queryset=default_manager.get_queryset(),
|
|
||||||
request=info.context,
|
|
||||||
).qs
|
|
||||||
|
|
||||||
return super(DjangoFilterConnectionField, cls).connection_resolver(
|
def get_queryset_resolver(self):
|
||||||
resolver,
|
|
||||||
connection,
|
|
||||||
qs,
|
|
||||||
max_limit,
|
|
||||||
enforce_first_or_last,
|
|
||||||
root,
|
|
||||||
info,
|
|
||||||
**args
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
|
||||||
return partial(
|
return partial(
|
||||||
self.connection_resolver,
|
self.resolve_queryset,
|
||||||
parent_resolver,
|
filterset_class=self.filterset_class,
|
||||||
self.type,
|
filtering_args=self.filtering_args,
|
||||||
self.get_manager(),
|
|
||||||
self.max_limit,
|
|
||||||
self.enforce_first_or_last,
|
|
||||||
self.filterset_class,
|
|
||||||
self.filtering_args,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,8 +45,7 @@ class GrapheneFilterSetMixin(BaseFilterSet):
|
||||||
|
|
||||||
FILTER_DEFAULTS = dict(
|
FILTER_DEFAULTS = dict(
|
||||||
itertools.chain(
|
itertools.chain(
|
||||||
FILTER_FOR_DBFIELD_DEFAULTS.items(),
|
FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items()
|
||||||
GRAPHENE_FILTER_SET_OVERRIDES.items()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,7 +58,6 @@ if VERSION[0] < 2:
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
|
||||||
class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
|
class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_for_reverse_field(cls, f, name):
|
def filter_for_reverse_field(cls, f, name):
|
||||||
"""Handles retrieving filters for reverse relationships
|
"""Handles retrieving filters for reverse relationships
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.db.models import TextField, Value
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
|
||||||
from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
|
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
from graphene_django.tests.models import Article, Pet, Reporter
|
from graphene_django.tests.models import Article, Pet, Reporter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
# for annotation test
|
|
||||||
from django.db.models import TextField, Value
|
|
||||||
from django.db.models.functions import Concat
|
|
||||||
|
|
||||||
pytestmark = []
|
pytestmark = []
|
||||||
|
|
||||||
if DJANGO_FILTER_INSTALLED:
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
@ -57,8 +56,6 @@ if DJANGO_FILTER_INSTALLED:
|
||||||
model = Pet
|
model = Pet
|
||||||
interfaces = (Node,)
|
interfaces = (Node,)
|
||||||
|
|
||||||
# schema = Schema()
|
|
||||||
|
|
||||||
|
|
||||||
def get_args(field):
|
def get_args(field):
|
||||||
return field.args
|
return field.args
|
||||||
|
@ -227,6 +224,74 @@ def test_filter_filterset_information_on_meta_related():
|
||||||
assert_not_orderable(articles_field)
|
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():
|
def test_filter_filterset_related_results():
|
||||||
class ReporterFilterNode(DjangoObjectType):
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -253,12 +318,14 @@ def test_filter_filterset_related_results():
|
||||||
pub_date=datetime.now(),
|
pub_date=datetime.now(),
|
||||||
pub_date_time=datetime.now(),
|
pub_date_time=datetime.now(),
|
||||||
reporter=r1,
|
reporter=r1,
|
||||||
|
editor=r1,
|
||||||
)
|
)
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline="a2",
|
headline="a2",
|
||||||
pub_date=datetime.now(),
|
pub_date=datetime.now(),
|
||||||
pub_date_time=datetime.now(),
|
pub_date_time=datetime.now(),
|
||||||
reporter=r2,
|
reporter=r2,
|
||||||
|
editor=r2,
|
||||||
)
|
)
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
|
@ -382,7 +449,7 @@ def test_global_id_multiple_field_explicit_reverse():
|
||||||
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
|
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
|
||||||
|
|
||||||
|
|
||||||
def test_filter_filterset_related_results():
|
def test_filter_filterset_related_results_with_filter():
|
||||||
class ReporterFilterNode(DjangoObjectType):
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Reporter
|
model = Reporter
|
||||||
|
@ -392,15 +459,15 @@ def test_filter_filterset_related_results():
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||||
|
|
||||||
r1 = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="A test user", last_name="Last Name", email="test1@test.com"
|
first_name="A test user", last_name="Last Name", email="test1@test.com"
|
||||||
)
|
)
|
||||||
r2 = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="Other test user",
|
first_name="Other test user",
|
||||||
last_name="Other Last Name",
|
last_name="Other Last Name",
|
||||||
email="test2@test.com",
|
email="test2@test.com",
|
||||||
)
|
)
|
||||||
r3 = Reporter.objects.create(
|
Reporter.objects.create(
|
||||||
first_name="Random", last_name="RandomLast", email="random@test.com"
|
first_name="Random", last_name="RandomLast", email="random@test.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -541,58 +608,6 @@ def test_should_query_filter_node_limit():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_filter_node_double_limit_raises():
|
|
||||||
class ReporterFilter(FilterSet):
|
|
||||||
limit = NumberFilter(method="filter_limit")
|
|
||||||
|
|
||||||
def filter_limit(self, queryset, name, value):
|
|
||||||
return queryset[:value]
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Reporter
|
|
||||||
fields = ["first_name"]
|
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Reporter
|
|
||||||
interfaces = (Node,)
|
|
||||||
|
|
||||||
class Query(ObjectType):
|
|
||||||
all_reporters = DjangoFilterConnectionField(
|
|
||||||
ReporterType, filterset_class=ReporterFilter
|
|
||||||
)
|
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
|
||||||
return Reporter.objects.order_by("a_choice")[:2]
|
|
||||||
|
|
||||||
Reporter.objects.create(
|
|
||||||
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
|
|
||||||
)
|
|
||||||
r = Reporter.objects.create(
|
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
|
||||||
)
|
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
query = """
|
|
||||||
query NodeFilteringQuery {
|
|
||||||
allReporters(limit: 1) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert len(result.errors) == 1
|
|
||||||
assert str(result.errors[0]) == (
|
|
||||||
"Received two sliced querysets (high mark) in the connection, please slice only in one."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_order_by_is_perserved():
|
def test_order_by_is_perserved():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -614,7 +629,7 @@ def test_order_by_is_perserved():
|
||||||
return reporters
|
return reporters
|
||||||
|
|
||||||
Reporter.objects.create(first_name="b")
|
Reporter.objects.create(first_name="b")
|
||||||
r = Reporter.objects.create(first_name="a")
|
Reporter.objects.create(first_name="a")
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
schema = Schema(query=Query)
|
||||||
query = """
|
query = """
|
||||||
|
@ -654,7 +669,7 @@ def test_order_by_is_perserved():
|
||||||
assert reverse_result.data == reverse_expected
|
assert reverse_result.data == reverse_expected
|
||||||
|
|
||||||
|
|
||||||
def test_annotation_is_perserved():
|
def test_annotation_is_preserved():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
full_name = String()
|
full_name = String()
|
||||||
|
|
||||||
|
@ -697,3 +712,290 @@ def test_annotation_is_perserved():
|
||||||
|
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotation_with_only():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
full_name = String()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterType)
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, **args):
|
||||||
|
return Reporter.objects.only("first_name", "last_name").annotate(
|
||||||
|
full_name=Concat(
|
||||||
|
"first_name", Value(" "), "last_name", output_field=TextField()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {"allReporters": {"edges": [{"node": {"fullName": "John Doe"}}]}}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_get_queryset_is_called():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
return queryset.filter(first_name="b")
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(
|
||||||
|
ReporterType, reverse_order=Boolean()
|
||||||
|
)
|
||||||
|
|
||||||
|
Reporter.objects.create(first_name="b")
|
||||||
|
Reporter.objects.create(first_name="a")
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {"allReporters": {"edges": [{"node": {"firstName": "b"}}]}}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_integer_field_filter_type():
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = {"age": ["exact"]}
|
||||||
|
fields = ("age",)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
pets = DjangoFilterConnectionField(PetType)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert str(schema) == dedent(
|
||||||
|
"""\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetType implements Node {
|
||||||
|
age: Int!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetTypeConnection {
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
edges: [PetTypeEdge]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetTypeEdge {
|
||||||
|
node: PetType
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_filter_types():
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = {"age": ["exact", "isnull", "lt"]}
|
||||||
|
fields = ("age",)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
pets = DjangoFilterConnectionField(PetType)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert str(schema) == dedent(
|
||||||
|
"""\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetType implements Node {
|
||||||
|
age: Int!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetTypeConnection {
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
edges: [PetTypeEdge]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetTypeEdge {
|
||||||
|
node: PetType
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_filterset_based_on_mixin():
|
||||||
|
class ArticleFilterMixin(FilterSet):
|
||||||
|
@classmethod
|
||||||
|
def get_filters(cls):
|
||||||
|
filters = super(FilterSet, cls).get_filters()
|
||||||
|
filters.update(
|
||||||
|
{
|
||||||
|
"viewer__email__in": django_filters.CharFilter(
|
||||||
|
method="filter_email_in", field_name="reporter__email__in"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def filter_email_in(cls, queryset, name, value):
|
||||||
|
return queryset.filter(**{name: [value]})
|
||||||
|
|
||||||
|
class NewArticleFilter(ArticleFilterMixin, ArticleFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NewReporterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class NewArticleFilterNode(DjangoObjectType):
|
||||||
|
viewer = Field(NewReporterNode)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
interfaces = (Node,)
|
||||||
|
filterset_class = NewArticleFilter
|
||||||
|
|
||||||
|
def resolve_viewer(self, info):
|
||||||
|
return self.reporter
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_articles = DjangoFilterConnectionField(NewArticleFilterNode)
|
||||||
|
|
||||||
|
reporter_1 = Reporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="john@doe.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
article_1 = Article.objects.create(
|
||||||
|
headline="Hello",
|
||||||
|
reporter=reporter_1,
|
||||||
|
editor=reporter_1,
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
reporter_2 = Reporter.objects.create(
|
||||||
|
first_name="Adam", last_name="Doe", email="adam@doe.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
article_2 = Article.objects.create(
|
||||||
|
headline="Good Bye",
|
||||||
|
reporter=reporter_2,
|
||||||
|
editor=reporter_2,
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
"""
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allArticles(viewer_Email_In: "%s") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
headline
|
||||||
|
viewer {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% reporter_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"allArticles": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"headline": article_1.headline,
|
||||||
|
"viewer": {"email": reporter_1.email},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from django_filters.utils import get_model_field
|
||||||
from .filterset import custom_filterset_factory, setup_filterset
|
from .filterset import custom_filterset_factory, setup_filterset
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,8 +12,26 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
||||||
from ..forms.converter import convert_form_field
|
from ..forms.converter import convert_form_field
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
|
model = filterset_class._meta.model
|
||||||
for name, filter_field in six.iteritems(filterset_class.base_filters):
|
for name, filter_field in six.iteritems(filterset_class.base_filters):
|
||||||
field_type = convert_form_field(filter_field.field).Argument()
|
form_field = None
|
||||||
|
|
||||||
|
if name in filterset_class.declared_filters:
|
||||||
|
form_field = filter_field.field
|
||||||
|
else:
|
||||||
|
model_field = get_model_field(model, filter_field.field_name)
|
||||||
|
filter_type = filter_field.lookup_expr
|
||||||
|
if filter_type != "isnull" and hasattr(model_field, "formfield"):
|
||||||
|
form_field = model_field.formfield(
|
||||||
|
required=filter_field.extra.get("required", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback to field defined on filter if we can't get it from the
|
||||||
|
# model field
|
||||||
|
if not form_field:
|
||||||
|
form_field = filter_field.field
|
||||||
|
|
||||||
|
field_type = convert_form_field(form_field).Argument()
|
||||||
field_type.description = filter_field.label
|
field_type.description = filter_field.label
|
||||||
args[name] = field_type
|
args[name] = field_type
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time, Enum
|
from graphene import (
|
||||||
|
ID,
|
||||||
|
Boolean,
|
||||||
|
Float,
|
||||||
|
Int,
|
||||||
|
List,
|
||||||
|
String,
|
||||||
|
UUID,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Time,
|
||||||
|
Enum,
|
||||||
|
)
|
||||||
from graphene.utils.str_converters import to_camel_case
|
from graphene.utils.str_converters import to_camel_case
|
||||||
|
|
||||||
from graphene_django.converter import get_choices
|
from graphene_django.converter import get_choices
|
||||||
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
from ..utils import import_single_dispatch
|
from ..utils import import_single_dispatch
|
||||||
|
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
singledispatch = import_single_dispatch()
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +56,7 @@ def convert_form_field_to_int(field):
|
||||||
|
|
||||||
@convert_form_field.register(forms.BooleanField)
|
@convert_form_field.register(forms.BooleanField)
|
||||||
def convert_form_field_to_boolean(field):
|
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)
|
@convert_form_field.register(forms.NullBooleanField)
|
||||||
|
@ -88,7 +99,7 @@ def convert_form_field_to_id(field):
|
||||||
|
|
||||||
def get_form_name(form):
|
def get_form_name(form):
|
||||||
"""Get form name"""
|
"""Get form name"""
|
||||||
class_name = str(form.__class__).split('.')[-1]
|
class_name = str(form.__class__).split(".")[-1]
|
||||||
return class_name[:-2]
|
return class_name[:-2]
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,15 +111,20 @@ def convert_form_field_with_choices(field, name=None, form=None):
|
||||||
:param form: field's form
|
:param form: field's form
|
||||||
:return: graphene Field
|
:return: graphene Field
|
||||||
"""
|
"""
|
||||||
choices = getattr(field, 'choices', None)
|
choices = getattr(field, "choices", None)
|
||||||
|
|
||||||
# If is a choice field, but not depends on models
|
# If is a choice field, but not depends on models
|
||||||
if not isinstance(field, (forms.ModelMultipleChoiceField, forms.ModelChoiceField)) and choices:
|
if (
|
||||||
|
not isinstance(field, (forms.ModelMultipleChoiceField, forms.ModelChoiceField))
|
||||||
|
and choices
|
||||||
|
):
|
||||||
if form:
|
if form:
|
||||||
name = to_camel_case("{}_{}".format(get_form_name(form), field.label or name))
|
name = to_camel_case(
|
||||||
|
"{}_{}".format(get_form_name(form), field.label or name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
name = field.label or name
|
name = field.label or name
|
||||||
name = to_camel_case(name.replace(' ', '_'))
|
name = to_camel_case(name.replace(" ", "_"))
|
||||||
choices = list(get_choices(choices))
|
choices = list(get_choices(choices))
|
||||||
named_choices = [(c[0], c[1]) for c in choices]
|
named_choices = [(c[0], c[1]) for c in choices]
|
||||||
named_choices_descriptions = {c[0]: c[2] for c in choices}
|
named_choices_descriptions = {c[0]: c[2] for c in choices}
|
||||||
|
@ -123,5 +139,7 @@ def convert_form_field_with_choices(field, name=None, form=None):
|
||||||
return named_choices_descriptions[self.name]
|
return named_choices_descriptions[self.name]
|
||||||
|
|
||||||
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||||
return enum(description=field.help_text, required=field.required) # pylint: disable=E1102
|
return enum(
|
||||||
|
description=field.help_text, required=field.required
|
||||||
|
) # pylint: disable=E1102
|
||||||
return convert_form_field(field)
|
return convert_form_field(field)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import binascii
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import CharField, Field, MultipleChoiceField
|
from django.forms import CharField, Field, MultipleChoiceField
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from graphql_relay import from_global_id
|
from graphql_relay import from_global_id
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,10 @@ from graphene.types.mutation import MutationOptions
|
||||||
# InputObjectType,
|
# InputObjectType,
|
||||||
# )
|
# )
|
||||||
from graphene.types.utils import yank_fields_from_attrs
|
from graphene.types.utils import yank_fields_from_attrs
|
||||||
from graphene.utils.str_converters import to_camel_case
|
|
||||||
from graphene_django.registry import get_global_registry
|
from graphene_django.registry import get_global_registry
|
||||||
|
|
||||||
|
from ..types import ErrorType
|
||||||
from .converter import convert_form_field_with_choices
|
from .converter import convert_form_field_with_choices
|
||||||
from .types import ErrorType
|
|
||||||
|
|
||||||
|
|
||||||
def fields_for_form(form, only_fields, exclude_fields):
|
def fields_for_form(form, only_fields, exclude_fields):
|
||||||
|
@ -45,10 +44,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
return cls.perform_mutate(form, info)
|
return cls.perform_mutate(form, info)
|
||||||
else:
|
else:
|
||||||
errors = [
|
errors = ErrorType.from_errors(form.errors)
|
||||||
ErrorType(field=to_camel_case(key) if key != '__all__' else key, messages=value)
|
|
||||||
for key, value in form.errors.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
return cls(errors=errors)
|
return cls(errors=errors)
|
||||||
|
|
||||||
|
@ -69,28 +65,6 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
# class DjangoFormInputObjectTypeOptions(InputObjectTypeOptions):
|
|
||||||
# form_class = None
|
|
||||||
|
|
||||||
|
|
||||||
# class DjangoFormInputObjectType(InputObjectType):
|
|
||||||
# class Meta:
|
|
||||||
# abstract = True
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def __init_subclass_with_meta__(cls, form_class=None,
|
|
||||||
# only_fields=(), exclude_fields=(), _meta=None, **options):
|
|
||||||
# if not _meta:
|
|
||||||
# _meta = DjangoFormInputObjectTypeOptions(cls)
|
|
||||||
# assert isinstance(form_class, forms.Form), (
|
|
||||||
# 'form_class must be an instance of django.forms.Form'
|
|
||||||
# )
|
|
||||||
# _meta.form_class = form_class
|
|
||||||
# form = form_class()
|
|
||||||
# fields = fields_for_form(form, only_fields, exclude_fields)
|
|
||||||
# super(DjangoFormInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, fields=fields, **options)
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoFormMutationOptions(MutationOptions):
|
class DjangoFormMutationOptions(MutationOptions):
|
||||||
form_class = None
|
form_class = None
|
||||||
|
|
||||||
|
@ -103,7 +77,12 @@ class DjangoFormMutation(BaseDjangoFormMutation):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass_with_meta__(
|
def __init_subclass_with_meta__(
|
||||||
cls, form_class=None, mirror_input=False, only_fields=(), exclude_fields=(), **options
|
cls,
|
||||||
|
form_class=None,
|
||||||
|
mirror_input=False,
|
||||||
|
only_fields=(),
|
||||||
|
exclude_fields=(),
|
||||||
|
**options
|
||||||
):
|
):
|
||||||
|
|
||||||
if not form_class:
|
if not form_class:
|
||||||
|
@ -128,7 +107,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_mutate(cls, form, info):
|
def perform_mutate(cls, form, info):
|
||||||
form.save()
|
form.save()
|
||||||
return cls(errors=[])
|
return cls(errors=[], **form.cleaned_data)
|
||||||
|
|
||||||
|
|
||||||
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
|
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
|
||||||
|
@ -169,7 +148,9 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
||||||
|
|
||||||
registry = get_global_registry()
|
registry = get_global_registry()
|
||||||
model_type = registry.get_type_for_model(model)
|
model_type = registry.get_type_for_model(model)
|
||||||
return_field_name = return_field_name
|
if not model_type:
|
||||||
|
raise Exception("No type registered for model: {}".format(model.__name__))
|
||||||
|
|
||||||
if not return_field_name:
|
if not return_field_name:
|
||||||
model_name = model.__name__
|
model_name = model.__name__
|
||||||
return_field_name = model_name[:1].lower() + model_name[1:]
|
return_field_name = model_name[:1].lower() + model_name[1:]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
import graphene
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
String,
|
String,
|
||||||
Int,
|
Int,
|
||||||
|
@ -116,6 +115,8 @@ def test_should_manytoone_convert_connectionorlist():
|
||||||
|
|
||||||
|
|
||||||
def test_should_typed_choice_convert_enum():
|
def test_should_typed_choice_convert_enum():
|
||||||
field = forms.TypedChoiceField(choices=(('A', 'Choice A'), ('B', 'Choice B')), label='field')
|
field = forms.TypedChoiceField(
|
||||||
graphene_type = convert_form_field_with_choices(field, name='field_name')
|
choices=(("A", "Choice A"), ("B", "Choice B")), label="field"
|
||||||
|
)
|
||||||
|
graphene_type = convert_form_field_with_choices(field, name="field_name")
|
||||||
assert isinstance(graphene_type, Enum)
|
assert isinstance(graphene_type, Enum)
|
||||||
|
|
|
@ -1,21 +1,54 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
from graphene_django.tests.models import Pet, Film, FilmDetails
|
from graphene import ObjectType, Schema, String, Field
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from graphene_django.tests.models import Film, Pet
|
||||||
|
|
||||||
|
from ...settings import graphene_settings
|
||||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||||
|
|
||||||
|
|
||||||
class MyForm(forms.Form):
|
class MyForm(forms.Form):
|
||||||
text = forms.CharField()
|
text = forms.CharField(required=False)
|
||||||
|
|
||||||
|
def clean_text(self):
|
||||||
|
text = self.cleaned_data["text"]
|
||||||
|
if text == "INVALID_INPUT":
|
||||||
|
raise ValidationError("Invalid input")
|
||||||
|
return text
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PetForm(forms.ModelForm):
|
class PetForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pet
|
model = Pet
|
||||||
fields = '__all__'
|
fields = "__all__"
|
||||||
|
|
||||||
test_camel = forms.IntegerField(required=False)
|
test_camel = forms.IntegerField(required=False)
|
||||||
|
|
||||||
|
def clean_age(self):
|
||||||
|
age = self.cleaned_data["age"]
|
||||||
|
if age >= 99:
|
||||||
|
raise ValidationError("Too old")
|
||||||
|
return age
|
||||||
|
|
||||||
|
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
def test_needs_form_class():
|
def test_needs_form_class():
|
||||||
with raises(Exception) as exc:
|
with raises(Exception) as exc:
|
||||||
|
@ -42,6 +75,118 @@ def test_has_input_fields():
|
||||||
assert "text" in MyMutation.Input._meta.fields
|
assert "text" in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_error_camelcased():
|
||||||
|
class ExtraPetForm(PetForm):
|
||||||
|
test_field = forms.CharField(required=True)
|
||||||
|
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = ExtraPetForm
|
||||||
|
|
||||||
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
|
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
|
||||||
|
graphene_settings.CAMELCASE_ERRORS = True
|
||||||
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
|
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
||||||
|
graphene_settings.CAMELCASE_ERRORS = False
|
||||||
|
|
||||||
|
|
||||||
|
class MockQuery(ObjectType):
|
||||||
|
a = String()
|
||||||
|
|
||||||
|
|
||||||
|
class FormMutationTests(TestCase):
|
||||||
|
def test_form_invalid_form(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
mirror_input = True
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
my_mutation = MyMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyMutation {
|
||||||
|
myMutation(input: { text: "INVALID_INPUT" }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(result.errors, None)
|
||||||
|
self.assertEqual(
|
||||||
|
result.data["myMutation"]["errors"],
|
||||||
|
[{"field": "text", "messages": ["Invalid input"]}],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_form_valid_input(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
mirror_input = True
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
my_mutation = MyMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyMutation {
|
||||||
|
myMutation(input: { text: "VALID_INPUT" }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(result.errors, None)
|
||||||
|
self.assertEqual(result.data["myMutation"]["errors"], [])
|
||||||
|
self.assertEqual(result.data["myMutation"]["text"], "VALID_INPUT")
|
||||||
|
|
||||||
|
def test_default_meta_fields(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
self.assertNotIn("text", MyMutation._meta.fields)
|
||||||
|
|
||||||
|
def test_mirror_meta_fields(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
mirror_input = True
|
||||||
|
|
||||||
|
self.assertIn("text", MyMutation._meta.fields)
|
||||||
|
|
||||||
|
def test_default_input_meta_fields(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
self.assertIn("client_mutation_id", MyMutation.Input._meta.fields)
|
||||||
|
self.assertIn("text", MyMutation.Input._meta.fields)
|
||||||
|
|
||||||
|
def test_exclude_fields_input_meta_fields(self):
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
exclude_fields = ["text"]
|
||||||
|
|
||||||
|
self.assertNotIn("text", MyMutation.Input._meta.fields)
|
||||||
|
self.assertIn("client_mutation_id", MyMutation.Input._meta.fields)
|
||||||
|
|
||||||
|
|
||||||
class ModelFormMutationTests(TestCase):
|
class ModelFormMutationTests(TestCase):
|
||||||
def test_default_meta_fields(self):
|
def test_default_meta_fields(self):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
@ -67,7 +212,7 @@ class ModelFormMutationTests(TestCase):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
exclude_fields = ['id']
|
exclude_fields = ["id"]
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
self.assertEqual(PetMutation._meta.model, Pet)
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
||||||
|
@ -76,101 +221,137 @@ class ModelFormMutationTests(TestCase):
|
||||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
||||||
self.assertNotIn("id", PetMutation.Input._meta.fields)
|
self.assertNotIn("id", PetMutation.Input._meta.fields)
|
||||||
|
|
||||||
def test_return_field_name_is_camelcased(self):
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
model = FilmDetails
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, FilmDetails)
|
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "filmDetails")
|
|
||||||
|
|
||||||
def test_custom_return_field_name(self):
|
def test_custom_return_field_name(self):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
model = Film
|
model = Pet
|
||||||
return_field_name = "animal"
|
return_field_name = "animal"
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Film)
|
self.assertEqual(PetMutation._meta.model, Pet)
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "animal")
|
self.assertEqual(PetMutation._meta.return_field_name, "animal")
|
||||||
self.assertIn("animal", PetMutation._meta.fields)
|
self.assertIn("animal", PetMutation._meta.fields)
|
||||||
|
|
||||||
def test_model_form_mutation_mutate(self):
|
def test_model_form_mutation_mutate_existing(self):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
pet = Pet.objects.create(name="Axel", age=10)
|
pet = Pet.objects.create(name="Axel", age=10)
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name="Mia", age=10)
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation($pk: ID!) {
|
||||||
|
petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
variable_values={"pk": pet.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(result.errors, None)
|
||||||
|
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
self.assertEqual(Pet.objects.count(), 1)
|
||||||
pet.refresh_from_db()
|
pet.refresh_from_db()
|
||||||
self.assertEqual(pet.name, "Mia")
|
self.assertEqual(pet.name, "Mia")
|
||||||
self.assertEqual(result.errors, [])
|
|
||||||
|
|
||||||
def test_model_form_mutation_updates_existing_(self):
|
def test_model_form_mutation_creates_new(self):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None, name="Mia", age=10)
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation {
|
||||||
|
petMutation(input: { name: "Mia", age: 10 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.assertIs(result.errors, None)
|
||||||
|
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
self.assertEqual(Pet.objects.count(), 1)
|
||||||
pet = Pet.objects.get()
|
pet = Pet.objects.get()
|
||||||
self.assertEqual(pet.name, "Mia")
|
self.assertEqual(pet.name, "Mia")
|
||||||
self.assertEqual(pet.age, 10)
|
self.assertEqual(pet.age, 10)
|
||||||
self.assertEqual(result.errors, [])
|
|
||||||
|
def test_model_form_mutation_invalid_input(self):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation {
|
||||||
|
petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.assertIs(result.errors, None)
|
||||||
|
self.assertEqual(result.data["petMutation"]["pet"], None)
|
||||||
|
self.assertEqual(
|
||||||
|
result.data["petMutation"]["errors"],
|
||||||
|
[{"field": "age", "messages": ["Too old"],}],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(Pet.objects.count(), 0)
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_invalid_form(self):
|
def test_model_form_mutation_mutate_invalid_form(self):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None, test_camel='text')
|
result = PetMutation.mutate_and_get_payload(None, None, test_camel="text")
|
||||||
|
|
||||||
# A pet was not created
|
# A pet was not created
|
||||||
self.assertEqual(Pet.objects.count(), 0)
|
self.assertEqual(Pet.objects.count(), 0)
|
||||||
|
|
||||||
fields_w_error = {e.field: e.messages for e in result.errors}
|
fields_w_error = {e.field: e.messages for e in result.errors}
|
||||||
self.assertEqual(len(result.errors), 3)
|
self.assertEqual(len(result.errors), 3)
|
||||||
self.assertIn("testCamel", fields_w_error)
|
self.assertIn("test_camel", fields_w_error)
|
||||||
self.assertEqual(fields_w_error['testCamel'], ["Enter a whole number."])
|
self.assertEqual(fields_w_error["test_camel"], ["Enter a whole number."])
|
||||||
self.assertIn("name", fields_w_error)
|
self.assertIn("name", fields_w_error)
|
||||||
self.assertEqual(fields_w_error['name'], ["This field is required."])
|
self.assertEqual(fields_w_error["name"], ["This field is required."])
|
||||||
self.assertIn("age", fields_w_error)
|
self.assertIn("age", fields_w_error)
|
||||||
self.assertEqual(fields_w_error['age'], ["This field is required."])
|
self.assertEqual(fields_w_error["age"], ["This field is required."])
|
||||||
|
|
||||||
|
|
||||||
class FormMutationTests(TestCase):
|
|
||||||
def test_default_meta_fields(self):
|
|
||||||
class MyMutation(DjangoFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = MyForm
|
|
||||||
self.assertNotIn("text", MyMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_mirror_meta_fields(self):
|
|
||||||
class MyMutation(DjangoFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = MyForm
|
|
||||||
mirror_input = True
|
|
||||||
|
|
||||||
self.assertIn("text", MyMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_default_input_meta_fields(self):
|
|
||||||
class MyMutation(DjangoFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = MyForm
|
|
||||||
|
|
||||||
self.assertIn("client_mutation_id", MyMutation.Input._meta.fields)
|
|
||||||
self.assertIn("text", MyMutation.Input._meta.fields)
|
|
||||||
|
|
||||||
def test_exclude_fields_input_meta_fields(self):
|
|
||||||
class MyMutation(DjangoFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = MyForm
|
|
||||||
exclude_fields = ['text']
|
|
||||||
|
|
||||||
self.assertNotIn("text", MyMutation.Input._meta.fields)
|
|
||||||
self.assertIn("client_mutation_id", MyMutation.Input._meta.fields)
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
|
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
|
||||||
class ErrorType(graphene.ObjectType):
|
|
||||||
field = graphene.String()
|
|
||||||
messages = graphene.List(graphene.String)
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import functools
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils import autoreload
|
||||||
|
|
||||||
|
from graphql import print_schema
|
||||||
from graphene_django.settings import graphene_settings
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,14 +36,50 @@ class CommandArguments(BaseCommand):
|
||||||
help="Output file indent (default: None)",
|
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):
|
class Command(CommandArguments):
|
||||||
help = "Dump Graphene schema JSON to file"
|
help = "Dump Graphene schema as a JSON or GraphQL file"
|
||||||
can_import_settings = True
|
can_import_settings = True
|
||||||
|
|
||||||
def save_file(self, out, schema_dict, indent):
|
def save_json_file(self, out, schema_dict, indent):
|
||||||
with open(out, "w") as outfile:
|
with open(out, "w") as outfile:
|
||||||
json.dump(schema_dict, outfile, indent=indent)
|
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
||||||
|
|
||||||
|
def save_graphql_file(self, out, schema):
|
||||||
|
with open(out, "w") as outfile:
|
||||||
|
outfile.write(print_schema(schema))
|
||||||
|
|
||||||
|
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:
|
||||||
|
# Determine format
|
||||||
|
_, file_extension = os.path.splitext(out)
|
||||||
|
|
||||||
|
if file_extension == ".graphql":
|
||||||
|
self.save_graphql_file(out, schema)
|
||||||
|
elif file_extension == ".json":
|
||||||
|
self.save_json_file(out, schema_dict, indent)
|
||||||
|
else:
|
||||||
|
raise CommandError(
|
||||||
|
'Unrecognised file format "{}"'.format(file_extension)
|
||||||
|
)
|
||||||
|
|
||||||
|
style = getattr(self, "style", None)
|
||||||
|
success = getattr(style, "SUCCESS", lambda x: x)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
success("Successfully dumped GraphQL schema to {}".format(out))
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
options_schema = options.get("schema")
|
options_schema = options.get("schema")
|
||||||
|
@ -63,13 +103,10 @@ class Command(CommandArguments):
|
||||||
)
|
)
|
||||||
|
|
||||||
indent = options.get("indent")
|
indent = options.get("indent")
|
||||||
schema_dict = {"data": schema.introspect()}
|
watch = options.get("watch")
|
||||||
if out == '-':
|
if watch:
|
||||||
self.stdout.write(json.dumps(schema_dict, indent=indent))
|
autoreload.run_with_reloader(
|
||||||
|
functools.partial(self.get_schema, schema, out, indent)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.save_file(out, schema_dict, indent)
|
self.get_schema(schema, out, indent)
|
||||||
|
|
||||||
style = getattr(self, "style", None)
|
|
||||||
success = getattr(style, "SUCCESS", lambda x: x)
|
|
||||||
|
|
||||||
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
|
||||||
|
|
|
@ -4,3 +4,13 @@ from django.db import models
|
||||||
class MyFakeModel(models.Model):
|
class MyFakeModel(models.Model):
|
||||||
cool_name = models.CharField(max_length=50)
|
cool_name = models.CharField(max_length=50)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModelWithPassword(models.Model):
|
||||||
|
cool_name = models.CharField(max_length=50)
|
||||||
|
password = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModelWithDate(models.Model):
|
||||||
|
cool_name = models.CharField(max_length=50)
|
||||||
|
last_edited = models.DateField()
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
from graphene.relay.mutation import ClientIDMutation
|
||||||
from graphene.types import Field, InputField
|
from graphene.types import Field, InputField
|
||||||
from graphene.types.mutation import MutationOptions
|
from graphene.types.mutation import MutationOptions
|
||||||
from graphene.relay.mutation import ClientIDMutation
|
|
||||||
from graphene.types.objecttype import yank_fields_from_attrs
|
from graphene.types.objecttype import yank_fields_from_attrs
|
||||||
|
|
||||||
|
from ..types import ErrorType
|
||||||
from .serializer_converter import convert_serializer_field
|
from .serializer_converter import convert_serializer_field
|
||||||
from .types import ErrorType
|
|
||||||
|
|
||||||
|
|
||||||
class SerializerMutationOptions(MutationOptions):
|
class SerializerMutationOptions(MutationOptions):
|
||||||
|
@ -19,20 +20,31 @@ class SerializerMutationOptions(MutationOptions):
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
|
|
||||||
|
|
||||||
def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False):
|
def fields_for_serializer(
|
||||||
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=False,
|
||||||
|
convert_choices_to_enum=True,
|
||||||
|
):
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for name, field in serializer.fields.items():
|
for name, field in serializer.fields.items():
|
||||||
is_not_in_only = only_fields and name not in only_fields
|
is_not_in_only = only_fields and name not in only_fields
|
||||||
is_excluded = (
|
is_excluded = any(
|
||||||
name
|
[
|
||||||
in exclude_fields # or
|
name in exclude_fields,
|
||||||
# name in already_created_fields
|
field.write_only
|
||||||
|
and not is_input, # don't show write_only fields in Query
|
||||||
|
field.read_only and is_input, # don't show read_only fields in Input
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_not_in_only or is_excluded:
|
if is_not_in_only or is_excluded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fields[name] = convert_serializer_field(field, is_input=is_input)
|
fields[name] = convert_serializer_field(
|
||||||
|
field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum
|
||||||
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,9 +62,10 @@ class SerializerMutation(ClientIDMutation):
|
||||||
lookup_field=None,
|
lookup_field=None,
|
||||||
serializer_class=None,
|
serializer_class=None,
|
||||||
model_class=None,
|
model_class=None,
|
||||||
model_operations=["create", "update"],
|
model_operations=("create", "update"),
|
||||||
only_fields=(),
|
only_fields=(),
|
||||||
exclude_fields=(),
|
exclude_fields=(),
|
||||||
|
convert_choices_to_enum=True,
|
||||||
**options
|
**options
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -72,10 +85,18 @@ class SerializerMutation(ClientIDMutation):
|
||||||
lookup_field = model_class._meta.pk.name
|
lookup_field = model_class._meta.pk.name
|
||||||
|
|
||||||
input_fields = fields_for_serializer(
|
input_fields = fields_for_serializer(
|
||||||
serializer, only_fields, exclude_fields, is_input=True
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=True,
|
||||||
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
)
|
)
|
||||||
output_fields = fields_for_serializer(
|
output_fields = fields_for_serializer(
|
||||||
serializer, only_fields, exclude_fields, is_input=False
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=False,
|
||||||
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
)
|
)
|
||||||
|
|
||||||
_meta = SerializerMutationOptions(cls)
|
_meta = SerializerMutationOptions(cls)
|
||||||
|
@ -100,8 +121,10 @@ class SerializerMutation(ClientIDMutation):
|
||||||
instance = get_object_or_404(
|
instance = get_object_or_404(
|
||||||
model_class, **{lookup_field: input[lookup_field]}
|
model_class, **{lookup_field: input[lookup_field]}
|
||||||
)
|
)
|
||||||
|
partial = True
|
||||||
elif "create" in cls._meta.model_operations:
|
elif "create" in cls._meta.model_operations:
|
||||||
instance = None
|
instance = None
|
||||||
|
partial = False
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Invalid update operation. Input parameter "{}" required.'.format(
|
'Invalid update operation. Input parameter "{}" required.'.format(
|
||||||
|
@ -113,6 +136,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
"instance": instance,
|
"instance": instance,
|
||||||
"data": input,
|
"data": input,
|
||||||
"context": {"request": info.context},
|
"context": {"request": info.context},
|
||||||
|
"partial": partial,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"data": input, "context": {"request": info.context}}
|
return {"data": input, "context": {"request": info.context}}
|
||||||
|
@ -125,10 +149,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
return cls.perform_mutate(serializer, info)
|
return cls.perform_mutate(serializer, info)
|
||||||
else:
|
else:
|
||||||
errors = [
|
errors = ErrorType.from_errors(serializer.errors)
|
||||||
ErrorType(field=key, messages=value)
|
|
||||||
for key, value in serializer.errors.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
return cls(errors=errors)
|
return cls(errors=errors)
|
||||||
|
|
||||||
|
@ -138,6 +159,10 @@ class SerializerMutation(ClientIDMutation):
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for f, field in serializer.fields.items():
|
for f, field in serializer.fields.items():
|
||||||
kwargs[f] = field.get_attribute(obj)
|
if not field.write_only:
|
||||||
|
if isinstance(field, serializers.SerializerMethodField):
|
||||||
|
kwargs[f] = field.to_representation(obj)
|
||||||
|
else:
|
||||||
|
kwargs[f] = field.get_attribute(obj)
|
||||||
|
|
||||||
return cls(errors=None, **kwargs)
|
return cls(errors=None, **kwargs)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from ..registry import get_global_registry
|
from ..registry import get_global_registry
|
||||||
|
from ..converter import convert_choices_to_named_enum_with_descriptions
|
||||||
from ..utils import import_single_dispatch
|
from ..utils import import_single_dispatch
|
||||||
from .types import DictType
|
from .types import DictType
|
||||||
|
|
||||||
|
@ -18,14 +19,17 @@ def get_graphene_type_from_serializer_field(field):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_serializer_field(field, is_input=True):
|
def convert_serializer_field(field, is_input=True, convert_choices_to_enum=True):
|
||||||
"""
|
"""
|
||||||
Converts a django rest frameworks field to a graphql field
|
Converts a django rest frameworks field to a graphql field
|
||||||
and marks the field as required if we are creating an input type
|
and marks the field as required if we are creating an input type
|
||||||
and the field itself is required
|
and the field itself is required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
graphql_type = get_graphene_type_from_serializer_field(field)
|
if isinstance(field, serializers.ChoiceField) and not convert_choices_to_enum:
|
||||||
|
graphql_type = graphene.String
|
||||||
|
else:
|
||||||
|
graphql_type = get_graphene_type_from_serializer_field(field)
|
||||||
|
|
||||||
args = []
|
args = []
|
||||||
kwargs = {"description": field.help_text, "required": is_input and field.required}
|
kwargs = {"description": field.help_text, "required": is_input and field.required}
|
||||||
|
@ -57,18 +61,27 @@ def convert_serializer_field(field, is_input=True):
|
||||||
|
|
||||||
|
|
||||||
def convert_serializer_to_input_type(serializer_class):
|
def convert_serializer_to_input_type(serializer_class):
|
||||||
|
cached_type = convert_serializer_to_input_type.cache.get(
|
||||||
|
serializer_class.__name__, None
|
||||||
|
)
|
||||||
|
if cached_type:
|
||||||
|
return cached_type
|
||||||
serializer = serializer_class()
|
serializer = serializer_class()
|
||||||
|
|
||||||
items = {
|
items = {
|
||||||
name: convert_serializer_field(field)
|
name: convert_serializer_field(field)
|
||||||
for name, field in serializer.fields.items()
|
for name, field in serializer.fields.items()
|
||||||
}
|
}
|
||||||
|
ret_type = type(
|
||||||
return type(
|
|
||||||
"{}Input".format(serializer.__class__.__name__),
|
"{}Input".format(serializer.__class__.__name__),
|
||||||
(graphene.InputObjectType,),
|
(graphene.InputObjectType,),
|
||||||
items,
|
items,
|
||||||
)
|
)
|
||||||
|
convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type
|
||||||
|
return ret_type
|
||||||
|
|
||||||
|
|
||||||
|
convert_serializer_to_input_type.cache = {}
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.Field)
|
@get_graphene_type_from_serializer_field.register(serializers.Field)
|
||||||
|
@ -121,7 +134,6 @@ def convert_serializer_field_to_time(field):
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.ListField)
|
@get_graphene_type_from_serializer_field.register(serializers.ListField)
|
||||||
def convert_serializer_field_to_list(field, is_input=True):
|
def convert_serializer_field_to_list(field, is_input=True):
|
||||||
child_type = get_graphene_type_from_serializer_field(field.child)
|
child_type = get_graphene_type_from_serializer_field(field.child)
|
||||||
|
|
||||||
return (graphene.List, child_type)
|
return (graphene.List, child_type)
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,5 +148,13 @@ def convert_serializer_field_to_jsonstring(field):
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
|
@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
|
||||||
def convert_serializer_field_to_list_of_string(field):
|
def convert_serializer_field_to_list_of_enum(field):
|
||||||
return (graphene.List, graphene.String)
|
child_type = convert_serializer_field_to_enum(field)
|
||||||
|
return (graphene.List, child_type)
|
||||||
|
|
||||||
|
|
||||||
|
@get_graphene_type_from_serializer_field.register(serializers.ChoiceField)
|
||||||
|
def convert_serializer_field_to_enum(field):
|
||||||
|
# enums require a name
|
||||||
|
name = field.field_name or field.source or "Choices"
|
||||||
|
return convert_choices_to_named_enum_with_descriptions(name, field.choices)
|
||||||
|
|
|
@ -10,7 +10,9 @@ from ..serializer_converter import convert_serializer_field
|
||||||
from ..types import DictType
|
from ..types import DictType
|
||||||
|
|
||||||
|
|
||||||
def _get_type(rest_framework_field, is_input=True, **kwargs):
|
def _get_type(
|
||||||
|
rest_framework_field, is_input=True, convert_choices_to_enum=True, **kwargs
|
||||||
|
):
|
||||||
# prevents the following error:
|
# prevents the following error:
|
||||||
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
||||||
# Remove `source=` from the field declaration.
|
# Remove `source=` from the field declaration.
|
||||||
|
@ -21,7 +23,9 @@ def _get_type(rest_framework_field, is_input=True, **kwargs):
|
||||||
|
|
||||||
field = rest_framework_field(**kwargs)
|
field = rest_framework_field(**kwargs)
|
||||||
|
|
||||||
return convert_serializer_field(field, is_input=is_input)
|
return convert_serializer_field(
|
||||||
|
field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_conversion(rest_framework_field, graphene_field, **kwargs):
|
def assert_conversion(rest_framework_field, graphene_field, **kwargs):
|
||||||
|
@ -60,8 +64,27 @@ def test_should_url_convert_string():
|
||||||
assert_conversion(serializers.URLField, graphene.String)
|
assert_conversion(serializers.URLField, graphene.String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_choice_convert_string():
|
def test_should_choice_convert_enum():
|
||||||
assert_conversion(serializers.ChoiceField, graphene.String, choices=[])
|
field = assert_conversion(
|
||||||
|
serializers.ChoiceField,
|
||||||
|
graphene.Enum,
|
||||||
|
choices=[("h", "Hello"), ("w", "World")],
|
||||||
|
source="word",
|
||||||
|
)
|
||||||
|
assert field._meta.enum.__members__["H"].value == "h"
|
||||||
|
assert field._meta.enum.__members__["H"].description == "Hello"
|
||||||
|
assert field._meta.enum.__members__["W"].value == "w"
|
||||||
|
assert field._meta.enum.__members__["W"].description == "World"
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_choice_convert_string_if_enum_disabled():
|
||||||
|
assert_conversion(
|
||||||
|
serializers.ChoiceField,
|
||||||
|
graphene.String,
|
||||||
|
choices=[("h", "Hello"), ("w", "World")],
|
||||||
|
source="word",
|
||||||
|
convert_choices_to_enum=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_should_base_field_convert_string():
|
def test_should_base_field_convert_string():
|
||||||
|
@ -174,7 +197,7 @@ def test_should_file_convert_string():
|
||||||
|
|
||||||
|
|
||||||
def test_should_filepath_convert_string():
|
def test_should_filepath_convert_string():
|
||||||
assert_conversion(serializers.FilePathField, graphene.String, path="/")
|
assert_conversion(serializers.FilePathField, graphene.Enum, path="/")
|
||||||
|
|
||||||
|
|
||||||
def test_should_ip_convert_string():
|
def test_should_ip_convert_string():
|
||||||
|
@ -189,9 +212,9 @@ def test_should_json_convert_jsonstring():
|
||||||
assert_conversion(serializers.JSONField, graphene.types.json.JSONString)
|
assert_conversion(serializers.JSONField, graphene.types.json.JSONString)
|
||||||
|
|
||||||
|
|
||||||
def test_should_multiplechoicefield_convert_to_list_of_string():
|
def test_should_multiplechoicefield_convert_to_list_of_enum():
|
||||||
field = assert_conversion(
|
field = assert_conversion(
|
||||||
serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3]
|
serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert field.of_type == graphene.String
|
assert issubclass(field.of_type, graphene.Enum)
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import graphene
|
||||||
|
import pytest
|
||||||
|
from django.db import models
|
||||||
|
from graphene import Schema
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeChildModel(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeParentModel(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
child1 = models.OneToOneField(
|
||||||
|
MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
child2 = models.OneToOneField(
|
||||||
|
MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParentType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeParentModel
|
||||||
|
interfaces = (graphene.relay.Node,)
|
||||||
|
|
||||||
|
|
||||||
|
class ChildType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeChildModel
|
||||||
|
interfaces = (graphene.relay.Node,)
|
||||||
|
|
||||||
|
|
||||||
|
class MyModelChildSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeChildModel
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MyModelParentSerializer(serializers.ModelSerializer):
|
||||||
|
child1 = MyModelChildSerializer()
|
||||||
|
child2 = MyModelChildSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeParentModel
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MyParentModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelParentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class Mutation(graphene.ObjectType):
|
||||||
|
createParentWithChild = MyParentModelMutation.Field()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_schema():
|
||||||
|
schema = Schema(mutation=Mutation, types=[ParentType, ChildType])
|
||||||
|
assert schema
|
|
@ -1,13 +1,14 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from py.test import mark, raises
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from graphene import Field, ResolveInfo
|
from graphene import Field, ResolveInfo
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
from py.test import raises
|
|
||||||
from py.test import mark
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
|
from ...settings import graphene_settings
|
||||||
from ...types import DjangoObjectType
|
from ...types import DjangoObjectType
|
||||||
from ..models import MyFakeModel
|
from ..models import MyFakeModel, MyFakeModelWithPassword, MyFakeModelWithDate
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +33,18 @@ class MyModelSerializer(serializers.ModelSerializer):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MyModelSerializerWithMethod(serializers.ModelSerializer):
|
||||||
|
days_since_last_edit = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithDate
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def get_days_since_last_edit(self, obj):
|
||||||
|
now = datetime.date(2020, 1, 8)
|
||||||
|
return (now - obj.last_edited).days
|
||||||
|
|
||||||
|
|
||||||
class MyModelMutation(SerializerMutation):
|
class MyModelMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
serializer_class = MyModelSerializer
|
serializer_class = MyModelSerializer
|
||||||
|
@ -86,6 +99,70 @@ def test_exclude_fields():
|
||||||
assert "created" not in MyMutation.Input._meta.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"
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_read_only_fields():
|
||||||
|
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
cool_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithPassword
|
||||||
|
fields = ["cool_name", "password"]
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = ReadOnlyFieldModelSerializer
|
||||||
|
|
||||||
|
assert "password" in MyMutation.Input._meta.fields
|
||||||
|
assert (
|
||||||
|
"cool_name" not in MyMutation.Input._meta.fields
|
||||||
|
), "'cool_name' is read_only field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -137,6 +214,16 @@ def test_model_update_mutate_and_get_payload_success():
|
||||||
assert result.cool_name == "New Narf"
|
assert result.cool_name == "New Narf"
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_model_partial_update_mutate_and_get_payload_success():
|
||||||
|
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(
|
||||||
|
None, mock_info(), **{"id": instance.id}
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == "Narf"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
@mark.django_db
|
||||||
def test_model_invalid_update_mutate_and_get_payload_success():
|
def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
class InvalidModelMutation(SerializerMutation):
|
class InvalidModelMutation(SerializerMutation):
|
||||||
|
@ -152,6 +239,23 @@ def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
assert '"id" required' in str(exc.value)
|
assert '"id" required' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_perform_mutate_success():
|
||||||
|
class MyMethodMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializerWithMethod
|
||||||
|
|
||||||
|
result = MyMethodMutation.mutate_and_get_payload(
|
||||||
|
None,
|
||||||
|
mock_info(),
|
||||||
|
**{"cool_name": "Narf", "last_edited": datetime.date(2020, 1, 4)}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == "Narf"
|
||||||
|
assert result.days_since_last_edit == 4
|
||||||
|
|
||||||
|
|
||||||
def test_mutate_and_get_payload_error():
|
def test_mutate_and_get_payload_error():
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -168,6 +272,13 @@ def test_model_mutate_and_get_payload_error():
|
||||||
assert len(result.errors) > 0
|
assert len(result.errors) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_error_camelcased():
|
||||||
|
graphene_settings.CAMELCASE_ERRORS = True
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||||
|
assert result.errors[0].field == "coolName"
|
||||||
|
graphene_settings.CAMELCASE_ERRORS = False
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_serializer_operations():
|
def test_invalid_serializer_operations():
|
||||||
with raises(Exception) as exc:
|
with raises(Exception) as exc:
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,6 @@ import graphene
|
||||||
from graphene.types.unmountedtype import UnmountedType
|
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):
|
class DictType(UnmountedType):
|
||||||
key = graphene.String()
|
key = graphene.String()
|
||||||
value = graphene.String()
|
value = graphene.String()
|
||||||
|
|
|
@ -13,9 +13,9 @@ back to the defaults.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import six
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test.signals import setting_changed
|
from django.test.signals import setting_changed
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import importlib # Available in Python 3.1+
|
import importlib # Available in Python 3.1+
|
||||||
|
@ -28,13 +28,17 @@ except ImportError:
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
"SCHEMA": None,
|
"SCHEMA": None,
|
||||||
"SCHEMA_OUTPUT": "schema.json",
|
"SCHEMA_OUTPUT": "schema.json",
|
||||||
"SCHEMA_INDENT": None,
|
"SCHEMA_INDENT": 2,
|
||||||
"MIDDLEWARE": (),
|
"MIDDLEWARE": (),
|
||||||
# Set to True if the connection fields must have
|
# Set to True if the connection fields must have
|
||||||
# either the first or last argument
|
# either the first or last argument
|
||||||
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
|
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
|
||||||
# Max items returned in ConnectionFields / FilterConnectionFields
|
# Max items returned in ConnectionFields / FilterConnectionFields
|
||||||
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
||||||
|
"CAMELCASE_ERRORS": False,
|
||||||
|
# Set to True to enable v3 naming convention for choice field Enum's
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
102
graphene_django/static/graphene_django/graphiql.js
Normal file
102
graphene_django/static/graphene_django/graphiql.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
(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();
|
||||||
|
} else {
|
||||||
|
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.getElementById("editor")
|
||||||
|
);
|
||||||
|
})();
|
|
@ -5,119 +5,34 @@ exploring GraphQL.
|
||||||
If you wish to receive JSON, provide the header "Accept: application/json" or
|
If you wish to receive JSON, provide the header "Accept: application/json" or
|
||||||
add "&raw" to the end of the URL within a browser.
|
add "&raw" to the end of the URL within a browser.
|
||||||
-->
|
-->
|
||||||
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body, #editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
|
||||||
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
|
rel="stylesheet"
|
||||||
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
|
crossorigin="anonymous" />
|
||||||
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
|
||||||
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<div id="editor"></div>
|
||||||
// Parse the cookie value for a CSRF token
|
{% csrf_token %}
|
||||||
var csrftoken;
|
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||||
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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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)
|
|
@ -38,7 +38,7 @@ class Reporter(models.Model):
|
||||||
last_name = models.CharField(max_length=30)
|
last_name = models.CharField(max_length=30)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
pets = models.ManyToManyField("self")
|
pets = models.ManyToManyField("self")
|
||||||
a_choice = models.CharField(max_length=30, choices=CHOICES)
|
a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
doe_objects = DoeReporterManager()
|
doe_objects = DoeReporterManager()
|
||||||
|
|
||||||
|
@ -64,6 +64,14 @@ class Reporter(models.Model):
|
||||||
if self.reporter_type == 2: # quick and dirty way without enums
|
if self.reporter_type == 2: # quick and dirty way without enums
|
||||||
self.__class__ = CNNReporter
|
self.__class__ = CNNReporter
|
||||||
|
|
||||||
|
def some_method(self):
|
||||||
|
return 123
|
||||||
|
|
||||||
|
|
||||||
|
class CNNReporterManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
|
||||||
|
|
||||||
|
|
||||||
class CNNReporter(Reporter):
|
class CNNReporter(Reporter):
|
||||||
"""
|
"""
|
||||||
|
@ -74,6 +82,8 @@ class CNNReporter(Reporter):
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
|
objects = CNNReporterManager()
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
headline = models.CharField(max_length=100)
|
headline = models.CharField(max_length=100)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user