Compare commits

..

No commits in common. "main" and "v3.0.0" have entirely different histories.
main ... v3.0.0

126 changed files with 1251 additions and 4234 deletions

View File

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

View File

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

View File

@ -1,10 +1,6 @@
name: Tests name: Tests
on: on: [push, pull_request]
push:
branches: ["main"]
pull_request:
workflow_call:
jobs: jobs:
build: build:
@ -12,29 +8,15 @@ jobs:
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
django: ["3.2", "4.2", "5.0", "5.1", "5.2"] django: ["3.2", "4.0", "4.1"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["3.8", "3.9", "3.10"]
exclude: include:
- django: "3.2" - django: "3.2"
python-version: "3.11" python-version: "3.7"
- django: "3.2"
python-version: "3.12"
- django: "5.0"
python-version: "3.8"
- django: "5.0"
python-version: "3.9"
- django: "5.1"
python-version: "3.8"
- django: "5.1"
python-version: "3.9"
- django: "5.2"
python-version: "3.8"
- django: "5.2"
python-version: "3.9"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
@ -45,3 +27,4 @@ jobs:
run: tox run: tox
env: env:
DJANGO: ${{ matrix.django }} DJANGO: ${{ matrix.django }}
TOXENV: ${{ matrix.toxenv }}

8
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ make tests
## Opening Pull Requests ## Opening Pull Requests
Please fork the project and open a pull request against the `main` branch. Please fork the project and open a pull request against the master branch.
This will trigger a series of test and lint checks. This will trigger a series of test and lint checks.

View File

@ -6,19 +6,18 @@ help:
.PHONY: dev-setup ## Install development dependencies .PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
python -m pre_commit install
.PHONY: tests ## Run unit tests .PHONY: tests ## Run unit tests
tests: tests:
PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
.PHONY: format ## Format code .PHONY: format ## Format code
format: format:
ruff format graphene_django examples setup.py black --exclude "/migrations/" graphene_django examples setup.py
.PHONY: lint ## Lint code .PHONY: lint ## Lint code
lint: lint:
ruff graphene_django examples flake8 graphene_django examples
.PHONY: docs ## Generate docs .PHONY: docs ## Generate docs
docs: dev-setup docs: dev-setup

165
README.md
View File

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

122
README.rst Normal file
View File

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

View File

@ -144,21 +144,6 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
return queryset.filter(published=True) return queryset.filter(published=True)
return queryset return queryset
.. warning::
Defining a custom ``get_queryset`` gives the guaranteed it will be called
when resolving the ``DjangoObjectType``, even through related objects.
Note that because of this, benefits from using ``select_related``
in objects that define a relation to this ``DjangoObjectType`` will be canceled out.
In the case of ``prefetch_related``, the benefits of the optimization will be lost only
if the custom ``get_queryset`` modifies the queryset. For more information about this, refers
to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related.
If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving,
you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this
can lead to authorization leaks if you are performing authorization checks in the custom
``get_queryset``.
Filtering ID-based Node Access Filtering ID-based Node Access
------------------------------ ------------------------------

View File

@ -78,7 +78,7 @@ release = "1.0.dev"
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
# language = None language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
@ -445,7 +445,4 @@ epub_exclude_files = ["search.html"]
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = { intersphinx_mapping = {"https://docs.python.org/": None}
# "https://docs.python.org/": None,
"python": ("https://docs.python.org/", None),
}

View File

@ -2,8 +2,8 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results. `django-filter <https://django-filter.readthedocs.io/en/main/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__ See the `usage documentation <https://django-filter.readthedocs.io/en/main/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``.

View File

@ -33,6 +33,5 @@ For more advanced use, check out the Relay tutorial.
authorization authorization
debug debug
introspection introspection
validation
testing testing
settings settings

View File

@ -125,55 +125,6 @@ to change how the form is saved or to return a different Graphene object type.
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
DjangoFormInputObjectType
~~~~~~~~~~~~~~~~~~~~~~~~~
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
.. code:: python
from graphene_django.forms.types import DjangoFormInputObjectType
class PetFormInput(DjangoFormInputObjectType):
# any other fields can be placed here as well as
# other djangoforminputobjects and intputobjects
class Meta:
form_class = PetForm
object_type = PetType
class QuestionFormInput(DjangoFormInputObjectType)
class Meta:
form_class = QuestionForm
object_type = QuestionType
class SeveralFormsInputData(graphene.InputObjectType):
pet = PetFormInput(required=True)
question = QuestionFormInput(required=True)
class SomeSophisticatedMutation(graphene.Mutation):
class Arguments:
data = SeveralFormsInputData(required=True)
@staticmethod
def mutate(_root, _info, data):
pet_form_inst = PetForm(data=data.pet)
question_form_inst = QuestionForm(data=data.question)
if pet_form_inst.is_valid():
pet_model_instance = pet_form_inst.save(commit=False)
if question_form_inst.is_valid():
question_model_instance = question_form_inst.save(commit=False)
# ...
Additional to **InputObjectType** ``Meta`` class attributes:
* ``form_class`` is required and should be equal to django form class.
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
Django REST Framework Django REST Framework
--------------------- ---------------------

View File

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

View File

@ -8,22 +8,25 @@ from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
class Meta: class Meta:
model = Category model = Category
fields = "__all__" fields = '__all__'
class IngredientType(DjangoObjectType): class IngredientType(DjangoObjectType):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = "__all__" fields = '__all__'
class Query: class Query(object):
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
all_categories = graphene.List(CategoryType) all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String() ingredient = graphene.Field(IngredientType,
) id=graphene.Int(),
name=graphene.String())
all_ingredients = graphene.List(IngredientType) all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs): def resolve_all_categories(self, info, **kwargs):
@ -33,8 +36,8 @@ class Query:
return Ingredient.objects.all() return Ingredient.objects.all()
def resolve_category(self, info, **kwargs): def resolve_category(self, info, **kwargs):
id = kwargs.get("id") id = kwargs.get('id')
name = kwargs.get("name") name = kwargs.get('name')
if id is not None: if id is not None:
return Category.objects.get(pk=id) return Category.objects.get(pk=id)
@ -45,8 +48,8 @@ class Query:
return None return None
def resolve_ingredient(self, info, **kwargs): def resolve_ingredient(self, info, **kwargs):
id = kwargs.get("id") id = kwargs.get('id')
name = kwargs.get("name") name = kwargs.get('name')
if id is not None: if id is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(pk=id)

View File

@ -6,7 +6,7 @@ Graphene-Django can be customised using settings. This page explains each settin
Usage Usage
----- -----
Add settings to your Django project by creating a Dictionary with name ``GRAPHENE`` in the project's ``settings.py``: Add settings to your Django project by creating a Dictonary with name ``GRAPHENE`` in the project's ``settings.py``:
.. code:: python .. code:: python
@ -142,15 +142,6 @@ Default: ``False``
# ] # ]
``DJANGO_CHOICE_FIELD_ENUM_CONVERT``
--------------------------------------
When set to ``True`` Django choice fields are automatically converted into Enum types.
Can be disabled globally by setting it to ``False``.
Default: ``True``
``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING`` ``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING``
-------------------------------------- --------------------------------------
@ -206,6 +197,9 @@ Set to ``False`` if you want to disable GraphiQL headers editor tab for some rea
This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_. This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_.
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
Default: ``True`` Default: ``True``
.. code:: python .. code:: python
@ -230,12 +224,14 @@ Default: ``/graphql``
``GRAPHIQL_SHOULD_PERSIST_HEADERS`` ``GRAPHIQL_SHOULD_PERSIST_HEADERS``
----------------------------------- ---------------------
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page. Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_. This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
.. _GraphiQLDocs: https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
Default: ``False`` Default: ``False``
@ -244,48 +240,3 @@ Default: ``False``
GRAPHENE = { GRAPHENE = {
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False, 'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
} }
``GRAPHIQL_INPUT_VALUE_DEPRECATION``
------------------------------------
Set to ``True`` if you want GraphiQL to show any deprecated fields on input object types' docs.
For example, having this schema:
.. code:: python
class MyMutationInputType(graphene.InputObjectType):
old_field = graphene.String(deprecation_reason="You should now use 'newField' instead.")
new_field = graphene.String()
class MyMutation(graphene.Mutation):
class Arguments:
input = types.MyMutationInputType()
GraphiQL will add a ``Show Deprecated Fields`` button to toggle information display on ``oldField`` and its deprecation
reason. Otherwise, you would get neither a button nor any information at all on ``oldField``.
This setting is passed to ``inputValueDeprecation`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_INPUT_VALUE_DEPRECATION': False,
}
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
``MAX_VALIDATION_ERRORS``
------------------------------------
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
*i.e.* 100.
Default: ``None``

View File

@ -104,7 +104,7 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be Now is a good time to load up some test data. The easiest option will be
to `download the to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following: following:

View File

