Compare commits

..

No commits in common. "main" and "v2.4.0" have entirely different histories.
main ... v2.4.0

159 changed files with 2049 additions and 11377 deletions

View File

@ -1,34 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: "\U0001F41Bbug"
assignees: ''
---
**Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports.
* **What is the current behavior?**
* **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via
a github repo, https://repl.it or similar (you can use this template as a starting point: https://repl.it/@jkimbo/Graphene-Django-Example).
* **What is the expected behavior?**
* **What is the motivation / use case for changing the behavior?**
* **Please tell us about your environment:**
- Version:
- Platform:
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)

View File

@ -1 +0,0 @@
blank_issues_enabled: false

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: "✨enhancement"
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

17
.github/stale.yml vendored
View File

@ -1,22 +1,17 @@
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: false daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed # Number of days of inactivity before a stale issue is closed
daysUntilClose: false daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- pinned - pinned
- security - security
- 🐛bug
- 📖 documentation
- help wanted
- ✨enhancement
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: false markComment: >
# markComment: > This issue has been automatically marked as stale because it has not had
# 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
# recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
# for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false closeComment: false

View File

@ -1,31 +0,0 @@
name: 🚀 Deploy to PyPI
on:
push:
tags:
- 'v*'
jobs:
lint:
uses: ./.github/workflows/lint.yml
tests:
uses: ./.github/workflows/tests.yml
release:
runs-on: ubuntu-latest
needs: [lint, tests]
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build wheel and source tarball
run: |
pip install wheel
python setup.py sdist bdist_wheel
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.pypi_password }}

View File

@ -1,26 +0,0 @@
name: Lint
on:
push:
branches: ["main"]
pull_request:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run pre-commit 💅
run: tox
env:
TOXENV: pre-commit

View File

