mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-11 08:42:32 +03:00
Merge branch 'master' into use-djangos-testcase-client
This commit is contained in:
commit
6a0f082cb0
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
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)
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
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.
|
13
.github/stale.yml
vendored
13
.github/stale.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 90
|
daysUntilStale: false
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 14
|
daysUntilClose: false
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
|
@ -13,9 +13,10 @@ exemptLabels:
|
||||||
# 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: >
|
markComment: false
|
||||||
This issue has been automatically marked as stale because it has not had
|
# markComment: >
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
# This issue has been automatically marked as stale because it has not had
|
||||||
for your contributions.
|
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
# for your contributions.
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
26
.github/workflows/deploy.yml
vendored
Normal file
26
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: 🚀 Deploy to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.8
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
- 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@v1.1.0
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.pypi_password }}
|
22
.github/workflows/lint.yml
vendored
Normal file
22
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.8
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- name: Run lint 💅
|
||||||
|
run: tox
|
||||||
|
env:
|
||||||
|
TOXENV: flake8
|
31
.github/workflows/tests.yml
vendored
Normal file
31
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
django: ["1.11", "2.2", "3.0", "3.1"]
|
||||||
|
python-version: ["3.6", "3.7", "3.8"]
|
||||||
|
include:
|
||||||
|
- django: "1.11"
|
||||||
|
python-version: "2.7"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
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 }}
|
||||||
|
TOXENV: ${{ matrix.toxenv }}
|
79
.travis.yml
79
.travis.yml
|
@ -1,79 +0,0 @@
|
||||||
language: python
|
|
||||||
cache: pip
|
|
||||||
dist: xenial
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox tox-travis
|
|
||||||
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- pip install coveralls
|
|
||||||
- coveralls
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- name: deploy
|
|
||||||
if: tag IS present
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fast_finish: true
|
|
||||||
|
|
||||||
allow_failures:
|
|
||||||
- env: DJANGO=master
|
|
||||||
|
|
||||||
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=3.0
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.0
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.1
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=3.0
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
- python: 3.7
|
|
||||||
env: TOXENV=black,flake8
|
|
||||||
|
|
||||||
- stage: deploy
|
|
||||||
script: skip
|
|
||||||
python: 3.7
|
|
||||||
after_success: true
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: syrusakbary
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
password:
|
|
||||||
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
|
|
||||||
distributions: "sdist bdist_wheel"
|
|
|
@ -1,3 +1,6 @@
|
||||||
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
|
21
README.md
21
README.md
|
@ -1,26 +1,24 @@
|
||||||
Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md) to learn how to upgrade to Graphene `2.0`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#  Graphene-Django
|
#  Graphene-Django
|
||||||
|
|
||||||
|
|
||||||
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
||||||
|
|
||||||
[![travis][travis-image]][travis-url]
|
[![build][build-image]][build-url]
|
||||||
[![pypi][pypi-image]][pypi-url]
|
[![pypi][pypi-image]][pypi-url]
|
||||||
[![Anaconda-Server Badge][conda-image]][conda-url]
|
[![Anaconda-Server Badge][conda-image]][conda-url]
|
||||||
[![coveralls][coveralls-image]][coveralls-url]
|
[![coveralls][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
[travis-image]: https://travis-ci.org/graphql-python/graphene-django.svg?style=flat
|
[build-image]: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg
|
||||||
[travis-url]: https://travis-ci.org/graphql-python/graphene-django
|
[build-url]: https://github.com/graphql-python/graphene-django/actions
|
||||||
[pypi-image]: https://img.shields.io/pypi/v/graphene-django.svg?style=flat
|
[pypi-image]: https://img.shields.io/pypi/v/graphene-django.svg?style=flat
|
||||||
[pypi-url]: https://pypi.org/project/graphene-django/
|
[pypi-url]: https://pypi.org/project/graphene-django/
|
||||||
[coveralls-image]: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github
|
[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
|
[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-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg
|
||||||
[conda-url]: https://anaconda.org/conda-forge/graphene-django
|
[conda-url]: https://anaconda.org/conda-forge/graphene-django
|
||||||
|
|
||||||
|
[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/)
|
[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/)
|
||||||
|
@ -92,7 +90,7 @@ class Query(graphene.ObjectType):
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can simply query the schema:
|
Then you can query the schema:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
query = '''
|
query = '''
|
||||||
|
@ -112,6 +110,11 @@ To learn more check out the following [examples](examples/):
|
||||||
* **Relay Schema**: [Starwars Relay example](examples/starwars)
|
* **Relay Schema**: [Starwars Relay example](examples/starwars)
|
||||||
|
|
||||||
|
|
||||||
|
## GraphQL testing clients
|
||||||
|
- [Firecamp](https://firecamp.io/graphql)
|
||||||
|
- [GraphiQL](https://github.com/graphql/graphiql)
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
|
@ -114,8 +114,8 @@ Contributing
|
||||||
See `CONTRIBUTING.md <CONTRIBUTING.md>`__.
|
See `CONTRIBUTING.md <CONTRIBUTING.md>`__.
|
||||||
|
|
||||||
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
||||||
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
|
.. |Build Status| image:: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg
|
||||||
:target: https://travis-ci.org/graphql-python/graphene-django
|
:target: https://github.com/graphql-python/graphene-django/actions
|
||||||
.. |PyPI version| image:: https://badge.fury.io/py/graphene-django.svg
|
.. |PyPI version| image:: https://badge.fury.io/py/graphene-django.svg
|
||||||
:target: https://badge.fury.io/py/graphene-django
|
: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
|
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
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'
|
|
|
@ -166,7 +166,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
|
||||||
|
|
||||||
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
||||||
|
|
||||||
For Django 1.9 and below:
|
For Django 1.11:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
|
12
docs/extra-types.rst
Normal file
12
docs/extra-types.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
83
docs/fields.rst
Normal file
83
docs/fields.rst
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
*TODO*
|
|
@ -1,7 +1,7 @@
|
||||||
Filtering
|
Filtering
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Graphene integrates with
|
Graphene-Django integrates with
|
||||||
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
|
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
|
||||||
Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
|
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>`__
|
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
|
||||||
|
@ -14,8 +14,17 @@ You will need to install it manually, which can be done as follows:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
# You'll need to django-filter
|
# You'll need to install django-filter
|
||||||
pip install django-filter>=2
|
pip install django-filter>=2
|
||||||
|
|
||||||
|
After installing ``django-filter`` you'll need to add the application in the ``settings.py`` file:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# ...
|
||||||
|
"django_filters",
|
||||||
|
]
|
||||||
|
|
||||||
Note: The techniques below are demoed in the `cookbook example
|
Note: The techniques below are demoed in the `cookbook example
|
||||||
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
|
||||||
|
@ -192,7 +201,7 @@ Extend the tuple of fields if you want to order by more than one field.
|
||||||
|
|
||||||
order_by = OrderingFilter(
|
order_by = OrderingFilter(
|
||||||
fields=(
|
fields=(
|
||||||
('created_at', 'created_at'),
|
('name', 'created_at'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 tenants
|
Core tenets
|
||||||
------------
|
-----------
|
||||||
|
|
||||||
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,7 +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
|
||||||
|
|
|
@ -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.X
|
* >= Django 1.11
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
@ -25,13 +25,27 @@ 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 1.11:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# ...
|
||||||
|
url(r"graphql", GraphQLView.as_view(graphiql=True)),
|
||||||
|
]
|
||||||
|
|
||||||
|
For Django 2.0 and above:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
@ -49,7 +63,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.
|
||||||
|
@ -61,7 +75,7 @@ The most basic ``schema.py`` looks like this:
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
pass
|
hello = graphene.String(default_value="Hi!")
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
@ -71,7 +85,7 @@ To learn how to extend the schema object for your project, read the basic tutori
|
||||||
CSRF exempt
|
CSRF exempt
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
If have enabled `CSRF protection <https://docs.djangoproject.com/en/3.0/ref/csrf/>`_ in your Django app
|
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
|
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``
|
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:
|
decorator:
|
||||||
|
|
|
@ -29,6 +29,20 @@ 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
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ 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)
|
||||||
|
|
||||||
def mutate(self, info, text, id):
|
@classmethod
|
||||||
|
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()
|
||||||
|
|
155
docs/queries.rst
155
docs/queries.rst
|
@ -1,3 +1,5 @@
|
||||||
|
.. _queries-objecttypes:
|
||||||
|
|
||||||
Queries & ObjectTypes
|
Queries & ObjectTypes
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
@ -18,27 +20,26 @@ 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 = graphene.Field(QuestionType, question_id=graphene.String())
|
question_by_id = graphene.Field(QuestionType, id=graphene.String())
|
||||||
|
|
||||||
def resolve_questions(self, info, **kwargs):
|
def resolve_questions(root, info, **kwargs):
|
||||||
# Querying a list
|
# Querying a list
|
||||||
return Question.objects.all()
|
return Question.objects.all()
|
||||||
|
|
||||||
def resolve_question(self, info, question_id):
|
def resolve_question_by_id(root, info, id):
|
||||||
# Querying a single question
|
# Querying a single question
|
||||||
return Question.objects.get(pk=question_id)
|
return Question.objects.get(pk=id)
|
||||||
|
|
||||||
|
|
||||||
Specifying which fields to include
|
Specifying which fields to include
|
||||||
|
@ -58,21 +59,27 @@ 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``
|
||||||
|
@ -82,10 +89,13 @@ 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
|
||||||
|
@ -95,16 +105,19 @@ 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
|
||||||
|
@ -119,12 +132,19 @@ For example the following ``Model`` and ``DjangoObjectType``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class PetModel(models.Model):
|
from django.db import models
|
||||||
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
class PetModel(models.Model):
|
||||||
class Meta:
|
kind = models.CharField(
|
||||||
model = PetModel
|
max_length=100,
|
||||||
|
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:
|
||||||
|
|
||||||
|
@ -146,27 +166,35 @@ You can disable this automatic conversion by setting
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
from graphene_django import DjangoObjectType
|
||||||
class Meta:
|
from .models import PetModel
|
||||||
model = PetModel
|
|
||||||
convert_choices_to_enum = False
|
class Pet(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ("id", "kind",)
|
||||||
|
convert_choices_to_enum = False
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
type Pet {
|
type Pet {
|
||||||
id: ID!
|
id: ID!
|
||||||
kind: String!
|
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
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
from graphene_django import DjangoObjectType
|
||||||
class Meta:
|
from .models import PetModel
|
||||||
model = PetModel
|
|
||||||
convert_choices_to_enum = ['kind']
|
class Pet(DjangoObjectType):
|
||||||
|
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``.
|
||||||
|
@ -179,6 +207,8 @@ 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)
|
||||||
|
|
||||||
|
@ -190,10 +220,13 @@ 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
|
||||||
|
@ -201,9 +234,15 @@ 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
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -216,7 +255,6 @@ 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
|
||||||
|
@ -236,18 +274,22 @@ This resolve method should follow this format:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
def resolve_foo(self, info, **kwargs):
|
def resolve_foo(parent, 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
|
||||||
|
|
||||||
class Query:
|
import graphene
|
||||||
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
foo = graphene.List(QuestionType)
|
foo = graphene.List(QuestionType)
|
||||||
|
|
||||||
def resolve_foo(self, info, **kwargs):
|
def resolve_foo(root, info):
|
||||||
id = kwargs.get('id')
|
id = kwargs.get("id")
|
||||||
return QuestionModel.objects.get(id)
|
return Question.objects.get(id)
|
||||||
|
|
||||||
Arguments
|
Arguments
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -256,10 +298,18 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Query:
|
import graphene
|
||||||
question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int())
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
def resolve_question(self, info, foo, bar):
|
class Query(graphene.ObjectType):
|
||||||
|
question = graphene.Field(
|
||||||
|
QuestionType,
|
||||||
|
foo=graphene.String(),
|
||||||
|
bar=graphene.Int()
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_question(root, info, foo, bar):
|
||||||
# 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()
|
||||||
|
|
||||||
|
@ -274,7 +324,15 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
def resolve_questions(self, info, **kwargs):
|
import graphene
|
||||||
|
|
||||||
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
questions = graphene.List(QuestionType)
|
||||||
|
|
||||||
|
def resolve_questions(root, info):
|
||||||
# See if a user is authenticated
|
# See if a user is authenticated
|
||||||
if info.context.user.is_authenticated():
|
if info.context.user.is_authenticated():
|
||||||
return Question.objects.all()
|
return Question.objects.all()
|
||||||
|
@ -301,15 +359,13 @@ 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(self, info, question_id):
|
def resolve_question(root, 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
|
||||||
|
@ -339,25 +395,22 @@ 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,)
|
interfaces = (relay.Node,) # make sure you add this
|
||||||
|
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:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,3 +140,70 @@ Default: ``False``
|
||||||
# 'messages': ['This field is required.'],
|
# 'messages': ['This field is required.'],
|
||||||
# }
|
# }
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING``
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Set to ``True`` to use the new naming format for the auto generated Enum types from Django choice fields. The new format looks like this: ``{app_label}{object_name}{field_name}Choices``
|
||||||
|
|
||||||
|
Default: ``False``
|
||||||
|
|
||||||
|
|
||||||
|
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
|
||||||
|
|
||||||
|
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V3_NAMING`` setting is ignored.
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# myapp.utils
|
||||||
|
def enum_naming(field):
|
||||||
|
if isinstance(field.model, User):
|
||||||
|
return f"CustomUserEnum{field.name.title()}"
|
||||||
|
return f"CustomEnum{field.name.title()}"
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
``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_.
|
||||||
|
|
||||||
|
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||||
|
|
||||||
|
|
||||||
|
Default: ``True``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
|
||||||
|
}
|
||||||
|
|
42
docs/subscriptions.rst
Normal file
42
docs/subscriptions.rst
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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, you’ll 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.
|
|
@ -1,8 +1,13 @@
|
||||||
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`.
|
||||||
|
|
||||||
|
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -10,12 +15,8 @@ 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(
|
||||||
'''
|
'''
|
||||||
|
@ -80,3 +81,39 @@ Usage:
|
||||||
|
|
||||||
# Add some more asserts if you like
|
# 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''',
|
||||||
|
op_name='myModel'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = json.loads(response.content)
|
||||||
|
assert 'errors' not in content
|
||||||
|
|
|
@ -3,15 +3,11 @@ 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``
|
||||||
|
@ -28,13 +24,12 @@ 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
|
pip install django graphene_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.py startproject cookbook . # Note the trailing '.' character
|
django-admin startproject cookbook . # Note the trailing '.' character
|
||||||
cd cookbook
|
cd cookbook
|
||||||
django-admin.py startapp ingredients
|
django-admin startapp ingredients
|
||||||
|
|
||||||
Now sync your database for the first time:
|
Now sync your database for the first time:
|
||||||
|
|
||||||
|
@ -54,19 +49,18 @@ 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
|
||||||
|
@ -75,10 +69,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,13 +98,13 @@ 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 (``./manage.py createsuperuser``).
|
create a login for yourself too (``python manage.py createsuperuser``).
|
||||||
|
|
||||||
Register models with admin panel:
|
Register models with admin panel:
|
||||||
|
|
||||||
|
@ -138,66 +134,48 @@ 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.
|
||||||
|
|
||||||
This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType``
|
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.
|
||||||
|
|
||||||
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/ingredients/schema.py`` and type the following:
|
Create ``cookbook/schema.py`` and type the following:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
# cookbook/ingredients/schema.py
|
# cookbook/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_categories(self, info, **kwargs):
|
def resolve_all_ingredients(root, info):
|
||||||
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):
|
||||||
Note that the above ``Query`` class is a mixin, inheriting from
|
try:
|
||||||
``object``. This is because we will now create a project-level query
|
return Category.objects.get(name=name)
|
||||||
class which will combine all our app-level mixins.
|
except Category.DoesNotExist:
|
||||||
|
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 (although it currently lacks any namespacing).
|
file.
|
||||||
|
|
||||||
Testing everything so far
|
Testing everything so far
|
||||||
-------------------------
|
-------------------------
|
||||||
|
@ -216,18 +194,21 @@ Add ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/settings.py
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
# This will also make the `graphql_schema` management command available
|
"graphene_django",
|
||||||
'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,
|
||||||
|
@ -245,14 +226,17 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
# cookbook/urls.py
|
||||||
|
|
||||||
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 = [
|
||||||
url(r'^admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -261,16 +245,19 @@ as explained above, we can do so here using:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
# cookbook/urls.py
|
||||||
|
|
||||||
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 = [
|
||||||
url(r'^admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -283,10 +270,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 1.9, using settings 'cookbook.settings'
|
Django version 3.0.7, 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,24 +316,25 @@ If you are using the provided fixtures, you will see the following response:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
You can experiment with ``allCategories`` too.
|
|
||||||
|
|
||||||
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.
|
Congratulations, you have created a working GraphQL server 🥳!
|
||||||
|
|
||||||
|
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
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful!
|
Using the current schema we can query for relations too. This is where GraphQL becomes really powerful!
|
||||||
|
|
||||||
For example, we may want to list all categories and in each category, all ingredients that are in that category.
|
For example, we may want to get a specific categories and list 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 {
|
||||||
allCategories {
|
categoryByName(name: "Dairy") {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
ingredients {
|
ingredients {
|
||||||
|
@ -356,43 +344,26 @@ 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": {
|
||||||
"allCategories": [
|
"categoryByName": {
|
||||||
{
|
"id": "1",
|
||||||
"id": "1",
|
"name": "Dairy",
|
||||||
"name": "Dairy",
|
"ingredients": [
|
||||||
"ingredients": [
|
{
|
||||||
{
|
"id": "1",
|
||||||
"id": "1",
|
"name": "Eggs"
|
||||||
"name": "Eggs"
|
},
|
||||||
},
|
{
|
||||||
{
|
"id": "2",
|
||||||
"id": "2",
|
"name": "Milk"
|
||||||
"name": "Milk"
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Meat",
|
|
||||||
"ingredients": [
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "Beef"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "Chicken"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,71 +382,12 @@ 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.
|
|
||||||
Add the **Highlighted** lines to ``cookbook/ingredients/schema.py``
|
|
||||||
|
|
||||||
.. literalinclude:: schema.py
|
|
||||||
:emphasize-lines: 19-21,25-27,36-58
|
|
||||||
|
|
||||||
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 but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``.
|
As you can see, GraphQL is very powerful and integrating Django models allows you to get started with a working server quickly.
|
||||||
|
|
||||||
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.**
|
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the :ref:`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 but it is not essential to understand and use Graphene-Django in your project.
|
documentation so that you are familiar with it as well.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _Relay tutorial:
|
||||||
|
|
||||||
Relay tutorial
|
Relay tutorial
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
@ -277,7 +279,7 @@ from the command line.
|
||||||
$ python ./manage.py runserver
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.9, using settings 'cookbook.settings'
|
Django version 1.11, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
|
0
examples/__init__.py
Normal file
0
examples/__init__.py
Normal file
0
examples/cookbook-plain/__init__.py
Normal file
0
examples/cookbook-plain/__init__.py
Normal file
|
@ -1,4 +1,4 @@
|
||||||
graphene>=2.1,<3
|
graphene>=2.1,<3
|
||||||
graphene-django>=2.1,<3
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1,<3
|
graphql-core>=2.1,<3
|
||||||
django==3.0.3
|
django==3.0.7
|
||||||
|
|
0
examples/cookbook/__init__.py
Normal file
0
examples/cookbook/__init__.py
Normal file
|
@ -41,6 +41,7 @@ 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 = [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
graphene>=2.1,<3
|
graphene>=2.1,<3
|
||||||
graphene-django>=2.1,<3
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1,<3
|
graphql-core>=2.1,<3
|
||||||
django==3.0.3
|
django==3.0.7
|
||||||
django-filter>=2
|
django-filter>=2
|
||||||
|
|
30
examples/django_test_settings.py
Normal file
30
examples/django_test_settings.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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",
|
||||||
|
"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"
|
|
@ -1,6 +1,11 @@
|
||||||
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
from .fields import DjangoConnectionField
|
|
||||||
|
|
||||||
__version__ = "2.8.2"
|
__version__ = "2.13.0"
|
||||||
|
|
||||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"DjangoObjectType",
|
||||||
|
"DjangoListField",
|
||||||
|
"DjangoConnectionField",
|
||||||
|
]
|
||||||
|
|
|
@ -8,8 +8,14 @@ try:
|
||||||
from django.contrib.postgres.fields import (
|
from django.contrib.postgres.fields import (
|
||||||
ArrayField,
|
ArrayField,
|
||||||
HStoreField,
|
HStoreField,
|
||||||
JSONField,
|
JSONField as PGJSONField,
|
||||||
RangeField,
|
RangeField,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
|
ArrayField, HStoreField, PGJSONField, RangeField = (MissingType,) * 4
|
||||||
|
|
||||||
|
try:
|
||||||
|
# JSONField is only available from Django 3.1
|
||||||
|
from django.db.models import JSONField
|
||||||
|
except ImportError:
|
||||||
|
JSONField = MissingType
|
||||||
|
|
18
graphene_django/conftest.py
Normal file
18
graphene_django/conftest.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
|
@ -1,6 +1,7 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from graphene import (
|
from graphene import (
|
||||||
ID,
|
ID,
|
||||||
|
@ -19,12 +20,14 @@ from graphene import (
|
||||||
Time,
|
Time,
|
||||||
)
|
)
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
from graphene.utils.str_converters import to_camel_case, to_const
|
from graphene.utils.str_converters import to_camel_case
|
||||||
from graphql import assert_valid_name
|
from graphql import assert_valid_name
|
||||||
|
|
||||||
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
from .settings import graphene_settings
|
||||||
|
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
|
||||||
from .fields import DjangoListField, DjangoConnectionField
|
from .fields import DjangoListField, DjangoConnectionField
|
||||||
from .utils import import_single_dispatch
|
from .utils import import_single_dispatch
|
||||||
|
from .utils.str_converters import to_const
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
singledispatch = import_single_dispatch()
|
||||||
|
|
||||||
|
@ -68,6 +71,31 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
|
||||||
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_enum_name(django_model_meta, field):
|
||||||
|
if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
|
||||||
|
# Try and import custom function
|
||||||
|
custom_func = import_string(
|
||||||
|
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
|
||||||
|
)
|
||||||
|
name = custom_func(field)
|
||||||
|
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING is True:
|
||||||
|
name = "{app_label}{object_name}{field_name}Choices".format(
|
||||||
|
app_label=to_camel_case(django_model_meta.app_label.title()),
|
||||||
|
object_name=django_model_meta.object_name,
|
||||||
|
field_name=to_camel_case(field.name.title()),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def convert_choice_field_to_enum(field, name=None):
|
||||||
|
if name is None:
|
||||||
|
name = generate_enum_name(field.model._meta, field)
|
||||||
|
choices = field.choices
|
||||||
|
return convert_choices_to_named_enum_with_descriptions(name, choices)
|
||||||
|
|
||||||
|
|
||||||
def convert_django_field_with_choices(
|
def convert_django_field_with_choices(
|
||||||
field, registry=None, convert_choices_to_enum=True
|
field, registry=None, convert_choices_to_enum=True
|
||||||
):
|
):
|
||||||
|
@ -77,9 +105,7 @@ def convert_django_field_with_choices(
|
||||||
return converted
|
return converted
|
||||||
choices = getattr(field, "choices", None)
|
choices = getattr(field, "choices", None)
|
||||||
if choices and convert_choices_to_enum:
|
if choices and convert_choices_to_enum:
|
||||||
meta = field.model._meta
|
enum = convert_choice_field_to_enum(field)
|
||||||
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
|
|
||||||
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
|
|
||||||
required = not (field.blank or field.null)
|
required = not (field.blank or field.null)
|
||||||
converted = enum(description=field.help_text, required=required)
|
converted = enum(description=field.help_text, required=required)
|
||||||
else:
|
else:
|
||||||
|
@ -127,13 +153,9 @@ def convert_field_to_int(field, registry=None):
|
||||||
return Int(description=field.help_text, required=not field.null)
|
return Int(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.NullBooleanField)
|
||||||
@convert_django_field.register(models.BooleanField)
|
@convert_django_field.register(models.BooleanField)
|
||||||
def convert_field_to_boolean(field, registry=None):
|
def convert_field_to_boolean(field, registry=None):
|
||||||
return NonNull(Boolean, description=field.help_text)
|
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(models.NullBooleanField)
|
|
||||||
def convert_field_to_nullboolean(field, registry=None):
|
|
||||||
return Boolean(description=field.help_text, required=not field.null)
|
return Boolean(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,15 +256,20 @@ def convert_field_to_djangomodel(field, registry=None):
|
||||||
|
|
||||||
@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):
|
||||||
base_type = convert_django_field(field.base_field)
|
inner_type = convert_django_field(field.base_field)
|
||||||
if not isinstance(base_type, (List, NonNull)):
|
if not isinstance(inner_type, (List, NonNull)):
|
||||||
base_type = type(base_type)
|
inner_type = (
|
||||||
return List(base_type, description=field.help_text, required=not field.null)
|
NonNull(type(inner_type))
|
||||||
|
if inner_type.kwargs["required"]
|
||||||
|
else type(inner_type)
|
||||||
|
)
|
||||||
|
return List(inner_type, description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(HStoreField)
|
@convert_django_field.register(HStoreField)
|
||||||
|
@convert_django_field.register(PGJSONField)
|
||||||
@convert_django_field.register(JSONField)
|
@convert_django_field.register(JSONField)
|
||||||
def convert_postgres_field_to_string(field, registry=None):
|
def convert_pg_and_json_field_to_string(field, registry=None):
|
||||||
return JSONString(description=field.help_text, required=not field.null)
|
return JSONString(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
|
@ -250,5 +277,9 @@ def convert_postgres_field_to_string(field, registry=None):
|
||||||
def convert_postgres_range_to_string(field, registry=None):
|
def convert_postgres_range_to_string(field, registry=None):
|
||||||
inner_type = convert_django_field(field.base_field)
|
inner_type = convert_django_field(field.base_field)
|
||||||
if not isinstance(inner_type, (List, NonNull)):
|
if not isinstance(inner_type, (List, NonNull)):
|
||||||
inner_type = type(inner_type)
|
inner_type = (
|
||||||
|
NonNull(type(inner_type))
|
||||||
|
if inner_type.kwargs["required"]
|
||||||
|
else type(inner_type)
|
||||||
|
)
|
||||||
return List(inner_type, description=field.help_text, required=not field.null)
|
return List(inner_type, description=field.help_text, required=not field.null)
|
||||||
|
|
|
@ -148,10 +148,10 @@ class NormalCursorWrapper(object):
|
||||||
# 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=()):
|
def callproc(self, procname, params=None):
|
||||||
return self._record(self.cursor.callproc, procname, params)
|
return self._record(self.cursor.callproc, procname, params)
|
||||||
|
|
||||||
def execute(self, sql, params=()):
|
def execute(self, sql, params=None):
|
||||||
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):
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
import pytest
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
|
|
||||||
|
@ -13,11 +12,6 @@ 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()
|
||||||
|
@ -31,7 +25,7 @@ def test_should_query_field():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
reporter = graphene.Field(ReporterType)
|
reporter = graphene.Field(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||||
|
|
||||||
def resolve_reporter(self, info, **args):
|
def resolve_reporter(self, info, **args):
|
||||||
return Reporter.objects.first()
|
return Reporter.objects.first()
|
||||||
|
@ -41,7 +35,7 @@ def test_should_query_field():
|
||||||
reporter {
|
reporter {
|
||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
_debug {
|
__debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -50,7 +44,9 @@ def test_should_query_field():
|
||||||
"""
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
"reporter": {"lastName": "ABA"},
|
"reporter": {"lastName": "ABA"},
|
||||||
"_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]},
|
"__debug": {
|
||||||
|
"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
|
@ -60,7 +56,10 @@ def test_should_query_field():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_nested_field():
|
@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 = Reporter(last_name="ABA")
|
||||||
r1.save()
|
r1.save()
|
||||||
r2 = Reporter(last_name="Griffin")
|
r2 = Reporter(last_name="Griffin")
|
||||||
|
@ -140,7 +139,7 @@ def test_should_query_list():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = graphene.List(ReporterType)
|
all_reporters = graphene.List(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -150,7 +149,7 @@ def test_should_query_list():
|
||||||
allReporters {
|
allReporters {
|
||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
_debug {
|
__debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -159,7 +158,7 @@ def test_should_query_list():
|
||||||
"""
|
"""
|
||||||
expected = {
|
expected = {
|
||||||
"allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}],
|
"allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}],
|
||||||
"_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]},
|
"__debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]},
|
||||||
}
|
}
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
|
@ -169,7 +168,10 @@ def test_should_query_list():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_connection():
|
@pytest.mark.parametrize("max_limit", [None, 100])
|
||||||
|
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")
|
||||||
|
@ -182,7 +184,7 @@ def test_should_query_connection():
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -196,7 +198,7 @@ def test_should_query_connection():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_debug {
|
__debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -210,12 +212,16 @@ def test_should_query_connection():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["allReporters"] == expected["allReporters"]
|
assert result.data["allReporters"] == expected["allReporters"]
|
||||||
assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
|
assert len(result.data["__debug"]["sql"]) == 2
|
||||||
|
assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"]
|
||||||
query = str(Reporter.objects.all()[:1].query)
|
query = str(Reporter.objects.all()[:1].query)
|
||||||
assert result.data["_debug"]["sql"][1]["rawSql"] == query
|
assert result.data["__debug"]["sql"][1]["rawSql"] == query
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_connectionfilter():
|
@pytest.mark.parametrize("max_limit", [None, 100])
|
||||||
|
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")
|
||||||
|
@ -231,7 +237,7 @@ def test_should_query_connectionfilter():
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
|
all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"])
|
||||||
s = graphene.String(resolver=lambda *_: "S")
|
s = graphene.String(resolver=lambda *_: "S")
|
||||||
debug = graphene.Field(DjangoDebug, name="_debug")
|
debug = graphene.Field(DjangoDebug, name="__debug")
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Reporter.objects.all()
|
return Reporter.objects.all()
|
||||||
|
@ -245,7 +251,7 @@ def test_should_query_connectionfilter():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_debug {
|
__debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -259,6 +265,7 @@ def test_should_query_connectionfilter():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data["allReporters"] == expected["allReporters"]
|
assert result.data["allReporters"] == expected["allReporters"]
|
||||||
assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"]
|
assert len(result.data["__debug"]["sql"]) == 2
|
||||||
|
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
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
import six
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from graphql_relay.connection.arrayconnection import connection_from_list_slice
|
from graphql_relay.connection.arrayconnection import (
|
||||||
|
connection_from_list_slice,
|
||||||
|
cursor_to_offset,
|
||||||
|
get_offset_with_default,
|
||||||
|
offset_to_cursor,
|
||||||
|
)
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
|
||||||
from graphene import NonNull
|
from graphene import Int, NonNull
|
||||||
from graphene.relay import ConnectionField, PageInfo
|
from graphene.relay import ConnectionField, PageInfo
|
||||||
from graphene.types import Field, List
|
from graphene.types import Field, List
|
||||||
|
|
||||||
|
@ -19,30 +25,39 @@ class DjangoListField(Field):
|
||||||
if isinstance(_type, NonNull):
|
if isinstance(_type, NonNull):
|
||||||
_type = _type.of_type
|
_type = _type.of_type
|
||||||
|
|
||||||
assert issubclass(
|
|
||||||
_type, DjangoObjectType
|
|
||||||
), "DjangoListField only accepts DjangoObjectType types"
|
|
||||||
|
|
||||||
# Django would never return a Set of None vvvvvvv
|
# Django would never return a Set of None vvvvvvv
|
||||||
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
|
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
|
||||||
|
|
||||||
|
assert issubclass(
|
||||||
|
self._underlying_type, DjangoObjectType
|
||||||
|
), "DjangoListField only accepts DjangoObjectType types"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _underlying_type(self):
|
||||||
|
_type = self._type
|
||||||
|
while hasattr(_type, "of_type"):
|
||||||
|
_type = _type.of_type
|
||||||
|
return _type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
_type = self.type.of_type
|
return self._underlying_type._meta.model
|
||||||
if isinstance(_type, NonNull):
|
|
||||||
_type = _type.of_type
|
def get_default_queryset(self):
|
||||||
return _type._meta.model
|
return self.model._default_manager.get_queryset()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_resolver(django_object_type, resolver, root, info, **args):
|
def list_resolver(
|
||||||
|
django_object_type, resolver, default_queryset, root, info, **args
|
||||||
|
):
|
||||||
queryset = maybe_queryset(resolver(root, info, **args))
|
queryset = maybe_queryset(resolver(root, info, **args))
|
||||||
if queryset is None:
|
if queryset is None:
|
||||||
# Default to Django Model queryset
|
queryset = default_queryset
|
||||||
# N.B. This happens if DjangoListField is used in the top level Query object
|
|
||||||
model_manager = django_object_type._meta.model.objects
|
if isinstance(queryset, QuerySet):
|
||||||
queryset = maybe_queryset(
|
# Pass queryset to the DjangoObjectType get_queryset method
|
||||||
django_object_type.get_queryset(model_manager, info)
|
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
|
||||||
)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def get_resolver(self, parent_resolver):
|
||||||
|
@ -50,7 +65,12 @@ class DjangoListField(Field):
|
||||||
if isinstance(_type, NonNull):
|
if isinstance(_type, NonNull):
|
||||||
_type = _type.of_type
|
_type = _type.of_type
|
||||||
django_object_type = _type.of_type.of_type
|
django_object_type = _type.of_type.of_type
|
||||||
return partial(self.list_resolver, django_object_type, parent_resolver)
|
return partial(
|
||||||
|
self.list_resolver,
|
||||||
|
django_object_type,
|
||||||
|
parent_resolver,
|
||||||
|
self.get_default_queryset(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DjangoConnectionField(ConnectionField):
|
class DjangoConnectionField(ConnectionField):
|
||||||
|
@ -63,6 +83,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(DjangoConnectionField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -112,24 +133,49 @@ class DjangoConnectionField(ConnectionField):
|
||||||
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):
|
def resolve_connection(cls, connection, args, iterable, max_limit=None):
|
||||||
|
# Remove the offset parameter and convert it to an after cursor.
|
||||||
|
offset = args.pop("offset", None)
|
||||||
|
after = args.get("after")
|
||||||
|
if offset:
|
||||||
|
if after:
|
||||||
|
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)
|
||||||
|
|
||||||
iterable = maybe_queryset(iterable)
|
iterable = maybe_queryset(iterable)
|
||||||
|
|
||||||
if isinstance(iterable, QuerySet):
|
if isinstance(iterable, QuerySet):
|
||||||
_len = iterable.count()
|
list_length = iterable.count()
|
||||||
|
list_slice_length = (
|
||||||
|
min(max_limit, list_length) if max_limit is not None else list_length
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_len = len(iterable)
|
list_length = len(iterable)
|
||||||
|
list_slice_length = (
|
||||||
|
min(max_limit, list_length) if max_limit is not None else list_length
|
||||||
|
)
|
||||||
|
|
||||||
|
# If after is higher than list_length, connection_from_list_slice
|
||||||
|
# would try to do a negative slicing which makes django throw an
|
||||||
|
# AssertionError
|
||||||
|
after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length)
|
||||||
|
|
||||||
|
if max_limit is not None and "first" not in args:
|
||||||
|
args["first"] = max_limit
|
||||||
|
|
||||||
connection = connection_from_list_slice(
|
connection = connection_from_list_slice(
|
||||||
iterable,
|
iterable[after:],
|
||||||
args,
|
args,
|
||||||
slice_start=0,
|
slice_start=after,
|
||||||
list_length=_len,
|
list_length=list_length,
|
||||||
list_slice_length=_len,
|
list_slice_length=list_slice_length,
|
||||||
connection_type=connection,
|
connection_type=connection,
|
||||||
edge_type=connection.Edge,
|
edge_type=connection.Edge,
|
||||||
pageinfo_type=PageInfo,
|
pageinfo_type=PageInfo,
|
||||||
)
|
)
|
||||||
connection.iterable = iterable
|
connection.iterable = iterable
|
||||||
connection.length = _len
|
connection.length = list_length
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -147,6 +193,8 @@ class DjangoConnectionField(ConnectionField):
|
||||||
):
|
):
|
||||||
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, (
|
||||||
|
@ -166,6 +214,11 @@ 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)
|
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
|
||||||
# or a resolve_foo (does not accept queryset)
|
# or a resolve_foo (does not accept queryset)
|
||||||
iterable = resolver(root, info, **args)
|
iterable = resolver(root, info, **args)
|
||||||
|
@ -174,7 +227,9 @@ class DjangoConnectionField(ConnectionField):
|
||||||
# thus the iterable gets refiltered by resolve_queryset
|
# thus the iterable gets refiltered by resolve_queryset
|
||||||
# but iterable might be promise
|
# but iterable might be promise
|
||||||
iterable = queryset_resolver(connection, iterable, info, args)
|
iterable = queryset_resolver(connection, iterable, info, args)
|
||||||
on_resolve = partial(cls.resolve_connection, connection, 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)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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 ..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
|
||||||
|
@ -59,7 +60,12 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
connection, iterable, info, args
|
connection, iterable, info, args
|
||||||
)
|
)
|
||||||
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
||||||
return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs
|
filterset = filterset_class(
|
||||||
|
data=filter_kwargs, queryset=qs, request=info.context
|
||||||
|
)
|
||||||
|
if filterset.form.is_valid():
|
||||||
|
return filterset.qs
|
||||||
|
raise ValidationError(filterset.form.errors.as_json())
|
||||||
|
|
||||||
def get_queryset_resolver(self):
|
def get_queryset_resolver(self):
|
||||||
return partial(
|
return partial(
|
||||||
|
|
|
@ -35,9 +35,6 @@ else:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
pytestmark.append(pytest.mark.django_db)
|
|
||||||
|
|
||||||
|
|
||||||
if DJANGO_FILTER_INSTALLED:
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
|
||||||
class ArticleNode(DjangoObjectType):
|
class ArticleNode(DjangoObjectType):
|
||||||
|
@ -62,7 +59,7 @@ def get_args(field):
|
||||||
|
|
||||||
|
|
||||||
def assert_arguments(field, *arguments):
|
def assert_arguments(field, *arguments):
|
||||||
ignore = ("after", "before", "first", "last", "order_by")
|
ignore = ("offset", "after", "before", "first", "last", "order_by")
|
||||||
args = get_args(field)
|
args = get_args(field)
|
||||||
actual = [name for name in args if name not in ignore and not name.startswith("_")]
|
actual = [name for name in args if name not in ignore and not name.startswith("_")]
|
||||||
assert set(arguments) == set(
|
assert set(arguments) == set(
|
||||||
|
@ -403,6 +400,114 @@ def test_global_id_field_relation():
|
||||||
assert id_filter.field_class == GlobalIDFormField
|
assert id_filter.field_class == GlobalIDFormField
|
||||||
|
|
||||||
|
|
||||||
|
def test_global_id_field_relation_with_filter():
|
||||||
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
class ArticleFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ["headline", "reporter"]
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||||
|
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
|
||||||
|
reporter = Field(ReporterFilterNode)
|
||||||
|
article = Field(ArticleFilterNode)
|
||||||
|
|
||||||
|
r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com")
|
||||||
|
r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com")
|
||||||
|
Article.objects.create(
|
||||||
|
headline="a1",
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
reporter=r1,
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
Article.objects.create(
|
||||||
|
headline="a2",
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
reporter=r2,
|
||||||
|
editor=r2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Query articles created by the reporter `r1`
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allArticles (reporter: "UmVwb3J0ZXJGaWx0ZXJOb2RlOjE=") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
# We should only get back a single article
|
||||||
|
assert len(result.data["allArticles"]["edges"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_global_id_field_relation_with_filter_not_valid_id():
|
||||||
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ["first_name", "articles"]
|
||||||
|
|
||||||
|
class ArticleFilterNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ["headline", "reporter"]
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
|
||||||
|
all_articles = DjangoFilterConnectionField(ArticleFilterNode)
|
||||||
|
reporter = Field(ReporterFilterNode)
|
||||||
|
article = Field(ArticleFilterNode)
|
||||||
|
|
||||||
|
r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com")
|
||||||
|
r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com")
|
||||||
|
Article.objects.create(
|
||||||
|
headline="a1",
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
reporter=r1,
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
Article.objects.create(
|
||||||
|
headline="a2",
|
||||||
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
|
reporter=r2,
|
||||||
|
editor=r2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter by the global ID that does not exist
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allArticles (reporter: "fake_global_id") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert "Invalid ID specified." in result.errors[0].message
|
||||||
|
|
||||||
|
|
||||||
def test_global_id_multiple_field_implicit():
|
def test_global_id_multiple_field_implicit():
|
||||||
field = DjangoFilterConnectionField(ReporterNode, fields=["pets"])
|
field = DjangoFilterConnectionField(ReporterNode, fields=["pets"])
|
||||||
filterset_class = field.filterset_class
|
filterset_class = field.filterset_class
|
||||||
|
@ -840,7 +945,7 @@ def test_integer_field_filter_type():
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -892,7 +997,7 @@ def test_other_filter_types():
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -162,6 +162,17 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
||||||
_meta=_meta, input_fields=input_fields, **options
|
_meta=_meta, input_fields=input_fields, **options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **input):
|
||||||
|
form = cls.get_form(root, info, **input)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
return cls.perform_mutate(form, info)
|
||||||
|
else:
|
||||||
|
errors = ErrorType.from_errors(form.errors)
|
||||||
|
|
||||||
|
return cls(errors=errors)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_mutate(cls, form, info):
|
def perform_mutate(cls, form, info):
|
||||||
obj = form.save()
|
obj = form.save()
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
|
import pytest
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
from graphene import ObjectType, Schema, String, Field
|
from graphene import Field, ObjectType, Schema, String
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.tests.models import Film, FilmDetails, Pet
|
from graphene_django.tests.models import Pet
|
||||||
|
|
||||||
from ...settings import graphene_settings
|
|
||||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def pet_type():
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
return PetType
|
||||||
|
|
||||||
|
|
||||||
class MyForm(forms.Form):
|
class MyForm(forms.Form):
|
||||||
text = forms.CharField()
|
text = forms.CharField()
|
||||||
|
|
||||||
|
@ -29,23 +38,11 @@ class PetForm(forms.ModelForm):
|
||||||
model = Pet
|
model = Pet
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
def clean_age(self):
|
||||||
class PetType(DjangoObjectType):
|
age = self.cleaned_data["age"]
|
||||||
class Meta:
|
if age >= 99:
|
||||||
model = Pet
|
raise ValidationError("Too old")
|
||||||
fields = "__all__"
|
return age
|
||||||
|
|
||||||
|
|
||||||
class FilmType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Film
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
class FilmDetailsType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = FilmDetails
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
def test_needs_form_class():
|
def test_needs_form_class():
|
||||||
|
@ -73,7 +70,7 @@ def test_has_input_fields():
|
||||||
assert "text" in MyMutation.Input._meta.fields
|
assert "text" in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_mutation_error_camelcased():
|
def test_mutation_error_camelcased(pet_type, graphene_settings):
|
||||||
class ExtraPetForm(PetForm):
|
class ExtraPetForm(PetForm):
|
||||||
test_field = forms.CharField(required=True)
|
test_field = forms.CharField(required=True)
|
||||||
|
|
||||||
|
@ -86,203 +83,237 @@ def test_mutation_error_camelcased():
|
||||||
graphene_settings.CAMELCASE_ERRORS = True
|
graphene_settings.CAMELCASE_ERRORS = True
|
||||||
result = PetMutation.mutate_and_get_payload(None, None)
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
assert {f.field for f in result.errors} == {"name", "age", "testField"}
|
||||||
graphene_settings.CAMELCASE_ERRORS = False
|
|
||||||
|
|
||||||
|
|
||||||
class MockQuery(ObjectType):
|
class MockQuery(ObjectType):
|
||||||
a = String()
|
a = String()
|
||||||
|
|
||||||
|
|
||||||
class FormMutationTests(TestCase):
|
def test_form_invalid_form():
|
||||||
def test_form_invalid_form(self):
|
class MyMutation(DjangoFormMutation):
|
||||||
class MyMutation(DjangoFormMutation):
|
class Meta:
|
||||||
class Meta:
|
form_class = MyForm
|
||||||
form_class = MyForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
class Mutation(ObjectType):
|
||||||
my_mutation = MyMutation.Field()
|
my_mutation = MyMutation.Field()
|
||||||
|
|
||||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
result = schema.execute(
|
result = schema.execute(
|
||||||
""" mutation MyMutation {
|
""" mutation MyMutation {
|
||||||
myMutation(input: { text: "INVALID_INPUT" }) {
|
myMutation(input: { text: "INVALID_INPUT" }) {
|
||||||
errors {
|
errors {
|
||||||
field
|
field
|
||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["myMutation"]["errors"] == [
|
||||||
|
{"field": "text", "messages": ["Invalid input"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_valid_input():
|
||||||
|
class MyMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = MyForm
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
my_mutation = MyMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation MyMutation {
|
||||||
|
myMutation(input: { text: "VALID_INPUT" }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["myMutation"]["errors"] == []
|
||||||
|
assert result.data["myMutation"]["text"] == "VALID_INPUT"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_meta_fields(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
assert PetMutation._meta.model is Pet
|
||||||
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
|
assert "pet" in PetMutation._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_input_meta_fields(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
assert PetMutation._meta.model is Pet
|
||||||
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
|
assert "name" in PetMutation.Input._meta.fields
|
||||||
|
assert "client_mutation_id" in PetMutation.Input._meta.fields
|
||||||
|
assert "id" in PetMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclude_fields_input_meta_fields(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
exclude_fields = ["id"]
|
||||||
|
|
||||||
|
assert PetMutation._meta.model is Pet
|
||||||
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
|
assert "name" in PetMutation.Input._meta.fields
|
||||||
|
assert "age" in PetMutation.Input._meta.fields
|
||||||
|
assert "client_mutation_id" in PetMutation.Input._meta.fields
|
||||||
|
assert "id" not in PetMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_return_field_name(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
model = Pet
|
||||||
|
return_field_name = "animal"
|
||||||
|
|
||||||
|
assert PetMutation._meta.model is Pet
|
||||||
|
assert PetMutation._meta.return_field_name == "animal"
|
||||||
|
assert "animal" in PetMutation._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_mutate_existing(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(pet_type)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
pet = Pet.objects.create(name="Axel", age=10)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation($pk: ID!) {
|
||||||
|
petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
}
|
||||||
)
|
""",
|
||||||
|
variable_values={"pk": pet.pk},
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIs(result.errors, None)
|
assert result.errors is None
|
||||||
self.assertEqual(
|
assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
|
||||||
result.data["myMutation"]["errors"],
|
|
||||||
[{"field": "text", "messages": ["Invalid input"]}],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_form_valid_input(self):
|
assert Pet.objects.count() == 1
|
||||||
class MyMutation(DjangoFormMutation):
|
pet.refresh_from_db()
|
||||||
class Meta:
|
assert pet.name == "Mia"
|
||||||
form_class = MyForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
|
||||||
my_mutation = MyMutation.Field()
|
|
||||||
|
|
||||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
def test_model_form_mutation_creates_new(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(pet_type)
|
||||||
|
|
||||||
result = schema.execute(
|
class Meta:
|
||||||
""" mutation MyMutation {
|
form_class = PetForm
|
||||||
myMutation(input: { text: "VALID_INPUT" }) {
|
|
||||||
errors {
|
class Mutation(ObjectType):
|
||||||
field
|
pet_mutation = PetMutation.Field()
|
||||||
messages
|
|
||||||
}
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
text
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation {
|
||||||
|
petMutation(input: { name: "Mia", age: 10 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
}
|
||||||
)
|
"""
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
|
||||||
|
|
||||||
self.assertIs(result.errors, None)
|
assert Pet.objects.count() == 1
|
||||||
self.assertEqual(result.data["myMutation"]["errors"], [])
|
pet = Pet.objects.get()
|
||||||
self.assertEqual(result.data["myMutation"]["text"], "VALID_INPUT")
|
assert pet.name == "Mia"
|
||||||
|
assert pet.age == 10
|
||||||
|
|
||||||
|
|
||||||
class ModelFormMutationTests(TestCase):
|
def test_model_form_mutation_invalid_input(pet_type):
|
||||||
def test_default_meta_fields(self):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
pet = Field(pet_type)
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
class Meta:
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
form_class = PetForm
|
||||||
self.assertIn("pet", PetMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_default_input_meta_fields(self):
|
class Mutation(ObjectType):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
pet_mutation = PetMutation.Field()
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
|
||||||
self.assertIn("name", PetMutation.Input._meta.fields)
|
|
||||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
|
||||||
self.assertIn("id", PetMutation.Input._meta.fields)
|
|
||||||
|
|
||||||
def test_exclude_fields_input_meta_fields(self):
|
result = schema.execute(
|
||||||
class PetMutation(DjangoModelFormMutation):
|
""" mutation PetMutation {
|
||||||
class Meta:
|
petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
form_class = PetForm
|
pet {
|
||||||
exclude_fields = ["id"]
|
name
|
||||||
|
age
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
}
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
errors {
|
||||||
self.assertIn("name", PetMutation.Input._meta.fields)
|
field
|
||||||
self.assertIn("age", PetMutation.Input._meta.fields)
|
messages
|
||||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
|
||||||
self.assertNotIn("id", PetMutation.Input._meta.fields)
|
|
||||||
|
|
||||||
def test_return_field_name_is_camelcased(self):
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
model = FilmDetails
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, FilmDetails)
|
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "filmDetails")
|
|
||||||
|
|
||||||
def test_custom_return_field_name(self):
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
model = Film
|
|
||||||
return_field_name = "animal"
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Film)
|
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "animal")
|
|
||||||
self.assertIn("animal", PetMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_existing(self):
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
pet = Field(PetType)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
|
||||||
pet_mutation = PetMutation.Field()
|
|
||||||
|
|
||||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
|
||||||
|
|
||||||
pet = Pet.objects.create(name="Axel", age=10)
|
|
||||||
|
|
||||||
result = schema.execute(
|
|
||||||
""" mutation PetMutation($pk: ID!) {
|
|
||||||
petMutation(input: { id: $pk, name: "Mia", age: 10 }) {
|
|
||||||
pet {
|
|
||||||
name
|
|
||||||
age
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""",
|
}
|
||||||
variable_values={"pk": pet.pk},
|
"""
|
||||||
)
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["petMutation"]["pet"] is None
|
||||||
|
assert result.data["petMutation"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
self.assertIs(result.errors, None)
|
assert Pet.objects.count() == 0
|
||||||
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
|
||||||
pet.refresh_from_db()
|
|
||||||
self.assertEqual(pet.name, "Mia")
|
|
||||||
|
|
||||||
def test_model_form_mutation_creates_new(self):
|
def test_model_form_mutation_mutate_invalid_form(pet_type):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
pet = Field(PetType)
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
class Meta:
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
# A pet was not created
|
||||||
pet_mutation = PetMutation.Field()
|
Pet.objects.count() == 0
|
||||||
|
|
||||||
schema = Schema(query=MockQuery, mutation=Mutation)
|
fields_w_error = [e.field for e in result.errors]
|
||||||
|
assert len(result.errors) == 2
|
||||||
result = schema.execute(
|
assert result.errors[0].messages == ["This field is required."]
|
||||||
""" mutation PetMutation {
|
assert result.errors[1].messages == ["This field is required."]
|
||||||
petMutation(input: { name: "Mia", age: 10 }) {
|
assert "age" in fields_w_error
|
||||||
pet {
|
assert "name" in fields_w_error
|
||||||
name
|
|
||||||
age
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.assertIs(result.errors, None)
|
|
||||||
self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10})
|
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
|
||||||
pet = Pet.objects.get()
|
|
||||||
self.assertEqual(pet.name, "Mia")
|
|
||||||
self.assertEqual(pet.age, 10)
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_invalid_form(self):
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None)
|
|
||||||
|
|
||||||
# A pet was not created
|
|
||||||
self.assertEqual(Pet.objects.count(), 0)
|
|
||||||
|
|
||||||
fields_w_error = [e.field for e in result.errors]
|
|
||||||
self.assertEqual(len(result.errors), 2)
|
|
||||||
self.assertIn("name", fields_w_error)
|
|
||||||
self.assertEqual(result.errors[0].messages, ["This field is required."])
|
|
||||||
self.assertIn("age", fields_w_error)
|
|
||||||
self.assertEqual(result.errors[1].messages, ["This field is required."])
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
import functools
|
import functools
|
||||||
|
@ -5,6 +6,7 @@ import functools
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils import autoreload
|
from django.utils import autoreload
|
||||||
|
|
||||||
|
from graphql import print_schema
|
||||||
from graphene_django.settings import graphene_settings
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,24 +46,41 @@ class CommandArguments(BaseCommand):
|
||||||
|
|
||||||
|
|
||||||
class Command(CommandArguments):
|
class Command(CommandArguments):
|
||||||
help = "Dump Graphene schema JSON to file"
|
help = "Dump Graphene schema as a JSON or GraphQL file"
|
||||||
can_import_settings = True
|
can_import_settings = True
|
||||||
|
requires_system_checks = False
|
||||||
|
|
||||||
def save_file(self, out, schema_dict, indent):
|
def save_json_file(self, out, schema_dict, indent):
|
||||||
with open(out, "w") as outfile:
|
with open(out, "w") as outfile:
|
||||||
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
|
||||||
|
|
||||||
|
def save_graphql_file(self, out, schema):
|
||||||
|
with open(out, "w") as outfile:
|
||||||
|
outfile.write(print_schema(schema))
|
||||||
|
|
||||||
def get_schema(self, schema, out, indent):
|
def get_schema(self, schema, out, indent):
|
||||||
schema_dict = {"data": schema.introspect()}
|
schema_dict = {"data": schema.introspect()}
|
||||||
if out == "-":
|
if out == "-":
|
||||||
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
|
||||||
else:
|
else:
|
||||||
self.save_file(out, schema_dict, indent)
|
# Determine format
|
||||||
|
_, file_extension = os.path.splitext(out)
|
||||||
|
|
||||||
|
if file_extension == ".graphql":
|
||||||
|
self.save_graphql_file(out, schema)
|
||||||
|
elif file_extension == ".json":
|
||||||
|
self.save_json_file(out, schema_dict, indent)
|
||||||
|
else:
|
||||||
|
raise CommandError(
|
||||||
|
'Unrecognised file format "{}"'.format(file_extension)
|
||||||
|
)
|
||||||
|
|
||||||
style = getattr(self, "style", None)
|
style = getattr(self, "style", None)
|
||||||
success = getattr(style, "SUCCESS", lambda x: x)
|
success = getattr(style, "SUCCESS", lambda x: x)
|
||||||
|
|
||||||
self.stdout.write(success("Successfully dumped GraphQL schema to %s" % out))
|
self.stdout.write(
|
||||||
|
success("Successfully dumped GraphQL schema to {}".format(out))
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
options_schema = options.get("schema")
|
options_schema = options.get("schema")
|
||||||
|
|
|
@ -26,6 +26,7 @@ def fields_for_serializer(
|
||||||
exclude_fields,
|
exclude_fields,
|
||||||
is_input=False,
|
is_input=False,
|
||||||
convert_choices_to_enum=True,
|
convert_choices_to_enum=True,
|
||||||
|
lookup_field=None,
|
||||||
):
|
):
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for name, field in serializer.fields.items():
|
for name, field in serializer.fields.items():
|
||||||
|
@ -35,7 +36,9 @@ def fields_for_serializer(
|
||||||
name in exclude_fields,
|
name in exclude_fields,
|
||||||
field.write_only
|
field.write_only
|
||||||
and not is_input, # don't show write_only fields in Query
|
and not is_input, # don't show write_only fields in Query
|
||||||
field.read_only and is_input, # don't show read_only fields in Input
|
field.read_only
|
||||||
|
and is_input
|
||||||
|
and lookup_field != name, # don't show read_only fields in Input
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,6 +69,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
only_fields=(),
|
only_fields=(),
|
||||||
exclude_fields=(),
|
exclude_fields=(),
|
||||||
convert_choices_to_enum=True,
|
convert_choices_to_enum=True,
|
||||||
|
_meta=None,
|
||||||
**options
|
**options
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -90,6 +94,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
exclude_fields,
|
exclude_fields,
|
||||||
is_input=True,
|
is_input=True,
|
||||||
convert_choices_to_enum=convert_choices_to_enum,
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
|
lookup_field=lookup_field,
|
||||||
)
|
)
|
||||||
output_fields = fields_for_serializer(
|
output_fields = fields_for_serializer(
|
||||||
serializer,
|
serializer,
|
||||||
|
@ -97,9 +102,11 @@ class SerializerMutation(ClientIDMutation):
|
||||||
exclude_fields,
|
exclude_fields,
|
||||||
is_input=False,
|
is_input=False,
|
||||||
convert_choices_to_enum=convert_choices_to_enum,
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
|
lookup_field=lookup_field,
|
||||||
)
|
)
|
||||||
|
|
||||||
_meta = SerializerMutationOptions(cls)
|
if not _meta:
|
||||||
|
_meta = SerializerMutationOptions(cls)
|
||||||
_meta.lookup_field = lookup_field
|
_meta.lookup_field = lookup_field
|
||||||
_meta.model_operations = model_operations
|
_meta.model_operations = model_operations
|
||||||
_meta.serializer_class = serializer_class
|
_meta.serializer_class = serializer_class
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import graphene
|
|
||||||
import pytest
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from graphene import Schema
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
import graphene
|
||||||
|
from graphene import Schema
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
class MyFakeChildModel(models.Model):
|
class MyFakeChildModel(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from py.test import mark, raises
|
from py.test import raises
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from graphene import Field, ResolveInfo
|
from graphene import Field, ResolveInfo
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
|
|
||||||
from ...settings import graphene_settings
|
|
||||||
from ...types import DjangoObjectType
|
from ...types import DjangoObjectType
|
||||||
from ..models import MyFakeModel, MyFakeModelWithPassword, MyFakeModelWithDate
|
from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +98,6 @@ def test_exclude_fields():
|
||||||
assert "created" not in MyMutation.Input._meta.fields
|
assert "created" not in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_write_only_field():
|
def test_write_only_field():
|
||||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
password = serializers.CharField(write_only=True)
|
password = serializers.CharField(write_only=True)
|
||||||
|
@ -122,7 +120,6 @@ def test_write_only_field():
|
||||||
), "'password' is write_only field and shouldn't be visible"
|
), "'password' is write_only field and shouldn't be visible"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_write_only_field_using_extra_kwargs():
|
def test_write_only_field_using_extra_kwargs():
|
||||||
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
class WriteOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -144,20 +141,22 @@ def test_write_only_field_using_extra_kwargs():
|
||||||
), "'password' is write_only field and shouldn't be visible"
|
), "'password' is write_only field and shouldn't be visible"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_read_only_fields():
|
def test_read_only_fields():
|
||||||
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
|
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
id = serializers.CharField(read_only=True)
|
||||||
cool_name = serializers.CharField(read_only=True)
|
cool_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MyFakeModelWithPassword
|
model = MyFakeModelWithPassword
|
||||||
fields = ["cool_name", "password"]
|
lookup_field = "id"
|
||||||
|
fields = ["id", "cool_name", "password"]
|
||||||
|
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
serializer_class = ReadOnlyFieldModelSerializer
|
serializer_class = ReadOnlyFieldModelSerializer
|
||||||
|
|
||||||
assert "password" in MyMutation.Input._meta.fields
|
assert "password" in MyMutation.Input._meta.fields
|
||||||
|
assert "id" in MyMutation.Input._meta.fields
|
||||||
assert (
|
assert (
|
||||||
"cool_name" not in MyMutation.Input._meta.fields
|
"cool_name" not in MyMutation.Input._meta.fields
|
||||||
), "'cool_name' is read_only field and shouldn't be on arguments"
|
), "'cool_name' is read_only field and shouldn't be on arguments"
|
||||||
|
@ -194,7 +193,6 @@ def test_mutate_and_get_payload_success():
|
||||||
assert result.errors is None
|
assert result.errors is None
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_model_add_mutate_and_get_payload_success():
|
def test_model_add_mutate_and_get_payload_success():
|
||||||
result = MyModelMutation.mutate_and_get_payload(
|
result = MyModelMutation.mutate_and_get_payload(
|
||||||
None, mock_info(), **{"cool_name": "Narf"}
|
None, mock_info(), **{"cool_name": "Narf"}
|
||||||
|
@ -204,7 +202,6 @@ def test_model_add_mutate_and_get_payload_success():
|
||||||
assert isinstance(result.created, datetime.datetime)
|
assert isinstance(result.created, datetime.datetime)
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_model_update_mutate_and_get_payload_success():
|
def test_model_update_mutate_and_get_payload_success():
|
||||||
instance = MyFakeModel.objects.create(cool_name="Narf")
|
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||||
result = MyModelMutation.mutate_and_get_payload(
|
result = MyModelMutation.mutate_and_get_payload(
|
||||||
|
@ -214,7 +211,6 @@ def test_model_update_mutate_and_get_payload_success():
|
||||||
assert result.cool_name == "New Narf"
|
assert result.cool_name == "New Narf"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_model_partial_update_mutate_and_get_payload_success():
|
def test_model_partial_update_mutate_and_get_payload_success():
|
||||||
instance = MyFakeModel.objects.create(cool_name="Narf")
|
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||||
result = MyModelMutation.mutate_and_get_payload(
|
result = MyModelMutation.mutate_and_get_payload(
|
||||||
|
@ -224,7 +220,6 @@ def test_model_partial_update_mutate_and_get_payload_success():
|
||||||
assert result.cool_name == "Narf"
|
assert result.cool_name == "Narf"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_model_invalid_update_mutate_and_get_payload_success():
|
def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
class InvalidModelMutation(SerializerMutation):
|
class InvalidModelMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -239,7 +234,6 @@ def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
assert '"id" required' in str(exc.value)
|
assert '"id" required' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
|
||||||
def test_perform_mutate_success():
|
def test_perform_mutate_success():
|
||||||
class MyMethodMutation(SerializerMutation):
|
class MyMethodMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -272,11 +266,10 @@ def test_model_mutate_and_get_payload_error():
|
||||||
assert len(result.errors) > 0
|
assert len(result.errors) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_mutation_error_camelcased():
|
def test_mutation_error_camelcased(graphene_settings):
|
||||||
graphene_settings.CAMELCASE_ERRORS = True
|
graphene_settings.CAMELCASE_ERRORS = True
|
||||||
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||||
assert result.errors[0].field == "coolName"
|
assert result.errors[0].field == "coolName"
|
||||||
graphene_settings.CAMELCASE_ERRORS = False
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_serializer_operations():
|
def test_invalid_serializer_operations():
|
||||||
|
|
|
@ -36,6 +36,15 @@ DEFAULTS = {
|
||||||
# Max items returned in ConnectionFields / FilterConnectionFields
|
# Max items returned in ConnectionFields / FilterConnectionFields
|
||||||
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
"RELAY_CONNECTION_MAX_LIMIT": 100,
|
||||||
"CAMELCASE_ERRORS": False,
|
"CAMELCASE_ERRORS": False,
|
||||||
|
# Set to True to enable v3 naming convention for choice field Enum's
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
||||||
|
# Use a separate path for handling subscriptions.
|
||||||
|
"SUBSCRIPTION_PATH": None,
|
||||||
|
# By default GraphiQL headers editor tab is enabled, set to False to hide it
|
||||||
|
# This sets headerEditorEnabled GraphiQL option, for details go to
|
||||||
|
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||||
|
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -1,35 +1,55 @@
|
||||||
(function() {
|
(function (
|
||||||
|
document,
|
||||||
|
|
||||||
|
GRAPHENE_SETTINGS,
|
||||||
|
GraphiQL,
|
||||||
|
React,
|
||||||
|
ReactDOM,
|
||||||
|
SubscriptionsTransportWs,
|
||||||
|
fetch,
|
||||||
|
history,
|
||||||
|
location,
|
||||||
|
) {
|
||||||
// Parse the cookie value for a CSRF token
|
// Parse the cookie value for a CSRF token
|
||||||
var csrftoken;
|
var csrftoken;
|
||||||
var cookies = ('; ' + document.cookie).split('; csrftoken=');
|
var cookies = ("; " + document.cookie).split("; csrftoken=");
|
||||||
if (cookies.length == 2) {
|
if (cookies.length == 2) {
|
||||||
csrftoken = cookies.pop().split(';').shift();
|
csrftoken = cookies.pop().split(";").shift();
|
||||||
} else {
|
} else {
|
||||||
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect the URL parameters
|
// Collect the URL parameters
|
||||||
var parameters = {};
|
var parameters = {};
|
||||||
window.location.hash.substr(1).split('&').forEach(function (entry) {
|
location.hash
|
||||||
var eq = entry.indexOf('=');
|
.substr(1)
|
||||||
if (eq >= 0) {
|
.split("&")
|
||||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
.forEach(function (entry) {
|
||||||
decodeURIComponent(entry.slice(eq + 1));
|
var eq = entry.indexOf("=");
|
||||||
}
|
if (eq >= 0) {
|
||||||
});
|
parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent(
|
||||||
|
entry.slice(eq + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
// Produce a Location fragment string from a parameter object.
|
// Produce a Location fragment string from a parameter object.
|
||||||
function locationQuery(params) {
|
function locationQuery(params) {
|
||||||
return '#' + Object.keys(params).map(function (key) {
|
return (
|
||||||
return encodeURIComponent(key) + '=' +
|
"#" +
|
||||||
encodeURIComponent(params[key]);
|
Object.keys(params)
|
||||||
}).join('&');
|
.map(function (key) {
|
||||||
|
return (
|
||||||
|
encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("&")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||||
var graphqlParamNames = {
|
var graphqlParamNames = {
|
||||||
query: true,
|
query: true,
|
||||||
variables: true,
|
variables: true,
|
||||||
operationName: true
|
operationName: true,
|
||||||
};
|
};
|
||||||
var otherParams = {};
|
var otherParams = {};
|
||||||
for (var k in parameters) {
|
for (var k in parameters) {
|
||||||
|
@ -41,29 +61,98 @@
|
||||||
var fetchURL = locationQuery(otherParams);
|
var fetchURL = locationQuery(otherParams);
|
||||||
|
|
||||||
// Defines a GraphQL fetcher using the fetch API.
|
// Defines a GraphQL fetcher using the fetch API.
|
||||||
function graphQLFetcher(graphQLParams) {
|
function httpClient(graphQLParams, opts) {
|
||||||
var headers = {
|
if (typeof opts === 'undefined') {
|
||||||
'Accept': 'application/json',
|
opts = {};
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
};
|
var headers = opts.headers || {};
|
||||||
|
headers['Accept'] = headers['Accept'] || 'application/json';
|
||||||
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
||||||
if (csrftoken) {
|
if (csrftoken) {
|
||||||
headers['X-CSRFToken'] = csrftoken;
|
headers['X-CSRFToken'] = csrftoken
|
||||||
}
|
}
|
||||||
return fetch(fetchURL, {
|
return fetch(fetchURL, {
|
||||||
method: 'post',
|
method: "post",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: JSON.stringify(graphQLParams),
|
body: JSON.stringify(graphQLParams),
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.text();
|
.then(function (response) {
|
||||||
}).then(function (responseBody) {
|
return response.text();
|
||||||
try {
|
})
|
||||||
return JSON.parse(responseBody);
|
.then(function (responseBody) {
|
||||||
} catch (error) {
|
try {
|
||||||
return responseBody;
|
return JSON.parse(responseBody);
|
||||||
}
|
} catch (error) {
|
||||||
});
|
return responseBody;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
|
||||||
|
// assumes the current window location with an appropriate websocket protocol.
|
||||||
|
var subscribeURL =
|
||||||
|
location.origin.replace(/^http/, "ws") +
|
||||||
|
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
|
||||||
|
|
||||||
|
// Create a subscription client.
|
||||||
|
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
|
||||||
|
subscribeURL,
|
||||||
|
{
|
||||||
|
// Reconnect after any interruptions.
|
||||||
|
reconnect: true,
|
||||||
|
// Delay socket initialization until the first subscription is started.
|
||||||
|
lazy: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep a reference to the currently-active subscription, if available.
|
||||||
|
var activeSubscription = null;
|
||||||
|
|
||||||
|
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
|
||||||
|
function graphQLFetcher(graphQLParams, opts) {
|
||||||
|
var operationType = getOperationType(graphQLParams);
|
||||||
|
|
||||||
|
// If we're about to execute a new operation, and we have an active subscription,
|
||||||
|
// unsubscribe before continuing.
|
||||||
|
if (activeSubscription) {
|
||||||
|
activeSubscription.unsubscribe();
|
||||||
|
activeSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationType === "subscription") {
|
||||||
|
return {
|
||||||
|
subscribe: function (observer) {
|
||||||
|
subscriptionClient.request(graphQLParams).subscribe(observer);
|
||||||
|
activeSubscription = subscriptionClient;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return httpClient(graphQLParams, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the type of operation being executed for a given set of GraphQL parameters.
|
||||||
|
function getOperationType(graphQLParams) {
|
||||||
|
// Run a regex against the query to determine the operation type (query, mutation, subscription).
|
||||||
|
var operationRegex = new RegExp(
|
||||||
|
// Look for lines that start with an operation keyword, ignoring whitespace.
|
||||||
|
"^\\s*(query|mutation|subscription)\\s*" +
|
||||||
|
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available).
|
||||||
|
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") +
|
||||||
|
// The line should eventually encounter an opening curly brace.
|
||||||
|
"[^\\{]*\\{",
|
||||||
|
// Enable multiline matching.
|
||||||
|
"m",
|
||||||
|
);
|
||||||
|
var match = operationRegex.exec(graphQLParams.query);
|
||||||
|
if (!match) {
|
||||||
|
return "query";
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
// When the query and variables string is edited, update the URL bar so
|
// When the query and variables string is edited, update the URL bar so
|
||||||
// that it can be easily shared.
|
// that it can be easily shared.
|
||||||
function onEditQuery(newQuery) {
|
function onEditQuery(newQuery) {
|
||||||
|
@ -83,11 +172,12 @@
|
||||||
}
|
}
|
||||||
var options = {
|
var options = {
|
||||||
fetcher: graphQLFetcher,
|
fetcher: graphQLFetcher,
|
||||||
onEditQuery: onEditQuery,
|
onEditQuery: onEditQuery,
|
||||||
onEditVariables: onEditVariables,
|
onEditVariables: onEditVariables,
|
||||||
onEditOperationName: onEditOperationName,
|
onEditOperationName: onEditOperationName,
|
||||||
query: parameters.query,
|
headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
|
||||||
}
|
query: parameters.query,
|
||||||
|
};
|
||||||
if (parameters.variables) {
|
if (parameters.variables) {
|
||||||
options.variables = parameters.variables;
|
options.variables = parameters.variables;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +187,17 @@
|
||||||
// Render <GraphiQL /> into the body.
|
// Render <GraphiQL /> into the body.
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(GraphiQL, options),
|
React.createElement(GraphiQL, options),
|
||||||
document.getElementById("editor")
|
document.getElementById("editor"),
|
||||||
);
|
);
|
||||||
})();
|
})(
|
||||||
|
document,
|
||||||
|
|
||||||
|
window.GRAPHENE_SETTINGS,
|
||||||
|
window.GraphiQL,
|
||||||
|
window.React,
|
||||||
|
window.ReactDOM,
|
||||||
|
window.SubscriptionsTransportWs,
|
||||||
|
window.fetch,
|
||||||
|
window.history,
|
||||||
|
window.location,
|
||||||
|
);
|
||||||
|
|
|
@ -17,22 +17,37 @@ add "&raw" to the end of the URL within a browser.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
|
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.css"
|
||||||
|
integrity="{{graphiql_css_sri}}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
crossorigin="anonymous" />
|
crossorigin="anonymous" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
||||||
integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
|
integrity="{{whatwg_fetch_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/react@{{react_version}}/umd/react.production.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/react@{{react_version}}/umd/react.production.min.js"
|
||||||
|
integrity="{{react_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/react-dom@{{react_version}}/umd/react-dom.production.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/react-dom@{{react_version}}/umd/react-dom.production.min.js"
|
||||||
|
integrity="{{react_dom_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
||||||
|
integrity="{{graphiql_sri}}"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js"
|
||||||
|
integrity="{{subscriptions_transport_ws_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="editor"></div>
|
<div id="editor"></div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.GRAPHENE_SETTINGS = {
|
||||||
|
{% if subscription_path %}
|
||||||
|
subscriptionPath: "{{subscription_path}}",
|
||||||
|
{% endif %}
|
||||||
|
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from mock import patch, mock_open
|
from mock import mock_open, patch
|
||||||
from six import StringIO
|
from six import StringIO
|
||||||
|
|
||||||
|
from graphene import ObjectType, Schema, String
|
||||||
|
|
||||||
@patch("graphene_django.management.commands.graphql_schema.Command.save_file")
|
|
||||||
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
|
@patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
|
||||||
|
def test_generate_json_file_on_call_graphql_schema(savefile_mock):
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
management.call_command("graphql_schema", schema="", stdout=out)
|
management.call_command("graphql_schema", schema="", stdout=out)
|
||||||
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
|
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@patch("json.dump")
|
@patch("json.dump")
|
||||||
def test_files_are_canonical(dump_mock):
|
def test_json_files_are_canonical(dump_mock):
|
||||||
open_mock = mock_open()
|
open_mock = mock_open()
|
||||||
with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
|
with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
|
||||||
management.call_command("graphql_schema", schema="")
|
management.call_command("graphql_schema", schema="")
|
||||||
|
@ -25,3 +29,34 @@ def test_files_are_canonical(dump_mock):
|
||||||
assert (
|
assert (
|
||||||
dump_mock.call_args[1]["indent"] > 0
|
dump_mock.call_args[1]["indent"] > 0
|
||||||
), "output should be pretty-printed by default"
|
), "output should be pretty-printed by default"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_graphql_file_on_call_graphql_schema():
|
||||||
|
class Query(ObjectType):
|
||||||
|
hi = String()
|
||||||
|
|
||||||
|
mock_schema = Schema(query=Query)
|
||||||
|
|
||||||
|
open_mock = mock_open()
|
||||||
|
with patch("graphene_django.management.commands.graphql_schema.open", open_mock):
|
||||||
|
management.call_command(
|
||||||
|
"graphql_schema", schema=mock_schema, out="schema.graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
open_mock.assert_called_once()
|
||||||
|
|
||||||
|
handle = open_mock()
|
||||||
|
assert handle.write.called_once()
|
||||||
|
|
||||||
|
schema_output = handle.write.call_args[0][0]
|
||||||
|
assert schema_output == dedent(
|
||||||
|
"""\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
@ -1,31 +1,50 @@
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from graphene import NonNull
|
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
from graphene import NonNull
|
||||||
from graphene.relay import ConnectionField, Node
|
from graphene.relay import ConnectionField, Node
|
||||||
from graphene.types.datetime import DateTime, Date, Time
|
from graphene.types.datetime import Date, DateTime, Time
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
|
|
||||||
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
|
from ..compat import (
|
||||||
from ..converter import convert_django_field, convert_django_field_with_choices
|
ArrayField,
|
||||||
|
HStoreField,
|
||||||
|
JSONField,
|
||||||
|
PGJSONField,
|
||||||
|
MissingType,
|
||||||
|
RangeField,
|
||||||
|
)
|
||||||
|
from ..converter import (
|
||||||
|
convert_django_field,
|
||||||
|
convert_django_field_with_choices,
|
||||||
|
generate_enum_name,
|
||||||
|
)
|
||||||
from ..registry import Registry
|
from ..registry import Registry
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
from .models import Article, Film, FilmDetails, Reporter
|
from .models import Article, Film, FilmDetails, Reporter
|
||||||
|
|
||||||
|
|
||||||
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
# from graphene.core.types.custom_scalars import DateTime, Time, JSONString
|
||||||
|
|
||||||
|
|
||||||
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
def assert_conversion(django_field, graphene_field, *args, **kwargs):
|
||||||
field = django_field(help_text="Custom Help Text", null=True, *args, **kwargs)
|
_kwargs = kwargs.copy()
|
||||||
|
if "null" not in kwargs:
|
||||||
|
_kwargs["null"] = True
|
||||||
|
field = django_field(help_text="Custom Help Text", *args, **_kwargs)
|
||||||
graphene_type = convert_django_field(field)
|
graphene_type = convert_django_field(field)
|
||||||
assert isinstance(graphene_type, graphene_field)
|
assert isinstance(graphene_type, graphene_field)
|
||||||
field = graphene_type.Field()
|
field = graphene_type.Field()
|
||||||
assert field.description == "Custom Help Text"
|
assert field.description == "Custom Help Text"
|
||||||
nonnull_field = django_field(null=False, *args, **kwargs)
|
|
||||||
|
_kwargs = kwargs.copy()
|
||||||
|
if "null" not in kwargs:
|
||||||
|
_kwargs["null"] = False
|
||||||
|
nonnull_field = django_field(*args, **_kwargs)
|
||||||
if not nonnull_field.null:
|
if not nonnull_field.null:
|
||||||
nonnull_graphene_type = convert_django_field(nonnull_field)
|
nonnull_graphene_type = convert_django_field(nonnull_field)
|
||||||
nonnull_field = nonnull_graphene_type.Field()
|
nonnull_field = nonnull_graphene_type.Field()
|
||||||
|
@ -121,7 +140,12 @@ def test_should_integer_convert_int():
|
||||||
|
|
||||||
|
|
||||||
def test_should_boolean_convert_boolean():
|
def test_should_boolean_convert_boolean():
|
||||||
field = assert_conversion(models.BooleanField, graphene.NonNull)
|
assert_conversion(models.BooleanField, graphene.Boolean, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_boolean_convert_non_null_boolean():
|
||||||
|
field = assert_conversion(models.BooleanField, graphene.Boolean, null=False)
|
||||||
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
assert field.type.of_type == graphene.Boolean
|
assert field.type.of_type == graphene.Boolean
|
||||||
|
|
||||||
|
|
||||||
|
@ -293,6 +317,14 @@ def test_should_postgres_array_convert_list():
|
||||||
)
|
)
|
||||||
assert isinstance(field.type, graphene.NonNull)
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
assert isinstance(field.type.of_type, graphene.List)
|
assert isinstance(field.type.of_type, graphene.List)
|
||||||
|
assert isinstance(field.type.of_type.of_type, graphene.NonNull)
|
||||||
|
assert field.type.of_type.of_type.of_type == graphene.String
|
||||||
|
|
||||||
|
field = assert_conversion(
|
||||||
|
ArrayField, graphene.List, models.CharField(max_length=100, null=True)
|
||||||
|
)
|
||||||
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
|
assert isinstance(field.type.of_type, graphene.List)
|
||||||
assert field.type.of_type.of_type == graphene.String
|
assert field.type.of_type.of_type == graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
@ -304,6 +336,17 @@ def test_should_postgres_array_multiple_convert_list():
|
||||||
assert isinstance(field.type, graphene.NonNull)
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
assert isinstance(field.type.of_type, graphene.List)
|
assert isinstance(field.type.of_type, graphene.List)
|
||||||
assert isinstance(field.type.of_type.of_type, graphene.List)
|
assert isinstance(field.type.of_type.of_type, graphene.List)
|
||||||
|
assert isinstance(field.type.of_type.of_type.of_type, graphene.NonNull)
|
||||||
|
assert field.type.of_type.of_type.of_type.of_type == graphene.String
|
||||||
|
|
||||||
|
field = assert_conversion(
|
||||||
|
ArrayField,
|
||||||
|
graphene.List,
|
||||||
|
ArrayField(models.CharField(max_length=100, null=True)),
|
||||||
|
)
|
||||||
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
|
assert isinstance(field.type.of_type, graphene.List)
|
||||||
|
assert isinstance(field.type.of_type.of_type, graphene.List)
|
||||||
assert field.type.of_type.of_type.of_type == graphene.String
|
assert field.type.of_type.of_type.of_type == graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
@ -312,8 +355,13 @@ def test_should_postgres_hstore_convert_string():
|
||||||
assert_conversion(HStoreField, JSONString)
|
assert_conversion(HStoreField, JSONString)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist")
|
@pytest.mark.skipif(PGJSONField is MissingType, reason="PGJSONField should exist")
|
||||||
def test_should_postgres_json_convert_string():
|
def test_should_postgres_json_convert_string():
|
||||||
|
assert_conversion(PGJSONField, JSONString)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(JSONField is MissingType, reason="JSONField should exist")
|
||||||
|
def test_should_json_convert_string():
|
||||||
assert_conversion(JSONField, JSONString)
|
assert_conversion(JSONField, JSONString)
|
||||||
|
|
||||||
|
|
||||||
|
@ -324,4 +372,25 @@ def test_should_postgres_range_convert_list():
|
||||||
field = assert_conversion(IntegerRangeField, graphene.List)
|
field = assert_conversion(IntegerRangeField, graphene.List)
|
||||||
assert isinstance(field.type, graphene.NonNull)
|
assert isinstance(field.type, graphene.NonNull)
|
||||||
assert isinstance(field.type.of_type, graphene.List)
|
assert isinstance(field.type.of_type, graphene.List)
|
||||||
assert field.type.of_type.of_type == graphene.Int
|
assert isinstance(field.type.of_type.of_type, graphene.NonNull)
|
||||||
|
assert field.type.of_type.of_type.of_type == graphene.Int
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_enum_name(graphene_settings):
|
||||||
|
MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"])
|
||||||
|
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
|
||||||
|
|
||||||
|
# Simple case
|
||||||
|
field = graphene.Field(graphene.String, name="type")
|
||||||
|
model_meta = MockDjangoModelMeta(app_label="users", object_name="User")
|
||||||
|
assert generate_enum_name(model_meta, field) == "UsersUserTypeChoices"
|
||||||
|
|
||||||
|
# More complicated multiple work case
|
||||||
|
field = graphene.Field(graphene.String, name="fizz_buzz")
|
||||||
|
model_meta = MockDjangoModelMeta(
|
||||||
|
app_label="some_long_app_name", object_name="SomeObject"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
generate_enum_name(model_meta, field)
|
||||||
|
== "SomeLongAppNameSomeObjectFizzBuzzChoices"
|
||||||
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -10,7 +11,6 @@ from .models import Article as ArticleModel
|
||||||
from .models import Reporter as ReporterModel
|
from .models import Reporter as ReporterModel
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestDjangoListField:
|
class TestDjangoListField:
|
||||||
def test_only_django_object_types(self):
|
def test_only_django_object_types(self):
|
||||||
class TestType(ObjectType):
|
class TestType(ObjectType):
|
||||||
|
@ -19,6 +19,12 @@ class TestDjangoListField:
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
list_field = DjangoListField(TestType)
|
list_field = DjangoListField(TestType)
|
||||||
|
|
||||||
|
def test_only_import_paths(self):
|
||||||
|
list_field = DjangoListField("graphene_django.tests.schema.Human")
|
||||||
|
from .schema import Human
|
||||||
|
|
||||||
|
assert list_field._type.of_type.of_type is Human
|
||||||
|
|
||||||
def test_non_null_type(self):
|
def test_non_null_type(self):
|
||||||
class Reporter(DjangoObjectType):
|
class Reporter(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -136,13 +142,26 @@ class TestDjangoListField:
|
||||||
pub_date_time=datetime.datetime.now(),
|
pub_date_time=datetime.datetime.now(),
|
||||||
editor=r1,
|
editor=r1,
|
||||||
)
|
)
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Not so good news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
|
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == {
|
assert result.data == {
|
||||||
"reporters": [
|
"reporters": [
|
||||||
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
|
{
|
||||||
|
"firstName": "Tara",
|
||||||
|
"articles": [
|
||||||
|
{"headline": "Amazing news"},
|
||||||
|
{"headline": "Not so good news"},
|
||||||
|
],
|
||||||
|
},
|
||||||
{"firstName": "Debra", "articles": []},
|
{"firstName": "Debra", "articles": []},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -158,8 +177,8 @@ class TestDjangoListField:
|
||||||
model = ReporterModel
|
model = ReporterModel
|
||||||
fields = ("first_name", "articles")
|
fields = ("first_name", "articles")
|
||||||
|
|
||||||
def resolve_reporters(reporter, info):
|
def resolve_articles(reporter, info):
|
||||||
return reporter.articles.all()
|
return reporter.articles.filter(headline__contains="Amazing")
|
||||||
|
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
reporters = DjangoListField(Reporter)
|
reporters = DjangoListField(Reporter)
|
||||||
|
@ -187,6 +206,165 @@ class TestDjangoListField:
|
||||||
pub_date_time=datetime.datetime.now(),
|
pub_date_time=datetime.datetime.now(),
|
||||||
editor=r1,
|
editor=r1,
|
||||||
)
|
)
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Not so good news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {
|
||||||
|
"reporters": [
|
||||||
|
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
|
||||||
|
{"firstName": "Debra", "articles": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_queryset_filter(self):
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ("first_name", "articles")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
# Only get reporters with at least 1 article
|
||||||
|
return queryset.annotate(article_count=Count("articles")).filter(
|
||||||
|
article_count__gt=0
|
||||||
|
)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
reporters = DjangoListField(Reporter)
|
||||||
|
|
||||||
|
def resolve_reporters(_, info):
|
||||||
|
return ReporterModel.objects.all()
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
reporters {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
|
||||||
|
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
|
||||||
|
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Amazing news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"reporters": [{"firstName": "Tara"}]}
|
||||||
|
|
||||||
|
def test_resolve_list(self):
|
||||||
|
"""Resolving a plain list should work (and not call get_queryset)"""
|
||||||
|
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ("first_name", "articles")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
# Only get reporters with at least 1 article
|
||||||
|
return queryset.annotate(article_count=Count("articles")).filter(
|
||||||
|
article_count__gt=0
|
||||||
|
)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
reporters = DjangoListField(Reporter)
|
||||||
|
|
||||||
|
def resolve_reporters(_, info):
|
||||||
|
return [ReporterModel.objects.get(first_name="Debra")]
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
reporters {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
|
||||||
|
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
|
||||||
|
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Amazing news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"reporters": [{"firstName": "Debra"}]}
|
||||||
|
|
||||||
|
def test_get_queryset_foreign_key(self):
|
||||||
|
class Article(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ArticleModel
|
||||||
|
fields = ("headline",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
# Rose tinted glasses
|
||||||
|
return queryset.exclude(headline__contains="Not so good")
|
||||||
|
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ("first_name", "articles")
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
reporters = DjangoListField(Reporter)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
reporters {
|
||||||
|
firstName
|
||||||
|
articles {
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
|
||||||
|
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
|
||||||
|
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Amazing news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
ArticleModel.objects.create(
|
||||||
|
headline="Not so good news",
|
||||||
|
reporter=r1,
|
||||||
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
|
editor=r1,
|
||||||
|
)
|
||||||
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import base64
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import base64
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
|
from graphql_relay import to_global_id
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from graphql_relay import to_global_id
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
from ..utils import DJANGO_FILTER_INSTALLED
|
from ..compat import JSONField, MissingType
|
||||||
from ..compat import MissingType, JSONField
|
|
||||||
from ..fields import DjangoConnectionField
|
from ..fields import DjangoConnectionField
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
from ..settings import graphene_settings
|
from ..utils import DJANGO_FILTER_INSTALLED
|
||||||
from .models import Article, CNNReporter, Reporter, Film, FilmDetails
|
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_only_fields():
|
def test_should_query_only_fields():
|
||||||
|
@ -65,6 +61,31 @@ def test_should_query_simplelazy_objects():
|
||||||
assert result.data == {"reporter": {"id": "1"}}
|
assert result.data == {"reporter": {"id": "1"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_query_wrapped_simplelazy_objects():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
fields = ("id",)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
reporter = graphene.Field(ReporterType)
|
||||||
|
|
||||||
|
def resolve_reporter(self, info):
|
||||||
|
return SimpleLazyObject(lambda: SimpleLazyObject(lambda: Reporter(id=1)))
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
reporter {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {"reporter": {"id": "1"}}
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_well():
|
def test_should_query_well():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -147,9 +168,6 @@ def test_should_query_postgres_fields():
|
||||||
|
|
||||||
|
|
||||||
def test_should_node():
|
def test_should_node():
|
||||||
# reset_global_registry()
|
|
||||||
# Node._meta.registry = get_global_registry()
|
|
||||||
|
|
||||||
class ReporterNode(DjangoObjectType):
|
class ReporterNode(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Reporter
|
model = Reporter
|
||||||
|
@ -588,7 +606,7 @@ def test_should_query_node_multiple_filtering():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_enforce_first_or_last():
|
def test_should_enforce_first_or_last(graphene_settings):
|
||||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
|
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
|
@ -627,7 +645,7 @@ def test_should_enforce_first_or_last():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_error_if_first_is_greater_than_max():
|
def test_should_error_if_first_is_greater_than_max(graphene_settings):
|
||||||
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
|
@ -667,10 +685,8 @@ def test_should_error_if_first_is_greater_than_max():
|
||||||
)
|
)
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
|
||||||
|
|
||||||
|
def test_should_error_if_last_is_greater_than_max(graphene_settings):
|
||||||
def test_should_error_if_last_is_greater_than_max():
|
|
||||||
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
|
@ -710,8 +726,6 @@ def test_should_error_if_last_is_greater_than_max():
|
||||||
)
|
)
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_promise_connectionfields():
|
def test_should_query_promise_connectionfields():
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
@ -1071,6 +1085,134 @@ def test_should_resolve_get_queryset_connectionfields():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_limit_after_to_list_length():
|
||||||
|
reporter_1 = Reporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
reporter_2 = Reporter.objects.create(
|
||||||
|
first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($after: String) {
|
||||||
|
allReporters(first: 1 after: $after) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
after = base64.b64encode(b"arrayconnection:10").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(after=after))
|
||||||
|
expected = {"allReporters": {"edges": []}}
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
REPORTERS = [
|
||||||
|
dict(
|
||||||
|
first_name="First {}".format(i),
|
||||||
|
last_name="Last {}".format(i),
|
||||||
|
email="johndoe+{}@example.com".format(i),
|
||||||
|
a_choice=1,
|
||||||
|
)
|
||||||
|
for i in range(6)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_return_max_limit(graphene_settings):
|
||||||
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4
|
||||||
|
reporters = [Reporter(**kwargs) for kwargs in REPORTERS]
|
||||||
|
Reporter.objects.bulk_create(reporters)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query AllReporters {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert len(result.data["allReporters"]["edges"]) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_have_next_page(graphene_settings):
|
||||||
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4
|
||||||
|
reporters = [Reporter(**kwargs) for kwargs in REPORTERS]
|
||||||
|
Reporter.objects.bulk_create(reporters)
|
||||||
|
db_reporters = Reporter.objects.all()
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query AllReporters($first: Int, $after: String) {
|
||||||
|
allReporters(first: $first, after: $after) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query, variable_values={})
|
||||||
|
assert not result.errors
|
||||||
|
assert len(result.data["allReporters"]["edges"]) == 4
|
||||||
|
assert result.data["allReporters"]["pageInfo"]["hasNextPage"]
|
||||||
|
|
||||||
|
last_result = result.data["allReporters"]["pageInfo"]["endCursor"]
|
||||||
|
result2 = schema.execute(query, variable_values=dict(first=4, after=last_result))
|
||||||
|
assert not result2.errors
|
||||||
|
assert len(result2.data["allReporters"]["edges"]) == 2
|
||||||
|
assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"]
|
||||||
|
gql_reporters = (
|
||||||
|
result.data["allReporters"]["edges"] + result2.data["allReporters"]["edges"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == {
|
||||||
|
gql_reporter["node"]["id"] for gql_reporter in gql_reporters
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_should_preserve_prefetch_related(django_assert_num_queries):
|
def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1172,3 +1314,144 @@ def test_should_preserve_annotations():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert result.data == expected, str(result.data)
|
assert result.data == expected, str(result.data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_enable_offset_filtering():
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allReporters(first: 1, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_enable_offset_filtering_higher_than_max_limit(
|
||||||
|
graphene_settings,
|
||||||
|
):
|
||||||
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Lady")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allReporters(first: 1, offset: 3) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {
|
||||||
|
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_forbid_offset_filtering_with_before():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($before: String) {
|
||||||
|
allReporters(first: 1, before: $before, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
before = base64.b64encode(b"arrayconnection:2").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(before=before))
|
||||||
|
expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == expected_error
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_allow_offset_filtering_with_after():
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Lady")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($after: String) {
|
||||||
|
allReporters(first: 1, after: $after, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
after = base64.b64encode(b"arrayconnection:0").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(after=after))
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
|
@ -9,12 +9,11 @@ from graphene import Connection, Field, Interface, ObjectType, Schema, String
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
from .. import registry
|
from .. import registry
|
||||||
|
from ..filter import DjangoFilterConnectionField
|
||||||
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
||||||
from .models import Article as ArticleModel
|
from .models import Article as ArticleModel
|
||||||
from .models import Reporter as ReporterModel
|
from .models import Reporter as ReporterModel
|
||||||
|
|
||||||
registry.reset_global_registry()
|
|
||||||
|
|
||||||
|
|
||||||
class Reporter(DjangoObjectType):
|
class Reporter(DjangoObjectType):
|
||||||
"""Reporter description"""
|
"""Reporter description"""
|
||||||
|
@ -173,7 +172,7 @@ type Reporter {
|
||||||
pets: [Reporter!]!
|
pets: [Reporter!]!
|
||||||
aChoice: ReporterAChoice
|
aChoice: ReporterAChoice
|
||||||
reporterType: ReporterReporterType
|
reporterType: ReporterReporterType
|
||||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReporterAChoice {
|
enum ReporterAChoice {
|
||||||
|
@ -196,7 +195,6 @@ type RootQuery {
|
||||||
def with_local_registry(func):
|
def with_local_registry(func):
|
||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
old = registry.get_global_registry()
|
old = registry.get_global_registry()
|
||||||
registry.reset_global_registry()
|
|
||||||
try:
|
try:
|
||||||
retval = func(*args, **kwargs)
|
retval = func(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -233,6 +231,17 @@ def test_django_objecttype_fields():
|
||||||
assert fields == ["id", "email", "films"]
|
assert fields == ["id", "email", "films"]
|
||||||
|
|
||||||
|
|
||||||
|
@with_local_registry
|
||||||
|
def test_django_objecttype_fields_empty():
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ()
|
||||||
|
|
||||||
|
fields = list(Reporter._meta.fields.keys())
|
||||||
|
assert fields == []
|
||||||
|
|
||||||
|
|
||||||
@with_local_registry
|
@with_local_registry
|
||||||
def test_django_objecttype_only_fields_and_fields():
|
def test_django_objecttype_only_fields_and_fields():
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
|
@ -386,6 +395,10 @@ def test_django_objecttype_exclude_fields_exist_on_model():
|
||||||
assert len(record) == 0
|
assert len(record) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def custom_enum_name(field):
|
||||||
|
return "CustomEnum{}".format(field.name.title())
|
||||||
|
|
||||||
|
|
||||||
class TestDjangoObjectType:
|
class TestDjangoObjectType:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def PetModel(self):
|
def PetModel(self):
|
||||||
|
@ -492,3 +505,104 @@ class TestDjangoObjectType:
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_django_objecttype_convert_choices_enum_naming_collisions(
|
||||||
|
self, PetModel, graphene_settings
|
||||||
|
):
|
||||||
|
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V3_NAMING = True
|
||||||
|
|
||||||
|
class PetModelKind(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ["id", "kind"]
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
pet = Field(PetModelKind)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert str(schema) == dedent(
|
||||||
|
"""\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetModelKind {
|
||||||
|
id: ID!
|
||||||
|
kind: TestsPetModelKindChoices!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
pet: PetModelKind
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestsPetModelKindChoices {
|
||||||
|
CAT
|
||||||
|
DOG
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_django_objecttype_choices_custom_enum_name(
|
||||||
|
self, PetModel, graphene_settings
|
||||||
|
):
|
||||||
|
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = (
|
||||||
|
"graphene_django.tests.test_types.custom_enum_name"
|
||||||
|
)
|
||||||
|
|
||||||
|
class PetModelKind(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ["id", "kind"]
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
pet = Field(PetModelKind)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
assert str(schema) == dedent(
|
||||||
|
"""\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CustomEnumKind {
|
||||||
|
CAT
|
||||||
|
DOG
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetModelKind {
|
||||||
|
id: ID!
|
||||||
|
kind: CustomEnumKind!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
pet: PetModelKind
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@with_local_registry
|
||||||
|
def test_django_objecttype_name_connection_propagation():
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
name = "CustomReporterName"
|
||||||
|
filter_fields = ["email"]
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
reporter = Node.Field(Reporter)
|
||||||
|
reporters = DjangoFilterConnectionField(Reporter)
|
||||||
|
|
||||||
|
assert Reporter._meta.name == "CustomReporterName"
|
||||||
|
schema = str(Schema(query=Query))
|
||||||
|
|
||||||
|
assert "type CustomReporterName implements Node {" in schema
|
||||||
|
assert "type CustomReporterNameConnection {" in schema
|
||||||
|
assert "type CustomReporterNameEdge {" in schema
|
||||||
|
|
||||||
|
assert "type Reporter implements Node {" not in schema
|
||||||
|
assert "type ReporterConnection {" not in schema
|
||||||
|
assert "type ReporterEdge {" not in schema
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
from django.utils.translation import gettext_lazy
|
import json
|
||||||
|
|
||||||
from ..utils import camelize, get_model_fields
|
import pytest
|
||||||
|
from django.utils.translation import gettext_lazy
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from ..utils import camelize, get_model_fields, GraphQLTestCase
|
||||||
from .models import Film, Reporter
|
from .models import Film, Reporter
|
||||||
|
from ..utils.testing import graphql_query
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_fields_no_duplication():
|
def test_get_model_fields_no_duplication():
|
||||||
|
@ -30,3 +35,53 @@ def test_camelize():
|
||||||
"valueA": "value_b"
|
"valueA": "value_b"
|
||||||
}
|
}
|
||||||
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
|
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("graphene_django.utils.testing.Client.post")
|
||||||
|
def test_graphql_test_case_op_name(post_mock):
|
||||||
|
"""
|
||||||
|
Test that `GraphQLTestCase.query()`'s `op_name` argument produces an `operationName` field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class TestClass(GraphQLTestCase):
|
||||||
|
GRAPHQL_SCHEMA = True
|
||||||
|
|
||||||
|
def runTest(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
tc = TestClass()
|
||||||
|
tc.setUpClass()
|
||||||
|
tc.query("query { }", op_name="QueryName")
|
||||||
|
body = json.loads(post_mock.call_args.args[1])
|
||||||
|
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
|
||||||
|
assert (
|
||||||
|
"operationName",
|
||||||
|
"QueryName",
|
||||||
|
) in body.items(), "Field 'operationName' is not present in the final request."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("graphene_django.utils.testing.Client.post")
|
||||||
|
def test_graphql_query_case_op_name(post_mock):
|
||||||
|
graphql_query("query { }", op_name="QueryName")
|
||||||
|
body = json.loads(post_mock.call_args.args[1])
|
||||||
|
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
|
||||||
|
assert (
|
||||||
|
"operationName",
|
||||||
|
"QueryName",
|
||||||
|
) in body.items(), "Field 'operationName' is not present in the final request."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_query(client):
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
return graphql_query(*args, client=client, **kwargs)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def test_pytest_fixture_usage(client_query):
|
||||||
|
response = graphql_query("query { test }")
|
||||||
|
content = json.loads(response.content)
|
||||||
|
assert content == {"data": {"test": "Hello World"}}
|
||||||
|
|
|
@ -35,9 +35,15 @@ def construct_fields(
|
||||||
|
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for name, field in _model_fields:
|
for name, field in _model_fields:
|
||||||
is_not_in_only = only_fields and name not in only_fields
|
is_not_in_only = (
|
||||||
|
only_fields is not None
|
||||||
|
and only_fields != ALL_FIELDS
|
||||||
|
and name not in only_fields
|
||||||
|
)
|
||||||
# is_already_created = name in options.fields
|
# is_already_created = name in options.fields
|
||||||
is_excluded = name in exclude_fields # or is_already_created
|
is_excluded = (
|
||||||
|
exclude_fields is not None and name in exclude_fields
|
||||||
|
) # or is_already_created
|
||||||
# https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name
|
# https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name
|
||||||
is_no_backref = str(name).endswith("+")
|
is_no_backref = str(name).endswith("+")
|
||||||
if is_not_in_only or is_excluded or is_no_backref:
|
if is_not_in_only or is_excluded or is_no_backref:
|
||||||
|
@ -65,6 +71,7 @@ def construct_fields(
|
||||||
def validate_fields(type_, model, fields, only_fields, exclude_fields):
|
def validate_fields(type_, model, fields, only_fields, exclude_fields):
|
||||||
# Validate the given fields against the model's fields and custom fields
|
# Validate the given fields against the model's fields and custom fields
|
||||||
all_field_names = set(fields.keys())
|
all_field_names = set(fields.keys())
|
||||||
|
only_fields = only_fields if only_fields is not ALL_FIELDS else ()
|
||||||
for name in only_fields or ():
|
for name in only_fields or ():
|
||||||
if name in all_field_names:
|
if name in all_field_names:
|
||||||
continue
|
continue
|
||||||
|
@ -142,10 +149,10 @@ class DjangoObjectType(ObjectType):
|
||||||
model=None,
|
model=None,
|
||||||
registry=None,
|
registry=None,
|
||||||
skip_registry=False,
|
skip_registry=False,
|
||||||
only_fields=(), # deprecated in favour of `fields`
|
only_fields=None, # deprecated in favour of `fields`
|
||||||
fields=(),
|
fields=None,
|
||||||
exclude_fields=(), # deprecated in favour of `exclude`
|
exclude_fields=None, # deprecated in favour of `exclude`
|
||||||
exclude=(),
|
exclude=None,
|
||||||
filter_fields=None,
|
filter_fields=None,
|
||||||
filterset_class=None,
|
filterset_class=None,
|
||||||
connection=None,
|
connection=None,
|
||||||
|
@ -200,9 +207,6 @@ class DjangoObjectType(ObjectType):
|
||||||
"Got %s." % type(fields).__name__
|
"Got %s." % type(fields).__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
if fields == ALL_FIELDS:
|
|
||||||
fields = None
|
|
||||||
|
|
||||||
# Alias exclude_fields -> exclude
|
# Alias exclude_fields -> exclude
|
||||||
if exclude_fields and exclude:
|
if exclude_fields and exclude:
|
||||||
raise Exception("Can't set both exclude_fields and exclude")
|
raise Exception("Can't set both exclude_fields and exclude")
|
||||||
|
@ -235,7 +239,7 @@ class DjangoObjectType(ObjectType):
|
||||||
connection_class = Connection
|
connection_class = Connection
|
||||||
|
|
||||||
connection = connection_class.create_type(
|
connection = connection_class.create_type(
|
||||||
"{}Connection".format(cls.__name__), node=cls
|
"{}Connection".format(options.get("name") or cls.__name__), node=cls
|
||||||
)
|
)
|
||||||
|
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
|
@ -268,12 +272,9 @@ class DjangoObjectType(ObjectType):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_type_of(cls, root, info):
|
def is_type_of(cls, root, info):
|
||||||
if isinstance(root, SimpleLazyObject):
|
|
||||||
root._setup()
|
|
||||||
root = root._wrapped
|
|
||||||
if isinstance(root, cls):
|
if isinstance(root, cls):
|
||||||
return True
|
return True
|
||||||
if not is_valid_django_model(type(root)):
|
if not is_valid_django_model(root.__class__):
|
||||||
raise Exception(('Received incompatible instance "{}".').format(root))
|
raise Exception(('Received incompatible instance "{}".').format(root))
|
||||||
|
|
||||||
if cls._meta.model._meta.proxy:
|
if cls._meta.model._meta.proxy:
|
||||||
|
|
6
graphene_django/utils/str_converters.py
Normal file
6
graphene_django/utils/str_converters.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import re
|
||||||
|
from unidecode import unidecode
|
||||||
|
|
||||||
|
|
||||||
|
def to_const(string):
|
||||||
|
return re.sub(r"[\W|^]+", "_", unidecode(string)).upper()
|
|
@ -2,6 +2,63 @@ import json
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
DEFAULT_GRAPHQL_URL = "/graphql/"
|
||||||
|
|
||||||
|
|
||||||
|
def graphql_query(
|
||||||
|
query,
|
||||||
|
op_name=None,
|
||||||
|
input_data=None,
|
||||||
|
variables=None,
|
||||||
|
headers=None,
|
||||||
|
client=None,
|
||||||
|
graphql_url=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
query (string) - GraphQL query to run
|
||||||
|
op_name (string) - If the query is a mutation or named query, you must
|
||||||
|
supply the op_name. For annon queries ("{ ... }"),
|
||||||
|
should be None (default).
|
||||||
|
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||||
|
to this value. If both ``input_data`` and ``variables``,
|
||||||
|
are provided, the ``input`` field in the ``variables``
|
||||||
|
dict will be overwritten with this value.
|
||||||
|
variables (dict) - If provided, the "variables" field in GraphQL will be
|
||||||
|
set to this value.
|
||||||
|
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
||||||
|
will be set to this value.
|
||||||
|
client (django.test.Client) - Test client. Defaults to django.test.Client.
|
||||||
|
graphql_url (string) - URL to graphql endpoint. Defaults to "/graphql".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response object from client
|
||||||
|
"""
|
||||||
|
if client is None:
|
||||||
|
client = Client()
|
||||||
|
if not graphql_url:
|
||||||
|
graphql_url = DEFAULT_GRAPHQL_URL
|
||||||
|
|
||||||
|
body = {"query": query}
|
||||||
|
if op_name:
|
||||||
|
body["operationName"] = op_name
|
||||||
|
if variables:
|
||||||
|
body["variables"] = variables
|
||||||
|
if input_data:
|
||||||
|
if "variables" in body:
|
||||||
|
body["variables"]["input"] = input_data
|
||||||
|
else:
|
||||||
|
body["variables"] = {"input": input_data}
|
||||||
|
if headers:
|
||||||
|
resp = client.post(
|
||||||
|
graphql_url, json.dumps(body), content_type="application/json", **headers
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
resp = client.post(
|
||||||
|
graphql_url, json.dumps(body), content_type="application/json"
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class GraphQLTestCase(TestCase):
|
class GraphQLTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -9,18 +66,7 @@ class GraphQLTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# URL to graphql endpoint
|
# URL to graphql endpoint
|
||||||
GRAPHQL_URL = "/graphql/"
|
GRAPHQL_URL = DEFAULT_GRAPHQL_URL
|
||||||
# Here you need to set your graphql schema for the tests
|
|
||||||
GRAPHQL_SCHEMA = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super(GraphQLTestCase, cls).setUpClass()
|
|
||||||
|
|
||||||
if not cls.GRAPHQL_SCHEMA:
|
|
||||||
raise AttributeError(
|
|
||||||
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
|
|
||||||
)
|
|
||||||
|
|
||||||
def query(self, query, op_name=None, input_data=None, variables=None, headers=None):
|
def query(self, query, op_name=None, input_data=None, variables=None, headers=None):
|
||||||
"""
|
"""
|
||||||
|
@ -30,41 +76,28 @@ class GraphQLTestCase(TestCase):
|
||||||
supply the op_name. For annon queries ("{ ... }"),
|
supply the op_name. For annon queries ("{ ... }"),
|
||||||
should be None (default).
|
should be None (default).
|
||||||
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||||
to this value. If both ``input_data`` and ``variables``,
|
to this value. If both ``input_data`` and ``variables``,
|
||||||
are provided, the ``input`` field in the ``variables``
|
are provided, the ``input`` field in the ``variables``
|
||||||
dict will be overwritten with this value.
|
dict will be overwritten with this value.
|
||||||
variables (dict) - If provided, the "variables" field in GraphQL will be
|
variables (dict) - If provided, the "variables" field in GraphQL will be
|
||||||
set to this value.
|
set to this value.
|
||||||
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
||||||
will be set to this value.
|
will be set to this value.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response object from client
|
Response object from client
|
||||||
"""
|
"""
|
||||||
body = {"query": query}
|
return graphql_query(
|
||||||
if op_name:
|
query,
|
||||||
body["operation_name"] = op_name
|
op_name=op_name,
|
||||||
if variables:
|
input_data=input_data,
|
||||||
body["variables"] = variables
|
variables=variables,
|
||||||
if input_data:
|
headers=headers,
|
||||||
if variables in body:
|
client=self.client,
|
||||||
body["variables"]["input"] = input_data
|
graphql_url=self.GRAPHQL_URL,
|
||||||
else:
|
)
|
||||||
body["variables"] = {"input": input_data}
|
|
||||||
if headers:
|
|
||||||
resp = self.client.post(
|
|
||||||
self.GRAPHQL_URL,
|
|
||||||
json.dumps(body),
|
|
||||||
content_type="application/json",
|
|
||||||
**headers
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
resp = self.client.post(
|
|
||||||
self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def assertResponseNoErrors(self, resp):
|
def assertResponseNoErrors(self, resp, msg=None):
|
||||||
"""
|
"""
|
||||||
Assert that the call went through correctly. 200 means the syntax is ok, if there are no `errors`,
|
Assert that the call went through correctly. 200 means the syntax is ok, if there are no `errors`,
|
||||||
the call was fine.
|
the call was fine.
|
||||||
|
@ -72,12 +105,12 @@ class GraphQLTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
content = json.loads(resp.content)
|
content = json.loads(resp.content)
|
||||||
self.assertNotIn("errors", list(content.keys()))
|
self.assertNotIn("errors", list(content.keys()), msg)
|
||||||
|
|
||||||
def assertResponseHasErrors(self, resp):
|
def assertResponseHasErrors(self, resp, msg=None):
|
||||||
"""
|
"""
|
||||||
Assert that the call was failing. Take care: Even with errors, GraphQL returns status 200!
|
Assert that the call was failing. Take care: Even with errors, GraphQL returns status 200!
|
||||||
:resp HttpResponse: Response
|
:resp HttpResponse: Response
|
||||||
"""
|
"""
|
||||||
content = json.loads(resp.content)
|
content = json.loads(resp.content)
|
||||||
self.assertIn("errors", list(content.keys()))
|
self.assertIn("errors", list(content.keys()), msg)
|
||||||
|
|
0
graphene_django/utils/tests/__init__.py
Normal file
0
graphene_django/utils/tests/__init__.py
Normal file
10
graphene_django/utils/tests/test_str_converters.py
Normal file
10
graphene_django/utils/tests/test_str_converters.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# coding: utf-8
|
||||||
|
from ..str_converters import to_const
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_const():
|
||||||
|
assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_const_unicode():
|
||||||
|
assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF"
|
|
@ -15,6 +15,7 @@ from graphql.error import format_error as format_graphql_error
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
from graphql.execution import ExecutionResult
|
from graphql.execution import ExecutionResult
|
||||||
from graphql.type.schema import GraphQLSchema
|
from graphql.type.schema import GraphQLSchema
|
||||||
|
from graphql.execution.middleware import MiddlewareManager
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
|
|
||||||
|
@ -51,9 +52,27 @@ def instantiate_middleware(middlewares):
|
||||||
|
|
||||||
|
|
||||||
class GraphQLView(View):
|
class GraphQLView(View):
|
||||||
graphiql_version = "0.14.0"
|
|
||||||
graphiql_template = "graphene/graphiql.html"
|
graphiql_template = "graphene/graphiql.html"
|
||||||
react_version = "16.8.6"
|
|
||||||
|
# Polyfill for window.fetch.
|
||||||
|
whatwg_fetch_version = "3.2.0"
|
||||||
|
whatwg_fetch_sri = "sha256-l6HCB9TT2v89oWbDdo2Z3j+PSVypKNLA/nqfzSbM8mo="
|
||||||
|
|
||||||
|
# React and ReactDOM.
|
||||||
|
react_version = "16.13.1"
|
||||||
|
react_sri = "sha256-yUhvEmYVhZ/GGshIQKArLvySDSh6cdmdcIx0spR3UP4="
|
||||||
|
react_dom_sri = "sha256-vFt3l+illeNlwThbDUdoPTqF81M8WNSZZZt3HEjsbSU="
|
||||||
|
|
||||||
|
# The GraphiQL React app.
|
||||||
|
graphiql_version = "1.0.3"
|
||||||
|
graphiql_sri = "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
||||||
|
graphiql_css_sri = "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
||||||
|
|
||||||
|
# The websocket transport library for subscriptions.
|
||||||
|
subscriptions_transport_ws_version = "0.9.17"
|
||||||
|
subscriptions_transport_ws_sri = (
|
||||||
|
"sha256-kCDzver8iRaIQ/SVlfrIwxaBQ/avXf9GQFJRLlErBnk="
|
||||||
|
)
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
|
@ -63,6 +82,7 @@ class GraphQLView(View):
|
||||||
root_value = None
|
root_value = None
|
||||||
pretty = False
|
pretty = False
|
||||||
batch = False
|
batch = False
|
||||||
|
subscription_path = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -74,6 +94,7 @@ class GraphQLView(View):
|
||||||
pretty=False,
|
pretty=False,
|
||||||
batch=False,
|
batch=False,
|
||||||
backend=None,
|
backend=None,
|
||||||
|
subscription_path=None,
|
||||||
):
|
):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
@ -86,13 +107,18 @@ class GraphQLView(View):
|
||||||
|
|
||||||
self.schema = self.schema or schema
|
self.schema = self.schema or schema
|
||||||
if middleware is not None:
|
if middleware is not None:
|
||||||
self.middleware = list(instantiate_middleware(middleware))
|
if isinstance(middleware, MiddlewareManager):
|
||||||
|
self.middleware = middleware
|
||||||
|
else:
|
||||||
|
self.middleware = list(instantiate_middleware(middleware))
|
||||||
self.executor = executor
|
self.executor = executor
|
||||||
self.root_value = root_value
|
self.root_value = root_value
|
||||||
self.pretty = self.pretty or pretty
|
self.pretty = self.pretty or pretty
|
||||||
self.graphiql = self.graphiql or graphiql
|
self.graphiql = self.graphiql or graphiql
|
||||||
self.batch = self.batch or batch
|
self.batch = self.batch or batch
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
if subscription_path is None:
|
||||||
|
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
self.schema, GraphQLSchema
|
self.schema, GraphQLSchema
|
||||||
|
@ -128,8 +154,21 @@ class GraphQLView(View):
|
||||||
if show_graphiql:
|
if show_graphiql:
|
||||||
return self.render_graphiql(
|
return self.render_graphiql(
|
||||||
request,
|
request,
|
||||||
graphiql_version=self.graphiql_version,
|
# Dependency parameters.
|
||||||
|
whatwg_fetch_version=self.whatwg_fetch_version,
|
||||||
|
whatwg_fetch_sri=self.whatwg_fetch_sri,
|
||||||
react_version=self.react_version,
|
react_version=self.react_version,
|
||||||
|
react_sri=self.react_sri,
|
||||||
|
react_dom_sri=self.react_dom_sri,
|
||||||
|
graphiql_version=self.graphiql_version,
|
||||||
|
graphiql_sri=self.graphiql_sri,
|
||||||
|
graphiql_css_sri=self.graphiql_css_sri,
|
||||||
|
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
|
||||||
|
subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri,
|
||||||
|
# The SUBSCRIPTION_PATH setting.
|
||||||
|
subscription_path=self.subscription_path,
|
||||||
|
# GraphiQL headers tab,
|
||||||
|
graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.batch:
|
if self.batch:
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
DJANGO_SETTINGS_MODULE = examples.django_test_settings
|
||||||
|
|
19
setup.py
19
setup.py
|
@ -26,10 +26,10 @@ tests_require = [
|
||||||
|
|
||||||
|
|
||||||
dev_requires = [
|
dev_requires = [
|
||||||
"black==19.3b0",
|
"black==19.10b0",
|
||||||
"flake8==3.7.7",
|
"flake8==3.7.9",
|
||||||
"flake8-black==0.1.0",
|
"flake8-black==0.1.1",
|
||||||
"flake8-bugbear==19.3.0",
|
"flake8-bugbear==20.1.4",
|
||||||
] + tests_require
|
] + tests_require
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -48,13 +48,17 @@ setup(
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
"Programming Language :: Python :: 2.7",
|
"Programming Language :: Python :: 2.7",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.4",
|
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Framework :: Django",
|
||||||
|
"Framework :: Django :: 1.11",
|
||||||
|
"Framework :: Django :: 2.2",
|
||||||
|
"Framework :: Django :: 3.0",
|
||||||
],
|
],
|
||||||
keywords="api graphql protocol rest relay graphene",
|
keywords="api graphql protocol rest relay graphene",
|
||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests", "examples", "examples.*"]),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"six>=1.10.0",
|
"six>=1.10.0",
|
||||||
"graphene>=2.1.7,<3",
|
"graphene>=2.1.7,<3",
|
||||||
|
@ -62,6 +66,7 @@ setup(
|
||||||
"Django>=1.11",
|
"Django>=1.11",
|
||||||
"singledispatch>=3.4.0.3",
|
"singledispatch>=3.4.0.3",
|
||||||
"promise>=2.1",
|
"promise>=2.1",
|
||||||
|
"unidecode>=1.1.1,<2",
|
||||||
],
|
],
|
||||||
setup_requires=["pytest-runner"],
|
setup_requires=["pytest-runner"],
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
|
|
26
tox.ini
26
tox.ini
|
@ -1,42 +1,52 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{27,35,36,37}-django{111,20,21,22,master},
|
py{27,35,36,37,38}-django{111,20,21,22,master},
|
||||||
py{36,37}-django30,
|
py{36,37,38}-django{30,31},
|
||||||
black,flake8
|
black,flake8
|
||||||
|
|
||||||
[travis:env]
|
[gh-actions]
|
||||||
|
python =
|
||||||
|
2.7: py27
|
||||||
|
3.6: py36
|
||||||
|
3.7: py37
|
||||||
|
3.8: py38
|
||||||
|
|
||||||
|
[gh-actions:env]
|
||||||
DJANGO =
|
DJANGO =
|
||||||
1.11: django111
|
1.11: django111
|
||||||
2.0: django20
|
2.0: django20
|
||||||
2.1: django21
|
2.1: django21
|
||||||
2.2: django22
|
2.2: django22
|
||||||
3.0: django30
|
3.0: django30
|
||||||
|
3.1: django31
|
||||||
master: djangomaster
|
master: djangomaster
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
passenv = *
|
passenv = *
|
||||||
usedevelop = True
|
usedevelop = True
|
||||||
setenv =
|
setenv =
|
||||||
DJANGO_SETTINGS_MODULE=django_test_settings
|
DJANGO_SETTINGS_MODULE=examples.django_test_settings
|
||||||
deps =
|
deps =
|
||||||
-e.[test]
|
-e.[test]
|
||||||
psycopg2
|
psycopg2-binary
|
||||||
django111: Django>=1.11,<2.0
|
django111: Django>=1.11,<2.0
|
||||||
|
django111: djangorestframework<3.12
|
||||||
django20: Django>=2.0,<2.1
|
django20: Django>=2.0,<2.1
|
||||||
django21: Django>=2.1,<2.2
|
django21: Django>=2.1,<2.2
|
||||||
django22: Django>=2.2,<3.0
|
django22: Django>=2.2,<3.0
|
||||||
django30: Django>=3.0a1,<3.1
|
django30: Django>=3.0a1,<3.1
|
||||||
|
django31: Django>=3.1,<3.2
|
||||||
djangomaster: https://github.com/django/django/archive/master.zip
|
djangomaster: https://github.com/django/django/archive/master.zip
|
||||||
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
commands = {posargs:py.test --cov=graphene_django graphene_django examples}
|
||||||
|
|
||||||
[testenv:black]
|
[testenv:black]
|
||||||
basepython = python3.7
|
basepython = python3.8
|
||||||
deps = -e.[dev]
|
deps = -e.[dev]
|
||||||
commands =
|
commands =
|
||||||
black --exclude "/migrations/" graphene_django examples setup.py --check
|
black --exclude "/migrations/" graphene_django examples setup.py --check
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
basepython = python3.7
|
basepython = python3.8
|
||||||
deps = -e.[dev]
|
deps = -e.[dev]
|
||||||
commands =
|
commands =
|
||||||
flake8 graphene_django examples
|
flake8 graphene_django examples setup.py
|
||||||
|
|
Loading…
Reference in New Issue
Block a user