@ -7,12 +7,12 @@ Graphene has a number of additional features that are designed to make
working with Django *really simple*. working with Django *really simple*.
Note: The code in this quickstart is pulled from the `cookbook example Note: The code in this quickstart is pulled from the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__. app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
A good idea is to check the following things first: A good idea is to check the following things first:
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__ * `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
* `GraphQL Relay Specification <https://relay.dev/docs/guides/graphql-server-specification/>`__ * `GraphQL Relay Specification <https://facebook.github.io/relay/docs/en/graphql-server-specification.html>`__
Setup the Django project Setup the Django project
------------------------ ------------------------
@ -87,7 +87,7 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be Now is a good time to load up some test data. The easiest option will be
to `download the to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following: following:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ class IngredientType(DjangoObjectType):
fields = "__all__" fields = "__all__"
class Query: class Query(object):
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType) all_categories = graphene.List(CategoryType)

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ class RecipeIngredientType(DjangoObjectType):
fields = "__all__" fields = "__all__"
class Query: class Query(object):
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType) all_recipes = graphene.List(RecipeType)

View File

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

View File

@ -5,10 +5,10 @@ Django settings for cookbook project.
Generated by 'django-admin startproject' using Django 1.9. Generated by 'django-admin startproject' using Django 1.9.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/ https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/1.9/ref/settings/
""" """
import os import os
@ -18,7 +18,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
@ -81,7 +81,7 @@ WSGI_APPLICATION = "cookbook.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = { DATABASES = {
"default": { "default": {
@ -90,11 +90,9 @@ DATABASES = {
} }
} }
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
@ -107,7 +105,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
@ -121,6 +119,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/ # https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = "/static/" STATIC_URL = "/static/"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
from cookbook.ingredients.models import Category, Ingredient
from graphene import Node from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
# Graphene will automatically map the Category model's fields onto the CategoryNode. # Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below) # This is configured in the CategoryNode's Meta class (as you can see below)
@ -29,7 +28,7 @@ class IngredientNode(DjangoObjectType):
} }
class Query: class Query(object):
category = Node.Field(CategoryNode) category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)

View File

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

View File

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

View File

@ -6,9 +6,7 @@ from cookbook.ingredients.models import Ingredient
class Recipe(models.Model): class Recipe(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
instructions = models.TextField() instructions = models.TextField()
__unicode__ = lambda self: self.title
def __unicode__(self):
return self.title
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):

View File

@ -1,9 +1,8 @@
from cookbook.recipes.models import Recipe, RecipeIngredient
from graphene import Node from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.recipes.models import Recipe, RecipeIngredient
class RecipeNode(DjangoObjectType): class RecipeNode(DjangoObjectType):
class Meta: class Meta:
@ -26,7 +25,7 @@ class RecipeIngredientNode(DjangoObjectType):
} }
class Query: class Query(object):
recipe = Node.Field(RecipeNode) recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

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

View File

@ -3,6 +3,7 @@ from django.contrib import admin
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r"^admin/", admin.site.urls), url(r"^admin/", admin.site.urls),
url(r"^graphql$", GraphQLView.as_view(graphiql=True)), url(r"^graphql$", GraphQLView.as_view(graphiql=True)),

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import os
import sys import sys
import os
ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + "/examples/") sys.path.insert(0, ROOT_PATH + "/examples/")
@ -28,5 +28,3 @@ TEMPLATES = [
GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"} GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"}
ROOT_URLCONF = "graphene_django.tests.urls" ROOT_URLCONF = "graphene_django.tests.urls"
USE_TZ = True

View File

@ -28,7 +28,7 @@ def initialize():
# Yeah, technically it's Corellian. But it flew in the service of the rebels, # Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship. # so for the purposes of this demo it's a rebel ship.
falcon = Ship(id="4", name="Millennium Falcon", faction=rebels) falcon = Ship(id="4", name="Millenium Falcon", faction=rebels)
falcon.save() falcon.save()
homeOne = Ship(id="5", name="Home One", faction=rebels) homeOne = Ship(id="5", name="Home One", faction=rebels)

View File

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

View File

@ -1,13 +1,11 @@
import graphene import graphene
from graphene import Schema, relay from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType from graphene_django import DjangoConnectionField, DjangoObjectType
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
from .models import ( from .models import Character as CharacterModel
Character as CharacterModel, from .models import Faction as FactionModel
Faction as FactionModel, from .models import Ship as ShipModel
Ship as ShipModel,
)
class Ship(DjangoObjectType): class Ship(DjangoObjectType):
@ -62,13 +60,16 @@ class Query(graphene.ObjectType):
node = relay.Node.Field() node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description="All the ships.") ships = DjangoConnectionField(Ship, description="All the ships.")
def resolve_ships(self, info): @resolve_only_args
def resolve_ships(self):
return get_ships() return get_ships()
def resolve_rebels(self, info): @resolve_only_args
def resolve_rebels(self):
return get_rebels() return get_rebels()
def resolve_empire(self, info): @resolve_only_args
def resolve_empire(self):
return get_empire() return get_empire()

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
import inspect from collections import OrderedDict
from functools import partial, singledispatch, wraps from functools import singledispatch, wraps
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from graphql import GraphQLError
from graphene import ( from graphene import (
ID, ID,
@ -13,7 +12,6 @@ from graphene import (
Boolean, Boolean,
Date, Date,
DateTime, DateTime,
Decimal,
Dynamic, Dynamic,
Enum, Enum,
Field, Field,
@ -23,21 +21,16 @@ from graphene import (
NonNull, NonNull,
String, String,
Time, Time,
Decimal,
) )
from graphene.types.json import JSONString from graphene.types.json import JSONString
from graphene.types.resolver import get_default_resolver
from graphene.types.scalars import BigInt from graphene.types.scalars import BigInt
from graphene.utils.str_converters import to_camel_case from graphene.utils.str_converters import to_camel_case
from graphql import GraphQLError, assert_valid_name
try:
from graphql import assert_name
except ImportError:
# Support for older versions of graphql
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description from graphql.pyutils import register_description
from .compat import ArrayField, HStoreField, RangeField, normalize_choices from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
from .fields import DjangoConnectionField, DjangoListField from .fields import DjangoListField, DjangoConnectionField
from .settings import graphene_settings from .settings import graphene_settings
from .utils.str_converters import to_const from .utils.str_converters import to_const
@ -60,28 +53,10 @@ class BlankValueField(Field):
return blank_field_wrapper(resolver) return blank_field_wrapper(resolver)
class EnumValueField(BlankValueField):
def wrap_resolve(self, parent_resolver):
resolver = super().wrap_resolve(parent_resolver)
# create custom resolver
def enum_field_wrapper(func):
@wraps(func)
def wrapped_resolver(*args, **kwargs):
return_value = func(*args, **kwargs)
if isinstance(return_value, models.Choices):
return_value = return_value.value
return return_value
return wrapped_resolver
return enum_field_wrapper(resolver)
def convert_choice_name(name): def convert_choice_name(name):
name = to_const(force_str(name)) name = to_const(force_str(name))
try: try:
assert_name(name) assert_valid_name(name)
except GraphQLError: except GraphQLError:
name = "A_%s" % name name = "A_%s" % name
return name return name
@ -89,10 +64,12 @@ def convert_choice_name(name):
def get_choices(choices): def get_choices(choices):
converted_names = [] converted_names = []
choices = normalize_choices(choices) if isinstance(choices, OrderedDict):
choices = choices.items()
for value, help_text in choices: for value, help_text in choices:
if isinstance(help_text, (tuple, list)): if isinstance(help_text, (tuple, list)):
yield from get_choices(help_text) for choice in get_choices(help_text):
yield choice
else: else:
name = convert_choice_name(value) name = convert_choice_name(value)
while name in converted_names: while name in converted_names:
@ -109,17 +86,12 @@ def convert_choices_to_named_enum_with_descriptions(name, choices):
named_choices = [(c[0], c[1]) for c in choices] named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices} named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType: class EnumWithDescriptionsType(object):
@property @property
def description(self): def description(self):
return str(named_choices_descriptions[self.name]) return str(named_choices_descriptions[self.name])
return_type = Enum( return_type = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
name,
list(named_choices),
type=EnumWithDescriptionsType,
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
)
return return_type return return_type
@ -131,7 +103,7 @@ def generate_enum_name(django_model_meta, field):
) )
name = custom_func(field) name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True: elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True:
name = to_camel_case(f"{django_model_meta.object_name}_{field.name}") name = to_camel_case("{}_{}".format(django_model_meta.object_name, field.name))
else: else:
name = "{app_label}{object_name}{field_name}Choices".format( name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()), app_label=to_camel_case(django_model_meta.app_label.title()),
@ -149,24 +121,20 @@ def convert_choice_field_to_enum(field, name=None):
def convert_django_field_with_choices( def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=None field, registry=None, convert_choices_to_enum=True
): ):
if registry is not None: if registry is not None:
converted = registry.get_converted_field(field) converted = registry.get_converted_field(field)
if converted: if converted:
return converted return converted
choices = getattr(field, "choices", None) choices = getattr(field, "choices", None)
if convert_choices_to_enum is None:
convert_choices_to_enum = bool(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT
)
if choices and convert_choices_to_enum: if choices and convert_choices_to_enum:
EnumCls = convert_choice_field_to_enum(field) EnumCls = convert_choice_field_to_enum(field)
required = not (field.blank or field.null) required = not (field.blank or field.null)
converted = EnumCls( converted = EnumCls(
description=get_django_field_description(field), required=required description=get_django_field_description(field), required=required
).mount_as(EnumValueField) ).mount_as(BlankValueField)
else: else:
converted = convert_django_field(field, registry) converted = convert_django_field(field, registry)
if registry is not None: if registry is not None:
@ -181,7 +149,7 @@ def get_django_field_description(field):
@singledispatch @singledispatch
def convert_django_field(field, registry=None): def convert_django_field(field, registry=None):
raise Exception( raise Exception(
f"Don't know how to convert the Django field {field} ({field.__class__})" "Don't know how to convert the Django field %s (%s)" % (field, field.__class__)
) )
@ -199,13 +167,19 @@ def convert_field_to_string(field, registry=None):
) )
@convert_django_field.register(models.AutoField)
@convert_django_field.register(models.BigAutoField) @convert_django_field.register(models.BigAutoField)
@convert_django_field.register(models.SmallAutoField) @convert_django_field.register(models.AutoField)
def convert_field_to_id(field, registry=None): def convert_field_to_id(field, registry=None):
return ID(description=get_django_field_description(field), required=not field.null) return ID(description=get_django_field_description(field), required=not field.null)
if hasattr(models, "SmallAutoField"):
@convert_django_field.register(models.SmallAutoField)
def convert_field_small_to_id(field, registry=None):
return convert_field_to_id(field, registry)
@convert_django_field.register(models.UUIDField) @convert_django_field.register(models.UUIDField)
def convert_field_to_uuid(field, registry=None): def convert_field_to_uuid(field, registry=None):
return UUID( return UUID(
@ -272,10 +246,6 @@ def convert_time_to_string(field, registry=None):
@convert_django_field.register(models.OneToOneRel) @convert_django_field.register(models.OneToOneRel)
def convert_onetoone_field_to_djangomodel(field, registry=None): def convert_onetoone_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model model = field.related_model
def dynamic_type(): def dynamic_type():
@ -283,55 +253,7 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
if not _type: if not _type:
return return
class CustomField(Field): return Field(_type, required=not field.null)
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which goes through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args):
# Note: this function is used to resolve 1:1 relation fields
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
field_name = to_snake_case(info.field_name)
reversed_field_name = root.__class__._meta.get_field(
field_name
).remote_field.name
try:
return _type.get_queryset(
_type._meta.model.objects.filter(
**{reversed_field_name: root.pk}
),
info,
).get()
except _type._meta.model.DoesNotExist:
return None
return custom_resolver
return CustomField(
_type,
required=not field.null,
)
return Dynamic(dynamic_type) return Dynamic(dynamic_type)
@ -379,10 +301,6 @@ def convert_field_to_list_or_connection(field, registry=None):
@convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey) @convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field, registry=None): def convert_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model model = field.related_model
def dynamic_type(): def dynamic_type():
@ -393,72 +311,17 @@ def convert_field_to_djangomodel(field, registry=None):
class CustomField(Field): class CustomField(Field):
def wrap_resolve(self, parent_resolver): def wrap_resolve(self, parent_resolver):
""" """
Implements a custom resolver which goes through the `get_node` method to ensure that Implements a custom resolver which go through the `get_node` method to insure that
it goes through the `get_queryset` method of the DjangoObjectType. it goes through the `get_queryset` method of the DjangoObjectType.
""" """
resolver = super().wrap_resolve(parent_resolver) resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args): def custom_resolver(root, info, **args):
# Note: this function is used to resolve FK or 1:1 fields fk_obj = resolver(root, info, **args)
# it does not differentiate between custom-resolved fields if fk_obj is None:
# and default resolved fields.
# because this is a django foreign key or one-to-one field, the primary-key for
# this node can be accessed from the root node.
# ex: article.reporter_id
# get the name of the id field from the root's model
field_name = to_snake_case(info.field_name)
db_field_key = root.__class__._meta.get_field(field_name).attname
if hasattr(root, db_field_key):
# get the object's primary-key from root
object_pk = getattr(root, db_field_key)
else:
return None return None
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
instance_from_get_node = _type.get_node(info, object_pk)
if instance_from_get_node is None:
# no instance to return
return
elif (
isinstance(resolver, partial)
and resolver.func is get_default_resolver()
):
return instance_from_get_node
elif resolver is not get_default_resolver():
# Default resolver is overridden
# For optimization, add the instance to the resolver
setattr(root, field_name, instance_from_get_node)
# Explanation:
# previously, _type.get_node` is called which results in at least one hit to the database.
# But, if we did not pass the instance to the root, calling the resolver will result in
# another call to get the instance which results in at least two database queries in total
# to resolve this node only.
# That's why the value of the object is set in the root so when the object is accessed
# in the resolver (root.field_name) it does not access the database unless queried explicitly.
fk_obj = resolver(root, info, **args)
return fk_obj
else: else:
return instance_from_get_node return _type.get_node(info, fk_obj.pk)
return custom_resolver return custom_resolver
@ -488,8 +351,9 @@ def convert_postgres_array_to_list(field, registry=None):
@convert_django_field.register(HStoreField) @convert_django_field.register(HStoreField)
@convert_django_field.register(models.JSONField) @convert_django_field.register(PGJSONField)
def convert_json_field_to_string(field, registry=None): @convert_django_field.register(JSONField)
def convert_pg_and_json_field_to_string(field, registry=None):
return JSONString( return JSONString(
description=get_django_field_description(field), required=not field.null description=get_django_field_description(field), required=not field.null
) )

View File

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

View File

@ -1,4 +1,5 @@
# Code obtained from django-debug-toolbar sql panel tracking # Code obtained from django-debug-toolbar sql panel tracking
from __future__ import absolute_import, unicode_literals
import json import json
from threading import local from threading import local
@ -49,7 +50,7 @@ def unwrap_cursor(connection):
del connection._graphene_cursor del connection._graphene_cursor
class ExceptionCursorWrapper: class ExceptionCursorWrapper(object):
""" """
Wraps a cursor and raises an exception on any operation. Wraps a cursor and raises an exception on any operation.
Used in Templates panel. Used in Templates panel.
@ -62,7 +63,7 @@ class ExceptionCursorWrapper:
raise SQLQueryTriggered() raise SQLQueryTriggered()
class NormalCursorWrapper: class NormalCursorWrapper(object):
""" """
Wraps a cursor and logs queries. Wraps a cursor and logs queries.
""" """
@ -84,7 +85,7 @@ class NormalCursorWrapper:
if not params: if not params:
return params return params
if isinstance(params, dict): if isinstance(params, dict):
return {key: self._quote_expr(value) for key, value in params.items()} return dict((key, self._quote_expr(value)) for key, value in params.items())
return list(map(self._quote_expr, params)) return list(map(self._quote_expr, params))
def _decode(self, param): def _decode(self, param):

View File

