mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-11 16:52:25 +03:00
Merge branch 'v3' into chore/default-camelcase-errors
This commit is contained in:
commit
18dac205ea
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
|
@ -1,11 +1,15 @@
|
||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 60
|
daysUntilStale: 120
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 7
|
daysUntilClose: 30
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
|
- 🐛bug
|
||||||
|
- 📖 documentation
|
||||||
|
- help wanted
|
||||||
|
- ✨enhancement
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
|
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
|
28
.github/workflows/tests.yml
vendored
Normal file
28
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
django: ["2.2", "3.0"]
|
||||||
|
python-version: ["3.6", "3.7", "3.8"]
|
||||||
|
|
||||||
|
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 }}
|
65
.travis.yml
65
.travis.yml
|
@ -1,65 +0,0 @@
|
||||||
language: python
|
|
||||||
cache: pip
|
|
||||||
dist: xenial
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox tox-travis
|
|
||||||
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- pip install coveralls
|
|
||||||
- coveralls
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
include:
|
|
||||||
- python: 2.7
|
|
||||||
env: DJANGO=1.11
|
|
||||||
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=2.0
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=2.1
|
|
||||||
- python: 3.5
|
|
||||||
env: DJANGO=2.2
|
|
||||||
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=2.0
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=2.1
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.6
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=1.11
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.0
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.1
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=2.2
|
|
||||||
- python: 3.7
|
|
||||||
env: DJANGO=master
|
|
||||||
|
|
||||||
- python: 3.7
|
|
||||||
env: TOXENV=black,flake8
|
|
||||||
|
|
||||||
allow_failures:
|
|
||||||
- env: DJANGO=master
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: syrusakbary
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
password:
|
|
||||||
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
|
|
||||||
distributions: "sdist bdist_wheel"
|
|
30
README.md
30
README.md
|
@ -1,12 +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 [](https://travis-ci.org/graphql-python/graphene-django) [](https://badge.fury.io/py/graphene-django) [](https://coveralls.io/github/graphql-python/graphene-django?branch=master)
|
|
||||||
|
|
||||||
|
|
||||||
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/).
|
||||||
|
|
||||||
|
[![travis][travis-image]][travis-url]
|
||||||
|
[![pypi][pypi-image]][pypi-url]
|
||||||
|
[![Anaconda-Server Badge][conda-image]][conda-url]
|
||||||
|
[![coveralls][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
[travis-image]: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master&style=flat
|
||||||
|
[travis-url]: https://travis-ci.org/graphql-python/graphene-django
|
||||||
|
[pypi-image]: https://img.shields.io/pypi/v/graphene-django.svg?style=flat
|
||||||
|
[pypi-url]: https://pypi.org/project/graphene-django/
|
||||||
|
[coveralls-image]: https://coveralls.io/repos/github/graphql-python/graphene-django/badge.svg?branch=master
|
||||||
|
[coveralls-url]: https://coveralls.io/github/graphql-python/graphene-django?branch=master
|
||||||
|
[conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg
|
||||||
|
[conda-url]: https://anaconda.org/conda-forge/graphene-django
|
||||||
|
|
||||||
|
[💬 Join the community on Slack](https://join.slack.com/t/graphenetools/shared_invite/enQtOTE2MDQ1NTg4MDM1LTA4Nzk0MGU0NGEwNzUxZGNjNDQ4ZjAwNDJjMjY0OGE1ZDgxZTg4YjM2ZTc4MjE2ZTAzZjE2ZThhZTQzZTkyMmM)
|
||||||
|
|
||||||
## Documentation
|
## 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/)
|
||||||
|
@ -16,7 +28,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
|
||||||
For installing graphene, just run this command in your shell
|
For installing graphene, just run this command in your shell
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "graphene-django>=2.0"
|
pip install "graphene-django>=3"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
@ -78,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 = '''
|
||||||
|
@ -101,3 +113,7 @@ To learn more check out the following [examples](examples/):
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Release Notes
|
||||||
|
|
||||||
|
* See [Releases page on github](https://github.com/graphql-python/graphene-django/releases)
|
||||||
|
|
|
@ -23,7 +23,7 @@ For installing graphene, just run this command in your shell
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
pip install "graphene-django>=2.0"
|
pip install "graphene-django>=3"
|
||||||
|
|
||||||
Settings
|
Settings
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
|
@ -166,16 +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 2.2 and above:
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# some other urls
|
|
||||||
url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
|
||||||
]
|
|
||||||
|
|
||||||
For Django 2.0 and above:
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -184,4 +175,4 @@ For Django 2.0 and above:
|
||||||
path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
||||||
]
|
]
|
||||||
|
|
||||||
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin
|
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin
|
||||||
|
|
16
docs/conf.py
16
docs/conf.py
|
@ -60,18 +60,18 @@ source_suffix = ".rst"
|
||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u"Graphene Django"
|
project = "Graphene Django"
|
||||||
copyright = u"Graphene 2017"
|
copyright = "Graphene 2017"
|
||||||
author = u"Syrus Akbary"
|
author = "Syrus Akbary"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u"1.0"
|
version = "1.0"
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = u"1.0.dev"
|
release = "1.0.dev"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -276,7 +276,7 @@ latex_elements = {
|
||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual")
|
(master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual")
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
@ -317,7 +317,7 @@ latex_documents = [
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
(master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
|
(master_doc, "graphene_django", "Graphene Django Documentation", [author], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
# If true, show URL addresses after external links.
|
||||||
|
@ -334,7 +334,7 @@ texinfo_documents = [
|
||||||
(
|
(
|
||||||
master_doc,
|
master_doc,
|
||||||
"Graphene-Django",
|
"Graphene-Django",
|
||||||
u"Graphene Django Documentation",
|
"Graphene Django Documentation",
|
||||||
author,
|
author,
|
||||||
"Graphene Django",
|
"Graphene Django",
|
||||||
"One line description of project.",
|
"One line description of project.",
|
||||||
|
|
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 queryeset 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*
|
|
@ -2,9 +2,8 @@ Filtering
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Graphene integrates with
|
Graphene integrates with
|
||||||
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
|
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ to provide filtering of results.
|
||||||
Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
|
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>`__
|
|
||||||
for details on the format for ``filter_fields``.
|
for details on the format for ``filter_fields``.
|
||||||
|
|
||||||
This filtering is automatically available when implementing a ``relay.Node``.
|
This filtering is automatically available when implementing a ``relay.Node``.
|
||||||
|
@ -14,9 +13,18 @@ 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>`__.
|
||||||
|
|
||||||
|
@ -115,6 +123,15 @@ create your own ``FilterSet``. You can pass it directly as follows:
|
||||||
class AnimalFilter(django_filters.FilterSet):
|
class AnimalFilter(django_filters.FilterSet):
|
||||||
# Do case-insensitive lookups on 'name'
|
# Do case-insensitive lookups on 'name'
|
||||||
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
||||||
|
# Allow multiple genera to be selected at once
|
||||||
|
genera = django_filters.MultipleChoiceFilter(
|
||||||
|
field_name='genus',
|
||||||
|
choices=(
|
||||||
|
('Canis', 'Canis'),
|
||||||
|
('Panthera', 'Panthera'),
|
||||||
|
('Seahorse', 'Seahorse')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Animal
|
model = Animal
|
||||||
|
@ -127,6 +144,22 @@ create your own ``FilterSet``. You can pass it directly as follows:
|
||||||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||||
filterset_class=AnimalFilter)
|
filterset_class=AnimalFilter)
|
||||||
|
|
||||||
|
|
||||||
|
If you were interested in selecting all dogs and cats, you might query as follows:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
query {
|
||||||
|
allAnimals(genera: ["Canis", "Panthera"]) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
You can also specify the ``FilterSet`` class using the ``filterset_class``
|
You can also specify the ``FilterSet`` class using the ``filterset_class``
|
||||||
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
||||||
in unison with the ``filter_fields`` parameter:
|
in unison with the ``filter_fields`` parameter:
|
||||||
|
@ -154,6 +187,7 @@ in unison with the ``filter_fields`` parameter:
|
||||||
animal = relay.Node.Field(AnimalNode)
|
animal = relay.Node.Field(AnimalNode)
|
||||||
all_animals = DjangoFilterConnectionField(AnimalNode)
|
all_animals = DjangoFilterConnectionField(AnimalNode)
|
||||||
|
|
||||||
|
|
||||||
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
|
||||||
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
||||||
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||||
|
|
|
@ -25,6 +25,8 @@ For more advanced use, check out the Relay tutorial.
|
||||||
tutorial-relay
|
tutorial-relay
|
||||||
schema
|
schema
|
||||||
queries
|
queries
|
||||||
|
fields
|
||||||
|
extra-types
|
||||||
mutations
|
mutations
|
||||||
filtering
|
filtering
|
||||||
authorization
|
authorization
|
||||||
|
|
|
@ -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 2.2
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
@ -32,6 +32,8 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
|
||||||
|
|
||||||
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
|
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
|
||||||
|
|
||||||
|
For Django 2.2 and above:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
@ -67,3 +69,25 @@ The most basic ``schema.py`` looks like this:
|
||||||
|
|
||||||
|
|
||||||
To learn how to extend the schema object for your project, read the basic tutorial.
|
To learn how to extend the schema object for your project, read the basic tutorial.
|
||||||
|
|
||||||
|
CSRF exempt
|
||||||
|
-----------
|
||||||
|
|
||||||
|
If have enabled `CSRF protection <https://docs.djangoproject.com/en/3.0/ref/csrf/>`_ in your Django app
|
||||||
|
you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either
|
||||||
|
update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt``
|
||||||
|
decorator:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# urls.py
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# ...
|
||||||
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||||
|
]
|
||||||
|
|
|
@ -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
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -44,7 +58,8 @@ specify the parameters in your settings.py:
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'tutorial.quickstart.schema',
|
'SCHEMA': 'tutorial.quickstart.schema',
|
||||||
'SCHEMA_OUTPUT': 'data/schema.json' # defaults to schema.json
|
'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json,
|
||||||
|
'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _queries-objecttypes:
|
||||||
|
|
||||||
Queries & ObjectTypes
|
Queries & ObjectTypes
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
@ -205,6 +207,8 @@ need to create the most basic class for this to work:
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
|
|
||||||
|
.. _django-objecttype-get-queryset:
|
||||||
|
|
||||||
Default QuerySet
|
Default QuerySet
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -282,6 +286,13 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
|
||||||
return Question.objects.none()
|
return Question.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
DjangoObjectTypes
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset.
|
||||||
|
Queryset methods like `values` will return dictionaries, use `defer` instead.
|
||||||
|
|
||||||
|
|
||||||
Plain ObjectTypes
|
Plain ObjectTypes
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
58
docs/schema.py
Normal file
58
docs/schema.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import graphene
|
||||||
|
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
|
class Query(object):
|
||||||
|
category = graphene.Field(CategoryType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_categories = graphene.List(CategoryType)
|
||||||
|
|
||||||
|
|
||||||
|
ingredient = graphene.Field(IngredientType,
|
||||||
|
id=graphene.Int(),
|
||||||
|
name=graphene.String())
|
||||||
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
def resolve_all_categories(self, info, **kwargs):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def resolve_all_ingredients(self, info, **kwargs):
|
||||||
|
return Ingredient.objects.all()
|
||||||
|
|
||||||
|
def resolve_category(self, info, **kwargs):
|
||||||
|
id = kwargs.get('id')
|
||||||
|
name = kwargs.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Category.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Category.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_ingredient(self, info, **kwargs):
|
||||||
|
id = kwargs.get('id')
|
||||||
|
name = kwargs.get('name')
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
return Ingredient.objects.get(name=name)
|
||||||
|
|
||||||
|
return None
|
|
@ -140,3 +140,33 @@ 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"
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ Testing API calls with django
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -286,7 +286,7 @@ from the command line.
|
||||||
$ python ./manage.py runserver
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.9, using settings 'cookbook.settings'
|
Django version 1.11, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
@ -417,67 +417,10 @@ Getting single objects
|
||||||
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
||||||
|
|
||||||
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
||||||
|
Add the **Highlighted** lines to ``cookbook/ingredients/schema.py``
|
||||||
|
|
||||||
.. code:: python
|
.. literalinclude:: schema.py
|
||||||
|
:emphasize-lines: 19-21,25-27,36-58
|
||||||
import graphene
|
|
||||||
|
|
||||||
from graphene_django.types import DjangoObjectType
|
|
||||||
|
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Category
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Ingredient
|
|
||||||
|
|
||||||
|
|
||||||
class Query(object):
|
|
||||||
category = graphene.Field(CategoryType,
|
|
||||||
id=graphene.Int(),
|
|
||||||
name=graphene.String())
|
|
||||||
all_categories = graphene.List(CategoryType)
|
|
||||||
|
|
||||||
|
|
||||||
ingredient = graphene.Field(IngredientType,
|
|
||||||
id=graphene.Int(),
|
|
||||||
name=graphene.String())
|
|
||||||
all_ingredients = graphene.List(IngredientType)
|
|
||||||
|
|
||||||
def resolve_all_categories(self, info, **kwargs):
|
|
||||||
return Category.objects.all()
|
|
||||||
|
|
||||||
def resolve_all_ingredients(self, info, **kwargs):
|
|
||||||
return Ingredient.objects.all()
|
|
||||||
|
|
||||||
def resolve_category(self, info, **kwargs):
|
|
||||||
id = kwargs.get('id')
|
|
||||||
name = kwargs.get('name')
|
|
||||||
|
|
||||||
if id is not None:
|
|
||||||
return Category.objects.get(pk=id)
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
return Category.objects.get(name=name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def resolve_ingredient(self, info, **kwargs):
|
|
||||||
id = kwargs.get('id')
|
|
||||||
name = kwargs.get('name')
|
|
||||||
|
|
||||||
if id is not None:
|
|
||||||
return Ingredient.objects.get(pk=id)
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
return Ingredient.objects.get(name=name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
Now, with the code in place, we can query for single objects.
|
Now, with the code in place, we can query for single objects.
|
||||||
|
|
||||||
|
|
|
@ -277,7 +277,7 @@ from the command line.
|
||||||
$ python ./manage.py runserver
|
$ python ./manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.9, using settings 'cookbook.settings'
|
Django version 1.11, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
graphene
|
graphene>=2.1,<3
|
||||||
graphene-django
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1rc1
|
graphql-core>=2.1,<3
|
||||||
django==2.2.4
|
django==3.0.3
|
||||||
|
|
|
@ -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
|
graphene>=2.1,<3
|
||||||
graphene-django
|
graphene-django>=2.1,<3
|
||||||
graphql-core>=2.1rc1
|
graphql-core>=2.1,<3
|
||||||
django==2.2.4
|
django==3.0.3
|
||||||
django-filter>=2
|
django-filter>=2
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
from .fields import DjangoConnectionField
|
|
||||||
|
|
||||||
__version__ = "2.6.0"
|
__version__ = "3.0.0b1"
|
||||||
|
|
||||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"DjangoObjectType",
|
||||||
|
"DjangoListField",
|
||||||
|
"DjangoConnectionField",
|
||||||
|
]
|
||||||
|
|
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,10 +1,16 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from django.db import models
|
from functools import singledispatch
|
||||||
from django.utils.encoding import force_text
|
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import force_str
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from graphene import (
|
from graphene import (
|
||||||
ID,
|
ID,
|
||||||
|
UUID,
|
||||||
Boolean,
|
Boolean,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
Dynamic,
|
Dynamic,
|
||||||
Enum,
|
Enum,
|
||||||
Field,
|
Field,
|
||||||
|
@ -13,27 +19,23 @@ from graphene import (
|
||||||
List,
|
List,
|
||||||
NonNull,
|
NonNull,
|
||||||
String,
|
String,
|
||||||
UUID,
|
|
||||||
DateTime,
|
|
||||||
Date,
|
|
||||||
Time,
|
Time,
|
||||||
)
|
)
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
from graphene.utils.str_converters import to_camel_case, to_const
|
from graphene.utils.str_converters import to_camel_case, to_const
|
||||||
from graphql import assert_valid_name
|
from graphql import GraphQLError, assert_valid_name
|
||||||
|
from graphql.pyutils import register_description
|
||||||
|
|
||||||
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
from .compat import ArrayField, HStoreField, JSONField, RangeField
|
||||||
from .fields import DjangoListField, DjangoConnectionField
|
from .fields import DjangoConnectionField, DjangoListField
|
||||||
from .utils import import_single_dispatch
|
from .settings import graphene_settings
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
|
||||||
|
|
||||||
|
|
||||||
def convert_choice_name(name):
|
def convert_choice_name(name):
|
||||||
name = to_const(force_text(name))
|
name = to_const(force_str(name))
|
||||||
try:
|
try:
|
||||||
assert_valid_name(name)
|
assert_valid_name(name)
|
||||||
except AssertionError:
|
except GraphQLError:
|
||||||
name = "A_%s" % name
|
name = "A_%s" % name
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
@ -51,7 +53,9 @@ def get_choices(choices):
|
||||||
while name in converted_names:
|
while name in converted_names:
|
||||||
name += "_" + str(len(converted_names))
|
name += "_" + str(len(converted_names))
|
||||||
converted_names.append(name)
|
converted_names.append(name)
|
||||||
description = help_text
|
description = str(
|
||||||
|
help_text
|
||||||
|
) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58
|
||||||
yield name, value, description
|
yield name, value, description
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,11 +67,36 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
|
||||||
class EnumWithDescriptionsType(object):
|
class EnumWithDescriptionsType(object):
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
return named_choices_descriptions[self.name]
|
return str(named_choices_descriptions[self.name])
|
||||||
|
|
||||||
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 +106,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 +154,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,3 +275,8 @@ def convert_postgres_range_to_string(field, registry=None):
|
||||||
if not isinstance(inner_type, (List, NonNull)):
|
if not isinstance(inner_type, (List, NonNull)):
|
||||||
inner_type = type(inner_type)
|
inner_type = 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)
|
||||||
|
|
||||||
|
|
||||||
|
# Register Django lazy()-wrapped values as GraphQL description/help_text.
|
||||||
|
# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58.
|
||||||
|
register_description(Promise)
|
||||||
|
|
|
@ -17,7 +17,7 @@ class DjangoDebugContext(object):
|
||||||
if not self.debug_promise:
|
if not self.debug_promise:
|
||||||
self.debug_promise = Promise.all(self.promises)
|
self.debug_promise = Promise.all(self.promises)
|
||||||
self.promises = []
|
self.promises = []
|
||||||
return self.debug_promise.then(self.on_resolve_all_promises)
|
return self.debug_promise.then(self.on_resolve_all_promises).get()
|
||||||
|
|
||||||
def on_resolve_all_promises(self, values):
|
def on_resolve_all_promises(self, values):
|
||||||
if self.promises:
|
if self.promises:
|
||||||
|
|
|
@ -5,8 +5,7 @@ import json
|
||||||
from threading import local
|
from threading import local
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from django.utils import six
|
from django.utils.encoding import force_str
|
||||||
from django.utils.encoding import force_text
|
|
||||||
|
|
||||||
from .types import DjangoDebugSQL
|
from .types import DjangoDebugSQL
|
||||||
|
|
||||||
|
@ -77,8 +76,8 @@ class NormalCursorWrapper(object):
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
def _quote_expr(self, element):
|
def _quote_expr(self, element):
|
||||||
if isinstance(element, six.string_types):
|
if isinstance(element, str):
|
||||||
return "'%s'" % force_text(element).replace("'", "''")
|
return "'%s'" % force_str(element).replace("'", "''")
|
||||||
else:
|
else:
|
||||||
return repr(element)
|
return repr(element)
|
||||||
|
|
||||||
|
@ -91,7 +90,7 @@ class NormalCursorWrapper(object):
|
||||||
|
|
||||||
def _decode(self, param):
|
def _decode(self, param):
|
||||||
try:
|
try:
|
||||||
return force_text(param, strings_only=True)
|
return force_str(param, strings_only=True)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return "(encoded string)"
|
return "(encoded string)"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoConnectionField, DjangoObjectType
|
from graphene_django import DjangoConnectionField, DjangoObjectType
|
||||||
|
@ -13,11 +11,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()
|
||||||
|
@ -75,7 +68,7 @@ def test_should_query_nested_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()
|
||||||
|
@ -89,7 +82,7 @@ def test_should_query_nested_field():
|
||||||
pets { edges { node { lastName } } }
|
pets { edges { node { lastName } } }
|
||||||
} } }
|
} } }
|
||||||
}
|
}
|
||||||
__debug {
|
_debug {
|
||||||
sql {
|
sql {
|
||||||
rawSql
|
rawSql
|
||||||
}
|
}
|
||||||
|
@ -117,12 +110,12 @@ def test_should_query_nested_field():
|
||||||
)
|
)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
query = str(Reporter.objects.order_by("pk")[:1].query)
|
query = str(Reporter.objects.order_by("pk")[:1].query)
|
||||||
assert result.data["__debug"]["sql"][0]["rawSql"] == query
|
assert result.data["_debug"]["sql"][0]["rawSql"] == query
|
||||||
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
|
assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"]
|
||||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
|
assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"]
|
||||||
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
|
assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"]
|
||||||
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
|
assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"]
|
||||||
assert len(result.data["__debug"]["sql"]) == 5
|
assert len(result.data["_debug"]["sql"]) == 5
|
||||||
|
|
||||||
assert result.data["reporter"] == expected["reporter"]
|
assert result.data["reporter"] == expected["reporter"]
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from graphql_relay.connection.arrayconnection import connection_from_list_slice
|
from graphql_relay.connection.arrayconnection import connection_from_array_slice
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
|
||||||
from graphene import NonNull
|
from graphene import NonNull
|
||||||
from graphene.relay import ConnectionField, PageInfo
|
from graphene.relay import ConnectionField
|
||||||
|
from graphene.relay.connection import connection_adapter, page_info_adapter
|
||||||
from graphene.types import Field, List
|
from graphene.types import Field, List
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
|
@ -19,30 +20,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 = django_object_type._meta.model
|
if isinstance(queryset, QuerySet):
|
||||||
queryset = maybe_queryset(
|
# Pass queryset to the DjangoObjectType get_queryset method
|
||||||
django_object_type.get_queryset(model.objects, 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 +60,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):
|
||||||
|
@ -108,37 +123,25 @@ class DjangoConnectionField(ConnectionField):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_queryset(cls, connection, queryset, info, args):
|
def resolve_queryset(cls, connection, queryset, info, args):
|
||||||
|
# queryset is the resolved iterable from ObjectType
|
||||||
return connection._meta.node.get_queryset(queryset, info)
|
return connection._meta.node.get_queryset(queryset, info)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def merge_querysets(cls, default_queryset, queryset):
|
def resolve_connection(cls, connection, args, iterable):
|
||||||
if default_queryset.query.distinct and not queryset.query.distinct:
|
|
||||||
queryset = queryset.distinct()
|
|
||||||
elif queryset.query.distinct and not default_queryset.query.distinct:
|
|
||||||
default_queryset = default_queryset.distinct()
|
|
||||||
return queryset & default_queryset
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def resolve_connection(cls, connection, default_manager, args, iterable):
|
|
||||||
if iterable is None:
|
|
||||||
iterable = default_manager
|
|
||||||
iterable = maybe_queryset(iterable)
|
iterable = maybe_queryset(iterable)
|
||||||
if isinstance(iterable, QuerySet):
|
if isinstance(iterable, QuerySet):
|
||||||
if iterable.model.objects is not default_manager:
|
|
||||||
default_queryset = maybe_queryset(default_manager)
|
|
||||||
iterable = cls.merge_querysets(default_queryset, iterable)
|
|
||||||
_len = iterable.count()
|
_len = iterable.count()
|
||||||
else:
|
else:
|
||||||
_len = len(iterable)
|
_len = len(iterable)
|
||||||
connection = connection_from_list_slice(
|
connection = connection_from_array_slice(
|
||||||
iterable,
|
iterable,
|
||||||
args,
|
args,
|
||||||
slice_start=0,
|
slice_start=0,
|
||||||
list_length=_len,
|
array_length=_len,
|
||||||
list_slice_length=_len,
|
array_slice_length=_len,
|
||||||
connection_type=connection,
|
connection_type=partial(connection_adapter, connection),
|
||||||
edge_type=connection.Edge,
|
edge_type=connection.Edge,
|
||||||
pageinfo_type=PageInfo,
|
page_info_type=page_info_adapter,
|
||||||
)
|
)
|
||||||
connection.iterable = iterable
|
connection.iterable = iterable
|
||||||
connection.length = _len
|
connection.length = _len
|
||||||
|
@ -150,6 +153,7 @@ class DjangoConnectionField(ConnectionField):
|
||||||
resolver,
|
resolver,
|
||||||
connection,
|
connection,
|
||||||
default_manager,
|
default_manager,
|
||||||
|
queryset_resolver,
|
||||||
max_limit,
|
max_limit,
|
||||||
enforce_first_or_last,
|
enforce_first_or_last,
|
||||||
root,
|
root,
|
||||||
|
@ -177,9 +181,15 @@ class DjangoConnectionField(ConnectionField):
|
||||||
).format(last, info.field_name, max_limit)
|
).format(last, info.field_name, max_limit)
|
||||||
args["last"] = min(last, max_limit)
|
args["last"] = min(last, max_limit)
|
||||||
|
|
||||||
|
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
|
||||||
|
# or a resolve_foo (does not accept queryset)
|
||||||
iterable = resolver(root, info, **args)
|
iterable = resolver(root, info, **args)
|
||||||
queryset = cls.resolve_queryset(connection, default_manager, info, args)
|
if iterable is None:
|
||||||
on_resolve = partial(cls.resolve_connection, connection, queryset, args)
|
iterable = default_manager
|
||||||
|
# thus the iterable gets refiltered by resolve_queryset
|
||||||
|
# but iterable might be promise
|
||||||
|
iterable = queryset_resolver(connection, iterable, info, args)
|
||||||
|
on_resolve = partial(cls.resolve_connection, connection, args)
|
||||||
|
|
||||||
if Promise.is_thenable(iterable):
|
if Promise.is_thenable(iterable):
|
||||||
return Promise.resolve(iterable).then(on_resolve)
|
return Promise.resolve(iterable).then(on_resolve)
|
||||||
|
@ -192,6 +202,10 @@ class DjangoConnectionField(ConnectionField):
|
||||||
parent_resolver,
|
parent_resolver,
|
||||||
self.connection_type,
|
self.connection_type,
|
||||||
self.get_manager(),
|
self.get_manager(),
|
||||||
|
self.get_queryset_resolver(),
|
||||||
self.max_limit,
|
self.max_limit,
|
||||||
self.enforce_first_or_last,
|
self.enforce_first_or_last,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_queryset_resolver(self):
|
||||||
|
return self.resolve_queryset
|
||||||
|
|
|
@ -52,69 +52,18 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def merge_querysets(cls, default_queryset, queryset):
|
def resolve_queryset(
|
||||||
# There could be the case where the default queryset (returned from the filterclass)
|
cls, connection, iterable, info, args, filtering_args, filterset_class
|
||||||
# and the resolver queryset have some limits on it.
|
|
||||||
# We only would be able to apply one of those, but not both
|
|
||||||
# at the same time.
|
|
||||||
|
|
||||||
# See related PR: https://github.com/graphql-python/graphene-django/pull/126
|
|
||||||
|
|
||||||
assert not (
|
|
||||||
default_queryset.query.low_mark and queryset.query.low_mark
|
|
||||||
), "Received two sliced querysets (low mark) in the connection, please slice only in one."
|
|
||||||
assert not (
|
|
||||||
default_queryset.query.high_mark and queryset.query.high_mark
|
|
||||||
), "Received two sliced querysets (high mark) in the connection, please slice only in one."
|
|
||||||
low = default_queryset.query.low_mark or queryset.query.low_mark
|
|
||||||
high = default_queryset.query.high_mark or queryset.query.high_mark
|
|
||||||
default_queryset.query.clear_limits()
|
|
||||||
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(
|
|
||||||
default_queryset, queryset
|
|
||||||
)
|
|
||||||
queryset.query.set_limits(low, high)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def connection_resolver(
|
|
||||||
cls,
|
|
||||||
resolver,
|
|
||||||
connection,
|
|
||||||
default_manager,
|
|
||||||
max_limit,
|
|
||||||
enforce_first_or_last,
|
|
||||||
filterset_class,
|
|
||||||
filtering_args,
|
|
||||||
root,
|
|
||||||
info,
|
|
||||||
**args
|
|
||||||
):
|
):
|
||||||
|
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
|
||||||
|
connection, iterable, info, args
|
||||||
|
)
|
||||||
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
||||||
qs = filterset_class(
|
return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs
|
||||||
data=filter_kwargs,
|
|
||||||
queryset=default_manager.get_queryset(),
|
|
||||||
request=info.context,
|
|
||||||
).qs
|
|
||||||
|
|
||||||
return super(DjangoFilterConnectionField, cls).connection_resolver(
|
def get_queryset_resolver(self):
|
||||||
resolver,
|
|
||||||
connection,
|
|
||||||
qs,
|
|
||||||
max_limit,
|
|
||||||
enforce_first_or_last,
|
|
||||||
root,
|
|
||||||
info,
|
|
||||||
**args
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
|
||||||
return partial(
|
return partial(
|
||||||
self.connection_resolver,
|
self.resolve_queryset,
|
||||||
parent_resolver,
|
filterset_class=self.filterset_class,
|
||||||
self.connection_type,
|
filtering_args=self.filtering_args,
|
||||||
self.get_manager(),
|
|
||||||
self.max_limit,
|
|
||||||
self.enforce_first_or_last,
|
|
||||||
self.filterset_class,
|
|
||||||
self.filtering_args,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_filters import Filter, MultipleChoiceFilter, VERSION
|
from django_filters import Filter, MultipleChoiceFilter
|
||||||
from django_filters.filterset import BaseFilterSet, FilterSet
|
from django_filters.filterset import BaseFilterSet, FilterSet
|
||||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
||||||
|
|
||||||
|
@ -50,36 +50,6 @@ class GrapheneFilterSetMixin(BaseFilterSet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# To support a Django 1.11 + Python 2.7 combination django-filter must be
|
|
||||||
# < 2.x.x. To support the earlier version of django-filter, the
|
|
||||||
# filter_for_reverse_field method must be present on GrapheneFilterSetMixin and
|
|
||||||
# must not be present for later versions of django-filter.
|
|
||||||
if VERSION[0] < 2:
|
|
||||||
from django.utils.text import capfirst
|
|
||||||
|
|
||||||
class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
|
|
||||||
@classmethod
|
|
||||||
def filter_for_reverse_field(cls, f, name):
|
|
||||||
"""Handles retrieving filters for reverse relationships
|
|
||||||
We override the default implementation so that we can handle
|
|
||||||
Global IDs (the default implementation expects database
|
|
||||||
primary keys)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
rel = f.field.remote_field
|
|
||||||
except AttributeError:
|
|
||||||
rel = f.field.rel
|
|
||||||
default = {"name": name, "label": capfirst(rel.related_name)}
|
|
||||||
if rel.multiple:
|
|
||||||
# For to-many relationships
|
|
||||||
return GlobalIDMultipleChoiceFilter(**default)
|
|
||||||
else:
|
|
||||||
# For to-one relationships
|
|
||||||
return GlobalIDFilter(**default)
|
|
||||||
|
|
||||||
GrapheneFilterSetMixin = GrapheneFilterSetMixinPython2
|
|
||||||
|
|
||||||
|
|
||||||
def setup_filterset(filterset_class):
|
def setup_filterset(filterset_class):
|
||||||
""" Wrap a provided filterset in Graphene-specific functionality
|
""" Wrap a provided filterset in Graphene-specific functionality
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -180,7 +177,7 @@ def test_filter_shortcut_filterset_context():
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
schema = Schema(query=Query)
|
schema = Schema(query=Query)
|
||||||
result = schema.execute(query, context=context())
|
result = schema.execute(query, context_value=context())
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
|
||||||
assert len(result.data["contextArticles"]["edges"]) == 1
|
assert len(result.data["contextArticles"]["edges"]) == 1
|
||||||
|
@ -608,58 +605,6 @@ def test_should_query_filter_node_limit():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_filter_node_double_limit_raises():
|
|
||||||
class ReporterFilter(FilterSet):
|
|
||||||
limit = NumberFilter(method="filter_limit")
|
|
||||||
|
|
||||||
def filter_limit(self, queryset, name, value):
|
|
||||||
return queryset[:value]
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Reporter
|
|
||||||
fields = ["first_name"]
|
|
||||||
|
|
||||||
class ReporterType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Reporter
|
|
||||||
interfaces = (Node,)
|
|
||||||
|
|
||||||
class Query(ObjectType):
|
|
||||||
all_reporters = DjangoFilterConnectionField(
|
|
||||||
ReporterType, filterset_class=ReporterFilter
|
|
||||||
)
|
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
|
||||||
return Reporter.objects.order_by("a_choice")[:2]
|
|
||||||
|
|
||||||
Reporter.objects.create(
|
|
||||||
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
|
|
||||||
)
|
|
||||||
Reporter.objects.create(
|
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
|
||||||
)
|
|
||||||
|
|
||||||
schema = Schema(query=Query)
|
|
||||||
query = """
|
|
||||||
query NodeFilteringQuery {
|
|
||||||
allReporters(limit: 1) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = schema.execute(query)
|
|
||||||
assert len(result.errors) == 1
|
|
||||||
assert str(result.errors[0]) == (
|
|
||||||
"Received two sliced querysets (high mark) in the connection, please slice only in one."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_order_by_is_perserved():
|
def test_order_by_is_perserved():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -721,7 +666,7 @@ def test_order_by_is_perserved():
|
||||||
assert reverse_result.data == reverse_expected
|
assert reverse_result.data == reverse_expected
|
||||||
|
|
||||||
|
|
||||||
def test_annotation_is_perserved():
|
def test_annotation_is_preserved():
|
||||||
class ReporterType(DjangoObjectType):
|
class ReporterType(DjangoObjectType):
|
||||||
full_name = String()
|
full_name = String()
|
||||||
|
|
||||||
|
@ -766,6 +711,86 @@ def test_annotation_is_perserved():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotation_with_only():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
full_name = String()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterType)
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, **args):
|
||||||
|
return Reporter.objects.only("first_name", "last_name").annotate(
|
||||||
|
full_name=Concat(
|
||||||
|
"first_name", Value(" "), "last_name", output_field=TextField()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {"allReporters": {"edges": [{"node": {"fullName": "John Doe"}}]}}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_get_queryset_is_called():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, queryset, info):
|
||||||
|
return queryset.filter(first_name="b")
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(
|
||||||
|
ReporterType, reverse_order=Boolean()
|
||||||
|
)
|
||||||
|
|
||||||
|
Reporter.objects.create(first_name="b")
|
||||||
|
Reporter.objects.create(first_name="a")
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
expected = {"allReporters": {"edges": [{"node": {"firstName": "b"}}]}}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_integer_field_filter_type():
|
def test_integer_field_filter_type():
|
||||||
class PetType(DjangoObjectType):
|
class PetType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -781,38 +806,56 @@ def test_integer_field_filter_type():
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert str(schema) == dedent(
|
||||||
"""\
|
"""\
|
||||||
schema {
|
type Query {
|
||||||
query: Query
|
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null): PetTypeConnection
|
||||||
}
|
|
||||||
|
|
||||||
interface Node {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageInfo {
|
|
||||||
hasNextPage: Boolean!
|
|
||||||
hasPreviousPage: Boolean!
|
|
||||||
startCursor: String
|
|
||||||
endCursor: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type PetType implements Node {
|
|
||||||
age: Int!
|
|
||||||
id: ID!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PetTypeConnection {
|
type PetTypeConnection {
|
||||||
|
\"""Pagination data for this connection.\"""
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
|
|
||||||
|
\"""Contains the nodes in this connection.\"""
|
||||||
edges: [PetTypeEdge]!
|
edges: [PetTypeEdge]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\"""
|
||||||
|
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||||
|
\"""
|
||||||
|
type PageInfo {
|
||||||
|
\"""When paginating forwards, are there more items?\"""
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, are there more items?\"""
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, the cursor to continue.\"""
|
||||||
|
startCursor: String
|
||||||
|
|
||||||
|
\"""When paginating forwards, the cursor to continue.\"""
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""A Relay edge containing a `PetType` and its cursor.\"""
|
||||||
type PetTypeEdge {
|
type PetTypeEdge {
|
||||||
|
\"""The item at the end of the edge\"""
|
||||||
node: PetType
|
node: PetType
|
||||||
|
|
||||||
|
\"""A cursor for use in pagination\"""
|
||||||
cursor: String!
|
cursor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type PetType implements Node {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
\"""\"""
|
||||||
|
age: Int!
|
||||||
|
|
||||||
|
\"""The ID of the object\"""
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An object with an ID\"""
|
||||||
|
interface Node {
|
||||||
|
\"""The ID of the object\"""
|
||||||
|
id: ID!
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -833,40 +876,58 @@ def test_other_filter_types():
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert str(schema) == dedent(
|
||||||
"""\
|
"""\
|
||||||
schema {
|
type Query {
|
||||||
query: Query
|
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection
|
||||||
}
|
|
||||||
|
|
||||||
interface Node {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageInfo {
|
|
||||||
hasNextPage: Boolean!
|
|
||||||
hasPreviousPage: Boolean!
|
|
||||||
startCursor: String
|
|
||||||
endCursor: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type PetType implements Node {
|
|
||||||
age: Int!
|
|
||||||
id: ID!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PetTypeConnection {
|
type PetTypeConnection {
|
||||||
|
\"""Pagination data for this connection.\"""
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
|
|
||||||
|
\"""Contains the nodes in this connection.\"""
|
||||||
edges: [PetTypeEdge]!
|
edges: [PetTypeEdge]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\"""
|
||||||
|
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||||
|
\"""
|
||||||
|
type PageInfo {
|
||||||
|
\"""When paginating forwards, are there more items?\"""
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, are there more items?\"""
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, the cursor to continue.\"""
|
||||||
|
startCursor: String
|
||||||
|
|
||||||
|
\"""When paginating forwards, the cursor to continue.\"""
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""A Relay edge containing a `PetType` and its cursor.\"""
|
||||||
type PetTypeEdge {
|
type PetTypeEdge {
|
||||||
|
\"""The item at the end of the edge\"""
|
||||||
node: PetType
|
node: PetType
|
||||||
|
|
||||||
|
\"""A cursor for use in pagination\"""
|
||||||
cursor: String!
|
cursor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type PetType implements Node {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
\"""\"""
|
||||||
|
age: Int!
|
||||||
|
|
||||||
|
\"""The ID of the object\"""
|
||||||
|
id: ID!
|
||||||
}
|
}
|
||||||
"""
|
|
||||||
|
\"""An object with an ID\"""
|
||||||
|
interface Node {
|
||||||
|
\"""The ID of the object\"""
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import six
|
from django_filters.utils import get_model_field
|
||||||
|
|
||||||
from .filterset import custom_filterset_factory, setup_filterset
|
from .filterset import custom_filterset_factory, setup_filterset
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,28 +11,18 @@ def get_filtering_args_from_filterset(filterset_class, type):
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
model = filterset_class._meta.model
|
model = filterset_class._meta.model
|
||||||
for name, filter_field in six.iteritems(filterset_class.base_filters):
|
for name, filter_field in filterset_class.base_filters.items():
|
||||||
form_field = None
|
form_field = None
|
||||||
|
|
||||||
if name in filterset_class.declared_filters:
|
if name in filterset_class.declared_filters:
|
||||||
form_field = filter_field.field
|
form_field = filter_field.field
|
||||||
else:
|
else:
|
||||||
try:
|
model_field = get_model_field(model, filter_field.field_name)
|
||||||
field_name, filter_type = name.rsplit("__", 1)
|
filter_type = filter_field.lookup_expr
|
||||||
except ValueError:
|
if filter_type != "isnull" and hasattr(model_field, "formfield"):
|
||||||
field_name = name
|
form_field = model_field.formfield(
|
||||||
filter_type = None
|
required=filter_field.extra.get("required", False)
|
||||||
|
)
|
||||||
# If the filter type is `isnull` then use the filter provided by
|
|
||||||
# DjangoFilter (a BooleanFilter).
|
|
||||||
# Otherwise try and get a filter based on the actual model field
|
|
||||||
if filter_type != "isnull" and hasattr(model, field_name):
|
|
||||||
model_field = model._meta.get_field(field_name)
|
|
||||||
|
|
||||||
if hasattr(model_field, "formfield"):
|
|
||||||
form_field = model_field.formfield(
|
|
||||||
required=filter_field.extra.get("required", False)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fallback to field defined on filter if we can't get it from the
|
# Fallback to field defined on filter if we can't get it from the
|
||||||
# model field
|
# model field
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
|
from functools import singledispatch
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
|
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
|
||||||
|
|
||||||
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
from ..utils import import_single_dispatch
|
|
||||||
|
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
|
@ -57,9 +55,14 @@ def convert_form_field_to_float(field):
|
||||||
return Float(description=field.help_text, required=field.required)
|
return Float(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_form_field.register(forms.MultipleChoiceField)
|
||||||
|
def convert_form_field_to_string_list(field):
|
||||||
|
return List(String, description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.ModelMultipleChoiceField)
|
@convert_form_field.register(forms.ModelMultipleChoiceField)
|
||||||
@convert_form_field.register(GlobalIDMultipleChoiceField)
|
@convert_form_field.register(GlobalIDMultipleChoiceField)
|
||||||
def convert_form_field_to_list(field):
|
def convert_form_field_to_id_list(field):
|
||||||
return List(ID, required=field.required)
|
return List(ID, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import binascii
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import CharField, Field, MultipleChoiceField
|
from django.forms import CharField, Field, MultipleChoiceField
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from graphql_relay import from_global_id
|
from graphql_relay import from_global_id
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
||||||
else:
|
else:
|
||||||
errors = ErrorType.from_errors(form.errors)
|
errors = ErrorType.from_errors(form.errors)
|
||||||
|
|
||||||
return cls(errors=errors)
|
return cls(errors=errors, **form.data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_form(cls, root, info, **input):
|
def get_form(cls, root, info, **input):
|
||||||
|
@ -66,28 +66,6 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
# class DjangoFormInputObjectTypeOptions(InputObjectTypeOptions):
|
|
||||||
# form_class = None
|
|
||||||
|
|
||||||
|
|
||||||
# class DjangoFormInputObjectType(InputObjectType):
|
|
||||||
# class Meta:
|
|
||||||
# abstract = True
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def __init_subclass_with_meta__(cls, form_class=None,
|
|
||||||
# only_fields=(), exclude_fields=(), _meta=None, **options):
|
|
||||||
# if not _meta:
|
|
||||||
# _meta = DjangoFormInputObjectTypeOptions(cls)
|
|
||||||
# assert isinstance(form_class, forms.Form), (
|
|
||||||
# 'form_class must be an instance of django.forms.Form'
|
|
||||||
# )
|
|
||||||
# _meta.form_class = form_class
|
|
||||||
# form = form_class()
|
|
||||||
# fields = fields_for_form(form, only_fields, exclude_fields)
|
|
||||||
# super(DjangoFormInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, fields=fields, **options)
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoFormMutationOptions(MutationOptions):
|
class DjangoFormMutationOptions(MutationOptions):
|
||||||
form_class = None
|
form_class = None
|
||||||
|
|
||||||
|
@ -122,7 +100,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_mutate(cls, form, info):
|
def perform_mutate(cls, form, info):
|
||||||
form.save()
|
form.save()
|
||||||
return cls(errors=[])
|
return cls(errors=[], **form.cleaned_data)
|
||||||
|
|
||||||
|
|
||||||
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
|
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
|
||||||
|
@ -163,7 +141,9 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
||||||
|
|
||||||
registry = get_global_registry()
|
registry = get_global_registry()
|
||||||
model_type = registry.get_type_for_model(model)
|
model_type = registry.get_type_for_model(model)
|
||||||
return_field_name = return_field_name
|
if not model_type:
|
||||||
|
raise Exception("No type registered for model: {}".format(model.__name__))
|
||||||
|
|
||||||
if not return_field_name:
|
if not return_field_name:
|
||||||
model_name = model.__name__
|
model_name = model.__name__
|
||||||
return_field_name = model_name[:1].lower() + model_name[1:]
|
return_field_name = model_name[:1].lower() + model_name[1:]
|
||||||
|
@ -182,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()
|
||||||
|
|
|
@ -66,6 +66,10 @@ def test_should_choice_convert_string():
|
||||||
assert_conversion(forms.ChoiceField, String)
|
assert_conversion(forms.ChoiceField, String)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_multiple_choice_convert_list():
|
||||||
|
assert_conversion(forms.MultipleChoiceField, List)
|
||||||
|
|
||||||
|
|
||||||
def test_should_base_field_convert_string():
|
def test_should_base_field_convert_string():
|
||||||
assert_conversion(forms.Field, String)
|
assert_conversion(forms.Field, String)
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,49 @@
|
||||||
|
import pytest
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.core.exceptions import ValidationError
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
from graphene_django.tests.models import Film, FilmDetails, Pet
|
from graphene import Field, ObjectType, Schema, String
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
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()
|
||||||
|
|
||||||
|
def clean_text(self):
|
||||||
|
text = self.cleaned_data["text"]
|
||||||
|
if text == "INVALID_INPUT":
|
||||||
|
raise ValidationError("Invalid input")
|
||||||
|
return text
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PetForm(forms.ModelForm):
|
class PetForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pet
|
model = Pet
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
def clean_age(self):
|
||||||
|
age = self.cleaned_data["age"]
|
||||||
|
if age >= 99:
|
||||||
|
raise ValidationError("Too old")
|
||||||
|
return age
|
||||||
|
|
||||||
|
|
||||||
def test_needs_form_class():
|
def test_needs_form_class():
|
||||||
with raises(Exception) as exc:
|
with raises(Exception) as exc:
|
||||||
|
@ -43,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)
|
||||||
|
|
||||||
|
@ -56,105 +83,237 @@ def test_mutation_error_camelcased():
|
||||||
graphene_settings.CAMELCASE_ERRORS = False
|
graphene_settings.CAMELCASE_ERRORS = False
|
||||||
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", "test_field"}
|
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
|
||||||
graphene_settings.CAMELCASE_ERRORS = True
|
|
||||||
|
|
||||||
|
|
||||||
class ModelFormMutationTests(TestCase):
|
class MockQuery(ObjectType):
|
||||||
def test_default_meta_fields(self):
|
a = String()
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
|
||||||
self.assertIn("pet", PetMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_default_input_meta_fields(self):
|
def test_form_invalid_form():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class MyMutation(DjangoFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = MyForm
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
class Mutation(ObjectType):
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
my_mutation = MyMutation.Field()
|
||||||
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):
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
exclude_fields = ["id"]
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Pet)
|
result = schema.execute(
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "pet")
|
""" mutation MyMutation {
|
||||||
self.assertIn("name", PetMutation.Input._meta.fields)
|
myMutation(input: { text: "INVALID_INPUT" }) {
|
||||||
self.assertIn("age", PetMutation.Input._meta.fields)
|
errors {
|
||||||
self.assertIn("client_mutation_id", PetMutation.Input._meta.fields)
|
field
|
||||||
self.assertNotIn("id", PetMutation.Input._meta.fields)
|
messages
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
def test_return_field_name_is_camelcased(self):
|
assert result.errors is None
|
||||||
class PetMutation(DjangoModelFormMutation):
|
assert result.data["myMutation"]["errors"] == [
|
||||||
class Meta:
|
{"field": "text", "messages": ["Invalid input"]}
|
||||||
form_class = PetForm
|
]
|
||||||
model = FilmDetails
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, FilmDetails)
|
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "filmDetails")
|
|
||||||
|
|
||||||
def test_custom_return_field_name(self):
|
def test_form_valid_input():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class MyMutation(DjangoFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = MyForm
|
||||||
model = Film
|
|
||||||
return_field_name = "animal"
|
|
||||||
|
|
||||||
self.assertEqual(PetMutation._meta.model, Film)
|
class Mutation(ObjectType):
|
||||||
self.assertEqual(PetMutation._meta.return_field_name, "animal")
|
my_mutation = MyMutation.Field()
|
||||||
self.assertIn("animal", PetMutation._meta.fields)
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate(self):
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
pet = Pet.objects.create(name="Axel", age=10)
|
result = schema.execute(
|
||||||
|
""" mutation MyMutation {
|
||||||
|
myMutation(input: { text: "VALID_INPUT" }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(
|
assert result.errors is None
|
||||||
None, None, id=pet.pk, name="Mia", age=10
|
assert result.data["myMutation"]["errors"] == []
|
||||||
)
|
assert result.data["myMutation"]["text"] == "VALID_INPUT"
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
|
||||||
pet.refresh_from_db()
|
|
||||||
self.assertEqual(pet.name, "Mia")
|
|
||||||
self.assertEqual(result.errors, [])
|
|
||||||
|
|
||||||
def test_model_form_mutation_updates_existing_(self):
|
def test_default_meta_fields(pet_type):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None, name="Mia", age=10)
|
assert PetMutation._meta.model is Pet
|
||||||
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
|
assert "pet" in PetMutation._meta.fields
|
||||||
|
|
||||||
self.assertEqual(Pet.objects.count(), 1)
|
|
||||||
pet = Pet.objects.get()
|
|
||||||
self.assertEqual(pet.name, "Mia")
|
|
||||||
self.assertEqual(pet.age, 10)
|
|
||||||
self.assertEqual(result.errors, [])
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_invalid_form(self):
|
def test_default_input_meta_fields(pet_type):
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None)
|
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
|
||||||
|
|
||||||
# A pet was not created
|
|
||||||
self.assertEqual(Pet.objects.count(), 0)
|
|
||||||
|
|
||||||
fields_w_error = [e.field for e in result.errors]
|
def test_exclude_fields_input_meta_fields(pet_type):
|
||||||
self.assertEqual(len(result.errors), 2)
|
class PetMutation(DjangoModelFormMutation):
|
||||||
self.assertIn("name", fields_w_error)
|
class Meta:
|
||||||
self.assertEqual(result.errors[0].messages, ["This field is required."])
|
form_class = PetForm
|
||||||
self.assertIn("age", fields_w_error)
|
exclude_fields = ["id"]
|
||||||
self.assertEqual(result.errors[1].messages, ["This field is required."])
|
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 1
|
||||||
|
pet.refresh_from_db()
|
||||||
|
assert pet.name == "Mia"
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_creates_new(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)
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 1
|
||||||
|
pet = Pet.objects.get()
|
||||||
|
assert pet.name == "Mia"
|
||||||
|
assert pet.age == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_invalid_input(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)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
""" mutation PetMutation {
|
||||||
|
petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["petMutation"]["pet"] is None
|
||||||
|
assert result.data["petMutation"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_mutate_invalid_form(pet_type):
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
|
|
||||||
|
# A pet was not created
|
||||||
|
Pet.objects.count() == 0
|
||||||
|
|
||||||
|
fields_w_error = [e.field for e in result.errors]
|
||||||
|
assert len(result.errors) == 2
|
||||||
|
assert result.errors[0].messages == ["This field is required."]
|
||||||
|
assert result.errors[1].messages == ["This field is required."]
|
||||||
|
assert "age" in fields_w_error
|
||||||
|
assert "name" in fields_w_error
|
|
@ -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.graphql_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")
|
||||||
|
|
|
@ -9,3 +9,8 @@ class MyFakeModel(models.Model):
|
||||||
class MyFakeModelWithPassword(models.Model):
|
class MyFakeModelWithPassword(models.Model):
|
||||||
cool_name = models.CharField(max_length=50)
|
cool_name = models.CharField(max_length=50)
|
||||||
password = models.CharField(max_length=50)
|
password = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModelWithDate(models.Model):
|
||||||
|
cool_name = models.CharField(max_length=50)
|
||||||
|
last_edited = models.DateField()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay.mutation import ClientIDMutation
|
from graphene.relay.mutation import ClientIDMutation
|
||||||
|
@ -19,22 +20,31 @@ class SerializerMutationOptions(MutationOptions):
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
|
|
||||||
|
|
||||||
def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False):
|
def fields_for_serializer(
|
||||||
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=False,
|
||||||
|
convert_choices_to_enum=True,
|
||||||
|
):
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for name, field in serializer.fields.items():
|
for name, field in serializer.fields.items():
|
||||||
is_not_in_only = only_fields and name not in only_fields
|
is_not_in_only = only_fields and name not in only_fields
|
||||||
is_excluded = (
|
is_excluded = any(
|
||||||
name
|
[
|
||||||
in exclude_fields # or
|
name in exclude_fields,
|
||||||
# name in already_created_fields
|
field.write_only
|
||||||
) or (
|
and not is_input, # don't show write_only fields in Query
|
||||||
field.write_only and not is_input # don't show write_only fields in Query
|
field.read_only and is_input, # don't show read_only fields in Input
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_not_in_only or is_excluded:
|
if is_not_in_only or is_excluded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fields[name] = convert_serializer_field(field, is_input=is_input)
|
fields[name] = convert_serializer_field(
|
||||||
|
field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum
|
||||||
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +65,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
model_operations=("create", "update"),
|
model_operations=("create", "update"),
|
||||||
only_fields=(),
|
only_fields=(),
|
||||||
exclude_fields=(),
|
exclude_fields=(),
|
||||||
|
convert_choices_to_enum=True,
|
||||||
**options
|
**options
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -74,10 +85,18 @@ class SerializerMutation(ClientIDMutation):
|
||||||
lookup_field = model_class._meta.pk.name
|
lookup_field = model_class._meta.pk.name
|
||||||
|
|
||||||
input_fields = fields_for_serializer(
|
input_fields = fields_for_serializer(
|
||||||
serializer, only_fields, exclude_fields, is_input=True
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=True,
|
||||||
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
)
|
)
|
||||||
output_fields = fields_for_serializer(
|
output_fields = fields_for_serializer(
|
||||||
serializer, only_fields, exclude_fields, is_input=False
|
serializer,
|
||||||
|
only_fields,
|
||||||
|
exclude_fields,
|
||||||
|
is_input=False,
|
||||||
|
convert_choices_to_enum=convert_choices_to_enum,
|
||||||
)
|
)
|
||||||
|
|
||||||
_meta = SerializerMutationOptions(cls)
|
_meta = SerializerMutationOptions(cls)
|
||||||
|
@ -102,8 +121,10 @@ class SerializerMutation(ClientIDMutation):
|
||||||
instance = get_object_or_404(
|
instance = get_object_or_404(
|
||||||
model_class, **{lookup_field: input[lookup_field]}
|
model_class, **{lookup_field: input[lookup_field]}
|
||||||
)
|
)
|
||||||
|
partial = True
|
||||||
elif "create" in cls._meta.model_operations:
|
elif "create" in cls._meta.model_operations:
|
||||||
instance = None
|
instance = None
|
||||||
|
partial = False
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'Invalid update operation. Input parameter "{}" required.'.format(
|
'Invalid update operation. Input parameter "{}" required.'.format(
|
||||||
|
@ -115,6 +136,7 @@ class SerializerMutation(ClientIDMutation):
|
||||||
"instance": instance,
|
"instance": instance,
|
||||||
"data": input,
|
"data": input,
|
||||||
"context": {"request": info.context},
|
"context": {"request": info.context},
|
||||||
|
"partial": partial,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"data": input, "context": {"request": info.context}}
|
return {"data": input, "context": {"request": info.context}}
|
||||||
|
@ -138,6 +160,9 @@ class SerializerMutation(ClientIDMutation):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for f, field in serializer.fields.items():
|
for f, field in serializer.fields.items():
|
||||||
if not field.write_only:
|
if not field.write_only:
|
||||||
kwargs[f] = field.get_attribute(obj)
|
if isinstance(field, serializers.SerializerMethodField):
|
||||||
|
kwargs[f] = field.to_representation(obj)
|
||||||
|
else:
|
||||||
|
kwargs[f] = field.get_attribute(obj)
|
||||||
|
|
||||||
return cls(errors=None, **kwargs)
|
return cls(errors=None, **kwargs)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import singledispatch
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -5,11 +7,8 @@ import graphene
|
||||||
|
|
||||||
from ..registry import get_global_registry
|
from ..registry import get_global_registry
|
||||||
from ..converter import convert_choices_to_named_enum_with_descriptions
|
from ..converter import convert_choices_to_named_enum_with_descriptions
|
||||||
from ..utils import import_single_dispatch
|
|
||||||
from .types import DictType
|
from .types import DictType
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def get_graphene_type_from_serializer_field(field):
|
def get_graphene_type_from_serializer_field(field):
|
||||||
|
@ -19,14 +18,17 @@ def get_graphene_type_from_serializer_field(field):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_serializer_field(field, is_input=True):
|
def convert_serializer_field(field, is_input=True, convert_choices_to_enum=True):
|
||||||
"""
|
"""
|
||||||
Converts a django rest frameworks field to a graphql field
|
Converts a django rest frameworks field to a graphql field
|
||||||
and marks the field as required if we are creating an input type
|
and marks the field as required if we are creating an input type
|
||||||
and the field itself is required
|
and the field itself is required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
graphql_type = get_graphene_type_from_serializer_field(field)
|
if isinstance(field, serializers.ChoiceField) and not convert_choices_to_enum:
|
||||||
|
graphql_type = graphene.String
|
||||||
|
else:
|
||||||
|
graphql_type = get_graphene_type_from_serializer_field(field)
|
||||||
|
|
||||||
args = []
|
args = []
|
||||||
kwargs = {"description": field.help_text, "required": is_input and field.required}
|
kwargs = {"description": field.help_text, "required": is_input and field.required}
|
||||||
|
|
|
@ -10,7 +10,9 @@ from ..serializer_converter import convert_serializer_field
|
||||||
from ..types import DictType
|
from ..types import DictType
|
||||||
|
|
||||||
|
|
||||||
def _get_type(rest_framework_field, is_input=True, **kwargs):
|
def _get_type(
|
||||||
|
rest_framework_field, is_input=True, convert_choices_to_enum=True, **kwargs
|
||||||
|
):
|
||||||
# prevents the following error:
|
# prevents the following error:
|
||||||
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
# AssertionError: The `source` argument is not meaningful when applied to a `child=` field.
|
||||||
# Remove `source=` from the field declaration.
|
# Remove `source=` from the field declaration.
|
||||||
|
@ -21,7 +23,9 @@ def _get_type(rest_framework_field, is_input=True, **kwargs):
|
||||||
|
|
||||||
field = rest_framework_field(**kwargs)
|
field = rest_framework_field(**kwargs)
|
||||||
|
|
||||||
return convert_serializer_field(field, is_input=is_input)
|
return convert_serializer_field(
|
||||||
|
field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_conversion(rest_framework_field, graphene_field, **kwargs):
|
def assert_conversion(rest_framework_field, graphene_field, **kwargs):
|
||||||
|
@ -73,6 +77,16 @@ def test_should_choice_convert_enum():
|
||||||
assert field._meta.enum.__members__["W"].description == "World"
|
assert field._meta.enum.__members__["W"].description == "World"
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_choice_convert_string_if_enum_disabled():
|
||||||
|
assert_conversion(
|
||||||
|
serializers.ChoiceField,
|
||||||
|
graphene.String,
|
||||||
|
choices=[("h", "Hello"), ("w", "World")],
|
||||||
|
source="word",
|
||||||
|
convert_choices_to_enum=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_should_base_field_convert_string():
|
def test_should_base_field_convert_string():
|
||||||
assert_conversion(serializers.Field, graphene.String)
|
assert_conversion(serializers.Field, graphene.String)
|
||||||
|
|
||||||
|
|
|
@ -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
|
from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,12 +17,14 @@ def mock_info():
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
path=None,
|
||||||
schema=None,
|
schema=None,
|
||||||
fragments=None,
|
fragments=None,
|
||||||
root_value=None,
|
root_value=None,
|
||||||
operation=None,
|
operation=None,
|
||||||
variable_values=None,
|
variable_values=None,
|
||||||
context=None,
|
context=None,
|
||||||
|
is_awaitable=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +34,18 @@ class MyModelSerializer(serializers.ModelSerializer):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MyModelSerializerWithMethod(serializers.ModelSerializer):
|
||||||
|
days_since_last_edit = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithDate
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def get_days_since_last_edit(self, obj):
|
||||||
|
now = datetime.date(2020, 1, 8)
|
||||||
|
return (now - obj.last_edited).days
|
||||||
|
|
||||||
|
|
||||||
class MyModelMutation(SerializerMutation):
|
class MyModelMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
serializer_class = MyModelSerializer
|
serializer_class = MyModelSerializer
|
||||||
|
@ -87,7 +100,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)
|
||||||
|
@ -110,7 +122,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:
|
||||||
|
@ -132,6 +143,24 @@ 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"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_fields():
|
||||||
|
class ReadOnlyFieldModelSerializer(serializers.ModelSerializer):
|
||||||
|
cool_name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MyFakeModelWithPassword
|
||||||
|
fields = ["cool_name", "password"]
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = ReadOnlyFieldModelSerializer
|
||||||
|
|
||||||
|
assert "password" in MyMutation.Input._meta.fields
|
||||||
|
assert (
|
||||||
|
"cool_name" not in MyMutation.Input._meta.fields
|
||||||
|
), "'cool_name' is read_only field and shouldn't be on arguments"
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -163,7 +192,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"}
|
||||||
|
@ -173,7 +201,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(
|
||||||
|
@ -183,7 +210,15 @@ def test_model_update_mutate_and_get_payload_success():
|
||||||
assert result.cool_name == "New Narf"
|
assert result.cool_name == "New Narf"
|
||||||
|
|
||||||
|
|
||||||
@mark.django_db
|
def test_model_partial_update_mutate_and_get_payload_success():
|
||||||
|
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(
|
||||||
|
None, mock_info(), **{"id": instance.id}
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == "Narf"
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
@ -198,6 +233,22 @@ def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
assert '"id" required' in str(exc.value)
|
assert '"id" required' in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_mutate_success():
|
||||||
|
class MyMethodMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializerWithMethod
|
||||||
|
|
||||||
|
result = MyMethodMutation.mutate_and_get_payload(
|
||||||
|
None,
|
||||||
|
mock_info(),
|
||||||
|
**{"cool_name": "Narf", "last_edited": datetime.date(2020, 1, 4)}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == "Narf"
|
||||||
|
assert result.days_since_last_edit == 4
|
||||||
|
|
||||||
|
|
||||||
def test_mutate_and_get_payload_error():
|
def test_mutate_and_get_payload_error():
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -214,11 +265,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():
|
||||||
|
|
|
@ -15,7 +15,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test.signals import setting_changed
|
from django.test.signals import setting_changed
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import importlib # Available in Python 3.1+
|
import importlib # Available in Python 3.1+
|
||||||
|
@ -36,6 +35,9 @@ 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": True,
|
"CAMELCASE_ERRORS": True,
|
||||||
|
# Set to True to enable v3 naming convention for choice field Enum's
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_V3_NAMING": False,
|
||||||
|
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
@ -52,7 +54,7 @@ def perform_import(val, setting_name):
|
||||||
"""
|
"""
|
||||||
if val is None:
|
if val is None:
|
||||||
return None
|
return None
|
||||||
elif isinstance(val, six.string_types):
|
elif isinstance(val, str):
|
||||||
return import_from_string(val, setting_name)
|
return import_from_string(val, setting_name)
|
||||||
elif isinstance(val, (list, tuple)):
|
elif isinstance(val, (list, tuple)):
|
||||||
return [import_from_string(item, setting_name) for item in val]
|
return [import_from_string(item, setting_name) for item in val]
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
// 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 {
|
||||||
|
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect the URL parameters
|
// Collect the URL parameters
|
||||||
var parameters = {};
|
var parameters = {};
|
||||||
|
@ -94,6 +97,6 @@
|
||||||
// Render <GraphiQL /> into the body.
|
// Render <GraphiQL /> into the body.
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(GraphiQL, options),
|
React.createElement(GraphiQL, options),
|
||||||
document.body
|
document.getElementById("editor")
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -10,7 +10,7 @@ add "&raw" to the end of the URL within a browser.
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body, #editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -31,6 +31,8 @@ add "&raw" to the end of the URL within a browser.
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="editor"></div>
|
||||||
|
{% csrf_token %}
|
||||||
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
CHOICES = ((1, "this"), (2, _("that")))
|
CHOICES = ((1, "this"), (2, _("that")))
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class Reporter(models.Model):
|
||||||
"Reporter Type",
|
"Reporter Type",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
choices=[(1, u"Regular"), (2, u"CNN Reporter")],
|
choices=[(1, "Regular"), (2, "CNN Reporter")],
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self): # __unicode__ on Python 2
|
def __str__(self): # __unicode__ on Python 2
|
||||||
|
@ -64,6 +64,9 @@ class Reporter(models.Model):
|
||||||
if self.reporter_type == 2: # quick and dirty way without enums
|
if self.reporter_type == 2: # quick and dirty way without enums
|
||||||
self.__class__ = CNNReporter
|
self.__class__ = CNNReporter
|
||||||
|
|
||||||
|
def some_method(self):
|
||||||
|
return 123
|
||||||
|
|
||||||
|
|
||||||
class CNNReporterManager(models.Manager):
|
class CNNReporterManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -102,7 +105,7 @@ class Article(models.Model):
|
||||||
"Importance",
|
"Importance",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
choices=[(1, u"Very important"), (2, u"Not as important")],
|
choices=[(1, "Very important"), (2, "Not as important")],
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self): # __unicode__ on Python 2
|
def __str__(self): # __unicode__ on Python 2
|
||||||
|
|
|
@ -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 io import StringIO
|
||||||
from six import StringIO
|
from mock import mock_open, patch
|
||||||
|
|
||||||
|
from graphene import ObjectType, Schema, String
|
||||||
|
|
||||||
|
|
||||||
@patch("graphene_django.management.commands.graphql_schema.Command.save_file")
|
@patch("graphene_django.management.commands.graphql_schema.Command.save_json_file")
|
||||||
def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
|
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,30 @@ 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(
|
||||||
|
"""\
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
@ -1,31 +1,43 @@
|
||||||
|
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 gettext_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 ArrayField, HStoreField, JSONField, MissingType, RangeField
|
||||||
from ..converter import convert_django_field, convert_django_field_with_choices
|
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 +133,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -325,3 +342,23 @@ def test_should_postgres_range_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 field.type.of_type.of_type == graphene.Int
|
assert field.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,13 @@ 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)
|
||||||
|
|
||||||
|
@ -197,3 +223,155 @@ class TestDjangoListField:
|
||||||
{"firstName": "Debra", "articles": []},
|
{"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)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == {
|
||||||
|
"reporters": [
|
||||||
|
{"firstName": "Tara", "articles": [{"headline": "Amazing news"},],},
|
||||||
|
{"firstName": "Debra", "articles": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
import base64
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
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():
|
||||||
|
@ -147,9 +142,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 +580,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):
|
||||||
|
@ -620,14 +612,14 @@ def test_should_enforce_first_or_last():
|
||||||
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert str(result.errors[0]) == (
|
assert str(result.errors[0]).startswith(
|
||||||
"You must provide a `first` or `last` value to properly "
|
"You must provide a `first` or `last` value to properly "
|
||||||
"paginate the `allReporters` connection."
|
"paginate the `allReporters` connection.\n"
|
||||||
)
|
)
|
||||||
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):
|
||||||
|
@ -638,6 +630,8 @@ def test_should_error_if_first_is_greater_than_max():
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
assert Query.all_reporters.max_limit == 100
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
r = Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
@ -659,16 +653,14 @@ def test_should_error_if_first_is_greater_than_max():
|
||||||
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert str(result.errors[0]) == (
|
assert str(result.errors[0]).startswith(
|
||||||
"Requesting 101 records on the `allReporters` connection "
|
"Requesting 101 records on the `allReporters` connection "
|
||||||
"exceeds the `first` limit of 100 records."
|
"exceeds the `first` limit of 100 records.\n"
|
||||||
)
|
)
|
||||||
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):
|
||||||
|
@ -679,6 +671,8 @@ def test_should_error_if_last_is_greater_than_max():
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
assert Query.all_reporters.max_limit == 100
|
||||||
|
|
||||||
r = Reporter.objects.create(
|
r = Reporter.objects.create(
|
||||||
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
)
|
)
|
||||||
|
@ -700,14 +694,12 @@ def test_should_error_if_last_is_greater_than_max():
|
||||||
|
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert len(result.errors) == 1
|
assert len(result.errors) == 1
|
||||||
assert str(result.errors[0]) == (
|
assert str(result.errors[0]).startswith(
|
||||||
"Requesting 101 records on the `allReporters` connection "
|
"Requesting 101 records on the `allReporters` connection "
|
||||||
"exceeds the `last` limit of 100 records."
|
"exceeds the `last` limit of 100 records.\n"
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -721,7 +713,7 @@ def test_should_query_promise_connectionfields():
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
def resolve_all_reporters(self, info, **args):
|
def resolve_all_reporters(self, info, **args):
|
||||||
return Promise.resolve([Reporter(id=1)])
|
return Promise.resolve([Reporter(id=1)]).get()
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
query = """
|
query = """
|
||||||
|
@ -804,7 +796,7 @@ def test_should_query_connectionfields_with_manager():
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
query = """
|
query = """
|
||||||
query ReporterLastQuery {
|
query ReporterLastQuery {
|
||||||
allReporters(first: 2) {
|
allReporters(first: 1) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
|
@ -850,7 +842,7 @@ def test_should_query_dataloader_fields():
|
||||||
articles = DjangoConnectionField(ArticleType)
|
articles = DjangoConnectionField(ArticleType)
|
||||||
|
|
||||||
def resolve_articles(self, info, **args):
|
def resolve_articles(self, info, **args):
|
||||||
return article_loader.load(self.id)
|
return article_loader.load(self.id).get()
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
all_reporters = DjangoConnectionField(ReporterType)
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
@ -1083,7 +1075,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
films = DjangoConnectionField(FilmType)
|
films = DjangoConnectionField(FilmType)
|
||||||
|
|
||||||
def resolve_films(root, info):
|
def resolve_films(root, info, **kwargs):
|
||||||
qs = Film.objects.prefetch_related("reporters")
|
qs = Film.objects.prefetch_related("reporters")
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@ -1113,6 +1105,60 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
with django_assert_num_queries(3) as captured:
|
with django_assert_num_queries(3) as captured:
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_preserve_annotations():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (graphene.relay.Node,)
|
||||||
|
|
||||||
|
class FilmType(DjangoObjectType):
|
||||||
|
reporters = DjangoConnectionField(ReporterType)
|
||||||
|
reporters_count = graphene.Int()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Film
|
||||||
|
interfaces = (graphene.relay.Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
films = DjangoConnectionField(FilmType)
|
||||||
|
|
||||||
|
def resolve_films(root, info, **kwargs):
|
||||||
|
qs = Film.objects.prefetch_related("reporters")
|
||||||
|
return qs.annotate(reporters_count=models.Count("reporters"))
|
||||||
|
|
||||||
|
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
|
||||||
|
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
|
||||||
|
|
||||||
|
f1 = Film.objects.create()
|
||||||
|
f1.reporters.set([r1, r2])
|
||||||
|
f2 = Film.objects.create()
|
||||||
|
f2.reporters.set([r2])
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
films {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
reportersCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors, str(result)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"films": {
|
||||||
|
"edges": [{"node": {"reportersCount": 2}}, {"node": {"reportersCount": 1}}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert result.data == expected, str(result.data)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
|
|
|
@ -13,8 +13,6 @@ 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"""
|
||||||
|
@ -113,90 +111,171 @@ def test_django_objecttype_with_custom_meta():
|
||||||
|
|
||||||
|
|
||||||
def test_schema_representation():
|
def test_schema_representation():
|
||||||
expected = """
|
expected = dedent(
|
||||||
schema {
|
"""\
|
||||||
query: RootQuery
|
schema {
|
||||||
}
|
query: RootQuery
|
||||||
|
}
|
||||||
|
|
||||||
type Article implements Node {
|
\"""Article description\"""
|
||||||
id: ID!
|
type Article implements Node {
|
||||||
headline: String!
|
\"""The ID of the object\"""
|
||||||
pubDate: Date!
|
id: ID!
|
||||||
pubDateTime: DateTime!
|
|
||||||
reporter: Reporter!
|
|
||||||
editor: Reporter!
|
|
||||||
lang: ArticleLang!
|
|
||||||
importance: ArticleImportance
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArticleConnection {
|
\"""\"""
|
||||||
pageInfo: PageInfo!
|
headline: String!
|
||||||
edges: [ArticleEdge]!
|
|
||||||
test: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArticleEdge {
|
\"""\"""
|
||||||
node: Article
|
pubDate: Date!
|
||||||
cursor: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ArticleImportance {
|
\"""\"""
|
||||||
A_1
|
pubDateTime: DateTime!
|
||||||
A_2
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ArticleLang {
|
\"""\"""
|
||||||
ES
|
reporter: Reporter!
|
||||||
EN
|
|
||||||
}
|
|
||||||
|
|
||||||
scalar Date
|
\"""\"""
|
||||||
|
editor: Reporter!
|
||||||
|
|
||||||
scalar DateTime
|
\"""Language\"""
|
||||||
|
lang: ArticleLang!
|
||||||
|
|
||||||
interface Node {
|
\"""\"""
|
||||||
id: ID!
|
importance: ArticleImportance
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageInfo {
|
\"""An object with an ID\"""
|
||||||
hasNextPage: Boolean!
|
interface Node {
|
||||||
hasPreviousPage: Boolean!
|
\"""The ID of the object\"""
|
||||||
startCursor: String
|
id: ID!
|
||||||
endCursor: String
|
}
|
||||||
}
|
|
||||||
|
|
||||||
type Reporter {
|
\"""
|
||||||
id: ID!
|
The `Date` scalar type represents a Date
|
||||||
firstName: String!
|
value as specified by
|
||||||
lastName: String!
|
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
email: String!
|
\"""
|
||||||
pets: [Reporter!]!
|
scalar Date
|
||||||
aChoice: ReporterAChoice
|
|
||||||
reporterType: ReporterReporterType
|
|
||||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ReporterAChoice {
|
\"""
|
||||||
A_1
|
The `DateTime` scalar type represents a DateTime
|
||||||
A_2
|
value as specified by
|
||||||
}
|
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
|
\"""
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
enum ReporterReporterType {
|
\"""An enumeration.\"""
|
||||||
A_1
|
enum ArticleLang {
|
||||||
A_2
|
\"""Spanish\"""
|
||||||
}
|
ES
|
||||||
|
|
||||||
type RootQuery {
|
\"""English\"""
|
||||||
node(id: ID!): Node
|
EN
|
||||||
}
|
}
|
||||||
""".lstrip()
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum ArticleImportance {
|
||||||
|
\"""Very important\"""
|
||||||
|
A_1
|
||||||
|
|
||||||
|
\"""Not as important\"""
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""Reporter description\"""
|
||||||
|
type Reporter {
|
||||||
|
\"""\"""
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
firstName: String!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
lastName: String!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
email: String!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
pets: [Reporter!]!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
aChoice: ReporterAChoice
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
reporterType: ReporterReporterType
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
articles(before: String = null, after: String = null, first: Int = null, last: Int = null): ArticleConnection!
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum ReporterAChoice {
|
||||||
|
\"""this\"""
|
||||||
|
A_1
|
||||||
|
|
||||||
|
\"""that\"""
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum ReporterReporterType {
|
||||||
|
\"""Regular\"""
|
||||||
|
A_1
|
||||||
|
|
||||||
|
\"""CNN Reporter\"""
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleConnection {
|
||||||
|
\"""Pagination data for this connection.\"""
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
|
||||||
|
\"""Contains the nodes in this connection.\"""
|
||||||
|
edges: [ArticleEdge]!
|
||||||
|
test: String
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""
|
||||||
|
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
|
||||||
|
\"""
|
||||||
|
type PageInfo {
|
||||||
|
\"""When paginating forwards, are there more items?\"""
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, are there more items?\"""
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
|
||||||
|
\"""When paginating backwards, the cursor to continue.\"""
|
||||||
|
startCursor: String
|
||||||
|
|
||||||
|
\"""When paginating forwards, the cursor to continue.\"""
|
||||||
|
endCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""A Relay edge containing a `Article` and its cursor.\"""
|
||||||
|
type ArticleEdge {
|
||||||
|
\"""The item at the end of the edge\"""
|
||||||
|
node: Article
|
||||||
|
|
||||||
|
\"""A cursor for use in pagination\"""
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootQuery {
|
||||||
|
node(
|
||||||
|
\"""The ID of the object\"""
|
||||||
|
id: ID!
|
||||||
|
): Node
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
assert str(schema) == expected
|
assert str(schema) == expected
|
||||||
|
|
||||||
|
|
||||||
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 +312,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):
|
||||||
|
@ -315,7 +405,79 @@ def test_django_objecttype_fields_exclude_type_checking():
|
||||||
class Reporter2(DjangoObjectType):
|
class Reporter2(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReporterModel
|
model = ReporterModel
|
||||||
fields = "foo"
|
exclude = "foo"
|
||||||
|
|
||||||
|
|
||||||
|
@with_local_registry
|
||||||
|
def test_django_objecttype_fields_exist_on_model():
|
||||||
|
with pytest.warns(UserWarning, match=r"Field name .* doesn't exist"):
|
||||||
|
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ["first_name", "foo", "email"]
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=r"Field name .* matches an attribute on Django model .* but it's not a model field",
|
||||||
|
) as record:
|
||||||
|
|
||||||
|
class Reporter2(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ["first_name", "some_method", "email"]
|
||||||
|
|
||||||
|
# Don't warn if selecting a custom field
|
||||||
|
with pytest.warns(None) as record:
|
||||||
|
|
||||||
|
class Reporter3(DjangoObjectType):
|
||||||
|
custom_field = String()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
fields = ["first_name", "custom_field", "email"]
|
||||||
|
|
||||||
|
assert len(record) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@with_local_registry
|
||||||
|
def test_django_objecttype_exclude_fields_exist_on_model():
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=r"Django model .* does not have a field or attribute named .*",
|
||||||
|
):
|
||||||
|
|
||||||
|
class Reporter(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
exclude = ["foo"]
|
||||||
|
|
||||||
|
# Don't warn if selecting a custom field
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=r"Excluding the custom field .* on DjangoObjectType .* has no effect.",
|
||||||
|
):
|
||||||
|
|
||||||
|
class Reporter3(DjangoObjectType):
|
||||||
|
custom_field = String()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
exclude = ["custom_field"]
|
||||||
|
|
||||||
|
# Don't warn on exclude fields
|
||||||
|
with pytest.warns(None) as record:
|
||||||
|
|
||||||
|
class Reporter4(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ReporterModel
|
||||||
|
exclude = ["email", "first_name"]
|
||||||
|
|
||||||
|
assert len(record) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def custom_enum_name(field):
|
||||||
|
return "CustomEnum{}".format(field.name.title())
|
||||||
|
|
||||||
|
|
||||||
class TestDjangoObjectType:
|
class TestDjangoObjectType:
|
||||||
|
@ -346,20 +508,21 @@ class TestDjangoObjectType:
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert str(schema) == dedent(
|
||||||
"""\
|
"""\
|
||||||
schema {
|
type Query {
|
||||||
query: Query
|
pet: Pet
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pet {
|
type Pet {
|
||||||
id: ID!
|
\"""\"""
|
||||||
kind: String!
|
id: ID!
|
||||||
cuteness: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
\"""\"""
|
||||||
pet: Pet
|
kind: String!
|
||||||
}
|
|
||||||
"""
|
\"""\"""
|
||||||
|
cuteness: Int!
|
||||||
|
}
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
|
||||||
|
@ -375,25 +538,30 @@ class TestDjangoObjectType:
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert str(schema) == dedent(
|
||||||
"""\
|
"""\
|
||||||
schema {
|
type Query {
|
||||||
query: Query
|
pet: Pet
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pet {
|
type Pet {
|
||||||
id: ID!
|
\"""\"""
|
||||||
kind: PetModelKind!
|
id: ID!
|
||||||
cuteness: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PetModelKind {
|
\"""\"""
|
||||||
CAT
|
kind: PetModelKind!
|
||||||
DOG
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
\"""\"""
|
||||||
pet: Pet
|
cuteness: Int!
|
||||||
}
|
}
|
||||||
"""
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum PetModelKind {
|
||||||
|
\"""Cat\"""
|
||||||
|
CAT
|
||||||
|
|
||||||
|
\"""Dog\"""
|
||||||
|
DOG
|
||||||
|
}
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
|
||||||
|
@ -409,18 +577,101 @@ class TestDjangoObjectType:
|
||||||
|
|
||||||
assert str(schema) == dedent(
|
assert str(schema) == dedent(
|
||||||
"""\
|
"""\
|
||||||
schema {
|
type Query {
|
||||||
query: Query
|
pet: Pet
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pet {
|
type Pet {
|
||||||
id: ID!
|
\"""\"""
|
||||||
kind: String!
|
id: ID!
|
||||||
cuteness: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
\"""\"""
|
||||||
pet: Pet
|
kind: String!
|
||||||
}
|
|
||||||
"""
|
\"""\"""
|
||||||
|
cuteness: Int!
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
"""\
|
||||||
|
type Query {
|
||||||
|
pet: PetModelKind
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetModelKind {
|
||||||
|
\"""\"""
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
kind: TestsPetModelKindChoices!
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum TestsPetModelKindChoices {
|
||||||
|
\"""Cat\"""
|
||||||
|
CAT
|
||||||
|
|
||||||
|
\"""Dog\"""
|
||||||
|
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(
|
||||||
|
"""\
|
||||||
|
type Query {
|
||||||
|
pet: PetModelKind
|
||||||
|
}
|
||||||
|
|
||||||
|
type PetModelKind {
|
||||||
|
\"""\"""
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
\"""\"""
|
||||||
|
kind: CustomEnumKind!
|
||||||
|
}
|
||||||
|
|
||||||
|
\"""An enumeration.\"""
|
||||||
|
enum CustomEnumKind {
|
||||||
|
\"""Cat\"""
|
||||||
|
CAT
|
||||||
|
|
||||||
|
\"""Dog\"""
|
||||||
|
DOG
|
||||||
|
}
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,3 +34,27 @@ 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 `operation_name` argument produces an `operationName` field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class TestClass(GraphQLTestCase):
|
||||||
|
GRAPHQL_SCHEMA = True
|
||||||
|
|
||||||
|
def runTest(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
tc = TestClass()
|
||||||
|
tc.setUpClass()
|
||||||
|
tc.query("query { }", operation_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."
|
||||||
|
|
|
@ -99,12 +99,14 @@ def test_reports_validation_errors(client):
|
||||||
assert response_json(response) == {
|
assert response_json(response) == {
|
||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"message": 'Cannot query field "unknownOne" on type "QueryRoot".',
|
"message": "Cannot query field 'unknownOne' on type 'QueryRoot'.",
|
||||||
"locations": [{"line": 1, "column": 9}],
|
"locations": [{"line": 1, "column": 9}],
|
||||||
|
"path": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"message": 'Cannot query field "unknownTwo" on type "QueryRoot".',
|
"message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.",
|
||||||
"locations": [{"line": 1, "column": 21}],
|
"locations": [{"line": 1, "column": 21}],
|
||||||
|
"path": None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -124,7 +126,9 @@ def test_errors_when_missing_operation_name(client):
|
||||||
assert response_json(response) == {
|
assert response_json(response) == {
|
||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"message": "Must provide operation name if query contains multiple operations."
|
"message": "Must provide operation name if query contains multiple operations.",
|
||||||
|
"locations": None,
|
||||||
|
"path": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -464,8 +468,8 @@ def test_handles_syntax_errors_caught_by_graphql(client):
|
||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"locations": [{"column": 1, "line": 1}],
|
"locations": [{"column": 1, "line": 1}],
|
||||||
"message": "Syntax Error GraphQL (1:1) "
|
"message": "Syntax Error: Unexpected Name 'syntaxerror'.",
|
||||||
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n',
|
"path": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import warnings
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
import six
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
|
|
||||||
|
@ -21,9 +21,6 @@ from .utils import (
|
||||||
is_valid_django_model,
|
is_valid_django_model,
|
||||||
)
|
)
|
||||||
|
|
||||||
if six.PY3:
|
|
||||||
from typing import Type
|
|
||||||
|
|
||||||
|
|
||||||
ALL_FIELDS = "__all__"
|
ALL_FIELDS = "__all__"
|
||||||
|
|
||||||
|
@ -35,9 +32,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:
|
||||||
|
@ -62,6 +65,71 @@ def construct_fields(
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def validate_fields(type_, model, fields, only_fields, exclude_fields):
|
||||||
|
# Validate the given fields against the model's fields and custom fields
|
||||||
|
all_field_names = set(fields.keys())
|
||||||
|
only_fields = only_fields if only_fields is not ALL_FIELDS else ()
|
||||||
|
for name in only_fields or ():
|
||||||
|
if name in all_field_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(model, name):
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
'Field name "{field_name}" matches an attribute on Django model "{app_label}.{object_name}" '
|
||||||
|
"but it's not a model field so Graphene cannot determine what type it should be. "
|
||||||
|
'Either define the type of the field on DjangoObjectType "{type_}" or remove it from the "fields" list.'
|
||||||
|
).format(
|
||||||
|
field_name=name,
|
||||||
|
app_label=model._meta.app_label,
|
||||||
|
object_name=model._meta.object_name,
|
||||||
|
type_=type_,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
'Field name "{field_name}" doesn\'t exist on Django model "{app_label}.{object_name}". '
|
||||||
|
'Consider removing the field from the "fields" list of DjangoObjectType "{type_}" because it has no effect.'
|
||||||
|
).format(
|
||||||
|
field_name=name,
|
||||||
|
app_label=model._meta.app_label,
|
||||||
|
object_name=model._meta.object_name,
|
||||||
|
type_=type_,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate exclude fields
|
||||||
|
for name in exclude_fields or ():
|
||||||
|
if name in all_field_names:
|
||||||
|
# Field is a custom field
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
'Excluding the custom field "{field_name}" on DjangoObjectType "{type_}" has no effect. '
|
||||||
|
'Either remove the custom field or remove the field from the "exclude" list.'
|
||||||
|
).format(
|
||||||
|
field_name=name,
|
||||||
|
app_label=model._meta.app_label,
|
||||||
|
object_name=model._meta.object_name,
|
||||||
|
type_=type_,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not hasattr(model, name):
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
'Django model "{app_label}.{object_name}" does not have a field or attribute named "{field_name}". '
|
||||||
|
'Consider removing the field from the "exclude" list of DjangoObjectType "{type_}" because it has no effect'
|
||||||
|
).format(
|
||||||
|
field_name=name,
|
||||||
|
app_label=model._meta.app_label,
|
||||||
|
object_name=model._meta.object_name,
|
||||||
|
type_=type_,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DjangoObjectTypeOptions(ObjectTypeOptions):
|
class DjangoObjectTypeOptions(ObjectTypeOptions):
|
||||||
model = None # type: Model
|
model = None # type: Model
|
||||||
registry = None # type: Registry
|
registry = None # type: Registry
|
||||||
|
@ -78,10 +146,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,
|
||||||
|
@ -136,9 +204,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")
|
||||||
|
@ -193,6 +258,9 @@ class DjangoObjectType(ObjectType):
|
||||||
_meta=_meta, interfaces=interfaces, **options
|
_meta=_meta, interfaces=interfaces, **options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate fields
|
||||||
|
validate_fields(cls, model, _meta.fields, fields, exclude)
|
||||||
|
|
||||||
if not skip_registry:
|
if not skip_registry:
|
||||||
registry.register(cls)
|
registry.register(cls)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ from .utils import (
|
||||||
camelize,
|
camelize,
|
||||||
get_model_fields,
|
get_model_fields,
|
||||||
get_reverse_fields,
|
get_reverse_fields,
|
||||||
import_single_dispatch,
|
|
||||||
is_valid_django_model,
|
is_valid_django_model,
|
||||||
maybe_queryset,
|
maybe_queryset,
|
||||||
)
|
)
|
||||||
|
@ -16,6 +15,5 @@ __all__ = [
|
||||||
"get_model_fields",
|
"get_model_fields",
|
||||||
"camelize",
|
"camelize",
|
||||||
"is_valid_django_model",
|
"is_valid_django_model",
|
||||||
"import_single_dispatch",
|
|
||||||
"GraphQLTestCase",
|
"GraphQLTestCase",
|
||||||
]
|
]
|
||||||
|
|
|
@ -24,11 +24,13 @@ class GraphQLTestCase(TestCase):
|
||||||
|
|
||||||
cls._client = Client()
|
cls._client = Client()
|
||||||
|
|
||||||
def query(self, query, op_name=None, input_data=None, variables=None):
|
def query(
|
||||||
|
self, query, operation_name=None, input_data=None, variables=None, headers=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
query (string) - GraphQL query to run
|
query (string) - GraphQL query to run
|
||||||
op_name (string) - If the query is a mutation or named query, you must
|
operation_name (string) - If the query is a mutation or named query, you must
|
||||||
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
|
||||||
|
@ -37,13 +39,15 @@ class GraphQLTestCase(TestCase):
|
||||||
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
|
||||||
|
will be set to this value.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response object from client
|
Response object from client
|
||||||
"""
|
"""
|
||||||
body = {"query": query}
|
body = {"query": query}
|
||||||
if op_name:
|
if operation_name:
|
||||||
body["operation_name"] = op_name
|
body["operationName"] = operation_name
|
||||||
if variables:
|
if variables:
|
||||||
body["variables"] = variables
|
body["variables"] = variables
|
||||||
if input_data:
|
if input_data:
|
||||||
|
@ -51,10 +55,17 @@ class GraphQLTestCase(TestCase):
|
||||||
body["variables"]["input"] = input_data
|
body["variables"]["input"] = input_data
|
||||||
else:
|
else:
|
||||||
body["variables"] = {"input": input_data}
|
body["variables"] = {"input": input_data}
|
||||||
|
if headers:
|
||||||
resp = self._client.post(
|
resp = self._client.post(
|
||||||
self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
|
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
|
return resp
|
||||||
|
|
||||||
def assertResponseNoErrors(self, resp):
|
def assertResponseNoErrors(self, resp):
|
||||||
|
|
|
@ -2,8 +2,7 @@ import inspect
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.manager import Manager
|
from django.db.models.manager import Manager
|
||||||
from django.utils import six
|
from django.utils.encoding import force_str
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
|
|
||||||
from graphene.utils.str_converters import to_camel_case
|
from graphene.utils.str_converters import to_camel_case
|
||||||
|
@ -26,14 +25,14 @@ def isiterable(value):
|
||||||
|
|
||||||
def _camelize_django_str(s):
|
def _camelize_django_str(s):
|
||||||
if isinstance(s, Promise):
|
if isinstance(s, Promise):
|
||||||
s = force_text(s)
|
s = force_str(s)
|
||||||
return to_camel_case(s) if isinstance(s, six.string_types) else s
|
return to_camel_case(s) if isinstance(s, str) else s
|
||||||
|
|
||||||
|
|
||||||
def camelize(data):
|
def camelize(data):
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
|
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
|
||||||
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
|
if isiterable(data) and not isinstance(data, (str, Promise)):
|
||||||
return [camelize(d) for d in data]
|
return [camelize(d) for d in data]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -77,26 +76,3 @@ def get_model_fields(model):
|
||||||
|
|
||||||
def is_valid_django_model(model):
|
def is_valid_django_model(model):
|
||||||
return inspect.isclass(model) and issubclass(model, models.Model)
|
return inspect.isclass(model) and issubclass(model, models.Model)
|
||||||
|
|
||||||
|
|
||||||
def import_single_dispatch():
|
|
||||||
try:
|
|
||||||
from functools import singledispatch
|
|
||||||
except ImportError:
|
|
||||||
singledispatch = None
|
|
||||||
|
|
||||||
if not singledispatch:
|
|
||||||
try:
|
|
||||||
from singledispatch import singledispatch
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not singledispatch:
|
|
||||||
raise Exception(
|
|
||||||
"It seems your python version does not include "
|
|
||||||
"functools.singledispatch. Please install the 'singledispatch' "
|
|
||||||
"package. More information here: "
|
|
||||||
"https://pypi.python.org/pypi/singledispatch"
|
|
||||||
)
|
|
||||||
|
|
||||||
return singledispatch
|
|
||||||
|
|
|
@ -2,19 +2,19 @@ import inspect
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
|
||||||
from django.http import HttpResponse, HttpResponseNotAllowed
|
from django.http import HttpResponse, HttpResponseNotAllowed
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
from django.views.generic import View
|
||||||
from graphql import get_default_backend
|
from graphql import OperationType, get_operation_ast, parse, validate
|
||||||
from graphql.error import format_error as format_graphql_error
|
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
|
from graphql.error import format_error as format_graphql_error
|
||||||
from graphql.execution import ExecutionResult
|
from graphql.execution import ExecutionResult
|
||||||
from graphql.type.schema import GraphQLSchema
|
|
||||||
|
from graphene import Schema
|
||||||
|
from graphql.execution.middleware import MiddlewareManager
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
|
|
||||||
|
@ -57,8 +57,6 @@ class GraphQLView(View):
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
executor = None
|
|
||||||
backend = None
|
|
||||||
middleware = None
|
middleware = None
|
||||||
root_value = None
|
root_value = None
|
||||||
pretty = False
|
pretty = False
|
||||||
|
@ -67,35 +65,31 @@ class GraphQLView(View):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
schema=None,
|
schema=None,
|
||||||
executor=None,
|
|
||||||
middleware=None,
|
middleware=None,
|
||||||
root_value=None,
|
root_value=None,
|
||||||
graphiql=False,
|
graphiql=False,
|
||||||
pretty=False,
|
pretty=False,
|
||||||
batch=False,
|
batch=False,
|
||||||
backend=None,
|
|
||||||
):
|
):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
|
||||||
if backend is None:
|
|
||||||
backend = get_default_backend()
|
|
||||||
|
|
||||||
if middleware is None:
|
if middleware is None:
|
||||||
middleware = graphene_settings.MIDDLEWARE
|
middleware = graphene_settings.MIDDLEWARE
|
||||||
|
|
||||||
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.executor = executor
|
self.middleware = middleware
|
||||||
|
else:
|
||||||
|
self.middleware = list(instantiate_middleware(middleware))
|
||||||
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
|
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
self.schema, GraphQLSchema
|
self.schema, Schema
|
||||||
), "A Schema is required to be provided to GraphQLView."
|
), "A Schema is required to be provided to GraphQLView."
|
||||||
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
assert not all((graphiql, batch)), "Use either graphiql or batch processing"
|
||||||
|
|
||||||
|
@ -109,9 +103,6 @@ class GraphQLView(View):
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def get_backend(self, request):
|
|
||||||
return self.backend
|
|
||||||
|
|
||||||
@method_decorator(ensure_csrf_cookie)
|
@method_decorator(ensure_csrf_cookie)
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
@ -173,7 +164,9 @@ class GraphQLView(View):
|
||||||
self.format_error(e) for e in execution_result.errors
|
self.format_error(e) for e in execution_result.errors
|
||||||
]
|
]
|
||||||
|
|
||||||
if execution_result.invalid:
|
if execution_result.errors and any(
|
||||||
|
not e.path for e in execution_result.errors
|
||||||
|
):
|
||||||
status_code = 400
|
status_code = 400
|
||||||
else:
|
else:
|
||||||
response["data"] = execution_result.data
|
response["data"] = execution_result.data
|
||||||
|
@ -246,14 +239,13 @@ class GraphQLView(View):
|
||||||
raise HttpError(HttpResponseBadRequest("Must provide query string."))
|
raise HttpError(HttpResponseBadRequest("Must provide query string."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
backend = self.get_backend(request)
|
document = parse(query)
|
||||||
document = backend.document_from_string(self.schema, query)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
return ExecutionResult(errors=[e])
|
||||||
|
|
||||||
if request.method.lower() == "get":
|
if request.method.lower() == "get":
|
||||||
operation_type = document.get_operation_type(operation_name)
|
operation_ast = get_operation_ast(document, operation_name)
|
||||||
if operation_type and operation_type != "query":
|
if operation_ast and operation_ast.operation != OperationType.QUERY:
|
||||||
if show_graphiql:
|
if show_graphiql:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -261,28 +253,23 @@ class GraphQLView(View):
|
||||||
HttpResponseNotAllowed(
|
HttpResponseNotAllowed(
|
||||||
["POST"],
|
["POST"],
|
||||||
"Can only perform a {} operation from a POST request.".format(
|
"Can only perform a {} operation from a POST request.".format(
|
||||||
operation_type
|
operation_ast.operation.value
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
validation_errors = validate(self.schema.graphql_schema, document)
|
||||||
extra_options = {}
|
if validation_errors:
|
||||||
if self.executor:
|
return ExecutionResult(data=None, errors=validation_errors)
|
||||||
# We only include it optionally since
|
|
||||||
# executor is not a valid argument in all backends
|
|
||||||
extra_options["executor"] = self.executor
|
|
||||||
|
|
||||||
return document.execute(
|
return self.schema.execute(
|
||||||
root=self.get_root_value(request),
|
source=query,
|
||||||
variables=variables,
|
root_value=self.get_root_value(request),
|
||||||
operation_name=operation_name,
|
variable_values=variables,
|
||||||
context=self.get_context(request),
|
operation_name=operation_name,
|
||||||
middleware=self.get_middleware(request),
|
context_value=self.get_context(request),
|
||||||
**extra_options
|
middleware=self.get_middleware(request),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_display_graphiql(cls, request, data):
|
def can_display_graphiql(cls, request, data):
|
||||||
|
@ -314,7 +301,7 @@ class GraphQLView(View):
|
||||||
variables = request.GET.get("variables") or data.get("variables")
|
variables = request.GET.get("variables") or data.get("variables")
|
||||||
id = request.GET.get("id") or data.get("id")
|
id = request.GET.get("id") or data.get("id")
|
||||||
|
|
||||||
if variables and isinstance(variables, six.text_type):
|
if variables and isinstance(variables, str):
|
||||||
try:
|
try:
|
||||||
variables = json.loads(variables)
|
variables = json.loads(variables)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -331,7 +318,7 @@ class GraphQLView(View):
|
||||||
if isinstance(error, GraphQLError):
|
if isinstance(error, GraphQLError):
|
||||||
return format_graphql_error(error)
|
return format_graphql_error(error)
|
||||||
|
|
||||||
return {"message": six.text_type(error)}
|
return {"message": str(error)}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_content_type(request):
|
def get_content_type(request):
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[pytest]
|
|
||||||
DJANGO_SETTINGS_MODULE = django_test_settings
|
|
|
@ -43,3 +43,7 @@ include_trailing_comma=True
|
||||||
force_grid_wrap=0
|
force_grid_wrap=0
|
||||||
use_parentheses=True
|
use_parentheses=True
|
||||||
line_length=88
|
line_length=88
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE = django_test_settings
|
||||||
|
addopts = --random-order
|
||||||
|
|
30
setup.py
30
setup.py
|
@ -1,8 +1,8 @@
|
||||||
from setuptools import find_packages, setup
|
|
||||||
import sys
|
|
||||||
import ast
|
import ast
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
_version_re = re.compile(r"__version__\s+=\s+(.*)")
|
_version_re = re.compile(r"__version__\s+=\s+(.*)")
|
||||||
|
|
||||||
with open("graphene_django/__init__.py", "rb") as f:
|
with open("graphene_django/__init__.py", "rb") as f:
|
||||||
|
@ -16,20 +16,20 @@ rest_framework_require = ["djangorestframework>=3.6.3"]
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"pytest>=3.6.3",
|
"pytest>=3.6.3",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
|
"pytest-random-order",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"mock",
|
"mock",
|
||||||
"pytz",
|
"pytz",
|
||||||
"django-filter<2;python_version<'3'",
|
"django-filter>=2",
|
||||||
"django-filter>=2;python_version>='3'",
|
|
||||||
"pytest-django>=3.3.2",
|
"pytest-django>=3.3.2",
|
||||||
] + rest_framework_require
|
] + rest_framework_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(
|
||||||
|
@ -45,22 +45,18 @@ setup(
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
"Programming Language :: Python :: 2",
|
|
||||||
"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",
|
||||||
],
|
],
|
||||||
keywords="api graphql protocol rest relay graphene",
|
keywords="api graphql protocol rest relay graphene",
|
||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests"]),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"six>=1.10.0",
|
"graphene>=3.0.0b1,<4",
|
||||||
"graphene>=2.1.7,<3",
|
"graphql-core>=3.1.0,<4",
|
||||||
"graphql-core>=2.1.0,<3",
|
"Django>=2.2",
|
||||||
"Django>=1.11",
|
|
||||||
"singledispatch>=3.4.0.3",
|
|
||||||
"promise>=2.1",
|
"promise>=2.1",
|
||||||
],
|
],
|
||||||
setup_requires=["pytest-runner"],
|
setup_requires=["pytest-runner"],
|
||||||
|
|
26
tox.ini
26
tox.ini
|
@ -1,14 +1,18 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{27,35,36,37}-django{111,20,21,22,master},
|
py{36,37,38}-django{22,30,master},
|
||||||
black,flake8
|
black,flake8
|
||||||
|
|
||||||
[travis:env]
|
[gh-actions]
|
||||||
|
python =
|
||||||
|
3.6: py36
|
||||||
|
3.7: py37
|
||||||
|
3.8: py38
|
||||||
|
|
||||||
|
[gh-actions:env]
|
||||||
DJANGO =
|
DJANGO =
|
||||||
1.11: django111
|
|
||||||
2.0: django20
|
|
||||||
2.1: django21
|
|
||||||
2.2: django22
|
2.2: django22
|
||||||
|
3.0: django30
|
||||||
master: djangomaster
|
master: djangomaster
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
@ -18,22 +22,20 @@ setenv =
|
||||||
DJANGO_SETTINGS_MODULE=django_test_settings
|
DJANGO_SETTINGS_MODULE=django_test_settings
|
||||||
deps =
|
deps =
|
||||||
-e.[test]
|
-e.[test]
|
||||||
psycopg2
|
psycopg2-binary
|
||||||
django111: Django>=1.11,<2.0
|
|
||||||
django20: Django>=2.0,<2.1
|
|
||||||
django21: Django>=2.1,<2.2
|
|
||||||
django22: Django>=2.2,<3.0
|
django22: Django>=2.2,<3.0
|
||||||
|
django30: Django>=3.0a1,<3.1
|
||||||
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