@ -1,47 +0,0 @@
name: Tests
on:
push:
branches: ["main"]
pull_request:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
django: ["3.2", "4.2", "5.0", "5.1", "5.2"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
exclude:
- django: "3.2"
python-version: "3.11"
- django: "3.2"
python-version: "3.12"
- django: "5.0"
python-version: "3.8"
- django: "5.0"
python-version: "3.9"
- django: "5.1"
python-version: "3.8"
- django: "5.1"
python-version: "3.9"
- django: "5.2"
python-version: "3.8"
- django: "5.2"
python-version: "3.9"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
env:
DJANGO: ${{ matrix.django }}

8
.gitignore vendored
View File

@ -11,9 +11,6 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
env/ env/
.env/
venv/
.venv/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
@ -83,8 +80,3 @@ Session.vim
tags tags
.tox/ .tox/
.pytest_cache/ .pytest_cache/
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version

View File

@ -1,23 +0,0 @@
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ^docs/.*$
- id: pretty-format-json
args:
- --autofix
- id: trailing-whitespace
exclude: README.md
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
- id: ruff-format

View File

@ -1,18 +0,0 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.12"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt

View File

@ -1,32 +0,0 @@
select = [
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflake
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line-too-long
"B017", # pytest.raises(Exception) should be considered evil
"B028", # warnings.warn called without an explicit stacklevel keyword argument
"B904", # check for raise statements in exception handlers that lack a from clause
"W191", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
]
exclude = [
"**/docs",
]
target-version = "py38"
[per-file-ignores]
# Ignore unused imports (F401) in these files
"__init__.py" = ["F401"]
[isort]
known-first-party = ["graphene", "graphene-django"]
known-local-folder = ["cookbook"]
combine-as-imports = true

65
.travis.yml Normal file
View File

@ -0,0 +1,65 @@
language: python
cache: pip
dist: xenial
install:
- pip install tox tox-travis
script:
- tox
after_success:
- pip install coveralls
- coveralls
matrix:
fast_finish: true
include:
- python: 2.7
env: DJANGO=1.11
- python: 3.5
env: DJANGO=1.11
- python: 3.5
env: DJANGO=2.0
- python: 3.5
env: DJANGO=2.1
- python: 3.5
env: DJANGO=2.2
- python: 3.6
env: DJANGO=1.11
- python: 3.6
env: DJANGO=2.0
- python: 3.6
env: DJANGO=2.1
- python: 3.6
env: DJANGO=2.2
- python: 3.6
env: DJANGO=master
- python: 3.7
env: DJANGO=1.11
- python: 3.7
env: DJANGO=2.0
- python: 3.7
env: DJANGO=2.1
- python: 3.7
env: DJANGO=2.2
- python: 3.7
env: DJANGO=master
- python: 3.7
env: TOXENV=black,flake8
allow_failures:
- env: DJANGO=master
deploy:
provider: pypi
user: syrusakbary
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"

View File

@ -33,7 +33,7 @@ make tests
## Opening Pull Requests ## Opening Pull Requests
Please fork the project and open a pull request against the `main` branch. Please fork the project and open a pull request against the master branch.
This will trigger a series of test and lint checks. This will trigger a series of test and lint checks.
@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation:
```sh ```sh
make html make html
``` ```

View File

@ -1,6 +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 * recursive-include graphene_django/static *
include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json

View File

@ -1,24 +1,24 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: dev-setup ## Install development dependencies .PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
python -m pre_commit install
.PHONY: tests ## Run unit tests .PHONY: install-dev
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests: tests:
PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
.PHONY: format ## Format code .PHONY: test
test: tests # Alias test -> tests
.PHONY: format
format: format:
ruff format graphene_django examples setup.py black --exclude "/migrations/" graphene_django examples setup.py
.PHONY: lint ## Lint code .PHONY: lint
lint: lint:
ruff graphene_django examples flake8 graphene_django examples
.PHONY: docs ## Generate docs .PHONY: docs ## Generate docs
docs: dev-setup docs: dev-setup

174
README.md
View File

@ -1,151 +1,103 @@
# ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django 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`.
[![build][build-image]][build-url] ---
[![pypi][pypi-image]][pypi-url]
[![Anaconda-Server Badge][conda-image]][conda-url]
[![coveralls][coveralls-image]][coveralls-url]
[build-image]: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg # ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django [![Build Status](https://travis-ci.org/graphql-python/graphene-django.svg?branch=master)](https://travis-ci.org/graphql-python/graphene-django) [![PyPI version](https://badge.fury.io/py/graphene-django.svg)](https://badge.fury.io/py/graphene-django) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene-django?branch=master)
[build-url]: https://github.com/graphql-python/graphene-django/actions
[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
Graphene-Django is an open-source library that provides seamless integration between Django, a high-level Python web framework, and Graphene, a library for building GraphQL APIs. The library allows developers to create GraphQL APIs in Django quickly and efficiently while maintaining a high level of performance.
## Features A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
* Seamless integration with Django models ## Documentation
* Automatic generation of GraphQL schema
* Integration with Django's authentication and permission system
* Easy querying and filtering of data
* Support for Django's pagination system
* Compatible with Django's form and validation system
* Extensive documentation and community support
## Installation [Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/)
To install Graphene-Django, run the following command: ## Quickstart
```sh For installing graphene, just run this command in your shell
pip install graphene-django
```bash
pip install "graphene-django>=2.0"
``` ```
## Configuration ### Settings
After installation, add 'graphene_django' to your Django project's `INSTALLED_APPS` list and define the GraphQL schema in your project's settings:
```python ```python
INSTALLED_APPS = [ INSTALLED_APPS = (
# ... # ...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django', 'graphene_django',
] )
GRAPHENE = { GRAPHENE = {
'SCHEMA': 'myapp.schema.schema' 'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives
} }
``` ```
## Usage ### Urls
To use Graphene-Django, create a `schema.py` file in your Django app directory and define your GraphQL types and queries: We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
```python ```python
import graphene from django.conf.urls import url
from graphene_django import DjangoObjectType from graphene_django.views import GraphQLView
from .models import MyModel
class MyModelType(DjangoObjectType): urlpatterns = [
# ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
```
## Examples
Here is a simple Django model:
```python
from django.db import models
class UserModel(models.Model):
name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
```
To create a GraphQL schema for it you simply have to write the following:
```python
from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType):
class Meta: class Meta:
model = MyModel model = UserModel
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
mymodels = graphene.List(MyModelType) users = graphene.List(User)
def resolve_mymodels(self, info, **kwargs): def resolve_users(self, info):
return MyModel.objects.all() return UserModel.objects.all()
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
``` ```
Then, expose the GraphQL API in your Django project's `urls.py` file: Then you can simply query the schema:
```python ```python
from django.urls import path query = '''
from graphene_django.views import GraphQLView query {
from . import schema users {
name,
urlpatterns = [ lastName
# ... }
path('graphql/', GraphQLView.as_view(graphiql=True)), # Given that schema path is defined in GRAPHENE['SCHEMA'] in your settings.py }
] '''
result = schema.execute(query)
``` ```
## Testing To learn more check out the following [examples](examples/):
Graphene-Django provides support for testing GraphQL APIs using Django's test client. To create tests, create a `tests.py` file in your Django app directory and write your test cases: * **Schema with Filtering**: [Cookbook example](examples/cookbook)
* **Relay Schema**: [Starwars Relay example](examples/starwars)
```python
from django.test import TestCase
from graphene_django.utils.testing import GraphQLTestCase
from . import schema
class MyModelAPITestCase(GraphQLTestCase):
GRAPHENE_SCHEMA = schema.schema
def test_query_all_mymodels(self):
response = self.query(
'''
query {
mymodels {
id
name
}
}
'''
)
self.assertResponseNoErrors(response)
self.assertEqual(len(response.data['mymodels']), MyModel.objects.count())
```
## Contributing ## Contributing
Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/main/CONTRIBUTING.md). See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/main/LICENSE).
## Resources
* [Official GitHub Repository](https://github.com/graphql-python/graphene-django)
* [Graphene Documentation](http://docs.graphene-python.org/en/latest/)
* [Django Documentation](https://docs.djangoproject.com/en/stable/)
* [GraphQL Specification](https://spec.graphql.org/)
* [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL APIs
* [Graphene-Django Community](https://spectrum.chat/graphene) - Join the community to discuss questions and share ideas related to Graphene-Django
## Tutorials and Examples
* [Official Graphene-Django Tutorial](https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/)
* [Building a GraphQL API with Django and Graphene-Django](https://www.howtographql.com/graphql-python/0-introduction/)
* [Real-world example: Django, Graphene, and Relay](https://github.com/graphql-python/swapi-graphene)
## Related Projects
* [Graphene](https://github.com/graphql-python/graphene) - A library for building GraphQL APIs in Python
* [Graphene-SQLAlchemy](https://github.com/graphql-python/graphene-sqlalchemy) - Integration between Graphene and SQLAlchemy, an Object Relational Mapper (ORM) for Python
* [Graphene-File-Upload](https://github.com/lmcgartland/graphene-file-upload) - A package providing an Upload scalar for handling file uploads in Graphene
* [Graphene-Subscriptions](https://github.com/graphql-python/graphene-subscriptions) - A package for adding real-time subscriptions to Graphene-based GraphQL APIs
## Support
If you encounter any issues or have questions regarding Graphene-Django, feel free to [submit an issue](https://github.com/graphql-python/graphene-django/issues/new) on the official GitHub repository. You can also ask for help and share your experiences with the Graphene-Django community on [💬 Discord](https://discord.gg/Fftt273T79)
## Release Notes
* See [Releases page on github](https://github.com/graphql-python/graphene-django/releases)

122
README.rst Normal file
View File

@ -0,0 +1,122 @@
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 Logo| Graphene-Django |Build Status| |PyPI version| |Coverage Status|
===============================================================================
A `Django <https://www.djangoproject.com/>`__ integration for
`Graphene <http://graphene-python.org/>`__.
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
.. code:: bash
pip install "graphene-django>=2.0"
Settings
~~~~~~~~
.. code:: python
INSTALLED_APPS = (
# ...
'graphene_django',
)
GRAPHENE = {
'SCHEMA': 'app.schema.schema' # Where your Graphene schema lives
}
Urls
~~~~
We need to set up a ``GraphQL`` endpoint in our Django app, so we can
serve the queries.
.. code:: python
from django.conf.urls import url
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
]
Examples
--------
Here is a simple Django model:
.. code:: python
from django.db import models
class UserModel(models.Model):
name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
To create a GraphQL schema for it you simply have to write the
following:
.. code:: python
from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType):
class Meta:
model = UserModel
class Query(graphene.ObjectType):
users = graphene.List(User)
@graphene.resolve_only_args
def resolve_users(self):
return UserModel.objects.all()
schema = graphene.Schema(query=Query)
Then you can simply query the schema:
.. code:: python
query = '''
query {
users {
name,
lastName
}
}
'''
result = schema.execute(query)
To learn more check out the following `examples <examples/>`__:
- **Schema with Filtering**: `Cookbook example <examples/cookbook>`__
- **Relay Schema**: `Starwars Relay example <examples/starwars>`__
Contributing
------------
See `CONTRIBUTING.md <CONTRIBUTING.md>`__.
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
:target: https://travis-ci.org/graphql-python/graphene-django
.. |PyPI version| image:: https://badge.fury.io/py/graphene-django.svg
:target: https://badge.fury.io/py/graphene-django
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/graphql-python/graphene-django?branch=master

35
django_test_settings.py Normal file
View File

@ -0,0 +1,35 @@
import sys
import os
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + '/examples/')
SECRET_KEY = 1
INSTALLED_APPS = [
'graphene_django',
'graphene_django.rest_framework',
'graphene_django.tests',
'starwars',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'django_test.sqlite',
}
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
},
]
GRAPHENE = {
'SCHEMA': 'graphene_django.tests.schema_view.schema'
}
ROOT_URLCONF = 'graphene_django.tests.urls'

View 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 ``fields`` meta attribute. To limit fields in a GraphQL query simply use the ``only_fields`` meta attribute.
.. code:: python .. code:: python
@ -31,10 +31,10 @@ To limit fields in a GraphQL query simply use the ``fields`` meta attribute.
class PostNode(DjangoObjectType): class PostNode(DjangoObjectType):
class Meta: class Meta:
model = Post model = Post
fields = ('title', 'content') only_fields = ('title', 'content')
interfaces = (relay.Node, ) interfaces = (relay.Node, )
conversely you can use ``exclude`` meta attribute. conversely you can use ``exclude_fields`` meta attribute.
.. code:: python .. code:: python
@ -45,34 +45,9 @@ conversely you can use ``exclude`` meta attribute.
class PostNode(DjangoObjectType): class PostNode(DjangoObjectType):
class Meta: class Meta:
model = Post model = Post
exclude = ('published', 'owner') exclude_fields = ('published', 'owner')
interfaces = (relay.Node, ) interfaces = (relay.Node, )
Another pattern is to have a resolve method act as a gatekeeper, returning None
or raising an exception if the client isn't allowed to see the data.
.. code:: python
from graphene import relay
from graphene_django.types import DjangoObjectType
from .models import Post
class PostNode(DjangoObjectType):
class Meta:
model = Post
fields = ('title', 'content', 'owner')
interfaces = (relay.Node, )
def resolve_owner(self, info):
user = info.context.user
if user.is_anonymous:
raise PermissionDenied("Please login")
if not user.is_staff:
return None
return self.owner
Queryset Filtering On Lists Queryset Filtering On Lists
--------------------------- ---------------------------
@ -109,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)
@ -136,7 +111,6 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
class PostNode(DjangoObjectType): class PostNode(DjangoObjectType):
class Meta: class Meta:
model = Post model = Post
fields = '__all__'
@classmethod @classmethod
def get_queryset(cls, queryset, info): def get_queryset(cls, queryset, info):
@ -144,21 +118,6 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
return queryset.filter(published=True) return queryset.filter(published=True)
return queryset return queryset
.. warning::
Defining a custom ``get_queryset`` gives the guaranteed it will be called
when resolving the ``DjangoObjectType``, even through related objects.
Note that because of this, benefits from using ``select_related``
in objects that define a relation to this ``DjangoObjectType`` will be canceled out.
In the case of ``prefetch_related``, the benefits of the optimization will be lost only
if the custom ``get_queryset`` modifies the queryset. For more information about this, refers
to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related.
If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving,
you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this
can lead to authorization leaks if you are performing authorization checks in the custom
``get_queryset``.
Filtering ID-based Node Access Filtering ID-based Node Access
------------------------------ ------------------------------
@ -174,7 +133,7 @@ method to your ``DjangoObjectType``.
class PostNode(DjangoObjectType): class PostNode(DjangoObjectType):
class Meta: class Meta:
model = Post model = Post
fields = ('title', 'content') only_fields = ('title', 'content')
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@classmethod @classmethod
@ -207,13 +166,22 @@ 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 2.2 and above: For Django 1.9 and below:
.. code:: python .. code:: python
urlpatterns = [ urlpatterns = [
# some other urls # some other urls
path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
] ]
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin For Django 2.0 and above:
.. code:: python
urlpatterns = [
# some other urls
path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin

View File

@ -60,25 +60,25 @@ source_suffix = ".rst"
master_doc = "index" master_doc = "index"
# General information about the project. # General information about the project.
project = "Graphene Django" project = u"Graphene Django"
copyright = "Graphene 2017" copyright = u"Graphene 2017"
author = "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 = "1.0" version = u"1.0"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "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.
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
# language = None language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
@ -276,7 +276,7 @@ latex_elements = {
# (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", "Graphene Documentation", "Syrus Akbary", "manual") (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -317,7 +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", "Graphene Django Documentation", [author], 1) (master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@ -334,7 +334,7 @@ texinfo_documents = [
( (
master_doc, master_doc,
"Graphene-Django", "Graphene-Django",
"Graphene Django Documentation", u"Graphene Django Documentation",
author, author,
"Graphene Django", "Graphene Django",
"One line description of project.", "One line description of project.",
@ -445,7 +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 = { intersphinx_mapping = {"https://docs.python.org/": None}
# "https://docs.python.org/": None,
"python": ("https://docs.python.org/", None),
}

View File

@ -3,8 +3,8 @@ Django Debug Middleware
You can debug your GraphQL queries in a similar way to You can debug your GraphQL queries in a similar way to
`django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__, `django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__,
but outputting in the results in GraphQL response as fields, instead of but outputing in the results in GraphQL response as fields, instead of
the graphical HTML interface. Exceptions with their stack traces are also exposed. the graphical HTML interface.
For that, you will need to add the plugin in your graphene schema. For that, you will need to add the plugin in your graphene schema.
@ -43,7 +43,7 @@ And in your ``settings.py``:
Querying Querying
-------- --------
You can query it for outputting all the sql transactions that happened in You can query it for outputing all the sql transactions that happened in
the GraphQL request, like: the GraphQL request, like:
.. code:: .. code::
@ -63,10 +63,6 @@ the GraphQL request, like:
sql { sql {
rawSql rawSql
} }
exceptions {
message
stack
}
} }
} }

View File

@ -1,12 +0,0 @@
Extra Types
===========
Here are some libraries that provide common types for Django specific fields.
GeoDjango
---------
Use the graphene-gis_ library to add GeoDjango types to your Schema.
.. _graphene-gis: https://github.com/EverWinter23/graphene-gis

View File

@ -1,85 +0,0 @@
Fields
======
Graphene-Django provides some useful fields to help integrate Django with your GraphQL
Schema.
DjangoListField
---------------
``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType<queries-objecttypes>`'s. By default it will resolve the default queryset of the Django model.
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
schema = Schema(query=Query)
The above code results in the following schema definition:
.. code::
schema {
query: Query
}
type Query {
recipes: [RecipeType!]
}
type RecipeType {
title: String!
instructions: String!
}
Custom resolvers
****************
If your ``DjangoObjectType`` has defined a custom
:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
``DjangoListField`` it will be called with either the return of the field
resolver (if one is defined) or the default queryset from the Django model.
For example the following schema will only resolve recipes which have been
published and have a title:
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
@classmethod
def get_queryset(cls, queryset, info):
# Filter out recipes that have no title
return queryset.exclude(title__exact="")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
def resolve_recipes(parent, info):
# Only get recipes that have been published
return Recipe.objects.filter(published=True)
schema = Schema(query=Query)
DjangoConnectionField
---------------------
``DjangoConnectionField`` acts similarly to ``DjangoListField`` but returns a
paginated connection following the `relay spec <https://relay.dev/graphql/connections.htm>`__
The field supports the following arguments: `first`, `last`, `offset`, `after` & `before`.

View File

@ -2,8 +2,9 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results. `django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__ Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. This filtering is automatically available when implementing a ``relay.Node``.
@ -13,20 +14,11 @@ You will need to install it manually, which can be done as follows:
.. code:: bash .. code:: bash
# You'll need to install django-filter # You'll need to 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/main/examples/cookbook>`__. app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
Filterable fields Filterable fields
----------------- -----------------
@ -34,7 +26,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering ``django-filter``, so see the `filtering
documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__ documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
for full details on the range of options available. for full details on the range of options available.
For example: For example:
@ -45,7 +37,6 @@ For example:
class Meta: class Meta:
# Assume you have an Animal model defined with the following fields # Assume you have an Animal model defined with the following fields
model = Animal model = Animal
fields = '__all__'
filter_fields = ['name', 'genus', 'is_domesticated'] filter_fields = ['name', 'genus', 'is_domesticated']
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@ -76,7 +67,6 @@ You can also make more complex lookup types available:
class AnimalNode(DjangoObjectType): class AnimalNode(DjangoObjectType):
class Meta: class Meta:
model = Animal model = Animal
fields = '__all__'
# Provide more complex lookup types # Provide more complex lookup types
filter_fields = { filter_fields = {
'name': ['exact', 'icontains', 'istartswith'], 'name': ['exact', 'icontains', 'istartswith'],
@ -118,7 +108,6 @@ create your own ``FilterSet``. You can pass it directly as follows:
class Meta: class Meta:
# Assume you have an Animal model defined with the following fields # Assume you have an Animal model defined with the following fields
model = Animal model = Animal
fields = '__all__'
filter_fields = ['name', 'genus', 'is_domesticated'] filter_fields = ['name', 'genus', 'is_domesticated']
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@ -126,15 +115,6 @@ create your own ``FilterSet``. You can pass it directly as follows:
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_expr=['iexact']) name = django_filters.CharFilter(lookup_expr=['iexact'])
# Allow multiple genera to be selected at once
genera = django_filters.MultipleChoiceFilter(
field_name='genus',
choices=(
('Canis', 'Canis'),
('Panthera', 'Panthera'),
('Seahorse', 'Seahorse')
)
)
class Meta: class Meta:
model = Animal model = Animal
@ -147,23 +127,7 @@ create your own ``FilterSet``. You can pass it directly 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 ``filerset_class``
If you were interested in selecting all dogs and cats, you might query as follows:
.. code::
query {
allAnimals(genera: ["Canis", "Panthera"]) {
edges {
node {
id,
name
}
}
}
}
You can also specify the ``FilterSet`` class using the ``filterset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter: in unison with the ``filter_fields`` parameter:
@ -182,7 +146,6 @@ in unison with the ``filter_fields`` parameter:
class AnimalNode(DjangoObjectType): class AnimalNode(DjangoObjectType):
class Meta: class Meta:
model = Animal model = Animal
fields = '__all__'
filterset_class = AnimalFilter filterset_class = AnimalFilter
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@ -191,8 +154,7 @@ in unison with the ``filter_fields`` parameter:
animal = relay.Node.Field(AnimalNode) animal = relay.Node.Field(AnimalNode)
all_animals = DjangoFilterConnectionField(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/main/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
pre-filter animals owned by the authenticated user (set in ``context.user``). pre-filter animals owned by the authenticated user (set in ``context.user``).
@ -230,7 +192,7 @@ Extend the tuple of fields if you want to order by more than one field.
order_by = OrderingFilter( order_by = OrderingFilter(
fields=( fields=(
('name', 'created_at'), ('created_at', 'created_at'),
) )
) )
@ -240,7 +202,6 @@ Extend the tuple of fields if you want to order by more than one field.
class Meta: class Meta:
name = 'Group' name = 'Group'
model = GroupModel model = GroupModel
fields = '__all__'
interfaces = (relay.Node,) interfaces = (relay.Node,)
def resolve_users(self, info, **kwargs): def resolve_users(self, info, **kwargs):
@ -257,87 +218,4 @@ with this set up, you can now order the users under group:
xxx xxx
} }
} }
} }
PostgreSQL `ArrayField`
-----------------------
Graphene provides an easy to implement filters on `ArrayField` as they are not natively supported by django_filters:
.. code:: python
from django.db import models
from django_filters import FilterSet, OrderingFilter
from graphene_django.filter import ArrayFilter
class Event(models.Model):
name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50))
class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact", "contains"],
}
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
fields = "__all__"
filterset_class = EventFilterSet
with this set up, you can now filter events by tags:
.. code::
query {
events(tags_Overlap: ["concert", "festival"]) {
name
}
}
`TypedFilter`
-------------
Sometimes the automatic detection of the filter input type is not satisfactory for what you are trying to achieve.
You can then explicitly specify the input type you want for your filter by using a `TypedFilter`:
.. code:: python
from django.db import models
from django_filters import FilterSet, OrderingFilter
import graphene
from graphene_django.filter import TypedFilter
class Event(models.Model):
name = models.CharField(max_length=50)
class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact", "contains"],
}
only_first = TypedFilter(input_type=graphene.Boolean, method="only_first_filter")
def only_first_filter(self, queryset, _name, value):
if value:
return queryset[:1]
else:
return queryset
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
fields = "__all__"
filterset_class = EventFilterSet

View File

@ -9,8 +9,8 @@ Graphene-Django provides some additional abstractions that make it easy to add G
First time? We recommend you start with the installation guide to get set up and the basic tutorial. 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. It is worth reading the `core graphene docs <https://docs.graphene-python.org/en/latest/>`__ to familiarize yourself with the basic utilities.
Core tenets Core tenants
----------- ------------
If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section. If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section.
@ -25,14 +25,10 @@ For more advanced use, check out the Relay tutorial.
tutorial-relay tutorial-relay
schema schema
queries queries
fields
extra-types
mutations mutations
subscriptions
filtering filtering
authorization authorization
debug debug
introspection introspection
validation
testing testing
settings settings

View File

@ -8,7 +8,7 @@ Requirements
Graphene-Django currently supports the following versions of Django: Graphene-Django currently supports the following versions of Django:
* >= Django 2.2 * Django 2.X
Installation Installation
------------ ------------
@ -25,15 +25,13 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
INSTALLED_APPS = [ INSTALLED_APPS = [
... ...
"django.contrib.staticfiles", # Required for GraphiQL 'django.contrib.staticfiles', # Required for GraphiQL
"graphene_django" 'graphene_django'
] ]
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
For Django 2.2 and above:
.. code:: python .. code:: python
from django.urls import path from django.urls import path
@ -51,7 +49,7 @@ Finally, define the schema location for Graphene in the ``settings.py`` file of
.. code:: python .. code:: python
GRAPHENE = { GRAPHENE = {
"SCHEMA": "django_root.schema.schema" 'SCHEMA': 'django_root.schema.schema'
} }
Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project. Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
@ -63,31 +61,9 @@ The most basic ``schema.py`` looks like this:
import graphene import graphene
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
hello = graphene.String(default_value="Hi!") pass
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
To learn how to extend the schema object for your project, read the basic tutorial. To learn how to extend the schema object for your project, read the basic tutorial.
CSRF exempt
-----------
If you 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))),
]

View File

@ -29,20 +29,6 @@ you're ready to use Relay with Graphene GraphQL implementation.
The schema file is sorted to create a reproducible canonical representation. 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
-------------- --------------
@ -57,9 +43,8 @@ specify the parameters in your settings.py:
.. code:: python .. code:: python
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)
} }

View File

@ -25,7 +25,6 @@ Simple example
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = '__all__'
class QuestionMutation(graphene.Mutation): class QuestionMutation(graphene.Mutation):
@ -37,8 +36,7 @@ Simple example
# The class attributes define the response of the mutation # The class attributes define the response of the mutation
question = graphene.Field(QuestionType) question = graphene.Field(QuestionType)
@classmethod def mutate(self, info, text, id):
def mutate(cls, root, info, text, id):
question = Question.objects.get(pk=id) question = Question.objects.get(pk=id)
question.text = text question.text = text
question.save() question.save()
@ -46,7 +44,7 @@ Simple example
return QuestionMutation(question=question) return QuestionMutation(question=question)
class Mutation(graphene.ObjectType): class Mutation:
update_question = QuestionMutation.Field() update_question = QuestionMutation.Field()
@ -92,7 +90,6 @@ DjangoModelFormMutation
class PetType(DjangoObjectType): class PetType(DjangoObjectType):
class Meta: class Meta:
model = Pet model = Pet
fields = '__all__'
class PetMutation(DjangoModelFormMutation): class PetMutation(DjangoModelFormMutation):
pet = Field(PetType) pet = Field(PetType)
@ -125,55 +122,6 @@ 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 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. containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
DjangoFormInputObjectType
~~~~~~~~~~~~~~~~~~~~~~~~~
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
.. code:: python
from graphene_django.forms.types import DjangoFormInputObjectType
class PetFormInput(DjangoFormInputObjectType):
# any other fields can be placed here as well as
# other djangoforminputobjects and intputobjects
class Meta:
form_class = PetForm
object_type = PetType
class QuestionFormInput(DjangoFormInputObjectType)
class Meta:
form_class = QuestionForm
object_type = QuestionType
class SeveralFormsInputData(graphene.InputObjectType):
pet = PetFormInput(required=True)
question = QuestionFormInput(required=True)
class SomeSophisticatedMutation(graphene.Mutation):
class Arguments:
data = SeveralFormsInputData(required=True)
@staticmethod
def mutate(_root, _info, data):
pet_form_inst = PetForm(data=data.pet)
question_form_inst = QuestionForm(data=data.question)
if pet_form_inst.is_valid():
pet_model_instance = pet_form_inst.save(commit=False)
if question_form_inst.is_valid():
question_model_instance = question_form_inst.save(commit=False)
# ...
Additional to **InputObjectType** ``Meta`` class attributes:
* ``form_class`` is required and should be equal to django form class.
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
Django REST Framework Django REST Framework
--------------------- ---------------------
@ -203,7 +151,7 @@ customize the look up with the ``lookup_field`` attribute on the ``SerializerMut
.. code:: python .. code:: python
from graphene_django.rest_framework.mutation import SerializerMutation from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import MyModelSerializer from .serializers imoprt MyModelSerializer
class AwesomeModelMutation(SerializerMutation): class AwesomeModelMutation(SerializerMutation):
@ -220,7 +168,7 @@ Use the method ``get_serializer_kwargs`` to override how updates are applied.
.. code:: python .. code:: python
from graphene_django.rest_framework.mutation import SerializerMutation from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import MyModelSerializer from .serializers imoprt MyModelSerializer
class AwesomeModelMutation(SerializerMutation): class AwesomeModelMutation(SerializerMutation):
@ -251,7 +199,7 @@ You can use relay with mutations. A Relay mutation must inherit from
.. code:: python .. code:: python
import graphene import graphene
from graphene import relay from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphql_relay import from_global_id from graphql_relay import from_global_id
@ -281,121 +229,3 @@ This argument is also sent back to the client with the mutation result
(you do not have to do anything). For services that manage (you do not have to do anything). For services that manage
a pool of many GraphQL requests in bulk, the ``clientIDMutation`` a pool of many GraphQL requests in bulk, the ``clientIDMutation``
allows you to match up a specific mutation with the response. allows you to match up a specific mutation with the response.
Django Database Transactions
----------------------------
Django gives you a few ways to control how database transactions are managed.
Tying transactions to HTTP requests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A common way to handle transactions in Django is to wrap each request in a transaction.
Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for
which you want to enable this behavior.
It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the
response is produced without problems, Django commits the transaction. If the view, a
``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django
rolls back the transaction.
.. warning::
While the simplicity of this transaction model is appealing, it also makes it
inefficient when traffic increases. Opening a transaction for every request has some
overhead. The impact on performance depends on the query patterns of your application
and on how well your database handles locking.
Check the next section for a better solution.
Tying transactions to mutations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A mutation can contain multiple fields, just like a query. There's one important
distinction between queries and mutations, other than the name:
..
`While query fields are executed in parallel, mutation fields run in series, one
after the other.`
This means that if we send two ``incrementCredits`` mutations in one request, the first
is guaranteed to finish before the second begins, ensuring that we don't end up with a
race condition with ourselves.
On the other hand, if the first ``incrementCredits`` runs successfully but the second
one does not, the operation cannot be retried as it is. That's why is a good idea to
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings
to ``True`` in your settings file:
.. code:: python
GRAPHENE = {
# ...
"ATOMIC_MUTATIONS": True,
}
On the contrary, if you want to enable this behavior for a specific database, set
``ATOMIC_MUTATIONS`` to ``True`` in your database settings:
.. code:: python
DATABASES = {
"default": {
# ...
"ATOMIC_MUTATIONS": True,
},
# ...
}
Now, given the following example mutation:
.. code::
mutation IncreaseCreditsTwice {
increaseCredits1: increaseCredits(input: { amount: 10 }) {
balance
errors {
field
messages
}
}
increaseCredits2: increaseCredits(input: { amount: -1 }) {
balance
errors {
field
messages
}
}
}
The server is going to return something like:
.. code:: json
{
"data": {
"increaseCredits1": {
"balance": 10.0,
"errors": []
},
"increaseCredits2": {
"balance": null,
"errors": [
{
"field": "amount",
"message": "Amount should be a positive number"
}
]
},
}
}
But the balance will remain the same.

View File

@ -1,5 +1,3 @@
.. _queries-objecttypes:
Queries & ObjectTypes Queries & ObjectTypes
===================== =====================
@ -20,26 +18,27 @@ Full example
# my_app/schema.py # my_app/schema.py
import graphene import graphene
from graphene_django import DjangoObjectType
from graphene_django.types import DjangoObjectType
from .models import Question from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = ("id", "question_text")
class Query(graphene.ObjectType):
class Query:
questions = graphene.List(QuestionType) questions = graphene.List(QuestionType)
question_by_id = graphene.Field(QuestionType, id=graphene.String()) question = graphene.Field(QuestionType, question_id=graphene.String())
def resolve_questions(root, info, **kwargs): def resolve_questions(self, info, **kwargs):
# Querying a list # Querying a list
return Question.objects.all() return Question.objects.all()
def resolve_question_by_id(root, info, id): def resolve_question(self, info, question_id):
# Querying a single question # Querying a single question
return Question.objects.get(pk=id) return Question.objects.get(pk=question_id)
Specifying which fields to include Specifying which fields to include
@ -52,9 +51,6 @@ all fields that should be exposed using the fields attribute.
This will make it less likely to result in unintentionally exposing data when This will make it less likely to result in unintentionally exposing data when
your models change. your models change.
Setting neither ``fields`` nor ``exclude`` is deprecated and will raise a warning, you should at least explicitly make
``DjangoObjectType`` include all fields in the model as described below.
``fields`` ``fields``
~~~~~~~~~~ ~~~~~~~~~~
@ -62,27 +58,21 @@ Show **only** these fields on the model:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = ("id", "question_text") 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. 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: For example:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = "__all__" fields = '__all__'
``exclude`` ``exclude``
@ -92,13 +82,10 @@ Show all fields **except** those in ``exclude``:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
exclude = ("question_text",) exclude = ('question_text',)
Customising fields Customising fields
@ -108,19 +95,16 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = ("id", "question_text") fields = ('id', 'question_text')
extra_field = graphene.String() extra_field = graphene.String()
def resolve_extra_field(self, info): def resolve_extra_field(self, info):
return "hello!" return 'hello!'
Choices to Enum conversion Choices to Enum conversion
@ -135,23 +119,16 @@ For example the following ``Model`` and ``DjangoObjectType``:
.. code:: python .. code:: python
from django.db import models class PetModel(models.Model):
from graphene_django import DjangoObjectType kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
class PetModel(models.Model): class Pet(DjangoObjectType):
kind = models.CharField( class Meta:
max_length=100, model = PetModel
choices=(("cat", "Cat"), ("dog", "Dog"))
)
class Pet(DjangoObjectType):
class Meta:
model = PetModel
fields = ("id", "kind",)
Results in the following GraphQL schema definition: Results in the following GraphQL schema definition:
.. code:: graphql .. code::
type Pet { type Pet {
id: ID! id: ID!
@ -169,35 +146,27 @@ You can disable this automatic conversion by setting
.. code:: python .. code:: python
from graphene_django import DjangoObjectType class Pet(DjangoObjectType):
from .models import PetModel class Meta:
model = PetModel
convert_choices_to_enum = False
class Pet(DjangoObjectType): .. code::
class Meta:
model = PetModel
fields = ("id", "kind",)
convert_choices_to_enum = False
.. code:: graphql type Pet {
id: ID!
type Pet { kind: String!
id: ID! }
kind: String!
}
You can also set ``convert_choices_to_enum`` to a list of fields that should be You can also set ``convert_choices_to_enum`` to a list of fields that should be
automatically converted into enums: automatically converted into enums:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType class Pet(DjangoObjectType):
from .models import PetModel class Meta:
model = PetModel
class Pet(DjangoObjectType): convert_choices_to_enum = ['kind']
class Meta:
model = PetModel
fields = ("id", "kind",)
convert_choices_to_enum = ["kind"]
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to **Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
``False``. ``False``.
@ -210,8 +179,6 @@ Say you have the following models:
.. code:: python .. code:: python
from django.db import models
class Category(models.Model): class Category(models.Model):
foo = models.CharField(max_length=256) foo = models.CharField(max_length=256)
@ -223,13 +190,10 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = ("category",) fields = ('category',)
Then all query-able related models must be defined as DjangoObjectType subclass, 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 or they will fail to show if you are trying to query those relation fields. You only
@ -237,15 +201,9 @@ need to create the most basic class for this to work:
.. code:: python .. code:: python
from graphene_django import DjangoObjectType
from .models import Category
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = ("foo",)
.. _django-objecttype-get-queryset:
Default QuerySet Default QuerySet
----------------- -----------------
@ -258,10 +216,10 @@ Use this to control filtering on the ObjectType level instead of the Query objec
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from .models import Question from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
fields = "__all__"
@classmethod @classmethod
def get_queryset(cls, queryset, info): def get_queryset(cls, queryset, info):
@ -278,22 +236,18 @@ This resolve method should follow this format:
.. code:: python .. code:: python
def resolve_foo(parent, info, **kwargs): def resolve_foo(self, info, **kwargs):
Where "foo" is the name of the field declared in the ``Query`` object. Where "foo" is the name of the field declared in the ``Query`` object.
.. code:: python .. code:: python
import graphene class Query:
from .models import Question
from .types import QuestionType
class Query(graphene.ObjectType):
foo = graphene.List(QuestionType) foo = graphene.List(QuestionType)
def resolve_foo(root, info, **kwargs): def resolve_foo(self, info, **kwargs):
id = kwargs.get("id") id = kwargs.get('id')
return Question.objects.get(id) return QuestionModel.objects.get(id)
Arguments Arguments
~~~~~~~~~ ~~~~~~~~~
@ -302,18 +256,10 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
.. code:: python .. code:: python
import graphene class Query:
from .models import Question question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int())
from .types import QuestionType
class Query(graphene.ObjectType): def resolve_question(self, info, foo, bar):
question = graphene.Field(
QuestionType,
foo=graphene.String(),
bar=graphene.Int()
)
def resolve_question(root, info, foo=None, bar=None):
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None. # 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() return Question.objects.filter(foo=foo, bar=bar).first()
@ -328,27 +274,12 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
.. code:: python .. code:: python
import graphene def resolve_questions(self, info, **kwargs):
# See if a user is authenticated
from .models import Question if info.context.user.is_authenticated():
from .types import QuestionType return Question.objects.all()
else:
class Query(graphene.ObjectType): return Question.objects.none()
questions = graphene.List(QuestionType)
def resolve_questions(root, info):
# 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 Plain ObjectTypes
@ -363,13 +294,15 @@ Django models and your external API.
import graphene import graphene
from .models import Question from .models import Question
class MyQuestion(graphene.ObjectType): class MyQuestion(graphene.ObjectType):
text = graphene.String() text = graphene.String()
class Query(graphene.ObjectType):
class Query:
question = graphene.Field(MyQuestion, question_id=graphene.String()) question = graphene.Field(MyQuestion, question_id=graphene.String())
def resolve_question(root, info, question_id): def resolve_question(self, info, question_id):
question = Question.objects.get(pk=question_id) question = Question.objects.get(pk=question_id)
return MyQuestion( return MyQuestion(
text=question.question_text text=question.question_text
@ -399,48 +332,51 @@ the core graphene pages for more information on customizing the Relay experience
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from .models import Question from .models import Question
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
interfaces = (relay.Node,) # make sure you add this interfaces = (relay.Node,)
fields = "__all__"
class QuestionConnection(relay.Connection): class QuestionConnection(relay.Connection):
class Meta: class Meta:
node = QuestionType node = QuestionType
class Query: class Query:
questions = relay.ConnectionField(QuestionConnection) questions = relay.ConnectionField(QuestionConnection)
def resolve_questions(root, info, **kwargs): def resolve_questions(root, info, **kwargs):
return Question.objects.all() return Question.objects.all()
You can now execute queries like: You can now execute queries like:
.. code:: graphql .. code:: python
{ {
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
pageInfo { pageInfo {
startCursor startCursor
endCursor endCursor
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
} }
edges { edges {
cursor cursor
node { node {
id id
question_text question_text
} }
} }
} }
} }
Which returns: Which returns:
.. code:: json .. code:: python
{ {
"data": { "data": {

View File

@ -1,5 +1,4 @@
Sphinx==7.0.0 Sphinx==1.5.3
sphinx-autobuild==2021.3.14 sphinx-autobuild==0.7.1
pygments-graphql-lexer==0.1.0
# Docs template # Docs template
http://graphene-python.org/sphinx_graphene_theme.zip http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -1,57 +0,0 @@
import graphene
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = "__all__"
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = "__all__"
class Query:
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

View File

@ -6,7 +6,7 @@ Graphene-Django can be customised using settings. This page explains each settin
Usage Usage
----- -----
Add settings to your Django project by creating a Dictionary with name ``GRAPHENE`` in the project's ``settings.py``: Add settings to your Django project by creating a Dictonary with name ``GRAPHENE`` in the project's ``settings.py``:
.. code:: python .. code:: python
@ -104,7 +104,7 @@ Default: ``100``
``CAMELCASE_ERRORS`` ``CAMELCASE_ERRORS``
-------------------- ------------------------------------
When set to ``True`` field names in the ``errors`` object will be camel case. When set to ``True`` field names in the ``errors`` object will be camel case.
By default they will be snake case. By default they will be snake case.
@ -140,152 +140,3 @@ Default: ``False``
# 'messages': ['This field is required.'], # 'messages': ['This field is required.'],
# } # }
# ] # ]
``DJANGO_CHOICE_FIELD_ENUM_CONVERT``
--------------------------------------
When set to ``True`` Django choice fields are automatically converted into Enum types.
Can be disabled globally by setting it to ``False``.
Default: ``True``
``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING``
--------------------------------------
Set to ``True`` to use the old naming format for the auto generated Enum types from Django choice fields. The old format looks like this: ``{object_name}_{field_name}``
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_V2_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"
}
``SUBSCRIPTION_PATH``
---------------------
Define an alternative URL path where subscription operations should be routed.
The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``).
Default: ``None``
.. code:: python
GRAPHENE = {
'SUBSCRIPTION_PATH': "/ws/graphql"
}
``GRAPHIQL_HEADER_EDITOR_ENABLED``
----------------------------------
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
Set to ``False`` if you want to disable GraphiQL headers editor tab for some reason.
This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``True``
.. code:: python
GRAPHENE = {
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
}
``TESTING_ENDPOINT``
--------------------
Define the graphql endpoint url used for the `GraphQLTestCase` class.
Default: ``/graphql``
.. code:: python
GRAPHENE = {
'TESTING_ENDPOINT': '/customEndpoint'
}
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
-----------------------------------
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
}
``GRAPHIQL_INPUT_VALUE_DEPRECATION``
------------------------------------
Set to ``True`` if you want GraphiQL to show any deprecated fields on input object types' docs.
For example, having this schema:
.. code:: python
class MyMutationInputType(graphene.InputObjectType):
old_field = graphene.String(deprecation_reason="You should now use 'newField' instead.")
new_field = graphene.String()
class MyMutation(graphene.Mutation):
class Arguments:
input = types.MyMutationInputType()
GraphiQL will add a ``Show Deprecated Fields`` button to toggle information display on ``oldField`` and its deprecation
reason. Otherwise, you would get neither a button nor any information at all on ``oldField``.
This setting is passed to ``inputValueDeprecation`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_INPUT_VALUE_DEPRECATION': False,
}
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
``MAX_VALIDATION_ERRORS``
------------------------------------
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
*i.e.* 100.
Default: ``None``

View File

@ -1,42 +0,0 @@
Subscriptions
=============
The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are
several community-driven modules for adding subscription support, and the provided GraphiQL interface supports
running subscription operations over a websocket.
To implement websocket-based support for GraphQL subscriptions, youll need to do the following:
1. Install and configure `django-channels <https://channels.readthedocs.io/en/latest/installation.html>`_.
2. Install and configure* a third-party module for adding subscription support over websockets. A few options include:
- `graphql-python/graphql-ws <https://github.com/graphql-python/graphql-ws>`_
- `datavance/django-channels-graphql-ws <https://github.com/datadvance/DjangoChannelsGraphqlWs>`_
- `jaydenwindle/graphene-subscriptions <https://github.com/jaydenwindle/graphene-subscriptions>`_
3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like
daphne (built in to ``django-channels``), `uvicorn <https://www.uvicorn.org/>`_, or
`hypercorn <https://pgjones.gitlab.io/hypercorn/>`_.
..
*** Note:** By default, the GraphiQL interface that comes with
``graphene-django`` assumes that you are handling subscriptions at
the same path as any other operation (i.e., you configured both
``urls.py`` and ``routing.py`` to handle GraphQL operations at the
same path, like ``/graphql``).
If these URLs differ, GraphiQL will try to run your subscription over
HTTP, which will produce an error. If you need to use a different URL
for handling websocket connections, you can configure
``SUBSCRIPTION_PATH`` in your ``settings.py``:
.. code:: python
GRAPHENE = {
# ...
"SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash.
}
Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test
subscriptions like any other operation.

View File

@ -1,14 +1,8 @@
Testing API calls with django Testing API calls with django
============================= =============================
Using unittest
--------------
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
The default endpoint for testing is `/graphql`. You can override this in the `settings <https://docs.graphene-python.org/projects/django/en/latest/settings/#testing-endpoint>`__.
Usage: Usage:
.. code:: python .. code:: python
@ -16,8 +10,12 @@ Usage:
import json import json
from graphene_django.utils.testing import GraphQLTestCase from graphene_django.utils.testing import GraphQLTestCase
from my_project.config.schema import schema
class MyFancyTestCase(GraphQLTestCase): class MyFancyTestCase(GraphQLTestCase):
# Here you need to inject your test case's schema
GRAPHQL_SCHEMA = schema
def test_some_query(self): def test_some_query(self):
response = self.query( response = self.query(
''' '''
@ -28,29 +26,7 @@ Usage:
} }
} }
''', ''',
operation_name='myModel' 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
}
}
''',
operation_name='myModel',
variables={'id': 1}
) )
content = json.loads(response.content) content = json.loads(response.content)
@ -73,7 +49,7 @@ Usage:
} }
} }
''', ''',
operation_name='myMutation', op_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'} input_data={'my_field': 'foo', 'other_field': 'bar'}
) )
@ -82,74 +58,3 @@ Usage:
# Add some more asserts if you like # Add some more asserts if you like
... ...
For testing mutations that are executed within a transaction you should subclass `GraphQLTransactionTestCase`
Usage:
.. code:: python
import json
from graphene_django.utils.testing import GraphQLTransactionTestCase
class MyFancyTransactionTestCase(GraphQLTransactionTestCase):
def test_some_mutation_that_executes_within_a_transaction(self):
response = self.query(
'''
mutation myMutation($input: MyMutationInput!) {
myMutation(input: $input) {
my-model {
id
name
}
}
}
''',
operation_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
...
Using pytest
------------
To use pytest define a simple fixture using the query helper below
.. code:: python
# Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`.
import json
import pytest
from graphene_django.utils.testing import graphql_query
@pytest.fixture
def client_query(client):
def func(*args, **kwargs):
return graphql_query(*args, **kwargs, client=client)
return func
# Test you query using the client_query fixture
def test_some_query(client_query):
response = client_query(
'''
query {
myModel {
id
name
}
}
''',
operation_name='myModel'
)
content = json.loads(response.content)
assert 'errors' not in content

View File

@ -3,11 +3,15 @@ Basic Tutorial
Graphene Django 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 easy. Our primary focus in this tutorial is to give a good 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. understanding of how to connect models from Django ORM to graphene object types.
Set up the Django project Set up the Django project
------------------------- -------------------------
You can find the entire project in ``examples/cookbook-plain``.
----
We will set up the project, create the following: We will set up the project, create the following:
- A Django project called ``cookbook`` - A Django project called ``cookbook``
@ -24,18 +28,18 @@ We will set up the project, create the following:
source env/bin/activate # On Windows use `env\Scripts\activate` source env/bin/activate # On Windows use `env\Scripts\activate`
# Install Django and Graphene with Django support # Install Django and Graphene with Django support
pip install django graphene_django pip install django
pip install graphene_django
# Set up a new project with a single application # Set up a new project with a single application
django-admin startproject cookbook . # Note the trailing '.' character django-admin.py startproject cookbook . # Note the trailing '.' character
cd cookbook cd cookbook
django-admin startapp ingredients django-admin.py startapp ingredients
Now sync your database for the first time: Now sync your database for the first time:
.. code:: bash .. code:: bash
cd ..
python manage.py migrate python manage.py migrate
Let's create a few simple models... Let's create a few simple models...
@ -50,18 +54,19 @@ Let's get started with these models:
# cookbook/ingredients/models.py # cookbook/ingredients/models.py
from django.db import models from django.db import models
class Category(models.Model): class Category(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
def __str__(self): def __str__(self):
return self.name return self.name
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField() notes = models.TextField()
category = models.ForeignKey( category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE Category, related_name='ingredients', on_delete=models.CASCADE)
)
def __str__(self): def __str__(self):
return self.name return self.name
@ -70,26 +75,12 @@ Add ingredients as INSTALLED_APPS:
.. code:: python .. code:: python
# cookbook/settings.py
INSTALLED_APPS = [ INSTALLED_APPS = [
... ...
# Install the ingredients app # Install the ingredients app
"cookbook.ingredients", 'cookbook.ingredients',
] ]
Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``.
.. code:: python
# cookbook/ingredients/apps.py
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cookbook.ingredients'
Don't forget to create & run migrations: Don't forget to create & run migrations:
@ -104,20 +95,20 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be Now is a good time to load up some test data. The easiest option will be
to `download the to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following: following:
.. code:: bash .. code:: bash
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 (``python manage.py createsuperuser``). create a login for yourself too (``./manage.py createsuperuser``).
Register models with admin panel: Register models with admin panel:
@ -147,48 +138,66 @@ order to create this representation, Graphene needs to know about each
This graph also has a *root type* through which all access begins. This This graph also has a *root type* through which all access begins. This
is the ``Query`` class below. is the ``Query`` class below.
To create GraphQL types for each of our Django models, we are going to subclass the ``DjangoObjectType`` class which will automatically define GraphQL fields that correspond to the fields on the Django models. This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType``
After we've done that, we will list those types as fields in the ``Query`` class. After we've done that, we will list those types as fields in the ``Query`` class.
Create ``cookbook/schema.py`` and type the following: Create ``cookbook/ingredients/schema.py`` and type the following:
.. code:: python .. code:: python
# cookbook/schema.py # cookbook/ingredients/schema.py
import graphene import graphene
from graphene_django import DjangoObjectType
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = ("id", "name", "ingredients")
class IngredientType(DjangoObjectType): class IngredientType(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = ("id", "name", "notes", "category")
class Query(graphene.ObjectType):
class Query(object):
all_categories = graphene.List(CategoryType)
all_ingredients = graphene.List(IngredientType) all_ingredients = graphene.List(IngredientType)
category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True))
def resolve_all_ingredients(root, info): def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
# 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_by_name(root, info, name):
try: Note that the above ``Query`` class is a mixin, inheriting from
return Category.objects.get(name=name) ``object``. This is because we will now create a project-level query
except Category.DoesNotExist: class which will combine all our app-level mixins.
return None
Create the parent project-level ``cookbook/schema.py``:
.. code:: python
import graphene
import cookbook.ingredients.schema
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)
You can think of this as being something like your top-level ``urls.py`` You can think of this as being something like your top-level ``urls.py``
file. file (although it currently lacks any namespacing).
Testing everything so far Testing everything so far
------------------------- -------------------------
@ -207,21 +216,18 @@ Add ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:
.. code:: python .. code:: python
# cookbook/settings.py
INSTALLED_APPS = [ INSTALLED_APPS = [
... ...
"graphene_django", # This will also make the `graphql_schema` management command available
'graphene_django',
] ]
And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``: And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:
.. code:: python .. code:: python
# cookbook/settings.py
GRAPHENE = { GRAPHENE = {
"SCHEMA": "cookbook.schema.schema" 'SCHEMA': 'cookbook.schema.schema'
} }
Alternatively, we can specify the schema to be used in the urls definition, Alternatively, we can specify the schema to be used in the urls definition,
@ -239,17 +245,14 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``
.. code:: python .. code:: python
# cookbook/urls.py from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), url(r'^admin/', admin.site.urls),
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
] ]
@ -258,19 +261,16 @@ as explained above, we can do so here using:
.. code:: python .. code:: python
# cookbook/urls.py from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
from cookbook.schema import schema from cookbook.schema import schema
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), url(r'^admin/', admin.site.urls),
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
] ]
@ -283,10 +283,10 @@ from the command line.
.. code:: bash .. code:: bash
python manage.py runserver $ python ./manage.py runserver
Performing system checks... Performing system checks...
Django version 3.0.7, using settings 'cookbook.settings' Django version 1.9, 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.
@ -329,25 +329,24 @@ If you are using the provided fixtures, you will see the following response:
} }
} }
You can experiment with ``allCategories`` too.
Congratulations, you have created a working GraphQL server 🥳! Something to have in mind is the `auto camelcasing <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ that is happening.
Note: Graphene `automatically camelcases <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ all field names for better compatibility with JavaScript clients.
Getting relations Getting relations
----------------- -----------------
Using the current schema we can query for relations too. This is where GraphQL becomes really powerful! Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful!
For example, we may want to get a specific categories and list all ingredients that are in that category. For example, we may want to list all categories and in each category, all ingredients that are in that category.
We can do that with the following query: We can do that with the following query:
.. code:: .. code::
query { query {
categoryByName(name: "Dairy") { allCategories {
id id
name name
ingredients { ingredients {
@ -357,26 +356,43 @@ We can do that with the following query:
} }
} }
This will give you (in case you are using the fixtures) the following result: This will give you (in case you are using the fixtures) the following result:
.. code:: .. code::
{ {
"data": { "data": {
"categoryByName": { "allCategories": [
"id": "1", {
"name": "Dairy", "id": "1",
"ingredients": [ "name": "Dairy",
{ "ingredients": [
"id": "1", {
"name": "Eggs" "id": "1",
}, "name": "Eggs"
{ },
"id": "2", {
"name": "Milk" "id": "2",
} "name": "Milk"
] }
} ]
},
{
"id": "2",
"name": "Meat",
"ingredients": [
{
"id": "3",
"name": "Beef"
},
{
"id": "4",
"name": "Chicken"
}
]
}
]
} }
} }
@ -395,12 +411,128 @@ We can also list all ingredients and get information for the category they are i
} }
} }
Getting 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.
.. code:: python
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.
For example, lets query ``category``:
.. code::
query {
category(id: 1) {
name
}
anotherCategory: category(name: "Dairy") {
ingredients {
id
name
}
}
}
This will give us the following results:
.. code::
{
"data": {
"category": {
"name": "Dairy"
},
"anotherCategory": {
"ingredients": [
{
"id": "1",
"name": "Eggs"
},
{
"id": "2",
"name": "Milk"
}
]
}
}
}
As an exercise, you can try making some queries to ``ingredient``.
Something to keep in mind - since we are using one field several times in our query, we need `aliases <http://graphql.org/learn/queries/#aliases>`__
Summary Summary
------- -------
As you can see, GraphQL is very powerful and integrating Django models allows you to get started with a working server quickly. 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 :ref:`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/>`__ A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__
documentation so that you are familiar with it as well. documentation but it is not essential to understand and use Graphene-Django in your project.

View File

@ -1,5 +1,3 @@
.. _Relay tutorial:
Relay tutorial Relay tutorial
======================================== ========================================
@ -7,12 +5,12 @@ Graphene has a number of additional features that are designed to make
working with Django *really simple*. working with Django *really simple*.
Note: The code in this quickstart is pulled from the `cookbook example Note: The code in this quickstart is pulled from the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__. app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
A good idea is to check the following things first: A good idea is to check the following things first:
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__ * `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
* `GraphQL Relay Specification <https://relay.dev/docs/guides/graphql-server-specification/>`__ * `GraphQL Relay Specification <https://facebook.github.io/relay/docs/en/graphql-server-specification.html>`__
Setup the Django project Setup the Django project
------------------------ ------------------------
@ -70,7 +68,7 @@ Let's get started with these models:
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField() notes = models.TextField()
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) category = models.ForeignKey(Category, related_name='ingredients')
def __str__(self): def __str__(self):
return self.name return self.name
@ -87,7 +85,7 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be Now is a good time to load up some test data. The easiest option will be
to `download the to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following: following:
@ -132,7 +130,6 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
class CategoryNode(DjangoObjectType): class CategoryNode(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = '__all__'
filter_fields = ['name', 'ingredients'] filter_fields = ['name', 'ingredients']
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@ -140,7 +137,6 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
class IngredientNode(DjangoObjectType): class IngredientNode(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = '__all__'
# Allow for some more advanced filtering here # Allow for some more advanced filtering here
filter_fields = { filter_fields = {
'name': ['exact', 'icontains', 'istartswith'], 'name': ['exact', 'icontains', 'istartswith'],
@ -151,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, ) interfaces = (relay.Node, )
class Query(ObjectType): class Query(graphene.ObjectType):
category = relay.Node.Field(CategoryNode) category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)
@ -281,7 +277,7 @@ from the command line.
$ python ./manage.py runserver $ python ./manage.py runserver
Performing system checks... Performing system checks...
Django version 3.1.7, using settings 'cookbook.settings' Django version 1.9, 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.

View File

@ -1,29 +0,0 @@
Query Validation
================
Graphene-Django supports query validation by allowing passing a list of validation rules (subclasses of `ValidationRule <https://github.com/graphql-python/graphql-core/blob/v3.2.3/src/graphql/validation/rules/__init__.py>`_ from graphql-core) to the ``validation_rules`` option in ``GraphQLView``.
.. code:: python
from django.urls import path
from graphene.validation import DisableIntrospection
from graphene_django.views import GraphQLView
urlpatterns = [
path("graphql", GraphQLView.as_view(validation_rules=(DisableIntrospection,))),
]
or
.. code:: python
from django.urls import path
from graphene.validation import DisableIntrospection
from graphene_django.views import GraphQLView
class View(GraphQLView):
validation_rules = (DisableIntrospection,)
urlpatterns = [
path("graphql", View.as_view()),
]

View File

View File

@ -14,7 +14,7 @@ whole Graphene repository:
```bash ```bash
# Get the example project code # Get the example project code
git clone https://github.com/graphql-python/graphene-django.git git clone https://github.com/graphql-python/graphene-django.git
cd graphene-django/examples/cookbook-plain cd graphene-django/examples/cookbook
``` ```
It is good idea (but not required) to create a virtual environment It is good idea (but not required) to create a virtual environment
@ -62,12 +62,3 @@ Now head on over to
and run some queries! and run some queries!
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
for some example queries) for some example queries)
Testing local graphene-django changes
-------------------------------------
In `requirements.txt`, replace the entire `graphene-django=...` line with the following (so that we install the local version instead of the one from PyPI):
```
../../ # graphene-django
```

View File

@ -1,52 +1 @@
[ [{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,52 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Category", name='Category',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=100)),
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="Ingredient", name='Ingredient',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=100)),
models.AutoField( ('notes', models.TextField()),
auto_created=True, ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
], ],
), ),
] ]

View File

@ -1,17 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ('ingredients', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name="ingredient", model_name='ingredient',
name="notes", name='notes',
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -4,13 +4,14 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0002_auto_20161104_0050"), ('ingredients', '0002_auto_20161104_0050'),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name="category", name='category',
options={"verbose_name_plural": "Categories"}, options={'verbose_name_plural': 'Categories'},
), ),
] ]

View File

@ -7,16 +7,14 @@ from .models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = "__all__"
class IngredientType(DjangoObjectType): class IngredientType(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = "__all__"
class Query: class Query(object):
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType) all_categories = graphene.List(CategoryType)

View File

@ -1,69 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ('ingredients', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Recipe", name='Recipe',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('title', models.CharField(max_length=100)),
models.AutoField( ('instructions', models.TextField()),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="RecipeIngredient", name='RecipeIngredient',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('amount', models.FloatField()),
models.AutoField( ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
auto_created=True, ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
primary_key=True, ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
], ],
), ),
] ]

View File

@ -1,30 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0001_initial"), ('recipes', '0001_initial'),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name="recipeingredient", model_name='recipeingredient',
old_name="recipes", old_name='recipes',
new_name="recipe", new_name='recipe',
), ),
migrations.AlterField( migrations.AlterField(
model_name="recipeingredient", model_name='recipeingredient',
name="unit", name='unit',
field=models.CharField( field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20),
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -4,22 +4,15 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0002_auto_20161104_0106"), ('recipes', '0002_auto_20161104_0106'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name="recipeingredient", model_name='recipeingredient',
name="unit", name='unit',
field=models.CharField( field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20),
choices=[
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -7,16 +7,14 @@ from .models import Recipe, RecipeIngredient
class RecipeType(DjangoObjectType): class RecipeType(DjangoObjectType):
class Meta: class Meta:
model = Recipe model = Recipe
fields = "__all__"
class RecipeIngredientType(DjangoObjectType): class RecipeIngredientType(DjangoObjectType):
class Meta: class Meta:
model = RecipeIngredient model = RecipeIngredient
fields = "__all__"
class Query: class Query(object):
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType) all_recipes = graphene.List(RecipeType)

View File

@ -1,8 +1,8 @@
import graphene
from graphene_django.debug import DjangoDebug
import cookbook.ingredients.schema import cookbook.ingredients.schema
import cookbook.recipes.schema import cookbook.recipes.schema
import graphene
from graphene_django.debug import DjangoDebug
class Query( class Query(

View File

@ -5,10 +5,10 @@ Django settings for cookbook project.
Generated by 'django-admin startproject' using Django 1.9. Generated by 'django-admin startproject' using Django 1.9.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/ https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/1.9/ref/settings/
""" """
import os import os
@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/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"
@ -81,7 +81,7 @@ WSGI_APPLICATION = "cookbook.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = { DATABASES = {
"default": { "default": {
@ -90,11 +90,9 @@ DATABASES = {
} }
} }
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
@ -107,7 +105,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
@ -121,6 +119,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/ # https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = "/static/" STATIC_URL = "/static/"

View File

@ -1,8 +1,9 @@
from django.contrib import admin
from django.urls import path from django.urls import path
from django.contrib import admin
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("graphql/", GraphQLView.as_view(graphiql=True)), path("graphql/", GraphQLView.as_view(graphiql=True)),

View File

@ -1,3 +1,4 @@
django~=3.2
graphene graphene
graphene-django>=3.1 graphene-django
graphql-core>=2.1rc1
django==2.1.10

View File

@ -1,4 +1,4 @@
Cookbook Example (Relay) Django Project Cookbook Example Django Project
=============================== ===============================
This example project demos integration between Graphene and Django. This example project demos integration between Graphene and Django.
@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
and run some queries! and run some queries!
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-relay/#testing-our-graphql-schema) (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
for some example queries) for some example queries)

View File

@ -1,52 +1 @@
[ [{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}]
{
"fields": {
"name": "Dairy"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "Meat"
},
"model": "ingredients.category",
"pk": 2
},
{
"fields": {
"category": 1,
"name": "Eggs",
"notes": "Good old eggs"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 1,
"name": "Milk",
"notes": "Comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 2,
"name": "Beef",
"notes": "Much like milk, this comes from a cow"
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 2,
"name": "Chicken",
"notes": "Definitely doesn't come from a cow"
},
"model": "ingredients.ingredient",
"pk": 4
}
]

View File

@ -1,52 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:15 # Generated by Django 1.9 on 2015-12-04 18:15
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Category", name='Category',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=100)),
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="Ingredient", name='Ingredient',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=100)),
models.AutoField( ('notes', models.TextField()),
auto_created=True, ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')),
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("notes", models.TextField()),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ingredients",
to="ingredients.Category",
),
),
], ],
), ),
] ]

View File

@ -1,17 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 00:50 # Generated by Django 1.9 on 2016-11-04 00:50
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ('ingredients', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name="ingredient", model_name='ingredient',
name="notes", name='notes',
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -1,9 +1,8 @@
from cookbook.ingredients.models import Category, Ingredient
from graphene import Node 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
from cookbook.ingredients.models import Category, Ingredient
# 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)
@ -11,7 +10,6 @@ class CategoryNode(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
filter_fields = ["name", "ingredients"] filter_fields = ["name", "ingredients"]
@ -20,7 +18,6 @@ class IngredientNode(DjangoObjectType):
model = Ingredient model = Ingredient
# Allow for some more advanced filtering here # Allow for some more advanced filtering here
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
filter_fields = { filter_fields = {
"name": ["exact", "icontains", "istartswith"], "name": ["exact", "icontains", "istartswith"],
"notes": ["exact", "icontains"], "notes": ["exact", "icontains"],
@ -29,7 +26,7 @@ class IngredientNode(DjangoObjectType):
} }
class Query: class Query(object):
category = Node.Field(CategoryNode) category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,69 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-04 18:20 # Generated by Django 1.9 on 2015-12-04 18:20
from __future__ import unicode_literals
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("ingredients", "0001_initial"), ('ingredients', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Recipe", name='Recipe',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('title', models.CharField(max_length=100)),
models.AutoField( ('instructions', models.TextField()),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("instructions", models.TextField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="RecipeIngredient", name='RecipeIngredient',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('amount', models.FloatField()),
models.AutoField( ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)),
auto_created=True, ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')),
primary_key=True, ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')),
serialize=False,
verbose_name="ID",
),
),
("amount", models.FloatField()),
(
"unit",
models.CharField(
choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")],
max_length=20,
),
),
(
"ingredient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="used_by",
to="ingredients.Ingredient",
),
),
(
"recipes",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="amounts",
to="recipes.Recipe",
),
),
], ],
), ),
] ]

View File

@ -1,30 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-11-04 01:06 # Generated by Django 1.9 on 2016-11-04 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("recipes", "0001_initial"), ('recipes', '0001_initial'),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name="recipeingredient", model_name='recipeingredient',
old_name="recipes", old_name='recipes',
new_name="recipe", new_name='recipe',
), ),
migrations.AlterField( migrations.AlterField(
model_name="recipeingredient", model_name='recipeingredient',
name="unit", name='unit',
field=models.CharField( field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20),
choices=[
(b"unit", b"Units"),
(b"kg", b"Kilograms"),
(b"l", b"Litres"),
(b"st", b"Shots"),
],
max_length=20,
),
), ),
] ]

View File

@ -6,9 +6,7 @@ from cookbook.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 __unicode__(self):
return self.title
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):

View File

@ -1,15 +1,13 @@
from cookbook.recipes.models import Recipe, RecipeIngredient
from graphene import Node 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
from cookbook.recipes.models import Recipe, RecipeIngredient
class RecipeNode(DjangoObjectType): class RecipeNode(DjangoObjectType):
class Meta: class Meta:
model = Recipe model = Recipe
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
filter_fields = ["title", "amounts"] filter_fields = ["title", "amounts"]
@ -18,7 +16,6 @@ class RecipeIngredientNode(DjangoObjectType):
model = RecipeIngredient model = RecipeIngredient
# Allow for some more advanced filtering here # Allow for some more advanced filtering here
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
filter_fields = { filter_fields = {
"ingredient__name": ["exact", "icontains", "istartswith"], "ingredient__name": ["exact", "icontains", "istartswith"],
"recipe": ["exact"], "recipe": ["exact"],
@ -26,7 +23,7 @@ class RecipeIngredientNode(DjangoObjectType):
} }
class Query: class Query(object):
recipe = Node.Field(RecipeNode) recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

@ -1,8 +1,8 @@
import graphene
from graphene_django.debug import DjangoDebug
import cookbook.ingredients.schema import cookbook.ingredients.schema
import cookbook.recipes.schema import cookbook.recipes.schema
import graphene
from graphene_django.debug import DjangoDebug
class Query( class Query(

View File

@ -41,7 +41,6 @@ INSTALLED_APPS = [
"graphene_django", "graphene_django",
"cookbook.ingredients.apps.IngredientsConfig", "cookbook.ingredients.apps.IngredientsConfig",
"cookbook.recipes.apps.RecipesConfig", "cookbook.recipes.apps.RecipesConfig",
"django_filters",
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -3,6 +3,7 @@ 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), url(r"^admin/", admin.site.urls),
url(r"^graphql$", GraphQLView.as_view(graphiql=True)), url(r"^graphql$", GraphQLView.as_view(graphiql=True)),

View File

@ -1,302 +1 @@
[ [{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "last_login": "2016-11-04T00:46:58Z", "is_superuser": true, "username": "admin", "first_name": "", "last_name": "", "email": "asdf@example.com", "is_staff": true, "is_active": true, "date_joined": "2016-11-03T18:24:40Z", "groups": [], "user_permissions": []}}, {"model": "recipes.recipe", "pk": 1, "fields": {"title": "Cheerios With a Shot of Vermouth", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 2, "fields": {"title": "Quail Eggs in Whipped Cream and MSG", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 3, "fields": {"title": "Deep Fried Skittles", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 4, "fields": {"title": "Newt ala Doritos", "instructions": "https://xkcd.com/720/"}}, {"model": "recipes.recipe", "pk": 5, "fields": {"title": "Fruit Salad", "instructions": "Chop up and add together"}}, {"model": "recipes.recipeingredient", "pk": 1, "fields": {"recipes": 5, "ingredient": 9, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 2, "fields": {"recipes": 5, "ingredient": 10, "amount": 2.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 3, "fields": {"recipes": 5, "ingredient": 7, "amount": 3.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 4, "fields": {"recipes": 5, "ingredient": 8, "amount": 4.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 5, "fields": {"recipes": 4, "ingredient": 5, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 6, "fields": {"recipes": 4, "ingredient": 6, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 7, "fields": {"recipes": 3, "ingredient": 4, "amount": 1.0, "unit": "unit"}}, {"model": "recipes.recipeingredient", "pk": 8, "fields": {"recipes": 2, "ingredient": 2, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 9, "fields": {"recipes": 2, "ingredient": 11, "amount": 2.0, "unit": "l"}}, {"model": "recipes.recipeingredient", "pk": 10, "fields": {"recipes": 2, "ingredient": 12, "amount": 3.0, "unit": "st"}}, {"model": "recipes.recipeingredient", "pk": 11, "fields": {"recipes": 1, "ingredient": 1, "amount": 1.0, "unit": "kg"}}, {"model": "recipes.recipeingredient", "pk": 12, "fields": {"recipes": 1, "ingredient": 3, "amount": 1.0, "unit": "st"}}, {"model": "ingredients.category", "pk": 1, "fields": {"name": "fruit"}}, {"model": "ingredients.category", "pk": 3, "fields": {"name": "xkcd"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Cheerios", "notes": "this is a note", "category": 3}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Quail Eggs", "notes": "has more notes", "category": 3}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Vermouth", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Skittles", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 5, "fields": {"name": "Newt", "notes": "Braised and Confuesd", "category": 3}}, {"model": "ingredients.ingredient", "pk": 6, "fields": {"name": "Doritos", "notes": "Crushed", "category": 3}}, {"model": "ingredients.ingredient", "pk": 7, "fields": {"name": "Apple", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 8, "fields": {"name": "Orange", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 9, "fields": {"name": "Banana", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 10, "fields": {"name": "Grapes", "notes": "", "category": 1}}, {"model": "ingredients.ingredient", "pk": 11, "fields": {"name": "Whipped Cream", "notes": "", "category": 3}}, {"model": "ingredients.ingredient", "pk": 12, "fields": {"name": "MSG", "notes": "", "category": 3}}]
{
"fields": {
"date_joined": "2016-11-03T18:24:40Z",
"email": "asdf@example.com",
"first_name": "",
"groups": [],
"is_active": true,
"is_staff": true,
"is_superuser": true,
"last_login": "2016-11-04T00:46:58Z",
"last_name": "",
"password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=",
"user_permissions": [],
"username": "admin"
},
"model": "auth.user",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Cheerios With a Shot of Vermouth"
},
"model": "recipes.recipe",
"pk": 1
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Quail Eggs in Whipped Cream and MSG"
},
"model": "recipes.recipe",
"pk": 2
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Deep Fried Skittles"
},
"model": "recipes.recipe",
"pk": 3
},
{
"fields": {
"instructions": "https://xkcd.com/720/",
"title": "Newt ala Doritos"
},
"model": "recipes.recipe",
"pk": 4
},
{
"fields": {
"instructions": "Chop up and add together",
"title": "Fruit Salad"
},
"model": "recipes.recipe",
"pk": 5
},
{
"fields": {
"amount": 1.0,
"ingredient": 9,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 1
},
{
"fields": {
"amount": 2.0,
"ingredient": 10,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 2
},
{
"fields": {
"amount": 3.0,
"ingredient": 7,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 3
},
{
"fields": {
"amount": 4.0,
"ingredient": 8,
"recipes": 5,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 4
},
{
"fields": {
"amount": 1.0,
"ingredient": 5,
"recipes": 4,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 5
},
{
"fields": {
"amount": 2.0,
"ingredient": 6,
"recipes": 4,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 6
},
{
"fields": {
"amount": 1.0,
"ingredient": 4,
"recipes": 3,
"unit": "unit"
},
"model": "recipes.recipeingredient",
"pk": 7
},
{
"fields": {
"amount": 1.0,
"ingredient": 2,
"recipes": 2,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 8
},
{
"fields": {
"amount": 2.0,
"ingredient": 11,
"recipes": 2,
"unit": "l"
},
"model": "recipes.recipeingredient",
"pk": 9
},
{
"fields": {
"amount": 3.0,
"ingredient": 12,
"recipes": 2,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 10
},
{
"fields": {
"amount": 1.0,
"ingredient": 1,
"recipes": 1,
"unit": "kg"
},
"model": "recipes.recipeingredient",
"pk": 11
},
{
"fields": {
"amount": 1.0,
"ingredient": 3,
"recipes": 1,
"unit": "st"
},
"model": "recipes.recipeingredient",
"pk": 12
},
{
"fields": {
"name": "fruit"
},
"model": "ingredients.category",
"pk": 1
},
{
"fields": {
"name": "xkcd"
},
"model": "ingredients.category",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Cheerios",
"notes": "this is a note"
},
"model": "ingredients.ingredient",
"pk": 1
},
{
"fields": {
"category": 3,
"name": "Quail Eggs",
"notes": "has more notes"
},
"model": "ingredients.ingredient",
"pk": 2
},
{
"fields": {
"category": 3,
"name": "Vermouth",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 3
},
{
"fields": {
"category": 3,
"name": "Skittles",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 4
},
{
"fields": {
"category": 3,
"name": "Newt",
"notes": "Braised and Confused"
},
"model": "ingredients.ingredient",
"pk": 5
},
{
"fields": {
"category": 3,
"name": "Doritos",
"notes": "Crushed"
},
"model": "ingredients.ingredient",
"pk": 6
},
{
"fields": {
"category": 1,
"name": "Apple",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 7
},
{
"fields": {
"category": 1,
"name": "Orange",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 8
},
{
"fields": {
"category": 1,
"name": "Banana",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 9
},
{
"fields": {
"category": 1,
"name": "Grapes",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 10
},
{
"fields": {
"category": 3,
"name": "Whipped Cream",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 11
},
{
"fields": {
"category": 3,
"name": "MSG",
"notes": ""
},
"model": "ingredients.ingredient",
"pk": 12
}
]

View File

@ -1,5 +1,5 @@
graphene>=2.1,<3 graphene
graphene-django>=2.1,<3 graphene-django
graphql-core>=2.1,<3 graphql-core>=2.1rc1
django==4.2.18 django==2.2.3
django-filter>=2 django-filter>=2

View File

@ -1,32 +0,0 @@
import os
import sys
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + "/examples/")
SECRET_KEY = 1
INSTALLED_APPS = [
"graphene_django",
"graphene_django.rest_framework",
"graphene_django.tests",
"examples.starwars",
]
DATABASES = {
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "django_test.sqlite"}
}
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
}
]
GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"}
ROOT_URLCONF = "graphene_django.tests.urls"
USE_TZ = True

View File

@ -28,7 +28,7 @@ def initialize():
# 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(id="4", name="Millennium Falcon", faction=rebels) falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
falcon.save() falcon.save()
homeOne = Ship(id="5", name="Home One", faction=rebels) homeOne = Ship(id="5", name="Home One", faction=rebels)

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import
from django.db import models from django.db import models

View File

@ -1,20 +1,17 @@
import graphene import graphene
from graphene import Schema, relay 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, get_ships from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
from .models import ( from .models import Character as CharacterModel
Character as CharacterModel, from .models import Faction as FactionModel
Faction as FactionModel, from .models import Ship as ShipModel
Ship as ShipModel,
)
class Ship(DjangoObjectType): class Ship(DjangoObjectType):
class Meta: class Meta:
model = ShipModel model = ShipModel
interfaces = (relay.Node,) interfaces = (relay.Node,)
fields = "__all__"
@classmethod @classmethod
def get_node(cls, info, id): def get_node(cls, info, id):
@ -25,14 +22,12 @@ class Ship(DjangoObjectType):
class Character(DjangoObjectType): class Character(DjangoObjectType):
class Meta: class Meta:
model = CharacterModel model = CharacterModel
fields = "__all__"
class Faction(DjangoObjectType): class Faction(DjangoObjectType):
class Meta: class Meta:
model = FactionModel model = FactionModel
interfaces = (relay.Node,) interfaces = (relay.Node,)
fields = "__all__"
@classmethod @classmethod
def get_node(cls, info, id): def get_node(cls, info, id):
@ -62,13 +57,16 @@ class Query(graphene.ObjectType):
node = relay.Node.Field() node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description="All the ships.") ships = DjangoConnectionField(Ship, description="All the ships.")
def resolve_ships(self, info): @resolve_only_args
def resolve_ships(self):
return get_ships() return get_ships()
def resolve_rebels(self, info): @resolve_only_args
def resolve_rebels(self):
return get_rebels() return get_rebels()
def resolve_empire(self, info): @resolve_only_args
def resolve_empire(self):
return get_empire() return get_empire()

View File

@ -40,7 +40,7 @@ def test_mutations():
{"node": {"id": "U2hpcDox", "name": "X-Wing"}}, {"node": {"id": "U2hpcDox", "name": "X-Wing"}},
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}}, {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}}, {"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
{"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}}, {"node": {"id": "U2hpcDo0", "name": "Millenium Falcon"}},
{"node": {"id": "U2hpcDo1", "name": "Home One"}}, {"node": {"id": "U2hpcDo1", "name": "Home One"}},
{"node": {"id": "U2hpcDo5", "name": "Peter"}}, {"node": {"id": "U2hpcDo5", "name": "Peter"}},
] ]

View File

@ -1,13 +1,6 @@
from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType from .types import DjangoObjectType
from .utils import bypass_get_queryset from .fields import DjangoConnectionField
__version__ = "3.2.3" __version__ = "2.4.0"
__all__ = [ __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
"__version__",
"DjangoObjectType",
"DjangoListField",
"DjangoConnectionField",
"bypass_get_queryset",
]

View File

@ -1,16 +1,5 @@
import sys class MissingType(object):
from collections.abc import Callable pass
from pathlib import PurePath
# For backwards compatibility, we import JSONField to have it available for import via
# this compat module (https://github.com/graphql-python/graphene-django/issues/1428).
# Django's JSONField is available in Django 3.2+ (the minimum version we support)
from django.db.models import Choices, JSONField
class MissingType:
def __init__(self, *args, **kwargs):
pass
try: try:
@ -19,47 +8,8 @@ try:
from django.contrib.postgres.fields import ( from django.contrib.postgres.fields import (
ArrayField, ArrayField,
HStoreField, HStoreField,
IntegerRangeField, JSONField,
RangeField, RangeField,
) )
except ImportError: except ImportError:
IntegerRangeField, HStoreField, RangeField = (MissingType,) * 3 ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
# For unit tests we fake ArrayField using JSONFields
if any(
PurePath(sys.argv[0]).match(p)
for p in [
"**/pytest",
"**/py.test",
"**/pytest/__main__.py",
]
):
class ArrayField(JSONField):
def __init__(self, *args, **kwargs):
if len(args) > 0:
self.base_field = args[0]
super().__init__(**kwargs)
else:
ArrayField = MissingType
try:
from django.utils.choices import normalize_choices
except ImportError:
def normalize_choices(choices):
if isinstance(choices, type) and issubclass(choices, Choices):
choices = choices.choices
if isinstance(choices, Callable):
choices = choices()
# In restframework==3.15.0, choices are not passed
# as OrderedDict anymore, so it's safer to check
# for a dict
if isinstance(choices, dict):
choices = choices.items()
return choices

View File

@ -1,18 +0,0 @@
import pytest
from graphene_django.settings import graphene_settings as gsettings
from .registry import reset_global_registry
@pytest.fixture(autouse=True)
def reset_registry_fixture(db):
yield None
reset_global_registry()
@pytest.fixture()
def graphene_settings():
settings = dict(gsettings.__dict__)
yield gsettings
gsettings.__dict__ = settings

View File

@ -1 +0,0 @@
MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors"

View File

@ -1,19 +1,9 @@
import inspect
from functools import partial, singledispatch, wraps
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_text
from django.utils.functional import Promise
from django.utils.module_loading import import_string
from graphql import GraphQLError
from graphene import ( from graphene import (
ID, ID,
UUID,
Boolean, Boolean,
Date,
DateTime,
Decimal,
Dynamic, Dynamic,
Enum, Enum,
Field, Field,
@ -22,151 +12,68 @@ from graphene import (
List, List,
NonNull, NonNull,
String, String,
UUID,
DateTime,
Date,
Time, Time,
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.types.resolver import get_default_resolver from graphene.utils.str_converters import to_camel_case, to_const
from graphene.types.scalars import BigInt from graphql import assert_valid_name
from graphene.utils.str_converters import to_camel_case
try: from .compat import ArrayField, HStoreField, JSONField, RangeField
from graphql import assert_name from .fields import DjangoListField, DjangoConnectionField
except ImportError: from .utils import import_single_dispatch
# Support for older versions of graphql
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, RangeField, normalize_choices singledispatch = import_single_dispatch()
from .fields import DjangoConnectionField, DjangoListField
from .settings import graphene_settings
from .utils.str_converters import to_const
class BlankValueField(Field):
def wrap_resolve(self, parent_resolver):
resolver = self.resolver or parent_resolver
# create custom resolver
def blank_field_wrapper(func):
@wraps(func)
def wrapped_resolver(*args, **kwargs):
return_value = func(*args, **kwargs)
if return_value == "":
return None
return return_value
return wrapped_resolver
return blank_field_wrapper(resolver)
class EnumValueField(BlankValueField):
def wrap_resolve(self, parent_resolver):
resolver = super().wrap_resolve(parent_resolver)
# create custom resolver
def enum_field_wrapper(func):
@wraps(func)
def wrapped_resolver(*args, **kwargs):
return_value = func(*args, **kwargs)
if isinstance(return_value, models.Choices):
return_value = return_value.value
return return_value
return wrapped_resolver
return enum_field_wrapper(resolver)
def convert_choice_name(name): def convert_choice_name(name):
name = to_const(force_str(name)) name = to_const(force_text(name))
try: try:
assert_name(name) assert_valid_name(name)
except GraphQLError: except AssertionError:
name = "A_%s" % name name = "A_%s" % name
return name return name
def get_choices(choices): def get_choices(choices):
converted_names = [] converted_names = []
choices = normalize_choices(choices)
for value, help_text in choices: for value, help_text in choices:
if isinstance(help_text, (tuple, list)): if isinstance(help_text, (tuple, list)):
yield from get_choices(help_text) for choice in get_choices(help_text):
yield choice
else: else:
name = convert_choice_name(value) name = convert_choice_name(value)
while name in converted_names: while name in converted_names:
name += "_" + str(len(converted_names)) name += "_" + str(len(converted_names))
converted_names.append(name) converted_names.append(name)
description = str( description = help_text
help_text
) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58
yield name, value, description yield name, value, description
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:
@property
def description(self):
return str(named_choices_descriptions[self.name])
return_type = Enum(
name,
list(named_choices),
type=EnumWithDescriptionsType,
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
)
return return_type
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_V2_NAMING is True:
name = to_camel_case(f"{django_model_meta.object_name}_{field.name}")
else:
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()),
)
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( def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=None 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 convert_choices_to_enum is None:
convert_choices_to_enum = bool(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT
)
if choices and convert_choices_to_enum: if choices and convert_choices_to_enum:
EnumCls = convert_choice_field_to_enum(field) meta = field.model._meta
required = not (field.blank or field.null) name = to_camel_case("{}_{}".format(meta.object_name, field.name))
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}
converted = EnumCls( class EnumWithDescriptionsType(object):
description=get_django_field_description(field), required=required @property
).mount_as(EnumValueField) 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:
@ -174,14 +81,10 @@ def convert_django_field_with_choices(
return converted return converted
def get_django_field_description(field):
return str(field.help_text) if field.help_text else None
@singledispatch @singledispatch
def convert_django_field(field, registry=None): def convert_django_field(field, registry=None):
raise Exception( raise Exception(
f"Don't know how to convert the Django field {field} ({field.__class__})" "Don't know how to convert the Django field %s (%s)" % (field, field.__class__)
) )
@ -194,88 +97,62 @@ def convert_django_field(field, registry=None):
@convert_django_field.register(models.FileField) @convert_django_field.register(models.FileField)
@convert_django_field.register(models.FilePathField) @convert_django_field.register(models.FilePathField)
def convert_field_to_string(field, registry=None): def convert_field_to_string(field, registry=None):
return String( return String(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.AutoField) @convert_django_field.register(models.AutoField)
@convert_django_field.register(models.BigAutoField)
@convert_django_field.register(models.SmallAutoField)
def convert_field_to_id(field, registry=None): def convert_field_to_id(field, registry=None):
return ID(description=get_django_field_description(field), required=not field.null) return ID(description=field.help_text, required=not field.null)
@convert_django_field.register(models.UUIDField) @convert_django_field.register(models.UUIDField)
def convert_field_to_uuid(field, registry=None): def convert_field_to_uuid(field, registry=None):
return UUID( return UUID(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.BigIntegerField)
def convert_big_int_field(field, registry=None):
return BigInt(description=field.help_text, required=not field.null)
@convert_django_field.register(models.PositiveIntegerField) @convert_django_field.register(models.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField) @convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None): def convert_field_to_int(field, registry=None):
return Int(description=get_django_field_description(field), required=not field.null) return Int(description=field.help_text, required=not field.null)
@convert_django_field.register(models.BooleanField)
def convert_field_to_boolean(field, registry=None):
return NonNull(Boolean, description=field.help_text)
@convert_django_field.register(models.NullBooleanField) @convert_django_field.register(models.NullBooleanField)
@convert_django_field.register(models.BooleanField) def convert_field_to_nullboolean(field, registry=None):
def convert_field_to_boolean(field, registry=None): return Boolean(description=field.help_text, required=not field.null)
return Boolean(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DecimalField) @convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None):
return Decimal(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.FloatField) @convert_django_field.register(models.FloatField)
@convert_django_field.register(models.DurationField) @convert_django_field.register(models.DurationField)
def convert_field_to_float(field, registry=None): def convert_field_to_float(field, registry=None):
return Float( return Float(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DateTimeField) @convert_django_field.register(models.DateTimeField)
def convert_datetime_to_string(field, registry=None): def convert_datetime_to_string(field, registry=None):
return DateTime( return DateTime(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DateField) @convert_django_field.register(models.DateField)
def convert_date_to_string(field, registry=None): def convert_date_to_string(field, registry=None):
return Date( return Date(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.TimeField) @convert_django_field.register(models.TimeField)
def convert_time_to_string(field, registry=None): def convert_time_to_string(field, registry=None):
return Time( return Time(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.OneToOneRel) @convert_django_field.register(models.OneToOneRel)
def convert_onetoone_field_to_djangomodel(field, registry=None): def convert_onetoone_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model model = field.related_model
def dynamic_type(): def dynamic_type():
@ -283,55 +160,10 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
if not _type: if not _type:
return return
class CustomField(Field): # We do this for a bug in Django 1.8, where null attr
def wrap_resolve(self, parent_resolver): # is not available in the OneToOneRel instance
""" null = getattr(field, "null", True)
Implements a custom resolver which goes through the `get_node` method to ensure that return Field(_type, required=not null)
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args):
# Note: this function is used to resolve 1:1 relation fields
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
field_name = to_snake_case(info.field_name)
reversed_field_name = root.__class__._meta.get_field(
field_name
).remote_field.name
try:
return _type.get_queryset(
_type._meta.model.objects.filter(
**{reversed_field_name: root.pk}
),
info,
).get()
except _type._meta.model.DoesNotExist:
return None
return custom_resolver
return CustomField(
_type,
required=not field.null,
)
return Dynamic(dynamic_type) return Dynamic(dynamic_type)
@ -347,10 +179,11 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type: if not _type:
return return
if isinstance(field, models.ManyToManyField): description = (
description = get_django_field_description(field) field.help_text
else: if isinstance(field, models.ManyToManyField)
description = get_django_field_description(field.field) 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
@ -361,11 +194,9 @@ def convert_field_to_list_or_connection(field, registry=None):
if _type._meta.filter_fields or _type._meta.filterset_class: if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField( return DjangoFilterConnectionField(_type, description=description)
_type, required=True, description=description
)
return DjangoConnectionField(_type, required=True, description=description) return DjangoConnectionField(_type, description=description)
return DjangoListField( return DjangoListField(
_type, _type,
@ -379,10 +210,6 @@ def convert_field_to_list_or_connection(field, registry=None):
@convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey) @convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field, registry=None): def convert_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model model = field.related_model
def dynamic_type(): def dynamic_type():
@ -390,127 +217,28 @@ def convert_field_to_djangomodel(field, registry=None):
if not _type: if not _type:
return return
class CustomField(Field): return Field(_type, description=field.help_text, required=not field.null)
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which goes through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args):
# Note: this function is used to resolve FK or 1:1 fields
# it does not differentiate between custom-resolved fields
# and default resolved fields.
# because this is a django foreign key or one-to-one field, the primary-key for
# this node can be accessed from the root node.
# ex: article.reporter_id
# get the name of the id field from the root's model
field_name = to_snake_case(info.field_name)
db_field_key = root.__class__._meta.get_field(field_name).attname
if hasattr(root, db_field_key):
# get the object's primary-key from root
object_pk = getattr(root, db_field_key)
else:
return None
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
instance_from_get_node = _type.get_node(info, object_pk)
if instance_from_get_node is None:
# no instance to return
return
elif (
isinstance(resolver, partial)
and resolver.func is get_default_resolver()
):
return instance_from_get_node
elif resolver is not get_default_resolver():
# Default resolver is overridden
# For optimization, add the instance to the resolver
setattr(root, field_name, instance_from_get_node)
# Explanation:
# previously, _type.get_node` is called which results in at least one hit to the database.
# But, if we did not pass the instance to the root, calling the resolver will result in
# another call to get the instance which results in at least two database queries in total
# to resolve this node only.
# That's why the value of the object is set in the root so when the object is accessed
# in the resolver (root.field_name) it does not access the database unless queried explicitly.
fk_obj = resolver(root, info, **args)
return fk_obj
else:
return instance_from_get_node
return custom_resolver
return CustomField(
_type,
description=get_django_field_description(field),
required=not field.null,
)
return Dynamic(dynamic_type) return Dynamic(dynamic_type)
@convert_django_field.register(ArrayField) @convert_django_field.register(ArrayField)
def convert_postgres_array_to_list(field, registry=None): def convert_postgres_array_to_list(field, registry=None):
inner_type = convert_django_field(field.base_field) base_type = convert_django_field(field.base_field)
if not isinstance(inner_type, (List, NonNull)): if not isinstance(base_type, (List, NonNull)):
inner_type = ( base_type = type(base_type)
NonNull(type(inner_type)) return List(base_type, description=field.help_text, required=not field.null)
if inner_type.kwargs["required"]
else type(inner_type)
)
return List(
inner_type,
description=get_django_field_description(field),
required=not field.null,
)
@convert_django_field.register(HStoreField) @convert_django_field.register(HStoreField)
@convert_django_field.register(models.JSONField) @convert_django_field.register(JSONField)
def convert_json_field_to_string(field, registry=None): def convert_posgres_field_to_string(field, registry=None):
return JSONString( return JSONString(description=field.help_text, required=not field.null)
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(RangeField) @convert_django_field.register(RangeField)
def convert_postgres_range_to_string(field, registry=None): def convert_posgres_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 = ( inner_type = type(inner_type)
NonNull(type(inner_type)) return List(inner_type, description=field.help_text, required=not field.null)
if inner_type.kwargs["required"]
else type(inner_type)
)
return List(
inner_type,
description=get_django_field_description(field),
required=not field.null,
)
# Register Django lazy()-wrapped values as GraphQL description/help_text.
# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58.
register_description(Promise)

View File

@ -1,17 +0,0 @@
import traceback
from django.utils.encoding import force_str
from .types import DjangoDebugException
def wrap_exception(exception):
return DjangoDebugException(
message=force_str(exception),
exc_type=force_str(type(exception)),
stack="".join(
traceback.format_exception(
exception, value=exception, tb=exception.__traceback__
)
),
)

View File

@ -1,10 +0,0 @@
from graphene import ObjectType, String
class DjangoDebugException(ObjectType):
class Meta:
description = "Represents a single exception raised."
exc_type = String(required=True, description="The class of the exception")
message = String(required=True, description="The message of the exception")
stack = String(required=True, description="The stack trace")

View File

@ -1,38 +1,30 @@
from django.db import connections from django.db import connections
from .exception.formating import wrap_exception from promise import Promise
from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.tracking import unwrap_cursor, wrap_cursor
from .types import DjangoDebug from .types import DjangoDebug
class DjangoDebugContext: class DjangoDebugContext(object):
def __init__(self): def __init__(self):
self.debug_result = None self.debug_promise = None
self.results = [] self.promises = []
self.object = DjangoDebug(sql=[], exceptions=[])
self.enable_instrumentation() self.enable_instrumentation()
self.object = DjangoDebug(sql=[])
def get_debug_result(self): def get_debug_promise(self):
if not self.debug_result: if not self.debug_promise:
self.debug_result = self.results self.debug_promise = Promise.all(self.promises)
self.results = [] return self.debug_promise.then(self.on_resolve_all_promises)
return self.on_resolve_all_results()
def on_resolve_error(self, value): def on_resolve_all_promises(self, values):
if hasattr(self, "object"):
self.object.exceptions.append(wrap_exception(value))
return value
def on_resolve_all_results(self):
if self.results:
self.debug_result = None
return self.get_debug_result()
self.disable_instrumentation() self.disable_instrumentation()
return self.object return self.object
def add_result(self, result): def add_promise(self, promise):
if self.debug_result: if self.debug_promise and not self.debug_promise.is_fulfilled:
self.results.append(result) self.promises.append(promise)
def enable_instrumentation(self): def enable_instrumentation(self):
# This is thread-safe because database connections are thread-local. # This is thread-safe because database connections are thread-local.
@ -44,7 +36,7 @@ class DjangoDebugContext:
unwrap_cursor(connection) unwrap_cursor(connection)
class DjangoDebugMiddleware: class DjangoDebugMiddleware(object):
def resolve(self, next, root, info, **args): def resolve(self, next, root, info, **args):
context = info.context context = info.context
django_debug = getattr(context, "django_debug", None) django_debug = getattr(context, "django_debug", None)
@ -60,10 +52,7 @@ class DjangoDebugMiddleware:
) )
) )
if info.schema.get_type("DjangoDebug") == info.return_type: if info.schema.get_type("DjangoDebug") == info.return_type:
return context.django_debug.get_debug_result() return context.django_debug.get_debug_promise()
try: promise = next(root, info, **args)
result = next(root, info, **args) context.django_debug.add_promise(promise)
except Exception as e: return promise
return context.django_debug.on_resolve_error(e)
context.django_debug.add_result(result)
return result

View File

@ -1,10 +1,12 @@
# Code obtained from django-debug-toolbar sql panel tracking # Code obtained from django-debug-toolbar sql panel tracking
from __future__ import absolute_import, unicode_literals
import json import json
from threading import local from threading import local
from time import time from time import time
from django.utils.encoding import force_str from django.utils import six
from django.utils.encoding import force_text
from .types import DjangoDebugSQL from .types import DjangoDebugSQL
@ -49,7 +51,7 @@ def unwrap_cursor(connection):
del connection._graphene_cursor del connection._graphene_cursor
class ExceptionCursorWrapper: class ExceptionCursorWrapper(object):
""" """
Wraps a cursor and raises an exception on any operation. Wraps a cursor and raises an exception on any operation.
Used in Templates panel. Used in Templates panel.
@ -62,7 +64,7 @@ class ExceptionCursorWrapper:
raise SQLQueryTriggered() raise SQLQueryTriggered()
class NormalCursorWrapper: class NormalCursorWrapper(object):
""" """
Wraps a cursor and logs queries. Wraps a cursor and logs queries.
""" """
@ -75,8 +77,8 @@ class NormalCursorWrapper:
self.logger = logger self.logger = logger
def _quote_expr(self, element): def _quote_expr(self, element):
if isinstance(element, str): if isinstance(element, six.string_types):
return "'%s'" % force_str(element).replace("'", "''") return "'%s'" % force_text(element).replace("'", "''")
else: else:
return repr(element) return repr(element)
@ -84,12 +86,12 @@ class NormalCursorWrapper:
if not params: if not params:
return params return params
if isinstance(params, dict): if isinstance(params, dict):
return {key: self._quote_expr(value) for key, value in params.items()} return dict((key, self._quote_expr(value)) for key, value in params.items())
return list(map(self._quote_expr, params)) return list(map(self._quote_expr, params))
def _decode(self, param): def _decode(self, param):
try: try:
return force_str(param, strings_only=True) return force_text(param, strings_only=True)
except UnicodeDecodeError: except UnicodeDecodeError:
return "(encoded string)" return "(encoded string)"
@ -146,10 +148,10 @@ class NormalCursorWrapper:
# We keep `sql` to maintain backwards compatibility # We keep `sql` to maintain backwards compatibility
self.logger.object.sql.append(_sql) self.logger.object.sql.append(_sql)
def callproc(self, procname, params=None): def callproc(self, procname, params=()):
return self._record(self.cursor.callproc, procname, params) return self._record(self.cursor.callproc, procname, params)
def execute(self, sql, params=None): def execute(self, sql, params=()):
return self._record(self.cursor.execute, sql, params) return self._record(self.cursor.execute, sql, params)
def executemany(self, sql, param_list): def executemany(self, sql, param_list):

View File

@ -9,10 +9,15 @@ from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug from ..types import DjangoDebug
class context: class context(object):
pass pass
# from examples.starwars_django.models import Character
pytestmark = pytest.mark.django_db
def test_should_query_field(): def test_should_query_field():
r1 = Reporter(last_name="ABA") r1 = Reporter(last_name="ABA")
r1.save() r1.save()
@ -23,7 +28,6 @@ def test_should_query_field():
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType) reporter = graphene.Field(ReporterType)
@ -56,77 +60,6 @@ def test_should_query_field():
assert result.data == expected assert result.data == expected
@pytest.mark.parametrize("max_limit", [None, 100])
def test_should_query_nested_field(graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
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,)
fields = "__all__"
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()
@ -137,7 +70,6 @@ def test_should_query_list():
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = graphene.List(ReporterType) all_reporters = graphene.List(ReporterType)
@ -170,10 +102,7 @@ def test_should_query_list():
assert result.data == expected assert result.data == expected
@pytest.mark.parametrize("max_limit", [None, 100]) def test_should_query_connection():
def test_should_query_connection(graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
r1 = Reporter(last_name="ABA") r1 = Reporter(last_name="ABA")
r1.save() r1.save()
r2 = Reporter(last_name="Griffin") r2 = Reporter(last_name="Griffin")
@ -183,7 +112,6 @@ def test_should_query_connection(graphene_settings, max_limit):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
@ -215,16 +143,12 @@ def test_should_query_connection(graphene_settings, max_limit):
) )
assert not result.errors assert not result.errors
assert result.data["allReporters"] == expected["allReporters"] assert result.data["allReporters"] == expected["allReporters"]
assert len(result.data["_debug"]["sql"]) == 2
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
@pytest.mark.parametrize("max_limit", [None, 100]) def test_should_query_connectionfilter():
def test_should_query_connectionfilter(graphene_settings, max_limit):
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit
from ...filter import DjangoFilterConnectionField from ...filter import DjangoFilterConnectionField
r1 = Reporter(last_name="ABA") r1 = Reporter(last_name="ABA")
@ -236,7 +160,6 @@ def test_should_query_connectionfilter(graphene_settings, max_limit):
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"]) all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
@ -269,46 +192,6 @@ def test_should_query_connectionfilter(graphene_settings, max_limit):
) )
assert not result.errors assert not result.errors
assert result.data["allReporters"] == expected["allReporters"] assert result.data["allReporters"] == expected["allReporters"]
assert len(result.data["_debug"]["sql"]) == 2
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_stack_trace():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
fields = "__all__"
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
debug = graphene.Field(DjangoDebug, name="_debug")
def resolve_reporter(self, info, **args):
raise Exception("caught stack trace")
query = """
query ReporterQuery {
reporter {
lastName
}
_debug {
exceptions {
message
stack
}
}
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
)
assert result.errors
assert len(result.data["_debug"]["exceptions"])
debug_exception = result.data["_debug"]["exceptions"][0]
assert debug_exception["stack"].count("\n") > 1
assert "test_query.py" in debug_exception["stack"]
assert debug_exception["message"] == "caught stack trace"

View File

@ -1,6 +1,5 @@
from graphene import List, ObjectType from graphene import List, ObjectType
from .exception.types import DjangoDebugException
from .sql.types import DjangoDebugSQL from .sql.types import DjangoDebugSQL
@ -9,6 +8,3 @@ class DjangoDebug(ObjectType):
description = "Debugging information for the current query." description = "Debugging information for the current query."
sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.") sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.")
exceptions = List(
DjangoDebugException, description="Raise exceptions for this API query."
)

View File

@ -1,18 +1,13 @@
from functools import partial from functools import partial
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from graphql_relay import ( from graphene import NonNull
connection_from_array_slice,
cursor_to_offset,
get_offset_with_default,
offset_to_cursor,
)
from promise import Promise from promise import Promise
from graphene import Int, NonNull
from graphene.relay import ConnectionField
from graphene.relay.connection import connection_adapter, page_info_adapter
from graphene.types import Field, List from graphene.types import Field, List
from graphene.relay import ConnectionField, PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from .settings import graphene_settings from .settings import graphene_settings
from .utils import maybe_queryset from .utils import maybe_queryset
@ -20,61 +15,19 @@ from .utils import maybe_queryset
class DjangoListField(Field): class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs): def __init__(self, _type, *args, **kwargs):
if isinstance(_type, NonNull):
_type = _type.of_type
# Django would never return a Set of None vvvvvvv # Django would never return a Set of None vvvvvvv
super().__init__(List(NonNull(_type)), *args, **kwargs) super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
@property
def type(self):
from .types import DjangoObjectType
assert issubclass(
self._underlying_type, DjangoObjectType
), "DjangoListField only accepts DjangoObjectType types as underlying type"
return super().type
@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._underlying_type._meta.model return self.type.of_type._meta.node._meta.model
def get_manager(self):
return self.model._default_manager
@staticmethod @staticmethod
def list_resolver( def list_resolver(resolver, root, info, **args):
django_object_type, resolver, default_manager, root, info, **args return maybe_queryset(resolver(root, info, **args))
):
queryset = maybe_queryset(resolver(root, info, **args))
if queryset is None:
queryset = maybe_queryset(default_manager)
if isinstance(queryset, QuerySet): def get_resolver(self, parent_resolver):
# Pass queryset to the DjangoObjectType get_queryset method return partial(self.list_resolver, parent_resolver)
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
return queryset
def wrap_resolve(self, parent_resolver):
resolver = super().wrap_resolve(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,
resolver,
self.get_manager(),
)
class DjangoConnectionField(ConnectionField): class DjangoConnectionField(ConnectionField):
@ -87,8 +40,7 @@ class DjangoConnectionField(ConnectionField):
"enforce_first_or_last", "enforce_first_or_last",
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
) )
kwargs.setdefault("offset", Int()) super(DjangoConnectionField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
@property @property
def type(self): def type(self):
@ -133,57 +85,40 @@ class DjangoConnectionField(ConnectionField):
@classmethod @classmethod
def resolve_queryset(cls, connection, queryset, info, args): def resolve_queryset(cls, connection, queryset, info, args):
# queryset is the resolved iterable from ObjectType
return connection._meta.node.get_queryset(queryset, info) return connection._meta.node.get_queryset(queryset, info)
@classmethod @classmethod
def resolve_connection(cls, connection, args, iterable, max_limit=None): def merge_querysets(cls, default_queryset, queryset):
# Remove the offset parameter and convert it to an after cursor. if default_queryset.query.distinct and not queryset.query.distinct:
offset = args.pop("offset", None) queryset = queryset.distinct()
after = args.get("after") elif queryset.query.distinct and not default_queryset.query.distinct:
if offset: default_queryset = default_queryset.distinct()
if after: return queryset & default_queryset
offset += cursor_to_offset(after) + 1
# input offset starts at 1 while the graphene offset starts at 0
args["after"] = offset_to_cursor(offset - 1)
@classmethod
def resolve_connection(cls, connection, default_manager, args, iterable):
if iterable is None:
iterable = default_manager
iterable = maybe_queryset(iterable) iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet): if isinstance(iterable, QuerySet):
array_length = iterable.count() if iterable.model.objects is not default_manager:
default_queryset = maybe_queryset(default_manager)
iterable = cls.merge_querysets(default_queryset, iterable)
_len = iterable.count()
else: else:
array_length = len(iterable) _len = len(iterable)
connection = connection_from_list_slice(
# If after is higher than array_length, connection_from_array_slice iterable,
# would try to do a negative slicing which makes django throw an
# AssertionError
slice_start = min(
get_offset_with_default(args.get("after"), -1) + 1,
array_length,
)
array_slice_length = array_length - slice_start
# Impose the maximum limit via the `first` field if neither first or last are already provided
# (note that if any of them is provided they must be under max_limit otherwise an error is raised).
if (
max_limit is not None
and args.get("first", None) is None
and args.get("last", None) is None
):
args["first"] = max_limit
connection = connection_from_array_slice(
iterable[slice_start:],
args, args,
slice_start=slice_start, slice_start=0,
array_length=array_length, list_length=_len,
array_slice_length=array_slice_length, list_slice_length=_len,
connection_type=partial(connection_adapter, connection), connection_type=connection,
edge_type=connection.Edge, edge_type=connection.Edge,
page_info_type=page_info_adapter, pageinfo_type=PageInfo,
) )
connection.iterable = iterable connection.iterable = iterable
connection.length = array_length connection.length = _len
return connection return connection
@classmethod @classmethod
@ -192,17 +127,14 @@ 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,
info, info,
**args, **args
): ):
first = args.get("first") first = args.get("first")
last = args.get("last") last = args.get("last")
offset = args.get("offset")
before = args.get("before")
if enforce_first_or_last: if enforce_first_or_last:
assert first or last, ( assert first or last, (
@ -222,38 +154,21 @@ 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)
if offset is not None:
assert before is None, (
"You can't provide a `before` value at the same time as an `offset` value to properly paginate the `{}` connection."
).format(info.field_name)
# 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)
if iterable is None: queryset = cls.resolve_queryset(connection, default_manager, info, args)
iterable = default_manager on_resolve = partial(cls.resolve_connection, connection, queryset, args)
# 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, max_limit=max_limit
)
if Promise.is_thenable(iterable): if Promise.is_thenable(iterable):
return Promise.resolve(iterable).then(on_resolve) return Promise.resolve(iterable).then(on_resolve)
return on_resolve(iterable) return on_resolve(iterable)
def wrap_resolve(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial( return partial(
self.connection_resolver, self.connection_resolver,
self.resolver or parent_resolver, parent_resolver,
self.connection_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

View File

@ -1,5 +1,4 @@
import warnings import warnings
from ..utils import DJANGO_FILTER_INSTALLED from ..utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED: if not DJANGO_FILTER_INSTALLED:
@ -10,21 +9,10 @@ if not DJANGO_FILTER_INSTALLED:
) )
else: else:
from .fields import DjangoFilterConnectionField from .fields import DjangoFilterConnectionField
from .filters import ( from .filterset import GlobalIDFilter, GlobalIDMultipleChoiceFilter
ArrayFilter,
GlobalIDFilter,
GlobalIDMultipleChoiceFilter,
ListFilter,
RangeFilter,
TypedFilter,
)
__all__ = [ __all__ = [
"DjangoFilterConnectionField", "DjangoFilterConnectionField",
"GlobalIDFilter", "GlobalIDFilter",
"GlobalIDMultipleChoiceFilter", "GlobalIDMultipleChoiceFilter",
"ArrayFilter",
"ListFilter",
"RangeFilter",
"TypedFilter",
] ]

View File

@ -1,50 +1,28 @@
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
from django.core.exceptions import ValidationError
from graphene.types.argument import to_arguments from graphene.types.argument import to_arguments
from graphene.types.enum import EnumType
from graphene.utils.str_converters import to_snake_case
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from .utils import get_filtering_args_from_filterset, get_filterset_class from .utils import get_filtering_args_from_filterset, get_filterset_class
def convert_enum(data):
"""
Check if the data is a enum option (or potentially nested list of enum option)
and convert it to its value.
This method is used to pre-process the data for the filters as they can take an
graphene.Enum as argument, but filters (from django_filters) expect a simple value.
"""
if isinstance(data, list):
return [convert_enum(item) for item in data]
if isinstance(type(data), EnumType):
return data.value
else:
return data
class DjangoFilterConnectionField(DjangoConnectionField): class DjangoFilterConnectionField(DjangoConnectionField):
def __init__( def __init__(
self, self,
type_, type,
fields=None, fields=None,
order_by=None, order_by=None,
extra_filter_meta=None, extra_filter_meta=None,
filterset_class=None, filterset_class=None,
*args, *args,
**kwargs, **kwargs
): ):
self._fields = fields self._fields = fields
self._provided_filterset_class = filterset_class self._provided_filterset_class = filterset_class
self._filterset_class = None self._filterset_class = None
self._filtering_args = None
self._extra_filter_meta = extra_filter_meta self._extra_filter_meta = extra_filter_meta
self._base_args = None self._base_args = None
super().__init__(type_, *args, **kwargs) super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
@property @property
def args(self): def args(self):
@ -58,12 +36,12 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def filterset_class(self): def filterset_class(self):
if not self._filterset_class: if not self._filterset_class:
fields = self._fields or self.node_type._meta.filter_fields fields = self._fields or self.node_type._meta.filter_fields
meta = {"model": self.model, "fields": fields} meta = dict(model=self.model, fields=fields)
if self._extra_filter_meta: if self._extra_filter_meta:
meta.update(self._extra_filter_meta) meta.update(self._extra_filter_meta)
filterset_class = ( filterset_class = self._provided_filterset_class or (
self._provided_filterset_class or self.node_type._meta.filterset_class self.node_type._meta.filterset_class
) )
self._filterset_class = get_filterset_class(filterset_class, **meta) self._filterset_class = get_filterset_class(filterset_class, **meta)
@ -71,37 +49,72 @@ class DjangoFilterConnectionField(DjangoConnectionField):
@property @property
def filtering_args(self): def filtering_args(self):
if not self._filtering_args: return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
self._filtering_args = get_filtering_args_from_filterset(
self.filterset_class, self.node_type
)
return self._filtering_args
@classmethod @classmethod
def resolve_queryset( def merge_querysets(cls, default_queryset, queryset):
cls, connection, iterable, info, args, filtering_args, filterset_class # There could be the case where the default queryset (returned from the filterclass)
# 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
): ):
def filter_kwargs(): filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
kwargs = {} qs = filterset_class(
for k, v in args.items(): data=filter_kwargs,
if k in filtering_args: queryset=default_manager.get_queryset(),
if k == "order_by" and v is not None: request=info.context,
v = to_snake_case(v) ).qs
kwargs[k] = convert_enum(v)
return kwargs
qs = super().resolve_queryset(connection, iterable, info, args) return super(DjangoFilterConnectionField, cls).connection_resolver(
resolver,
filterset = filterset_class( connection,
data=filter_kwargs(), queryset=qs, request=info.context qs,
max_limit,
enforce_first_or_last,
root,
info,
**args
) )
if filterset.is_valid():
return filterset.qs
raise ValidationError(filterset.form.errors.as_json())
def get_queryset_resolver(self): def get_resolver(self, parent_resolver):
return partial( return partial(
self.resolve_queryset, self.connection_resolver,
filterset_class=self.filterset_class, parent_resolver,
filtering_args=self.filtering_args, self.type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,
self.filterset_class,
self.filtering_args,
) )

View File

@ -1,26 +0,0 @@
import warnings
from ...utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED:
warnings.warn(
"Use of django filtering requires the django-filter package "
"be installed. You can do so using `pip install django-filter`",
ImportWarning,
)
else:
from .array_filter import ArrayFilter
from .global_id_filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter
from .list_filter import ListFilter
from .range_filter import RangeFilter
from .typed_filter import TypedFilter
__all__ = [
"DjangoFilterConnectionField",
"GlobalIDFilter",
"GlobalIDMultipleChoiceFilter",
"ArrayFilter",
"ListFilter",
"RangeFilter",
"TypedFilter",
]

View File

@ -1,50 +0,0 @@
from django_filters.constants import EMPTY_VALUES
from django_filters.filters import FilterMethod
from .typed_filter import TypedFilter
class ArrayFilterMethod(FilterMethod):
def __call__(self, qs, value):
if value is None:
return qs
return self.method(qs, self.f.field_name, value)
class ArrayFilter(TypedFilter):
"""
Filter made for PostgreSQL ArrayField.
"""
@TypedFilter.method.setter
def method(self, value):
"""
Override method setter so that in case a custom `method` is provided
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
of the `FilterMethod` class) and instead use our ArrayFilterMethod that consider empty lists as values.
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
"""
TypedFilter.method.fset(self, value)
if value is not None:
self.filter = ArrayFilterMethod(self)
def filter(self, qs, value):
"""
Override the default filter class to check first whether the list is
empty or not.
This needs to be done as in this case we expect to get the filter applied with
an empty list since it's a valid value but django_filter consider an empty list
to be an empty input value (see `EMPTY_VALUES`) meaning that
the filter does not need to be applied (hence returning the original
queryset).
"""
if value in EMPTY_VALUES and value != []:
return qs
if self.distinct:
qs = qs.distinct()
lookup = f"{self.field_name}__{self.lookup_expr}"
qs = self.get_method(qs)(**{lookup: value})
return qs

View File

@ -1,27 +0,0 @@
from django_filters import Filter, MultipleChoiceFilter
from graphql_relay.node.node import from_global_id
from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField
class GlobalIDFilter(Filter):
"""
Filter for Relay global ID.
"""
field_class = GlobalIDFormField
def filter(self, qs, value):
"""Convert the filter value to a primary key before filtering"""
_id = None
if value is not None:
_, _id = from_global_id(value)
return super().filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
field_class = GlobalIDMultipleChoiceField
def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value]
return super().filter(qs, gids)

View File

@ -1,50 +0,0 @@
from django_filters.filters import FilterMethod
from .typed_filter import TypedFilter
class ListFilterMethod(FilterMethod):
def __call__(self, qs, value):
if value is None:
return qs
return self.method(qs, self.f.field_name, value)
class ListFilter(TypedFilter):
"""
Filter that takes a list of value as input.
It is for example used for `__in` filters.
"""
@TypedFilter.method.setter
def method(self, value):
"""
Override method setter so that in case a custom `method` is provided
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
of the `FilterMethod` class) and instead use our ListFilterMethod that consider empty lists as values.
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
"""
TypedFilter.method.fset(self, value)
if value is not None:
self.filter = ListFilterMethod(self)
def filter(self, qs, value):
"""
Override the default filter class to check first whether the list is
empty or not.
This needs to be done as in this case we expect to get an empty output
(if not an exclude filter) but django_filter consider an empty list
to be an empty input value (see `EMPTY_VALUES`) meaning that
the filter does not need to be applied (hence returning the original
queryset).
"""
if value is not None and len(value) == 0:
if self.exclude:
return qs
else:
return qs.none()
else:
return super().filter(qs, value)

View File

@ -1,24 +0,0 @@
from django.core.exceptions import ValidationError
from django.forms import Field
from .typed_filter import TypedFilter
def validate_range(value):
"""
Validator for range filter input: the list of value must be of length 2.
Note that validators are only run if the value is not empty.
"""
if len(value) != 2:
raise ValidationError(
"Invalid range specified: it needs to contain 2 values.", code="invalid"
)
class RangeField(Field):
default_validators = [validate_range]
empty_values = [None]
class RangeFilter(TypedFilter):
field_class = RangeField

View File

@ -1,27 +0,0 @@
from django_filters import Filter
from graphene.types.utils import get_type
class TypedFilter(Filter):
"""
Filter class for which the input GraphQL type can explicitly be provided.
If it is not provided, when building the schema, it will try to guess
it from the field.
"""
def __init__(self, input_type=None, *args, **kwargs):
self._input_type = input_type
super().__init__(*args, **kwargs)
@property
def input_type(self):
input_type = get_type(self._input_type)
if input_type is not None:
if not callable(getattr(input_type, "get_type", None)):
raise ValueError(
"Wrong `input_type` for {}: it only accepts graphene types, got {}".format(
self.__class__.__name__, input_type
)
)
return input_type

View File

@ -1,13 +1,33 @@
import itertools import itertools
from django.db import models from django.db import models
from django_filters.filterset import ( from django_filters import Filter, MultipleChoiceFilter, VERSION
FILTER_FOR_DBFIELD_DEFAULTS, from django_filters.filterset import BaseFilterSet, FilterSet
BaseFilterSet, from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
FilterSet,
) from graphql_relay.node.node import from_global_id
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
class GlobalIDFilter(Filter):
field_class = GlobalIDFormField
def filter(self, qs, value):
""" Convert the filter value to a primary key before filtering """
_id = None
if value is not None:
_, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
field_class = GlobalIDMultipleChoiceField
def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value]
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter
GRAPHENE_FILTER_SET_OVERRIDES = { GRAPHENE_FILTER_SET_OVERRIDES = {
models.AutoField: {"filter_class": GlobalIDFilter}, models.AutoField: {"filter_class": GlobalIDFilter},
@ -20,8 +40,8 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
class GrapheneFilterSetMixin(BaseFilterSet): class GrapheneFilterSetMixin(BaseFilterSet):
"""A django_filters.filterset.BaseFilterSet with default filter overrides """ A django_filters.filterset.BaseFilterSet with default filter overrides
to handle global IDs""" to handle global IDs """
FILTER_DEFAULTS = dict( FILTER_DEFAULTS = dict(
itertools.chain( itertools.chain(
@ -30,19 +50,51 @@ class GrapheneFilterSetMixin(BaseFilterSet):
) )
# To support a Django 1.11 + Python 2.7 combination django-filter must be
# < 2.x.x. To support the earlier version of django-filter, the
# filter_for_reverse_field method must be present on GrapheneFilterSetMixin and
# must not be present for later versions of django-filter.
if VERSION[0] < 2:
from django.utils.text import capfirst
class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
@classmethod
def filter_for_reverse_field(cls, f, name):
"""Handles retrieving filters for reverse relationships
We override the default implementation so that we can handle
Global IDs (the default implementation expects database
primary keys)
"""
try:
rel = f.field.remote_field
except AttributeError:
rel = f.field.rel
default = {"name": name, "label": capfirst(rel.related_name)}
if rel.multiple:
# For to-many relationships
return GlobalIDMultipleChoiceFilter(**default)
else:
# For to-one relationships
return GlobalIDFilter(**default)
GrapheneFilterSetMixin = GrapheneFilterSetMixinPython2
def setup_filterset(filterset_class): def setup_filterset(filterset_class):
"""Wrap a provided filterset in Graphene-specific functionality""" """ Wrap a provided filterset in Graphene-specific functionality
"""
return type( return type(
f"Graphene{filterset_class.__name__}", "Graphene{}".format(filterset_class.__name__),
(filterset_class, GrapheneFilterSetMixin), (filterset_class, GrapheneFilterSetMixin),
{}, {},
) )
def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta):
"""Create a filterset for the given model using the provided meta data""" """ Create a filterset for the given model using the provided meta data
"""
meta.update({"model": model}) meta.update({"model": model})
meta_class = type("Meta", (object,), meta) meta_class = type(str("Meta"), (object,), meta)
filterset = type( filterset = type(
str("%sFilterSet" % model._meta.object_name), str("%sFilterSet" % model._meta.object_name),
(filterset_base_class, GrapheneFilterSetMixin), (filterset_base_class, GrapheneFilterSetMixin),

View File

@ -1,192 +0,0 @@
from functools import reduce
import pytest
from django.db import models
from django.db.models.query import QuerySet
from django_filters import FilterSet
import graphene
from graphene.relay import Node
from graphene_django import DjangoObjectType
from graphene_django.filter import ArrayFilter
from graphene_django.utils import DJANGO_FILTER_INSTALLED
from ...compat import ArrayField
pytestmark = []
if DJANGO_FILTER_INSTALLED:
from graphene_django.filter import DjangoFilterConnectionField
else:
pytestmark.append(
pytest.mark.skipif(
True, reason="django_filters not installed or not compatible"
)
)
class Event(models.Model):
name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50))
tag_ids = ArrayField(models.IntegerField())
random_field = ArrayField(models.BooleanField())
def __repr__(self):
return f"Event [{self.name}]"
@pytest.fixture
def EventFilterSet():
class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact", "contains"],
}
# Those are actually usable with our Query fixture below
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
tags__len = ArrayFilter(
field_name="tags", lookup_expr="len", input_type=graphene.Int
)
tags__len__in = ArrayFilter(
field_name="tags",
method="tags__len__in_filter",
input_type=graphene.List(graphene.Int),
)
# Those are actually not usable and only to check type declarations
tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains")
tags_ids__overlap = ArrayFilter(field_name="tag_ids", lookup_expr="overlap")
tags_ids = ArrayFilter(field_name="tag_ids", lookup_expr="exact")
random_field__contains = ArrayFilter(
field_name="random_field", lookup_expr="contains"
)
random_field__overlap = ArrayFilter(
field_name="random_field", lookup_expr="overlap"
)
random_field = ArrayFilter(field_name="random_field", lookup_expr="exact")
def tags__len__in_filter(self, queryset, _name, value):
if not value:
return queryset.none()
return reduce(
lambda q1, q2: q1.union(q2),
[queryset.filter(tags__len=v) for v in value],
).distinct()
return EventFilterSet
@pytest.fixture
def EventType(EventFilterSet):
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
fields = "__all__"
filterset_class = EventFilterSet
return EventType
@pytest.fixture
def Query(EventType):
"""
Note that we have to use a custom resolver to replicate the arrayfield filter behavior as
we are running unit tests in sqlite which does not have ArrayFields.
"""
events = [
Event(name="Live Show", tags=["concert", "music", "rock"]),
Event(name="Musical", tags=["movie", "music"]),
Event(name="Ballet", tags=["concert", "dance"]),
Event(name="Speech", tags=[]),
]
class Query(graphene.ObjectType):
events = DjangoFilterConnectionField(EventType)
def resolve_events(self, info, **kwargs):
class FakeQuerySet(QuerySet):
def __init__(self, model=None):
self.model = Event
self.__store = list(events)
def all(self):
return self
def filter(self, **kwargs):
queryset = FakeQuerySet()
queryset.__store = list(self.__store)
if "tags__contains" in kwargs:
queryset.__store = list(
filter(
lambda e: set(kwargs["tags__contains"]).issubset(
set(e.tags)
),
queryset.__store,
)
)
if "tags__overlap" in kwargs:
queryset.__store = list(
filter(
lambda e: not set(kwargs["tags__overlap"]).isdisjoint(
set(e.tags)
),
queryset.__store,
)
)
if "tags__exact" in kwargs:
queryset.__store = list(
filter(
lambda e: set(kwargs["tags__exact"]) == set(e.tags),
queryset.__store,
)
)
if "tags__len" in kwargs:
queryset.__store = list(
filter(
lambda e: len(e.tags) == kwargs["tags__len"],
queryset.__store,
)
)
return queryset
def union(self, *args):
queryset = FakeQuerySet()
queryset.__store = self.__store
for arg in args:
queryset.__store += arg.__store
return queryset
def none(self):
queryset = FakeQuerySet()
queryset.__store = []
return queryset
def count(self):
return len(self.__store)
def distinct(self):
queryset = FakeQuerySet()
queryset.__store = []
for event in self.__store:
if event not in queryset.__store:
queryset.__store.append(event)
queryset.__store = sorted(queryset.__store, key=lambda e: e.name)
return queryset
def __getitem__(self, index):
return self.__store[index]
return FakeQuerySet()
return Query
@pytest.fixture
def schema(Query):
return graphene.Schema(query=Query)

View File

@ -10,7 +10,7 @@ class ArticleFilter(django_filters.FilterSet):
fields = { fields = {
"headline": ["exact", "icontains"], "headline": ["exact", "icontains"],
"pub_date": ["gt", "lt", "exact"], "pub_date": ["gt", "lt", "exact"],
"reporter": ["exact", "in"], "reporter": ["exact"],
} }
order_by = OrderingFilter(fields=("pub_date",)) order_by = OrderingFilter(fields=("pub_date",))
@ -21,7 +21,7 @@ class ReporterFilter(django_filters.FilterSet):
model = Reporter model = Reporter
fields = ["first_name", "last_name", "email", "pets"] fields = ["first_name", "last_name", "email", "pets"]
order_by = OrderingFilter(fields=("first_name",)) order_by = OrderingFilter(fields=("pub_date",))
class PetFilter(django_filters.FilterSet): class PetFilter(django_filters.FilterSet):

View File

@ -1,79 +0,0 @@
import pytest
from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_multiple(schema):
"""
Test contains filter on a array field of string.
"""
query = """
query {
events (tags_Contains: ["concert", "music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
]
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_one(schema):
"""
Test contains filter on a array field of string.
"""
query = """
query {
events (tags_Contains: ["music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
{"node": {"name": "Musical"}},
]
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_empty_list(schema):
"""
Test contains filter on a array field of string.
"""
query = """
query {
events (tags_Contains: []) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Live Show"}},
{"node": {"name": "Musical"}},
{"node": {"name": "Ballet"}},
{"node": {"name": "Speech"}},
]

View File

@ -1,186 +0,0 @@
import pytest
from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_len_filter(schema):
query = """
query {
events (tags_Len: 2) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Musical"}},
{"node": {"name": "Ballet"}},
]
query = """
query {
events (tags_Len: 0) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Speech"}},
]
query = """
query {
events (tags_Len: 10) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
query = """
query {
events (tags_Len: "2") {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert len(result.errors) == 1
assert result.errors[0].message == 'Int cannot represent non-integer value: "2"'
query = """
query {
events (tags_Len: True) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert len(result.errors) == 1
assert result.errors[0].message == "Int cannot represent non-integer value: True"
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_custom_filter(schema):
query = """
query {
events (tags_Len_In: 2) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Ballet"}},
{"node": {"name": "Musical"}},
]
query = """
query {
events (tags_Len_In: [0, 2]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Ballet"}},
{"node": {"name": "Musical"}},
{"node": {"name": "Speech"}},
]
query = """
query {
events (tags_Len_In: [10]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
query = """
query {
events (tags_Len_In: []) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
query = """
query {
events (tags_Len_In: "12") {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert len(result.errors) == 1
assert result.errors[0].message == 'Int cannot represent non-integer value: "12"'
query = """
query {
events (tags_Len_In: True) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert len(result.errors) == 1
assert result.errors[0].message == "Int cannot represent non-integer value: True"

View File

@ -1,120 +0,0 @@
import pytest
from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_no_match(schema):
"""
Test exact filter on a array field of string.
"""
query = """
query {
events (tags: ["concert", "music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == []
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_match(schema):
"""
Test exact filter on a array field of string.
"""
query = """
query {
events (tags: ["movie", "music"]) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Musical"}},
]
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_empty_list(schema):
"""
Test exact filter on a array field of string.
"""
query = """
query {
events (tags: []) {
edges {
node {
name
}
}
}
}
"""
result = schema.execute(query)
assert not result.errors
assert result.data["events"]["edges"] == [
{"node": {"name": "Speech"}},
]
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_filter_schema_type(schema):
"""
Check that the type in the filter is an array field like on the object type.
"""
schema_str = str(schema)
assert (
'''type EventType implements Node {
"""The ID of the object"""
id: ID!
name: String!
tags: [String!]!
tagIds: [Int!]!
randomField: [Boolean!]!
}'''
in schema_str
)
filters = {
"offset": "Int",
"before": "String",
"after": "String",
"first": "Int",
"last": "Int",
"name": "String",
"name_Contains": "String",
"tags_Contains": "[String!]",
"tags_Overlap": "[String!]",
"tags": "[String!]",
"tags_Len": "Int",
"tags_Len_In": "[Int]",
"tagsIds_Contains": "[Int!]",
"tagsIds_Overlap": "[Int!]",
"tagsIds": "[Int!]",
"randomField_Contains": "[Boolean!]",
"randomField_Overlap": "[Boolean!]",
"randomField": "[Boolean!]",
}
filters_str = ", ".join(
[f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()]
)
assert (
f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str
)

Some files were not shown because too many files have changed in this diff Show More