@ -1,6 +1,5 @@
import pytest
import graphene import graphene
import pytest
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoConnectionField, DjangoObjectType from graphene_django import DjangoConnectionField, DjangoObjectType
@ -9,7 +8,7 @@ from ..middleware import DjangoDebugMiddleware
from ..types import DjangoDebug from ..types import DjangoDebug
class context: class context(object):
pass pass

View File

@ -1,7 +1,7 @@
from graphene import List, ObjectType from graphene import List, ObjectType
from .exception.types import DjangoDebugException
from .sql.types import DjangoDebugSQL from .sql.types import DjangoDebugSQL
from .exception.types import DjangoDebugException
class DjangoDebug(ObjectType): class DjangoDebug(ObjectType):

View File

@ -1,12 +1,14 @@
from functools import partial from functools import partial
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from graphql_relay import ( from graphql_relay import (
connection_from_array_slice, connection_from_array_slice,
cursor_to_offset, cursor_to_offset,
get_offset_with_default, get_offset_with_default,
offset_to_cursor, offset_to_cursor,
) )
from promise import Promise from promise import Promise
from graphene import Int, NonNull from graphene import Int, NonNull
@ -20,20 +22,17 @@ from .utils import maybe_queryset
class DjangoListField(Field): class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs): def __init__(self, _type, *args, **kwargs):
from .types import DjangoObjectType
if isinstance(_type, NonNull): if isinstance(_type, NonNull):
_type = _type.of_type _type = _type.of_type
# Django would never return a Set of None vvvvvvv # Django would never return a Set of None vvvvvvv
super().__init__(List(NonNull(_type)), *args, **kwargs) super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
@property
def type(self):
from .types import DjangoObjectType
assert issubclass( assert issubclass(
self._underlying_type, DjangoObjectType self._underlying_type, DjangoObjectType
), "DjangoListField only accepts DjangoObjectType types as underlying type" ), "DjangoListField only accepts DjangoObjectType types"
return super().type
@property @property
def _underlying_type(self): def _underlying_type(self):
@ -64,7 +63,7 @@ class DjangoListField(Field):
return queryset return queryset
def wrap_resolve(self, parent_resolver): def wrap_resolve(self, parent_resolver):
resolver = super().wrap_resolve(parent_resolver) resolver = super(DjangoListField, self).wrap_resolve(parent_resolver)
_type = self.type _type = self.type
if isinstance(_type, NonNull): if isinstance(_type, NonNull):
_type = _type.of_type _type = _type.of_type
@ -88,7 +87,7 @@ class DjangoConnectionField(ConnectionField):
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
) )
kwargs.setdefault("offset", Int()) kwargs.setdefault("offset", Int())
super().__init__(*args, **kwargs) super(DjangoConnectionField, self).__init__(*args, **kwargs)
@property @property
def type(self): def type(self):
@ -197,7 +196,7 @@ class DjangoConnectionField(ConnectionField):
enforce_first_or_last, enforce_first_or_last,
root, root,
info, info,
**args, **args
): ):
first = args.get("first") first = args.get("first")
last = args.get("last") last = args.get("last")
@ -247,7 +246,7 @@ class DjangoConnectionField(ConnectionField):
def wrap_resolve(self, parent_resolver): def wrap_resolve(self, parent_resolver):
return partial( return partial(
self.connection_resolver, self.connection_resolver,
self.resolver or parent_resolver, parent_resolver,
self.connection_type, self.connection_type,
self.get_manager(), self.get_manager(),
self.get_queryset_resolver(), self.get_queryset_resolver(),

View File

@ -1,5 +1,4 @@
import warnings import warnings
from ..utils import DJANGO_FILTER_INSTALLED from ..utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED: if not DJANGO_FILTER_INSTALLED:

View File

@ -3,8 +3,8 @@ from functools import partial
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from graphene.types.argument import to_arguments
from graphene.types.enum import EnumType from graphene.types.enum import EnumType
from graphene.types.argument import to_arguments
from graphene.utils.str_converters import to_snake_case from graphene.utils.str_converters import to_snake_case
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
@ -36,7 +36,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
extra_filter_meta=None, extra_filter_meta=None,
filterset_class=None, filterset_class=None,
*args, *args,
**kwargs, **kwargs
): ):
self._fields = fields self._fields = fields
self._provided_filterset_class = filterset_class self._provided_filterset_class = filterset_class
@ -44,7 +44,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
self._filtering_args = None self._filtering_args = None
self._extra_filter_meta = extra_filter_meta self._extra_filter_meta = extra_filter_meta
self._base_args = None self._base_args = None
super().__init__(type_, *args, **kwargs) super(DjangoFilterConnectionField, self).__init__(type_, *args, **kwargs)
@property @property
def args(self): def args(self):
@ -58,7 +58,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def filterset_class(self): def filterset_class(self):
if not self._filterset_class: if not self._filterset_class:
fields = self._fields or self.node_type._meta.filter_fields fields = self._fields or self.node_type._meta.filter_fields
meta = {"model": self.model, "fields": fields} meta = dict(model=self.model, fields=fields)
if self._extra_filter_meta: if self._extra_filter_meta:
meta.update(self._extra_filter_meta) meta.update(self._extra_filter_meta)
@ -90,7 +90,9 @@ class DjangoFilterConnectionField(DjangoConnectionField):
kwargs[k] = convert_enum(v) kwargs[k] = convert_enum(v)
return kwargs return kwargs
qs = super().resolve_queryset(connection, iterable, info, args) qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
connection, iterable, info, args
)
filterset = filterset_class( filterset = filterset_class(
data=filter_kwargs(), queryset=qs, request=info.context data=filter_kwargs(), queryset=qs, request=info.context

View File

@ -1,5 +1,4 @@
import warnings import warnings
from ...utils import DJANGO_FILTER_INSTALLED from ...utils import DJANGO_FILTER_INSTALLED
if not DJANGO_FILTER_INSTALLED: if not DJANGO_FILTER_INSTALLED:

View File

@ -1,36 +1,13 @@
from django_filters.constants import EMPTY_VALUES from django_filters.constants import EMPTY_VALUES
from django_filters.filters import FilterMethod
from .typed_filter import TypedFilter from .typed_filter import TypedFilter
class ArrayFilterMethod(FilterMethod):
def __call__(self, qs, value):
if value is None:
return qs
return self.method(qs, self.f.field_name, value)
class ArrayFilter(TypedFilter): class ArrayFilter(TypedFilter):
""" """
Filter made for PostgreSQL ArrayField. Filter made for PostgreSQL ArrayField.
""" """
@TypedFilter.method.setter
def method(self, value):
"""
Override method setter so that in case a custom `method` is provided
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
of the `FilterMethod` class) and instead use our ArrayFilterMethod that consider empty lists as values.
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
"""
TypedFilter.method.fset(self, value)
if value is not None:
self.filter = ArrayFilterMethod(self)
def filter(self, qs, value): def filter(self, qs, value):
""" """
Override the default filter class to check first whether the list is Override the default filter class to check first whether the list is
@ -45,6 +22,6 @@ class ArrayFilter(TypedFilter):
return qs return qs
if self.distinct: if self.distinct:
qs = qs.distinct() qs = qs.distinct()
lookup = f"{self.field_name}__{self.lookup_expr}" lookup = "%s__%s" % (self.field_name, self.lookup_expr)
qs = self.get_method(qs)(**{lookup: value}) qs = self.get_method(qs)(**{lookup: value})
return qs return qs

View File

@ -1,4 +1,5 @@
from django_filters import Filter, MultipleChoiceFilter from django_filters import Filter, MultipleChoiceFilter
from graphql_relay.node.node import from_global_id from graphql_relay.node.node import from_global_id
from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField
@ -16,7 +17,7 @@ class GlobalIDFilter(Filter):
_id = None _id = None
if value is not None: if value is not None:
_, _id = from_global_id(value) _, _id = from_global_id(value)
return super().filter(qs, _id) return super(GlobalIDFilter, self).filter(qs, _id)
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
@ -24,4 +25,4 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
def filter(self, qs, value): def filter(self, qs, value):
gids = [from_global_id(v)[1] for v in value] gids = [from_global_id(v)[1] for v in value]
return super().filter(qs, gids) return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)

View File

@ -1,36 +1,12 @@
from django_filters.filters import FilterMethod
from .typed_filter import TypedFilter from .typed_filter import TypedFilter
class ListFilterMethod(FilterMethod):
def __call__(self, qs, value):
if value is None:
return qs
return self.method(qs, self.f.field_name, value)
class ListFilter(TypedFilter): class ListFilter(TypedFilter):
""" """
Filter that takes a list of value as input. Filter that takes a list of value as input.
It is for example used for `__in` filters. It is for example used for `__in` filters.
""" """
@TypedFilter.method.setter
def method(self, value):
"""
Override method setter so that in case a custom `method` is provided
(see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method),
it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method
of the `FilterMethod` class) and instead use our ListFilterMethod that consider empty lists as values.
Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)`
which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead.
"""
TypedFilter.method.fset(self, value)
if value is not None:
self.filter = ListFilterMethod(self)
def filter(self, qs, value): def filter(self, qs, value):
""" """
Override the default filter class to check first whether the list is Override the default filter class to check first whether the list is
@ -47,4 +23,4 @@ class ListFilter(TypedFilter):
else: else:
return qs.none() return qs.none()
else: else:
return super().filter(qs, value) return super(ListFilter, self).filter(qs, value)

View File

@ -12,7 +12,7 @@ class TypedFilter(Filter):
def __init__(self, input_type=None, *args, **kwargs): def __init__(self, input_type=None, *args, **kwargs):
self._input_type = input_type self._input_type = input_type
super().__init__(*args, **kwargs) super(TypedFilter, self).__init__(*args, **kwargs)
@property @property
def input_type(self): def input_type(self):

View File

@ -1,14 +1,12 @@
import itertools import itertools
from django.db import models from django.db import models
from django_filters.filterset import ( from django_filters.filterset import BaseFilterSet, FilterSet
FILTER_FOR_DBFIELD_DEFAULTS, from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
BaseFilterSet,
FilterSet,
)
from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter
GRAPHENE_FILTER_SET_OVERRIDES = { GRAPHENE_FILTER_SET_OVERRIDES = {
models.AutoField: {"filter_class": GlobalIDFilter}, models.AutoField: {"filter_class": GlobalIDFilter},
models.OneToOneField: {"filter_class": GlobalIDFilter}, models.OneToOneField: {"filter_class": GlobalIDFilter},
@ -33,7 +31,7 @@ class GrapheneFilterSetMixin(BaseFilterSet):
def setup_filterset(filterset_class): def setup_filterset(filterset_class):
"""Wrap a provided filterset in Graphene-specific functionality""" """Wrap a provided filterset in Graphene-specific functionality"""
return type( return type(
f"Graphene{filterset_class.__name__}", "Graphene{}".format(filterset_class.__name__),
(filterset_class, GrapheneFilterSetMixin), (filterset_class, GrapheneFilterSetMixin),
{}, {},
) )
@ -42,7 +40,7 @@ def setup_filterset(filterset_class):
def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta):
"""Create a filterset for the given model using the provided meta data""" """Create a filterset for the given model using the provided meta data"""
meta.update({"model": model}) meta.update({"model": model})
meta_class = type("Meta", (object,), meta) meta_class = type(str("Meta"), (object,), meta)
filterset = type( filterset = type(
str("%sFilterSet" % model._meta.object_name), str("%sFilterSet" % model._meta.object_name),
(filterset_base_class, GrapheneFilterSetMixin), (filterset_base_class, GrapheneFilterSetMixin),

View File

@ -1,15 +1,15 @@
from functools import reduce from mock import MagicMock
import pytest import pytest
from django.db import models from django.db import models
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django_filters import filters
from django_filters import FilterSet from django_filters import FilterSet
import graphene import graphene
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import ArrayFilter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
from graphene_django.filter import ArrayFilter, ListFilter
from ...compat import ArrayField from ...compat import ArrayField
@ -25,15 +25,15 @@ else:
) )
STORE = {"events": []}
class Event(models.Model): class Event(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50)) tags = ArrayField(models.CharField(max_length=50))
tag_ids = ArrayField(models.IntegerField()) tag_ids = ArrayField(models.IntegerField())
random_field = ArrayField(models.BooleanField()) random_field = ArrayField(models.BooleanField())
def __repr__(self):
return f"Event [{self.name}]"
@pytest.fixture @pytest.fixture
def EventFilterSet(): def EventFilterSet():
@ -44,18 +44,10 @@ def EventFilterSet():
"name": ["exact", "contains"], "name": ["exact", "contains"],
} }
# Those are actually usable with our Query fixture below # Those are actually usable with our Query fixture bellow
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains") tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap") tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
tags = ArrayFilter(field_name="tags", lookup_expr="exact") tags = ArrayFilter(field_name="tags", lookup_expr="exact")
tags__len = ArrayFilter(
field_name="tags", lookup_expr="len", input_type=graphene.Int
)
tags__len__in = ArrayFilter(
field_name="tags",
method="tags__len__in_filter",
input_type=graphene.List(graphene.Int),
)
# Those are actually not usable and only to check type declarations # Those are actually not usable and only to check type declarations
tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains") tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains")
@ -69,14 +61,6 @@ def EventFilterSet():
) )
random_field = ArrayFilter(field_name="random_field", lookup_expr="exact") random_field = ArrayFilter(field_name="random_field", lookup_expr="exact")
def tags__len__in_filter(self, queryset, _name, value):
if not value:
return queryset.none()
return reduce(
lambda q1, q2: q1.union(q2),
[queryset.filter(tags__len=v) for v in value],
).distinct()
return EventFilterSet return EventFilterSet
@ -99,6 +83,11 @@ def Query(EventType):
we are running unit tests in sqlite which does not have ArrayFields. we are running unit tests in sqlite which does not have ArrayFields.
""" """
class Query(graphene.ObjectType):
events = DjangoFilterConnectionField(EventType)
def resolve_events(self, info, **kwargs):
events = [ events = [
Event(name="Live Show", tags=["concert", "music", "rock"]), Event(name="Live Show", tags=["concert", "music", "rock"]),
Event(name="Musical", tags=["movie", "music"]), Event(name="Musical", tags=["movie", "music"]),
@ -106,87 +95,57 @@ def Query(EventType):
Event(name="Speech", tags=[]), Event(name="Speech", tags=[]),
] ]
class Query(graphene.ObjectType): STORE["events"] = events
events = DjangoFilterConnectionField(EventType)
def resolve_events(self, info, **kwargs): m_queryset = MagicMock(spec=QuerySet)
class FakeQuerySet(QuerySet): m_queryset.model = Event
def __init__(self, model=None):
self.model = Event
self.__store = list(events)
def all(self): def filter_events(**kwargs):
return self
def filter(self, **kwargs):
queryset = FakeQuerySet()
queryset.__store = list(self.__store)
if "tags__contains" in kwargs: if "tags__contains" in kwargs:
queryset.__store = list( STORE["events"] = list(
filter( filter(
lambda e: set(kwargs["tags__contains"]).issubset( lambda e: set(kwargs["tags__contains"]).issubset(
set(e.tags) set(e.tags)
), ),
queryset.__store, STORE["events"],
) )
) )
if "tags__overlap" in kwargs: if "tags__overlap" in kwargs:
queryset.__store = list( STORE["events"] = list(
filter( filter(
lambda e: not set(kwargs["tags__overlap"]).isdisjoint( lambda e: not set(kwargs["tags__overlap"]).isdisjoint(
set(e.tags) set(e.tags)
), ),
queryset.__store, STORE["events"],
) )
) )
if "tags__exact" in kwargs: if "tags__exact" in kwargs:
queryset.__store = list( STORE["events"] = list(
filter( filter(
lambda e: set(kwargs["tags__exact"]) == set(e.tags), lambda e: set(kwargs["tags__exact"]) == set(e.tags),
queryset.__store, STORE["events"],
) )
) )
if "tags__len" in kwargs:
queryset.__store = list(
filter(
lambda e: len(e.tags) == kwargs["tags__len"],
queryset.__store,
)
)
return queryset
def union(self, *args): def mock_queryset_filter(*args, **kwargs):
queryset = FakeQuerySet() filter_events(**kwargs)
queryset.__store = self.__store return m_queryset
for arg in args:
queryset.__store += arg.__store
return queryset
def none(self): def mock_queryset_none(*args, **kwargs):
queryset = FakeQuerySet() STORE["events"] = []
queryset.__store = [] return m_queryset
return queryset
def count(self): def mock_queryset_count(*args, **kwargs):
return len(self.__store) return len(STORE["events"])
def distinct(self): m_queryset.all.return_value = m_queryset
queryset = FakeQuerySet() m_queryset.filter.side_effect = mock_queryset_filter
queryset.__store = [] m_queryset.none.side_effect = mock_queryset_none
for event in self.__store: m_queryset.count.side_effect = mock_queryset_count
if event not in queryset.__store: m_queryset.__getitem__.side_effect = lambda index: STORE[
queryset.__store.append(event) "events"
queryset.__store = sorted(queryset.__store, key=lambda e: e.name) ].__getitem__(index)
return queryset
def __getitem__(self, index): return m_queryset
return self.__store[index]
return FakeQuerySet()
return Query return Query
@pytest.fixture
def schema(Query):
return graphene.Schema(query=Query)

View File

@ -1,14 +1,18 @@
import pytest import pytest
from graphene import Schema
from ...compat import ArrayField, MissingType from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_multiple(schema): def test_array_field_contains_multiple(Query):
""" """
Test contains filter on a array field of string. Test contains filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Contains: ["concert", "music"]) { events (tags_Contains: ["concert", "music"]) {
@ -28,11 +32,13 @@ def test_array_field_contains_multiple(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_one(schema): def test_array_field_contains_one(Query):
""" """
Test contains filter on a array field of string. Test contains filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Contains: ["music"]) { events (tags_Contains: ["music"]) {
@ -53,11 +59,13 @@ def test_array_field_contains_one(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_contains_empty_list(schema): def test_array_field_contains_empty_list(Query):
""" """
Test contains filter on a array field of string. Test contains filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Contains: []) { events (tags_Contains: []) {

View File

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

View File

@ -1,14 +1,18 @@
import pytest import pytest
from graphene import Schema
from ...compat import ArrayField, MissingType from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_no_match(schema): def test_array_field_exact_no_match(Query):
""" """
Test exact filter on a array field of string. Test exact filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags: ["concert", "music"]) { events (tags: ["concert", "music"]) {
@ -26,11 +30,13 @@ def test_array_field_exact_no_match(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_match(schema): def test_array_field_exact_match(Query):
""" """
Test exact filter on a array field of string. Test exact filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags: ["movie", "music"]) { events (tags: ["movie", "music"]) {
@ -50,11 +56,13 @@ def test_array_field_exact_match(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_exact_empty_list(schema): def test_array_field_exact_empty_list(Query):
""" """
Test exact filter on a array field of string. Test exact filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags: []) { events (tags: []) {
@ -74,10 +82,11 @@ def test_array_field_exact_empty_list(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_filter_schema_type(schema): def test_array_field_filter_schema_type(Query):
""" """
Check that the type in the filter is an array field like on the object type. Check that the type in the filter is an array field like on the object type.
""" """
schema = Schema(query=Query)
schema_str = str(schema) schema_str = str(schema)
assert ( assert (
@ -103,8 +112,6 @@ def test_array_field_filter_schema_type(schema):
"tags_Contains": "[String!]", "tags_Contains": "[String!]",
"tags_Overlap": "[String!]", "tags_Overlap": "[String!]",
"tags": "[String!]", "tags": "[String!]",
"tags_Len": "Int",
"tags_Len_In": "[Int]",
"tagsIds_Contains": "[Int!]", "tagsIds_Contains": "[Int!]",
"tagsIds_Overlap": "[Int!]", "tagsIds_Overlap": "[Int!]",
"tagsIds": "[Int!]", "tagsIds": "[Int!]",

View File

@ -1,14 +1,18 @@
import pytest import pytest
from graphene import Schema
from ...compat import ArrayField, MissingType from ...compat import ArrayField, MissingType
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_overlap_multiple(schema): def test_array_field_overlap_multiple(Query):
""" """
Test overlap filter on a array field of string. Test overlap filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Overlap: ["concert", "music"]) { events (tags_Overlap: ["concert", "music"]) {
@ -30,11 +34,13 @@ def test_array_field_overlap_multiple(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_overlap_one(schema): def test_array_field_overlap_one(Query):
""" """
Test overlap filter on a array field of string. Test overlap filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Overlap: ["music"]) { events (tags_Overlap: ["music"]) {
@ -55,11 +61,13 @@ def test_array_field_overlap_one(schema):
@pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist")
def test_array_field_overlap_empty_list(schema): def test_array_field_overlap_empty_list(Query):
""" """
Test overlap filter on a array field of string. Test overlap filter on a array field of string.
""" """
schema = Schema(query=Query)
query = """ query = """
query { query {
events (tags_Overlap: []) { events (tags_Overlap: []) {

View File

@ -2,7 +2,8 @@ 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 DjangoObjectType, DjangoConnectionField
from graphene_django.tests.models import Article, Reporter from graphene_django.tests.models import Article, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED

View File

@ -19,8 +19,8 @@ if DJANGO_FILTER_INSTALLED:
from django_filters import FilterSet, NumberFilter, OrderingFilter from django_filters import FilterSet, NumberFilter, OrderingFilter
from graphene_django.filter import ( from graphene_django.filter import (
DjangoFilterConnectionField,
GlobalIDFilter, GlobalIDFilter,
DjangoFilterConnectionField,
GlobalIDMultipleChoiceFilter, GlobalIDMultipleChoiceFilter,
) )
from graphene_django.filter.tests.filters import ( from graphene_django.filter.tests.filters import (
@ -67,7 +67,7 @@ def assert_arguments(field, *arguments):
actual = [name for name in args if name not in ignore and not name.startswith("_")] actual = [name for name in args if name not in ignore and not name.startswith("_")]
assert set(arguments) == set( assert set(arguments) == set(
actual actual
), f"Expected arguments ({arguments}) did not match actual ({actual})" ), "Expected arguments ({}) did not match actual ({})".format(arguments, actual)
def assert_orderable(field): def assert_orderable(field):
@ -141,7 +141,7 @@ def test_filter_shortcut_filterset_context():
@property @property
def qs(self): def qs(self):
qs = super().qs qs = super(ArticleContextFilter, self).qs
return qs.filter(reporter=self.request.reporter) return qs.filter(reporter=self.request.reporter)
class Query(ObjectType): class Query(ObjectType):
@ -166,7 +166,7 @@ def test_filter_shortcut_filterset_context():
editor=r2, editor=r2,
) )
class context: class context(object):
reporter = r2 reporter = r2
query = """ query = """
@ -222,7 +222,7 @@ def test_filter_filterset_information_on_meta_related():
reporter = Field(ReporterFilterNode) reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode) article = Field(ArticleFilterNode)
Schema(query=Query) schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields["articles"].get_type() articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
assert_arguments(articles_field, "headline", "reporter") assert_arguments(articles_field, "headline", "reporter")
assert_not_orderable(articles_field) assert_not_orderable(articles_field)
@ -294,7 +294,7 @@ def test_filter_filterset_class_information_on_meta_related():
reporter = Field(ReporterFilterNode) reporter = Field(ReporterFilterNode)
article = Field(ArticleFilterNode) article = Field(ArticleFilterNode)
Schema(query=Query) schema = Schema(query=Query)
articles_field = ReporterFilterNode._meta.fields["articles"].get_type() articles_field = ReporterFilterNode._meta.fields["articles"].get_type()
assert_arguments(articles_field, "headline", "reporter") assert_arguments(articles_field, "headline", "reporter")
assert_not_orderable(articles_field) assert_not_orderable(articles_field)
@ -789,7 +789,7 @@ def test_order_by():
query = """ query = """
query NodeFilteringQuery { query NodeFilteringQuery {
allReporters(orderBy: "-firstname") { allReporters(orderBy: "-firtsnaMe") {
edges { edges {
node { node {
firstName firstName
@ -802,7 +802,7 @@ def test_order_by():
assert result.errors assert result.errors
def test_order_by_is_preserved(): def test_order_by_is_perserved():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
@ -1186,7 +1186,7 @@ def test_filter_filterset_based_on_mixin():
first_name="Adam", last_name="Doe", email="adam@doe.com" first_name="Adam", last_name="Doe", email="adam@doe.com"
) )
Article.objects.create( article_2 = Article.objects.create(
headline="Good Bye", headline="Good Bye",
reporter=reporter_2, reporter=reporter_2,
editor=reporter_2, editor=reporter_2,

View File

@ -1,16 +1,14 @@
from datetime import datetime from datetime import datetime
import pytest import pytest
from django_filters import (
FilterSet,
rest_framework as filters,
)
from django_filters import FilterSet
from django_filters import rest_framework as filters
from graphene import ObjectType, Schema from graphene import ObjectType, Schema
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.tests.models import Pet, Person, Reporter, Article, Film
from graphene_django.filter.tests.filters import ArticleFilter from graphene_django.filter.tests.filters import ArticleFilter
from graphene_django.tests.models import Article, Film, Person, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
pytestmark = [] pytestmark = []
@ -350,20 +348,23 @@ def test_fk_id_in_filter(query):
schema = Schema(query=query) schema = Schema(query=query)
query = f""" query = """
query {{ query {
articles (reporter_In: [{john_doe.id}, {jean_bon.id}]) {{ articles (reporter_In: [%s, %s]) {
edges {{ edges {
node {{ node {
headline headline
reporter {{ reporter {
lastName lastName
}} }
}} }
}} }
}} }
}} }
""" """ % (
john_doe.id,
jean_bon.id,
)
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data["articles"]["edges"] == [ assert result.data["articles"]["edges"] == [

View File

@ -1,7 +1,8 @@
import json import json
import pytest import pytest
from django_filters import FilterSet
from django_filters import rest_framework as filters
from graphene import ObjectType, Schema from graphene import ObjectType, Schema
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType

View File

@ -1,12 +1,10 @@
import operator
from functools import reduce
import pytest import pytest
from django.db.models import Q
from django_filters import FilterSet from django_filters import FilterSet
import graphene import graphene
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.tests.models import Article, Reporter from graphene_django.tests.models import Article, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
@ -16,8 +14,8 @@ pytestmark = []
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
from graphene_django.filter import ( from graphene_django.filter import (
DjangoFilterConnectionField, DjangoFilterConnectionField,
ListFilter,
TypedFilter, TypedFilter,
ListFilter,
) )
else: else:
pytestmark.append( pytestmark.append(
@ -48,10 +46,6 @@ def schema():
only_first = TypedFilter( only_first = TypedFilter(
input_type=graphene.Boolean, method="only_first_filter" input_type=graphene.Boolean, method="only_first_filter"
) )
headline_search = ListFilter(
method="headline_search_filter",
input_type=graphene.List(graphene.String),
)
def first_n_filter(self, queryset, _name, value): def first_n_filter(self, queryset, _name, value):
return queryset[:value] return queryset[:value]
@ -62,13 +56,6 @@ def schema():
else: else:
return queryset return queryset
def headline_search_filter(self, queryset, _name, value):
if not value:
return queryset.none()
return queryset.filter(
reduce(operator.or_, [Q(headline__icontains=v) for v in value])
)
class ArticleType(DjangoObjectType): class ArticleType(DjangoObjectType):
class Meta: class Meta:
model = Article model = Article
@ -102,7 +89,6 @@ def test_typed_filter_schema(schema):
"lang_InStr": "[String]", "lang_InStr": "[String]",
"firstN": "Int", "firstN": "Int",
"onlyFirst": "Boolean", "onlyFirst": "Boolean",
"headlineSearch": "[String]",
} }
all_articles_filters = ( all_articles_filters = (
@ -112,7 +98,7 @@ def test_typed_filter_schema(schema):
) )
for filter_field, gql_type in filters.items(): for filter_field, gql_type in filters.items():
assert f"{filter_field}: {gql_type}" in all_articles_filters assert "{}: {}".format(filter_field, gql_type) in all_articles_filters
def test_typed_filters_work(schema): def test_typed_filters_work(schema):
@ -120,7 +106,24 @@ def test_typed_filters_work(schema):
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en") Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es")
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "B"}},
]
query = 'query { articles (lang_InStr: ["es"]) { edges { node { headline } } } }'
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "B"}},
]
query = 'query { articles (lang_Contains: "n") { edges { node { headline } } } }' query = 'query { articles (lang_Contains: "n") { edges { node { headline } } } }'
@ -136,7 +139,7 @@ def test_typed_filters_work(schema):
assert not result.errors assert not result.errors
assert result.data["articles"]["edges"] == [ assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}}, {"node": {"headline": "A"}},
{"node": {"headline": "AB"}}, {"node": {"headline": "B"}},
] ]
query = "query { articles (onlyFirst: true) { edges { node { headline } } } }" query = "query { articles (onlyFirst: true) { edges { node { headline } } } }"
@ -146,86 +149,3 @@ def test_typed_filters_work(schema):
assert result.data["articles"]["edges"] == [ assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}}, {"node": {"headline": "A"}},
] ]
def test_list_filters_work(schema):
reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="")
Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es")
Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en")
Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es")
query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
]
query = 'query { articles (lang_InStr: ["es"]) { edges { node { headline } } } }'
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
]
query = "query { articles (lang_InStr: []) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == []
query = "query { articles (lang_InStr: null) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
{"node": {"headline": "C"}},
]
query = 'query { articles (headlineSearch: ["a", "B"]) { edges { node { headline } } } }'
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
]
query = "query { articles (headlineSearch: []) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == []
query = "query { articles (headlineSearch: null) { edges { node { headline } } } }"
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
{"node": {"headline": "C"}},
]
query = 'query { articles (headlineSearch: [""]) { edges { node { headline } } } }'
result = schema.execute(query)
assert not result.errors
assert result.data["articles"]["edges"] == [
{"node": {"headline": "A"}},
{"node": {"headline": "AB"}},
{"node": {"headline": "B"}},
{"node": {"headline": "C"}},
]

View File

@ -1,11 +1,10 @@
from django import forms
from django_filters.utils import get_model_field
import graphene import graphene
from django import forms
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField from django_filters.utils import get_model_field, get_field_parts
from .filters import ListFilter, RangeFilter, TypedFilter from django_filters.filters import Filter, BaseCSVFilter
from .filters import ArrayFilter, ListFilter, RangeFilter, TypedFilter
from .filterset import custom_filterset_factory, setup_filterset from .filterset import custom_filterset_factory, setup_filterset
from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField
def get_field_type(registry, model, field_name): def get_field_type(registry, model, field_name):
@ -43,7 +42,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
isinstance(filter_field, TypedFilter) isinstance(filter_field, TypedFilter)
and filter_field.input_type is not None and filter_field.input_type is not None
): ):
# First check if the filter input type has been explicitly given # First check if the filter input type has been explicitely given
field_type = filter_field.input_type field_type = filter_field.input_type
else: else:
if name not in filterset_class.declared_filters or isinstance( if name not in filterset_class.declared_filters or isinstance(
@ -51,7 +50,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
): ):
# Get the filter field for filters that are no explicitly declared. # Get the filter field for filters that are no explicitly declared.
if filter_type == "isnull": if filter_type == "isnull":
field_type = graphene.Boolean field = graphene.Boolean(required=required)
else: else:
model_field = get_model_field(model, filter_field.field_name) model_field = get_model_field(model, filter_field.field_name)
@ -145,7 +144,7 @@ def replace_csv_filters(filterset_class):
label=filter_field.label, label=filter_field.label,
method=filter_field.method, method=filter_field.method,
exclude=filter_field.exclude, exclude=filter_field.exclude,
**filter_field.extra, **filter_field.extra
) )
elif filter_type == "range": elif filter_type == "range":
filterset_class.base_filters[name] = RangeFilter( filterset_class.base_filters[name] = RangeFilter(
@ -154,5 +153,5 @@ def replace_csv_filters(filterset_class):
label=filter_field.label, label=filter_field.label,
method=filter_field.method, method=filter_field.method,
exclude=filter_field.exclude, exclude=filter_field.exclude,
**filter_field.extra, **filter_field.extra
) )

View File

@ -5,15 +5,15 @@ from django.core.exceptions import ImproperlyConfigured
from graphene import ( from graphene import (
ID, ID,
UUID,
Boolean, Boolean,
Date,
DateTime,
Decimal, Decimal,
Float, Float,
Int, Int,
List, List,
String, String,
UUID,
Date,
DateTime,
Time, Time,
) )
@ -27,8 +27,8 @@ def get_form_field_description(field):
@singledispatch @singledispatch
def convert_form_field(field): def convert_form_field(field):
raise ImproperlyConfigured( raise ImproperlyConfigured(
f"Don't know how to convert the Django form field {field} ({field.__class__}) " "Don't know how to convert the Django form field %s (%s) "
"to Graphene type" "to Graphene type" % (field, field.__class__)
) )

View File

@ -3,6 +3,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 gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from graphql_relay import from_global_id from graphql_relay import from_global_id

View File

@ -23,7 +23,8 @@ def fields_for_form(form, only_fields, exclude_fields):
for name, field in form.fields.items(): for name, field in form.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 = (
name in exclude_fields # or name
in exclude_fields # or
# name in already_created_fields # name in already_created_fields
) )
@ -81,6 +82,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
def __init_subclass_with_meta__( def __init_subclass_with_meta__(
cls, form_class=None, only_fields=(), exclude_fields=(), **options cls, form_class=None, only_fields=(), exclude_fields=(), **options
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoFormMutation") raise Exception("form_class is required for DjangoFormMutation")
@ -93,7 +95,7 @@ class DjangoFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super().__init_subclass_with_meta__( super(DjangoFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )
@ -125,8 +127,9 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
return_field_name=None, return_field_name=None,
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
**options, **options
): ):
if not form_class: if not form_class:
raise Exception("form_class is required for DjangoModelFormMutation") raise Exception("form_class is required for DjangoModelFormMutation")
@ -144,7 +147,7 @@ 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)
if not model_type: if not model_type:
raise Exception(f"No type registered for model: {model.__name__}") 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__
@ -160,7 +163,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super().__init_subclass_with_meta__( super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )

View File

@ -1,34 +1,31 @@
from django import VERSION as DJANGO_VERSION, forms from django import forms
from pytest import raises from py.test import raises
import graphene
from graphene import ( from graphene import (
ID, String,
UUID, Int,
Boolean, Boolean,
Date,
DateTime,
Decimal, Decimal,
Float, Float,
Int, ID,
UUID,
List, List,
NonNull, NonNull,
String, DateTime,
Date,
Time, Time,
) )
from ..converter import convert_form_field from ..converter import convert_form_field
def assert_conversion(django_field, graphene_field, *args, **kwargs): def assert_conversion(django_field, graphene_field, *args):
# Arrange field = django_field(*args, help_text="Custom Help Text")
help_text = kwargs.setdefault("help_text", "Custom Help Text")
field = django_field(*args, **kwargs)
# Act
graphene_type = convert_form_field(field) graphene_type = convert_form_field(field)
# Assert
assert isinstance(graphene_type, graphene_field) assert isinstance(graphene_type, graphene_field)
field = graphene_type.Field() field = graphene_type.Field()
assert field.description == help_text assert field.description == "Custom Help Text"
return field return field
@ -63,12 +60,7 @@ def test_should_slug_convert_string():
def test_should_url_convert_string(): def test_should_url_convert_string():
kwargs = {} assert_conversion(forms.URLField, String)
if DJANGO_VERSION >= (5, 0):
# silence RemovedInDjango60Warning
kwargs["assume_scheme"] = "https"
assert_conversion(forms.URLField, String, **kwargs)
def test_should_choice_convert_string(): def test_should_choice_convert_string():
@ -84,6 +76,7 @@ def test_should_regex_convert_string():
def test_should_uuid_convert_string(): def test_should_uuid_convert_string():
if hasattr(forms, "UUIDField"):
assert_conversion(forms.UUIDField, UUID) assert_conversion(forms.UUIDField, UUID)

View File

@ -1,333 +0,0 @@
from django import forms
from pytest import raises
import graphene
from graphene_django import DjangoObjectType
from ...tests.models import CHOICES, Film, Reporter
from ..types import DjangoFormInputObjectType
# Reporter a_choice CHOICES = ((1, "this"), (2, _("that")))
THIS = CHOICES[0][0]
THIS_ON_CLIENT_CONVERTED = "A_1"
# Film genre choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")],
DOCUMENTARY = "do"
DOCUMENTARY_ON_CLIENT_CONVERTED = "DO"
class FilmForm(forms.ModelForm):
class Meta:
model = Film
exclude = ()
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
fields = "__all__"
class ReporterForm(forms.ModelForm):
class Meta:
model = Reporter
exclude = ("pets", "email", "fans")
class MyForm(forms.Form):
text_field = forms.CharField()
int_field = forms.IntegerField()
def test_needs_form_class():
with raises(Exception) as exc:
class MyInputType(DjangoFormInputObjectType):
pass
assert exc.value.args[0] == "form_class is required for DjangoFormInputObjectType"
def test_type_from_modelform_has_input_fields():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
only_fields = ("first_name", "last_name", "a_choice")
fields = ["first_name", "last_name", "a_choice", "id"]
assert all(f in ReporterInputType._meta.fields for f in fields)
def test_type_from_form_has_input_fields():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
fields = ["text_field", "int_field", "id"]
assert all(f in MyFormInputType._meta.fields for f in fields)
def test_type_custom_id_field():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
add_id_field_name = "pk"
fields = ["text_field", "int_field", "pk"]
assert all(f in MyFormInputType._meta.fields for f in fields)
assert MyFormInputType._meta.fields["pk"].type is graphene.ID
def test_type_custom_id_field_type():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
add_id_field_name = "pk"
add_id_field_type = graphene.String(required=False)
fields = ["text_field", "int_field", "pk"]
assert all(f in MyFormInputType._meta.fields for f in fields)
assert MyFormInputType._meta.fields["pk"].type is graphene.String
class MockQuery(graphene.ObjectType):
a = graphene.String()
def test_mutation_with_form_djangoforminputtype():
class MyFormInputType(DjangoFormInputObjectType):
class Meta:
form_class = MyForm
class MyFormMutation(graphene.Mutation):
class Arguments:
form_data = MyFormInputType(required=True)
result = graphene.Boolean()
def mutate(_root, _info, form_data):
form = MyForm(data=form_data)
if form.is_valid():
result = form.cleaned_data == {
"text_field": "text",
"int_field": 777,
}
return MyFormMutation(result=result)
return MyFormMutation(result=False)
class Mutation(graphene.ObjectType):
myForm_mutation = MyFormMutation.Field()
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
result = schema.execute(
""" mutation MyFormMutation($formData: MyFormInputType!) {
myFormMutation(formData: $formData) {
result
}
}
""",
variable_values={"formData": {"textField": "text", "intField": 777}},
)
assert result.errors is None
assert result.data == {"myFormMutation": {"result": True}}
def test_mutation_with_modelform_djangoforminputtype():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterType
only_fields = ("first_name", "last_name", "a_choice")
class ReporterMutation(graphene.Mutation):
class Arguments:
reporter_data = ReporterInputType(required=True)
result = graphene.Field(ReporterType)
def mutate(_root, _info, reporter_data):
reporter = Reporter.objects.get(pk=reporter_data.id)
form = ReporterForm(data=reporter_data, instance=reporter)
if form.is_valid():
reporter = form.save()
return ReporterMutation(result=reporter)
return ReporterMutation(result=None)
class Mutation(graphene.ObjectType):
report_mutation = ReporterMutation.Field()
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
reporter = Reporter.objects.create(
first_name="Bob", last_name="Roberts", a_choice=THIS
)
result = schema.execute(
""" mutation ReportMutation($reporterData: ReporterInputType!) {
reportMutation(reporterData: $reporterData) {
result {
id,
firstName,
lastName,
aChoice
}
}
}
""",
variable_values={
"reporterData": {
"id": reporter.pk,
"firstName": "Dave",
"lastName": "Smith",
"aChoice": THIS_ON_CLIENT_CONVERTED,
}
},
)
assert result.errors is None
assert result.data["reportMutation"]["result"] == {
"id": "1",
"firstName": "Dave",
"lastName": "Smith",
"aChoice": THIS_ON_CLIENT_CONVERTED,
}
assert Reporter.objects.count() == 1
reporter.refresh_from_db()
assert reporter.first_name == "Dave"
def reporter_enum_convert_mutation_result(
ReporterInputType, choice_val_on_client=THIS_ON_CLIENT_CONVERTED
):
class ReporterMutation(graphene.Mutation):
class Arguments:
reporter = ReporterInputType(required=True)
result_str = graphene.String()
result_int = graphene.Int()
def mutate(_root, _info, reporter):
if isinstance(reporter.a_choice, int) or reporter.a_choice.isdigit():
return ReporterMutation(result_int=reporter.a_choice, result_str=None)
return ReporterMutation(result_int=None, result_str=reporter.a_choice)
class Mutation(graphene.ObjectType):
report_mutation = ReporterMutation.Field()
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
return schema.execute(
""" mutation ReportMutation($reporter: ReporterInputType!) {
reportMutation(reporter: $reporter) {
resultStr,
resultInt
}
}
""",
variable_values={"reporter": {"aChoice": choice_val_on_client}},
)
def test_enum_not_converted():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
only_fields = ("a_choice",)
result = reporter_enum_convert_mutation_result(ReporterInputType)
assert result.errors is None
assert result.data["reportMutation"]["resultStr"] == THIS_ON_CLIENT_CONVERTED
assert result.data["reportMutation"]["resultInt"] is None
assert ReporterInputType._meta.fields["a_choice"].type is graphene.String
def test_enum_is_converted_to_original():
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterType
only_fields = ("a_choice",)
result = reporter_enum_convert_mutation_result(ReporterInputType)
assert result.errors is None
assert result.data["reportMutation"]["resultInt"] == THIS
assert result.data["reportMutation"]["resultStr"] is None
assert (
ReporterInputType._meta.fields["a_choice"].type.__name__
== "AChoiceEnumBackConvString"
)
def test_convert_choices_to_enum_is_false_and_field_type_as_in_model():
class ReporterTypeNotConvertChoices(DjangoObjectType):
class Meta:
model = Reporter
convert_choices_to_enum = False
fields = "__all__"
class ReporterInputType(DjangoFormInputObjectType):
class Meta:
form_class = ReporterForm
object_type = ReporterTypeNotConvertChoices
only_fields = ("a_choice",)
result = reporter_enum_convert_mutation_result(ReporterInputType, THIS)
assert result.errors is None
assert result.data["reportMutation"]["resultInt"] == THIS
assert result.data["reportMutation"]["resultStr"] is None
assert ReporterInputType._meta.fields["a_choice"].type is graphene.Int
def enum_convert_mutation_result_film(FilmInputType):
class FilmMutation(graphene.Mutation):
class Arguments:
film = FilmInputType(required=True)
result = graphene.String()
def mutate(_root, _info, film):
return FilmMutation(result=film.genre)
class Mutation(graphene.ObjectType):
film_mutation = FilmMutation.Field()
schema = graphene.Schema(query=MockQuery, mutation=Mutation)
return schema.execute(
""" mutation FilmMutation($film: FilmInputType!) {
filmMutation(film: $film) {
result
}
}
""",
variable_values={"film": {"genre": DOCUMENTARY_ON_CLIENT_CONVERTED}},
)
def test_enum_not_converted_required_non_number():
class FilmInputType(DjangoFormInputObjectType):
class Meta:
form_class = FilmForm
only_fields = ("genre",)
result = enum_convert_mutation_result_film(FilmInputType)
assert result.errors is None
assert result.data["filmMutation"]["result"] == DOCUMENTARY_ON_CLIENT_CONVERTED
def test_enum_is_converted_to_original_required_non_number():
class FilmType(DjangoObjectType):
class Meta:
model = Film
fields = "__all__"
class FilmInputType(DjangoFormInputObjectType):
class Meta:
form_class = FilmForm
object_type = FilmType
only_fields = ("genre",)
result = enum_convert_mutation_result_film(FilmInputType)
assert result.errors is None
assert result.data["filmMutation"]["result"] == DOCUMENTARY

View File

@ -1,6 +1,7 @@
import pytest
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from pytest import raises from py.test import raises
from graphene import Field, ObjectType, Schema, String from graphene import Field, ObjectType, Schema, String
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
@ -279,7 +280,7 @@ def test_model_form_mutation_mutate_invalid_form():
result = PetMutation.mutate_and_get_payload(None, None) result = PetMutation.mutate_and_get_payload(None, None)
# A pet was not created # A pet was not created
assert Pet.objects.count() == 0 Pet.objects.count() == 0
fields_w_error = [e.field for e in result.errors] fields_w_error = [e.field for e in result.errors]
assert len(result.errors) == 2 assert len(result.errors) == 2

View File

@ -1,116 +1 @@
import graphene from ..types import ErrorType # noqa Import ErrorType for backwards compatability
from graphene import ID
from graphene.types.inputobjecttype import InputObjectType
from graphene.utils.str_converters import to_camel_case
from ..converter import EnumValueField
from ..types import ErrorType # noqa Import ErrorType for backwards compatibility
from .mutation import fields_for_form
class DjangoFormInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls,
container=None,
_meta=None,
only_fields=(),
exclude_fields=(),
form_class=None,
object_type=None,
add_id_field_name=None,
add_id_field_type=None,
**options,
):
"""Retrieve fields from django form (Meta.form_class). Received
fields are set to cls (they will be converted to input fields
by InputObjectType). Type of fields with choices (converted
to enum) is set to custom scalar type (using Meta.object_type)
to dynamically convert enum values back.
class MyDjangoFormInput(DjangoFormInputObjectType):
# any other fields can be placed here and other inputobjectforms as well
class Meta:
form_class = MyDjangoModelForm
object_type = MyModelType
class SomeMutation(graphene.Mutation):
class Arguments:
data = MyDjangoFormInput(required=True)
@staticmethod
def mutate(_root, _info, data):
form_inst = MyDjangoModelForm(data=data)
if form_inst.is_valid():
django_model_instance = form_inst.save(commit=False)
# ... etc ...
"""
if not form_class:
raise Exception("form_class is required for DjangoFormInputObjectType")
form = form_class()
form_fields = fields_for_form(form, only_fields, exclude_fields)
for name, field in form_fields.items():
if (
object_type
and name in object_type._meta.fields
and isinstance(object_type._meta.fields[name], EnumValueField)
):
# Field type EnumValueField here means that field
# with choices have been converted to enum
setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type))
elif (
object_type
and name in object_type._meta.fields
and object_type._meta.convert_choices_to_enum is False
and form.fields[name].__class__.__name__ == "TypedChoiceField"
):
# FIXME
# in case if convert_choices_to_enum is False
# form field class is converted to String but original
# model field type is needed here... (.converter.py bug?)
# This is temp workaround to get field type from ObjectType field
# TEST: test_enum_not_converted_and_field_type_as_in_model
setattr(cls, name, object_type._meta.fields[name].type())
else:
# set input field according to django form field
setattr(cls, name, field)
# explicitly adding id field (absent in django form fields)
# with name and type from Meta or 'id' with graphene.ID by default
if add_id_field_name:
setattr(cls, add_id_field_name, add_id_field_type or ID(required=False))
elif "id" not in exclude_fields:
cls.id = ID(required=False)
super().__init_subclass_with_meta__(container=container, _meta=_meta, **options)
@staticmethod
def get_enum_cnv_cls_instance(field_name, object_type):
"""Saves args in context to convert enum values in
Dynamically created Scalar derived class
"""
@staticmethod
def parse_value(value):
# field_name & object_type have been saved in context (closure)
field = object_type._meta.fields[field_name]
if isinstance(field.type, graphene.NonNull):
val_before_convert = field.type._of_type[value].value
else:
val_before_convert = field.type[value].value
return graphene.String.parse_value(val_before_convert)
cls_doc = "String scalar to convert choice value back from enum to original"
scalar_type = type(
(
f"{field_name[0].upper()}{to_camel_case(field_name[1:])}"
"EnumBackConvString"
),
(graphene.String,),
{"parse_value": parse_value, "__doc__": cls_doc},
)
return scalar_type()

View File

@ -1,12 +1,12 @@
import functools import os
import importlib import importlib
import json import json
import os 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 graphql import print_schema
from graphene_django.settings import graphene_settings from graphene_django.settings import graphene_settings
@ -63,7 +63,7 @@ class Command(CommandArguments):
if out == "-" or out == "-.json": if out == "-" or out == "-.json":
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
elif out == "-.graphql": elif out == "-.graphql":
self.stdout.write(print_schema(schema.graphql_schema)) self.stdout.write(print_schema(schema))
else: else:
# Determine format # Determine format
_, file_extension = os.path.splitext(out) _, file_extension = os.path.splitext(out)
@ -73,17 +73,21 @@ class Command(CommandArguments):
elif file_extension == ".json": elif file_extension == ".json":
self.save_json_file(out, schema_dict, indent) self.save_json_file(out, schema_dict, indent)
else: else:
raise CommandError(f'Unrecognised file format "{file_extension}"') 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(f"Successfully dumped GraphQL schema to {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")
if options_schema and isinstance(options_schema, str): if options_schema and type(options_schema) is str:
module_str, schema_name = options_schema.rsplit(".", 1) module_str, schema_name = options_schema.rsplit(".", 1)
mod = importlib.import_module(module_str) mod = importlib.import_module(module_str)
schema = getattr(mod, schema_name) schema = getattr(mod, schema_name)

View File

@ -1,4 +1,4 @@
class Registry: class Registry(object):
def __init__(self): def __init__(self):
self._registry = {} self._registry = {}
self._field_registry = {} self._field_registry = {}
@ -8,7 +8,9 @@ class Registry:
assert issubclass( assert issubclass(
cls, DjangoObjectType cls, DjangoObjectType
), f'Only DjangoObjectTypes can be registered, received "{cls.__name__}"' ), 'Only DjangoObjectTypes can be registered, received "{}"'.format(
cls.__name__
)
assert cls._meta.registry == self, "Registry for a Model have to match." assert cls._meta.registry == self, "Registry for a Model have to match."
# assert self.get_type_for_model(cls._meta.model) == cls, ( # assert self.get_type_for_model(cls._meta.model) == cls, (
# 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) # 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model)

View File

@ -14,14 +14,3 @@ class MyFakeModelWithPassword(models.Model):
class MyFakeModelWithDate(models.Model): class MyFakeModelWithDate(models.Model):
cool_name = models.CharField(max_length=50) cool_name = models.CharField(max_length=50)
last_edited = models.DateField() last_edited = models.DateField()
class MyFakeModelWithChoiceField(models.Model):
class ChoiceType(models.Choices):
ASDF = "asdf"
HI = "hi"
choice_type = models.CharField(
max_length=4,
default=ChoiceType.HI.name,
)

View File

@ -1,5 +1,4 @@
from collections import OrderedDict from collections import OrderedDict
from enum import Enum
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import serializers from rest_framework import serializers
@ -19,7 +18,6 @@ class SerializerMutationOptions(MutationOptions):
model_class = None model_class = None
model_operations = ["create", "update"] model_operations = ["create", "update"]
serializer_class = None serializer_class = None
optional_fields = ()
def fields_for_serializer( def fields_for_serializer(
@ -29,7 +27,6 @@ def fields_for_serializer(
is_input=False, is_input=False,
convert_choices_to_enum=True, convert_choices_to_enum=True,
lookup_field=None, lookup_field=None,
optional_fields=(),
): ):
fields = OrderedDict() fields = OrderedDict()
for name, field in serializer.fields.items(): for name, field in serializer.fields.items():
@ -42,21 +39,14 @@ def fields_for_serializer(
field.read_only field.read_only
and is_input and is_input
and lookup_field != name, # don't show read_only fields in Input and lookup_field != name, # don't show read_only fields in Input
isinstance(
field, serializers.HiddenField
), # don't show hidden fields in Input
] ]
) )
if is_not_in_only or is_excluded: if is_not_in_only or is_excluded:
continue continue
is_optional = name in optional_fields or "__all__" in optional_fields
fields[name] = convert_serializer_field( fields[name] = convert_serializer_field(
field, field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum
is_input=is_input,
convert_choices_to_enum=convert_choices_to_enum,
force_optional=is_optional,
) )
return fields return fields
@ -80,9 +70,9 @@ class SerializerMutation(ClientIDMutation):
exclude_fields=(), exclude_fields=(),
convert_choices_to_enum=True, convert_choices_to_enum=True,
_meta=None, _meta=None,
optional_fields=(), **options
**options,
): ):
if not serializer_class: if not serializer_class:
raise Exception("serializer_class is required for the SerializerMutation") raise Exception("serializer_class is required for the SerializerMutation")
@ -105,7 +95,6 @@ class SerializerMutation(ClientIDMutation):
is_input=True, is_input=True,
convert_choices_to_enum=convert_choices_to_enum, convert_choices_to_enum=convert_choices_to_enum,
lookup_field=lookup_field, lookup_field=lookup_field,
optional_fields=optional_fields,
) )
output_fields = fields_for_serializer( output_fields = fields_for_serializer(
serializer, serializer,
@ -125,7 +114,7 @@ class SerializerMutation(ClientIDMutation):
_meta.fields = yank_fields_from_attrs(output_fields, _as=Field) _meta.fields = yank_fields_from_attrs(output_fields, _as=Field)
input_fields = yank_fields_from_attrs(input_fields, _as=InputField) input_fields = yank_fields_from_attrs(input_fields, _as=InputField)
super().__init_subclass_with_meta__( super(SerializerMutation, cls).__init_subclass_with_meta__(
_meta=_meta, input_fields=input_fields, **options _meta=_meta, input_fields=input_fields, **options
) )
@ -133,10 +122,8 @@ class SerializerMutation(ClientIDMutation):
def get_serializer_kwargs(cls, root, info, **input): def get_serializer_kwargs(cls, root, info, **input):
lookup_field = cls._meta.lookup_field lookup_field = cls._meta.lookup_field
model_class = cls._meta.model_class model_class = cls._meta.model_class
if model_class: if model_class:
for input_dict_key, maybe_enum in input.items():
if isinstance(maybe_enum, Enum):
input[input_dict_key] = maybe_enum.value
if "update" in cls._meta.model_operations and lookup_field in input: if "update" in cls._meta.model_operations and lookup_field in input:
instance = get_object_or_404( instance = get_object_or_404(
model_class, **{lookup_field: input[lookup_field]} model_class, **{lookup_field: input[lookup_field]}

View File

@ -5,22 +5,20 @@ from rest_framework import serializers
import graphene import graphene
from ..converter import convert_choices_to_named_enum_with_descriptions
from ..registry import get_global_registry from ..registry import get_global_registry
from ..converter import convert_choices_to_named_enum_with_descriptions
from .types import DictType from .types import DictType
@singledispatch @singledispatch
def get_graphene_type_from_serializer_field(field): def get_graphene_type_from_serializer_field(field):
raise ImproperlyConfigured( raise ImproperlyConfigured(
f"Don't know how to convert the serializer field {field} ({field.__class__}) " "Don't know how to convert the serializer field %s (%s) "
"to Graphene type" "to Graphene type" % (field, field.__class__)
) )
def convert_serializer_field( def convert_serializer_field(field, is_input=True, convert_choices_to_enum=True):
field, is_input=True, convert_choices_to_enum=True, force_optional=False
):
""" """
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
@ -33,10 +31,7 @@ def convert_serializer_field(
graphql_type = get_graphene_type_from_serializer_field(field) graphql_type = get_graphene_type_from_serializer_field(field)
args = [] args = []
kwargs = { kwargs = {"description": field.help_text, "required": is_input and field.required}
"description": field.help_text,
"required": is_input and field.required and not force_optional,
}
# if it is a tuple or a list it means that we are returning # if it is a tuple or a list it means that we are returning
# the graphql type and the child type # the graphql type and the child type
@ -77,7 +72,7 @@ def convert_serializer_to_input_type(serializer_class):
for name, field in serializer.fields.items() for name, field in serializer.fields.items()
} }
ret_type = type( ret_type = type(
f"{serializer.__class__.__name__}Input", "{}Input".format(serializer.__class__.__name__),
(graphene.InputObjectType,), (graphene.InputObjectType,),
items, items,
) )

View File

@ -1,10 +1,10 @@
import copy import copy
from django.db import models
from pytest import raises
from rest_framework import serializers
import graphene import graphene
from django.db import models
from graphene import InputObjectType
from py.test import raises
from rest_framework import serializers
from ..serializer_converter import convert_serializer_field from ..serializer_converter import convert_serializer_field
from ..types import DictType from ..types import DictType
@ -96,6 +96,7 @@ def test_should_regex_convert_string():
def test_should_uuid_convert_string(): def test_should_uuid_convert_string():
if hasattr(serializers, "UUIDField"):
assert_conversion(serializers.UUIDField, graphene.String) assert_conversion(serializers.UUIDField, graphene.String)

View File

@ -1,18 +1,13 @@
import datetime import datetime
from pytest import raises from py.test import raises
from rest_framework import serializers from rest_framework import serializers
from graphene import Field, ResolveInfo, String from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType from graphene.types.inputobjecttype import InputObjectType
from ...types import DjangoObjectType from ...types import DjangoObjectType
from ..models import ( from ..models import MyFakeModel, MyFakeModelWithDate, MyFakeModelWithPassword
MyFakeModel,
MyFakeModelWithChoiceField,
MyFakeModelWithDate,
MyFakeModelWithPassword,
)
from ..mutation import SerializerMutation from ..mutation import SerializerMutation
@ -105,16 +100,6 @@ def test_exclude_fields():
assert "created" not in MyMutation.Input._meta.fields assert "created" not in MyMutation.Input._meta.fields
def test_model_serializer_optional_fields():
class MyMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
optional_fields = ("cool_name",)
assert "cool_name" in MyMutation.Input._meta.fields
assert MyMutation.Input._meta.fields["cool_name"].type == String
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)
@ -179,21 +164,6 @@ def test_read_only_fields():
), "'cool_name' is read_only field and shouldn't be on arguments" ), "'cool_name' is read_only field and shouldn't be on arguments"
def test_hidden_fields():
class SerializerWithHiddenField(serializers.Serializer):
cool_name = serializers.CharField()
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class MyMutation(SerializerMutation):
class Meta:
serializer_class = SerializerWithHiddenField
assert "cool_name" in MyMutation.Input._meta.fields
assert (
"user" not in MyMutation.Input._meta.fields
), "'user' is hidden 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:
@ -260,7 +230,7 @@ def test_model_invalid_update_mutate_and_get_payload_success():
model_operations = ["update"] model_operations = ["update"]
with raises(Exception) as exc: with raises(Exception) as exc:
InvalidModelMutation.mutate_and_get_payload( result = InvalidModelMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "Narf"} None, mock_info(), **{"cool_name": "Narf"}
) )
@ -275,7 +245,7 @@ def test_perform_mutate_success():
result = MyMethodMutation.mutate_and_get_payload( result = MyMethodMutation.mutate_and_get_payload(
None, None,
mock_info(), mock_info(),
**{"cool_name": "Narf", "last_edited": datetime.date(2020, 1, 4)}, **{"cool_name": "Narf", "last_edited": datetime.date(2020, 1, 4)}
) )
assert result.errors is None assert result.errors is None
@ -283,39 +253,6 @@ def test_perform_mutate_success():
assert result.days_since_last_edit == 4 assert result.days_since_last_edit == 4
def test_perform_mutate_success_with_enum_choice_field():
class ListViewChoiceFieldSerializer(serializers.ModelSerializer):
choice_type = serializers.ChoiceField(
choices=[(x.name, x.value) for x in MyFakeModelWithChoiceField.ChoiceType],
required=False,
)
class Meta:
model = MyFakeModelWithChoiceField
fields = "__all__"
class SomeCreateSerializerMutation(SerializerMutation):
class Meta:
serializer_class = ListViewChoiceFieldSerializer
choice_type = {
"choice_type": SomeCreateSerializerMutation.Input.choice_type.type.get("ASDF")
}
name = MyFakeModelWithChoiceField.ChoiceType.ASDF.name
result = SomeCreateSerializerMutation.mutate_and_get_payload(
None, mock_info(), **choice_type
)
assert result.errors is None
assert result.choice_type == name
kwargs = SomeCreateSerializerMutation.get_serializer_kwargs(
None, mock_info(), **choice_type
)
assert kwargs["data"]["choice_type"] == name
assert 1 == MyFakeModelWithChoiceField.objects.count()
item = MyFakeModelWithChoiceField.objects.first()
assert item.choice_type == name
def test_mutate_and_get_payload_error(): def test_mutate_and_get_payload_error():
class MyMutation(SerializerMutation): class MyMutation(SerializerMutation):
class Meta: class Meta:

View File

@ -11,12 +11,14 @@ This module provides the `graphene_settings` object, that is used to access
Graphene settings, checking for user settings first, then falling Graphene settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals
import importlib # Available in Python 3.1+
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed from django.test.signals import setting_changed
import importlib # Available in Python 3.1+
# Copied shamelessly from Django REST Framework # Copied shamelessly from Django REST Framework
DEFAULTS = { DEFAULTS = {
@ -30,8 +32,6 @@ 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,
# Automatically convert Choice fields of Django into Enum fields
"DJANGO_CHOICE_FIELD_ENUM_CONVERT": True,
# Set to True to enable v2 naming convention for choice field Enum's # Set to True to enable v2 naming convention for choice field Enum's
"DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False, "DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False,
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None, "DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
@ -42,10 +42,8 @@ DEFAULTS = {
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
"GRAPHIQL_HEADER_EDITOR_ENABLED": True, "GRAPHIQL_HEADER_EDITOR_ENABLED": True,
"GRAPHIQL_SHOULD_PERSIST_HEADERS": False, "GRAPHIQL_SHOULD_PERSIST_HEADERS": False,
"GRAPHIQL_INPUT_VALUE_DEPRECATION": False,
"ATOMIC_MUTATIONS": False, "ATOMIC_MUTATIONS": False,
"TESTING_ENDPOINT": "/graphql", "TESTING_ENDPOINT": "/graphql",
"MAX_VALIDATION_ERRORS": None,
} }
if settings.DEBUG: if settings.DEBUG:
@ -80,7 +78,7 @@ def import_from_string(val, setting_name):
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format( msg = "Could not import '%s' for Graphene setting '%s'. %s: %s." % (
val, val,
setting_name, setting_name,
e.__class__.__name__, e.__class__.__name__,
@ -89,7 +87,7 @@ def import_from_string(val, setting_name):
raise ImportError(msg) raise ImportError(msg)
class GrapheneSettings: class GrapheneSettings(object):
""" """
A settings object, that allows API settings to be accessed as properties. A settings object, that allows API settings to be accessed as properties.
For example: For example:

View File

@ -5,8 +5,7 @@
GraphiQL, GraphiQL,
React, React,
ReactDOM, ReactDOM,
graphqlWs, SubscriptionsTransportWs,
GraphiQLPluginExplorer,
fetch, fetch,
history, history,
location, location,
@ -53,15 +52,17 @@
var fetchURL = locationQuery(otherParams); var fetchURL = locationQuery(otherParams);
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise // Defines a GraphQL fetcher using the fetch API.
// assumes the current window location with an appropriate websocket protocol. function httpClient(graphQLParams, opts) {
var subscribeURL = if (typeof opts === 'undefined') {
location.origin.replace(/^http/, "ws") + opts = {};
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname); }
var headers = opts.headers || {};
headers['Accept'] = headers['Accept'] || 'application/json';
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
function trueLambda() { return true; }; // Parse the cookie value for a CSRF token
var csrftoken;
var headers = {};
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();
@ -72,15 +73,87 @@
headers['X-CSRFToken'] = csrftoken headers['X-CSRFToken'] = csrftoken
} }
var graphQLFetcher = GraphiQL.createFetcher({ return fetch(fetchURL, {
url: fetchURL, method: "post",
wsClient: graphqlWs.createClient({ headers: headers,
url: subscribeURL, body: JSON.stringify(graphQLParams),
shouldRetry: trueLambda, credentials: "include",
lazy: true,
}),
headers: headers
}) })
.then(function (response) {
return response.text();
})
.then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
// assumes the current window location with an appropriate websocket protocol.
var subscribeURL =
location.origin.replace(/^http/, "ws") +
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
// Create a subscription client.
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
subscribeURL,
{
// Reconnect after any interruptions.
reconnect: true,
// Delay socket initialization until the first subscription is started.
lazy: true,
},
);
// Keep a reference to the currently-active subscription, if available.
var activeSubscription = null;
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
function graphQLFetcher(graphQLParams, opts) {
var operationType = getOperationType(graphQLParams);
// If we're about to execute a new operation, and we have an active subscription,
// unsubscribe before continuing.
if (activeSubscription) {
activeSubscription.unsubscribe();
activeSubscription = null;
}
if (operationType === "subscription") {
return {
subscribe: function (observer) {
activeSubscription = subscriptionClient;
return subscriptionClient.request(graphQLParams, opts).subscribe(observer);
},
};
} else {
return httpClient(graphQLParams, opts);
}
}
// Determine the type of operation being executed for a given set of GraphQL parameters.
function getOperationType(graphQLParams) {
// Run a regex against the query to determine the operation type (query, mutation, subscription).
var operationRegex = new RegExp(
// Look for lines that start with an operation keyword, ignoring whitespace.
"^\\s*(query|mutation|subscription)\\s*" +
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available).
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") +
// The line should eventually encounter an opening curly brace.
"[^\\{]*\\{",
// Enable multiline matching.
"m",
);
var match = operationRegex.exec(graphQLParams.query);
if (!match) {
return "query";
}
return match[1];
}
// When the query and variables string is edited, update the URL bar so // When the query and variables string is edited, update the URL bar so
// that it can be easily shared. // that it can be easily shared.
@ -99,31 +172,14 @@
function updateURL() { function updateURL() {
history.replaceState(null, null, locationQuery(parameters)); history.replaceState(null, null, locationQuery(parameters));
} }
function GraphiQLWithExplorer() {
var [query, setQuery] = React.useState(parameters.query);
function handleQuery(query) {
setQuery(query);
onEditQuery(query);
}
var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({
query: query,
onEdit: handleQuery,
});
var options = { var options = {
fetcher: graphQLFetcher, fetcher: graphQLFetcher,
plugins: [explorerPlugin], onEditQuery: onEditQuery,
defaultEditorToolsVisibility: true,
onEditQuery: handleQuery,
onEditVariables: onEditVariables, onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName, onEditOperationName: onEditOperationName,
isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, headerEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled,
shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders,
inputValueDeprecation: GRAPHENE_SETTINGS.graphiqlInputValueDeprecation, query: parameters.query,
query: query,
}; };
if (parameters.variables) { if (parameters.variables) {
options.variables = parameters.variables; options.variables = parameters.variables;
@ -131,13 +187,9 @@
if (parameters.operation_name) { if (parameters.operation_name) {
options.operationName = parameters.operation_name; options.operationName = parameters.operation_name;
} }
return React.createElement(GraphiQL, options);
}
// Render <GraphiQL /> into the body. // Render <GraphiQL /> into the body.
ReactDOM.render( ReactDOM.render(
React.createElement(GraphiQLWithExplorer), React.createElement(GraphiQL, options),
document.getElementById("editor"), document.getElementById("editor"),
); );
})( })(
@ -147,8 +199,7 @@
window.GraphiQL, window.GraphiQL,
window.React, window.React,
window.ReactDOM, window.ReactDOM,
window.graphqlWs, window.SubscriptionsTransportWs,
window.GraphiQLPluginExplorer,
window.fetch, window.fetch,
window.history, window.history,
window.location, window.location,

View File

@ -21,10 +21,6 @@ add "&raw" to the end of the URL within a browser.
integrity="{{graphiql_css_sri}}" integrity="{{graphiql_css_sri}}"
rel="stylesheet" rel="stylesheet"
crossorigin="anonymous" /> crossorigin="anonymous" />
<link href="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/style.css"
integrity="{{graphiql_plugin_explorer_css_sri}}"
rel="stylesheet"
crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js" <script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
integrity="{{whatwg_fetch_sri}}" integrity="{{whatwg_fetch_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
@ -37,12 +33,9 @@ add "&raw" to the end of the URL within a browser.
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js" <script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
integrity="{{graphiql_sri}}" integrity="{{graphiql_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/graphql-ws@{{subscriptions_transport_ws_version}}/umd/graphql-ws.min.js" <script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js"
integrity="{{subscriptions_transport_ws_sri}}" integrity="{{subscriptions_transport_ws_sri}}"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@graphiql/plugin-explorer@{{graphiql_plugin_explorer_version}}/dist/graphiql-plugin-explorer.umd.js"
integrity="{{graphiql_plugin_explorer_sri}}"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<div id="editor"></div> <div id="editor"></div>
@ -54,7 +47,6 @@ add "&raw" to the end of the URL within a browser.
{% endif %} {% endif %}
graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }}, graphiqlHeaderEditorEnabled: {{ graphiql_header_editor_enabled|yesno:"true,false" }},
graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }}, graphiqlShouldPersistHeaders: {{ graphiql_should_persist_headers|yesno:"true,false" }},
graphiqlInputValueDeprecation: {{ graphiql_input_value_deprecation|yesno:"true,false" }},
}; };
</script> </script>
<script src="{% static 'graphene_django/graphiql.js' %}"></script> <script src="{% static 'graphene_django/graphiql.js' %}"></script>

View File

@ -1,14 +1,21 @@
# https://github.com/graphql-python/graphene-django/issues/520 # https://github.com/graphql-python/graphene-django/issues/520
import datetime
from django import forms from django import forms
from rest_framework import serializers
import graphene import graphene
from ...forms.mutation import DjangoFormMutation from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...types import DjangoObjectType
from ...rest_framework.models import MyFakeModel from ...rest_framework.models import MyFakeModel
from ...rest_framework.mutation import SerializerMutation from ...rest_framework.mutation import SerializerMutation
from ...forms.mutation import DjangoFormMutation
class MyModelSerializer(serializers.ModelSerializer): class MyModelSerializer(serializers.ModelSerializer):

View File

@ -1,43 +1,13 @@
import django from __future__ import absolute_import
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
CHOICES = ((1, "this"), (2, _("that"))) CHOICES = ((1, "this"), (2, _("that")))
def get_choices_as_class(choices_class):
if django.VERSION >= (5, 0):
return choices_class
else:
return choices_class.choices
def get_choices_as_callable(choices_class):
if django.VERSION >= (5, 0):
def choices():
return choices_class.choices
return choices
else:
return choices_class.choices
class TypedIntChoice(models.IntegerChoices):
CHOICE_THIS = 1
CHOICE_THAT = 2
class TypedStrChoice(models.TextChoices):
CHOICE_THIS = "this"
CHOICE_THAT = "that"
class Person(models.Model): class Person(models.Model):
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
parent = models.ForeignKey(
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
)
class Pet(models.Model): class Pet(models.Model):
@ -51,11 +21,7 @@ class Pet(models.Model):
class FilmDetails(models.Model): class FilmDetails(models.Model):
location = models.CharField(max_length=30) location = models.CharField(max_length=30)
film = models.OneToOneField( film = models.OneToOneField(
"Film", "Film", on_delete=models.CASCADE, related_name="details"
on_delete=models.CASCADE,
related_name="details",
null=True,
blank=True,
) )
@ -71,7 +37,7 @@ class Film(models.Model):
class DoeReporterManager(models.Manager): class DoeReporterManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(last_name="Doe") return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe")
class Reporter(models.Model): class Reporter(models.Model):
@ -79,25 +45,9 @@ class Reporter(models.Model):
last_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30)
email = models.EmailField() email = models.EmailField()
pets = models.ManyToManyField("self") pets = models.ManyToManyField("self")
a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True) a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
typed_choice = models.IntegerField(
choices=TypedIntChoice.choices,
null=True,
blank=True,
)
class_choice = models.IntegerField(
choices=get_choices_as_class(TypedIntChoice),
null=True,
blank=True,
)
callable_choice = models.IntegerField(
choices=get_choices_as_callable(TypedStrChoice),
null=True,
blank=True,
)
objects = models.Manager() objects = models.Manager()
doe_objects = DoeReporterManager() doe_objects = DoeReporterManager()
fans = models.ManyToManyField(Person)
reporter_type = models.IntegerField( reporter_type = models.IntegerField(
"Reporter Type", "Reporter Type",
@ -107,7 +57,7 @@ class Reporter(models.Model):
) )
def __str__(self): # __unicode__ on Python 2 def __str__(self): # __unicode__ on Python 2
return f"{self.first_name} {self.last_name}" return "%s %s" % (self.first_name, self.last_name)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -117,7 +67,7 @@ class Reporter(models.Model):
when a CNNReporter is pulled from the database, it is still when a CNNReporter is pulled from the database, it is still
of type Reporter. This was added to test proxy model support. of type Reporter. This was added to test proxy model support.
""" """
super().__init__(*args, **kwargs) super(Reporter, self).__init__(*args, **kwargs)
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
@ -127,7 +77,7 @@ class Reporter(models.Model):
class CNNReporterManager(models.Manager): class CNNReporterManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(reporter_type=2) return super(CNNReporterManager, self).get_queryset().filter(reporter_type=2)
class CNNReporter(Reporter): class CNNReporter(Reporter):
@ -142,16 +92,6 @@ class CNNReporter(Reporter):
objects = CNNReporterManager() objects = CNNReporterManager()
class APNewsReporter(Reporter):
"""
This class only inherits from Reporter for testing multi table inheritance
similar to what you'd see in django-polymorphic
"""
alias = models.CharField(max_length=30)
objects = models.Manager()
class Article(models.Model): class Article(models.Model):
headline = models.CharField(max_length=100) headline = models.CharField(max_length=100)
pub_date = models.DateField(auto_now_add=True) pub_date = models.DateField(auto_now_add=True)

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