├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── stale.yml └── workflows │ ├── deploy.yml │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .ruff.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bin ├── autolinter └── convert_documentation ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── authorization.rst ├── conf.py ├── debug.rst ├── extra-types.rst ├── fields.rst ├── filtering.rst ├── index.rst ├── installation.rst ├── introspection.rst ├── mutations.rst ├── queries.rst ├── requirements.txt ├── schema.py ├── schema.rst ├── settings.rst ├── subscriptions.rst ├── testing.rst ├── tutorial-plain.rst ├── tutorial-relay.rst └── validation.rst ├── examples ├── __init__.py ├── cookbook-plain │ ├── README.md │ ├── __init__.py │ ├── cookbook │ │ ├── __init__.py │ │ ├── ingredients │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── fixtures │ │ │ │ └── ingredients.json │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20161104_0050.py │ │ │ │ ├── 0003_auto_20181018_1746.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── schema.py │ │ │ ├── tests.py │ │ │ └── views.py │ │ ├── recipes │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20161104_0106.py │ │ │ │ ├── 0003_auto_20181018_1728.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── schema.py │ │ │ ├── tests.py │ │ │ └── views.py │ │ ├── schema.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── requirements.txt │ └── setup.cfg ├── cookbook │ ├── README.md │ ├── __init__.py │ ├── cookbook │ │ ├── __init__.py │ │ ├── ingredients │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── fixtures │ │ │ │ └── ingredients.json │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20161104_0050.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── schema.py │ │ │ ├── tests.py │ │ │ └── views.py │ │ ├── recipes │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20161104_0106.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── schema.py │ │ │ ├── tests.py │ │ │ └── views.py │ │ ├── schema.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── dummy_data.json │ ├── manage.py │ ├── requirements.txt │ └── setup.cfg ├── django_test_settings.py └── starwars │ ├── __init__.py │ ├── data.py │ ├── models.py │ ├── schema.py │ └── tests │ ├── __init__.py │ ├── test_connections.py │ ├── test_mutation.py │ └── test_objectidentification.py ├── graphene_django ├── __init__.py ├── compat.py ├── conftest.py ├── constants.py ├── converter.py ├── debug │ ├── __init__.py │ ├── exception │ │ ├── __init__.py │ │ ├── formating.py │ │ └── types.py │ ├── middleware.py │ ├── sql │ │ ├── __init__.py │ │ ├── tracking.py │ │ └── types.py │ ├── tests │ │ ├── __init__.py │ │ └── test_query.py │ └── types.py ├── fields.py ├── filter │ ├── __init__.py │ ├── fields.py │ ├── filters │ │ ├── __init__.py │ │ ├── array_filter.py │ │ ├── global_id_filter.py │ │ ├── list_filter.py │ │ ├── range_filter.py │ │ └── typed_filter.py │ ├── filterset.py │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── filters.py │ │ ├── test_array_field_contains_filter.py │ │ ├── test_array_field_custom_filter.py │ │ ├── test_array_field_exact_filter.py │ │ ├── test_array_field_overlap_filter.py │ │ ├── test_enum_filtering.py │ │ ├── test_fields.py │ │ ├── test_in_filter.py │ │ ├── test_range_filter.py │ │ └── test_typed_filter.py │ └── utils.py ├── forms │ ├── __init__.py │ ├── converter.py │ ├── forms.py │ ├── mutation.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_converter.py │ │ ├── test_djangoinputobject.py │ │ └── test_mutation.py │ └── types.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── graphql_schema.py ├── registry.py ├── rest_framework │ ├── __init__.py │ ├── models.py │ ├── mutation.py │ ├── serializer_converter.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_field_converter.py │ │ ├── test_multiple_model_serializers.py │ │ └── test_mutation.py │ └── types.py ├── settings.py ├── static │ └── graphene_django │ │ └── graphiql.js ├── templates │ └── graphene │ │ └── graphiql.html ├── tests │ ├── __init__.py │ ├── forms.py │ ├── issues │ │ ├── __init__.py │ │ └── test_520.py │ ├── models.py │ ├── mutations.py │ ├── schema.py │ ├── schema_view.py │ ├── test_command.py │ ├── test_converter.py │ ├── test_fields.py │ ├── test_forms.py │ ├── test_get_queryset.py │ ├── test_query.py │ ├── test_schema.py │ ├── test_types.py │ ├── test_utils.py │ ├── test_views.py │ ├── types.py │ ├── urls.py │ ├── urls_inherited.py │ ├── urls_pretty.py │ └── urls_validation.py ├── types.py ├── utils │ ├── __init__.py │ ├── str_converters.py │ ├── testing.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_str_converters.py │ │ └── test_testing.py │ └── utils.py └── views.py ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */tests/*,graphene_django/debug/sql/* 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41Bbug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports. 11 | 12 | * **What is the current behavior?** 13 | 14 | 15 | 16 | * **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via 17 | a github repo, https://repl.it or similar (you can use this template as a starting point: https://repl.it/@jkimbo/Graphene-Django-Example). 18 | 19 | 20 | 21 | * **What is the expected behavior?** 22 | 23 | 24 | 25 | * **What is the motivation / use case for changing the behavior?** 26 | 27 | 28 | 29 | * **Please tell us about your environment:** 30 | 31 | - Version: 32 | - Platform: 33 | 34 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow) 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "✨enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: false 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: false 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - 🐛bug 10 | - 📖 documentation 11 | - help wanted 12 | - ✨enhancement 13 | # Label to use when marking an issue as stale 14 | staleLabel: wontfix 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: false 17 | # markComment: > 18 | # This issue has been automatically marked as stale because it has not had 19 | # recent activity. It will be closed if no further activity occurs. Thank you 20 | # for your contributions. 21 | # Comment to post when closing a stale issue. Set to `false` to disable 22 | closeComment: false 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | lint: 10 | uses: ./.github/workflows/lint.yml 11 | tests: 12 | uses: ./.github/workflows/tests.yml 13 | release: 14 | runs-on: ubuntu-latest 15 | needs: [lint, tests] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python 3.11 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.11' 23 | - name: Build wheel and source tarball 24 | run: | 25 | pip install wheel 26 | python setup.py sdist bdist_wheel 27 | - name: Publish a Python distribution to PyPI 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.pypi_password }} 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | workflow_call: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 3.11 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.11' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install tox 23 | - name: Run pre-commit 💅 24 | run: tox 25 | env: 26 | TOXENV: pre-commit 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | workflow_call: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 4 14 | matrix: 15 | django: ["3.2", "4.2", "5.0", "5.1"] 16 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 17 | exclude: 18 | - django: "3.2" 19 | python-version: "3.11" 20 | - django: "3.2" 21 | python-version: "3.12" 22 | - django: "5.0" 23 | python-version: "3.8" 24 | - django: "5.0" 25 | python-version: "3.9" 26 | - django: "5.1" 27 | python-version: "3.8" 28 | - django: "5.1" 29 | python-version: "3.9" 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install tox tox-gh-actions 40 | - name: Test with tox 41 | run: tox 42 | env: 43 | DJANGO: ${{ matrix.django }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | .env/ 15 | venv/ 16 | .venv/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # PyCharm 66 | .idea 67 | 68 | # Databases 69 | *.sqlite3 70 | .vscode 71 | 72 | # swap 73 | [._]*.s[a-v][a-z] 74 | [._]*.sw[a-p] 75 | [._]s[a-v][a-z] 76 | [._]sw[a-p] 77 | # session 78 | Session.vim 79 | # temporary 80 | .netrwhist 81 | *~ 82 | # auto-generated tag files 83 | tags 84 | .tox/ 85 | .pytest_cache/ 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | .python-version 91 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.11 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: check-merge-conflict 8 | - id: check-json 9 | - id: check-yaml 10 | - id: debug-statements 11 | - id: end-of-file-fixer 12 | exclude: ^docs/.*$ 13 | - id: pretty-format-json 14 | args: 15 | - --autofix 16 | - id: trailing-whitespace 17 | exclude: README.md 18 | - repo: https://github.com/astral-sh/ruff-pre-commit 19 | rev: v0.1.2 20 | hooks: 21 | - id: ruff 22 | args: [--fix, --exit-non-zero-on-fix, --show-fixes] 23 | - id: ruff-format 24 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.12" 10 | 11 | # Build documentation in the "docs/" directory with Sphinx 12 | sphinx: 13 | configuration: docs/conf.py 14 | 15 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | select = [ 2 | "E", # pycodestyle 3 | "W", # pycodestyle 4 | "F", # pyflake 5 | "I", # isort 6 | "B", # flake8-bugbear 7 | "C4", # flake8-comprehensions 8 | "UP", # pyupgrade 9 | ] 10 | 11 | ignore = [ 12 | "E501", # line-too-long 13 | "B017", # pytest.raises(Exception) should be considered evil 14 | "B028", # warnings.warn called without an explicit stacklevel keyword argument 15 | "B904", # check for raise statements in exception handlers that lack a from clause 16 | "W191", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 17 | ] 18 | 19 | exclude = [ 20 | "**/docs", 21 | ] 22 | 23 | target-version = "py38" 24 | 25 | [per-file-ignores] 26 | # Ignore unused imports (F401) in these files 27 | "__init__.py" = ["F401"] 28 | 29 | [isort] 30 | known-first-party = ["graphene", "graphene-django"] 31 | known-local-folder = ["cookbook"] 32 | combine-as-imports = true 33 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at me@syrusakbary.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for helping to make graphene-django great! 4 | 5 | We welcome all kinds of contributions: 6 | 7 | - Bug fixes 8 | - Documentation improvements 9 | - New features 10 | - Refactoring & tidying 11 | 12 | 13 | ## Getting started 14 | 15 | If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/graphene-django/issues) and [projects](https://github.com/graphql-python/graphene-django/projects) in progress - someone could already be working on something similar and you can help out. 16 | 17 | 18 | ## Project setup 19 | 20 | After cloning this repo, ensure dependencies are installed by running: 21 | 22 | ```sh 23 | make dev-setup 24 | ``` 25 | 26 | ## Running tests 27 | 28 | After developing, the full test suite can be evaluated by running: 29 | 30 | ```sh 31 | make tests 32 | ``` 33 | 34 | ## Opening Pull Requests 35 | 36 | Please fork the project and open a pull request against the `main` branch. 37 | 38 | This will trigger a series of test and lint checks. 39 | 40 | We advise that you format and run lint locally before doing this to save time: 41 | 42 | ```sh 43 | make format 44 | make lint 45 | ``` 46 | 47 | ## Documentation 48 | 49 | The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. 50 | 51 | The documentation dependencies are installed by running: 52 | 53 | ```sh 54 | cd docs 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | Then to produce a HTML version of the documentation: 59 | 60 | ```sh 61 | make html 62 | ``` 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-Present Syrus Akbary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | recursive-include graphene_django/templates * 3 | recursive-include graphene_django/static * 4 | 5 | include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json 6 | include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: 3 | @echo "Please use \`make ' where is one of" 4 | @grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' 5 | 6 | .PHONY: dev-setup ## Install development dependencies 7 | dev-setup: 8 | pip install -e ".[dev]" 9 | python -m pre_commit install 10 | 11 | .PHONY: tests ## Run unit tests 12 | tests: 13 | PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv 14 | 15 | .PHONY: format ## Format code 16 | format: 17 | ruff format graphene_django examples setup.py 18 | 19 | .PHONY: lint ## Lint code 20 | lint: 21 | ruff graphene_django examples 22 | 23 | .PHONY: docs ## Generate docs 24 | docs: dev-setup 25 | cd docs && make install && make html 26 | 27 | .PHONY: docs-live ## Generate docs with live reloading 28 | docs-live: dev-setup 29 | cd docs && make install && make livehtml 30 | -------------------------------------------------------------------------------- /bin/autolinter: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install the required scripts with 4 | # pip install autoflake autopep8 isort 5 | autoflake ./examples/ ./graphene_django/ -r --remove-unused-variables --remove-all-unused-imports --in-place 6 | autopep8 ./examples/ ./graphene_django/ -r --in-place --experimental --aggressive --max-line-length 120 7 | isort -rc ./examples/ ./graphene_django/ 8 | -------------------------------------------------------------------------------- /bin/convert_documentation: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pandoc README.md --from markdown --to rst -s -o README.rst 4 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/debug.rst: -------------------------------------------------------------------------------- 1 | Django Debug Middleware 2 | ======================= 3 | 4 | You can debug your GraphQL queries in a similar way to 5 | `django-debug-toolbar `__, 6 | but outputting in the results in GraphQL response as fields, instead of 7 | the graphical HTML interface. Exceptions with their stack traces are also exposed. 8 | 9 | For that, you will need to add the plugin in your graphene schema. 10 | 11 | Installation 12 | ------------ 13 | 14 | For use the Django Debug plugin in Graphene: 15 | 16 | * Add ``graphene_django.debug.DjangoDebugMiddleware`` into ``MIDDLEWARE`` in the ``GRAPHENE`` settings. 17 | 18 | * Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='_debug')``. 19 | 20 | 21 | .. code:: python 22 | 23 | from graphene_django.debug import DjangoDebug 24 | 25 | class Query(graphene.ObjectType): 26 | # ... 27 | debug = graphene.Field(DjangoDebug, name='_debug') 28 | 29 | schema = graphene.Schema(query=Query) 30 | 31 | 32 | And in your ``settings.py``: 33 | 34 | .. code:: python 35 | 36 | GRAPHENE = { 37 | ... 38 | 'MIDDLEWARE': [ 39 | 'graphene_django.debug.DjangoDebugMiddleware', 40 | ] 41 | } 42 | 43 | Querying 44 | -------- 45 | 46 | You can query it for outputting all the sql transactions that happened in 47 | the GraphQL request, like: 48 | 49 | .. code:: 50 | 51 | { 52 | # A example that will use the ORM for interact with the DB 53 | allIngredients { 54 | edges { 55 | node { 56 | id, 57 | name 58 | } 59 | } 60 | } 61 | # Here is the debug field that will output the SQL queries 62 | _debug { 63 | sql { 64 | rawSql 65 | } 66 | exceptions { 67 | message 68 | stack 69 | } 70 | } 71 | } 72 | 73 | Note that the ``_debug`` field must be the last field in your query. 74 | -------------------------------------------------------------------------------- /docs/extra-types.rst: -------------------------------------------------------------------------------- 1 | Extra Types 2 | =========== 3 | 4 | Here are some libraries that provide common types for Django specific fields. 5 | 6 | 7 | GeoDjango 8 | --------- 9 | 10 | Use the graphene-gis_ library to add GeoDjango types to your Schema. 11 | 12 | .. _graphene-gis: https://github.com/EverWinter23/graphene-gis 13 | -------------------------------------------------------------------------------- /docs/fields.rst: -------------------------------------------------------------------------------- 1 | Fields 2 | ====== 3 | 4 | Graphene-Django provides some useful fields to help integrate Django with your GraphQL 5 | Schema. 6 | 7 | DjangoListField 8 | --------------- 9 | 10 | ``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType`'s. By default it will resolve the default queryset of the Django model. 11 | 12 | .. code:: python 13 | 14 | from graphene import ObjectType, Schema 15 | from graphene_django import DjangoListField 16 | 17 | class RecipeType(DjangoObjectType): 18 | class Meta: 19 | model = Recipe 20 | fields = ("title", "instructions") 21 | 22 | class Query(ObjectType): 23 | recipes = DjangoListField(RecipeType) 24 | 25 | schema = Schema(query=Query) 26 | 27 | The above code results in the following schema definition: 28 | 29 | .. code:: 30 | 31 | schema { 32 | query: Query 33 | } 34 | 35 | type Query { 36 | recipes: [RecipeType!] 37 | } 38 | 39 | type RecipeType { 40 | title: String! 41 | instructions: String! 42 | } 43 | 44 | Custom resolvers 45 | **************** 46 | 47 | If your ``DjangoObjectType`` has defined a custom 48 | :ref:`get_queryset` method, when resolving a 49 | ``DjangoListField`` it will be called with either the return of the field 50 | resolver (if one is defined) or the default queryset from the Django model. 51 | 52 | For example the following schema will only resolve recipes which have been 53 | published and have a title: 54 | 55 | .. code:: python 56 | 57 | from graphene import ObjectType, Schema 58 | from graphene_django import DjangoListField 59 | 60 | class RecipeType(DjangoObjectType): 61 | class Meta: 62 | model = Recipe 63 | fields = ("title", "instructions") 64 | 65 | @classmethod 66 | def get_queryset(cls, queryset, info): 67 | # Filter out recipes that have no title 68 | return queryset.exclude(title__exact="") 69 | 70 | class Query(ObjectType): 71 | recipes = DjangoListField(RecipeType) 72 | 73 | def resolve_recipes(parent, info): 74 | # Only get recipes that have been published 75 | return Recipe.objects.filter(published=True) 76 | 77 | schema = Schema(query=Query) 78 | 79 | 80 | DjangoConnectionField 81 | --------------------- 82 | 83 | ``DjangoConnectionField`` acts similarly to ``DjangoListField`` but returns a 84 | paginated connection following the `relay spec `__ 85 | The field supports the following arguments: `first`, `last`, `offset`, `after` & `before`. 86 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Graphene-Django 2 | =============== 3 | 4 | Welcome to the Graphene-Django docs. 5 | 6 | Graphene-Django is built on top of `Graphene `__. 7 | Graphene-Django provides some additional abstractions that make it easy to add GraphQL functionality to your Django project. 8 | 9 | First time? We recommend you start with the installation guide to get set up and the basic tutorial. 10 | It is worth reading the `core graphene docs `__ to familiarize yourself with the basic utilities. 11 | 12 | Core tenets 13 | ----------- 14 | 15 | If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section. 16 | 17 | 18 | For more advanced use, check out the Relay tutorial. 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | installation 24 | tutorial-plain 25 | tutorial-relay 26 | schema 27 | queries 28 | fields 29 | extra-types 30 | mutations 31 | subscriptions 32 | filtering 33 | authorization 34 | debug 35 | introspection 36 | validation 37 | testing 38 | settings 39 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Graphene-Django takes a few seconds to install and set up. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Graphene-Django currently supports the following versions of Django: 10 | 11 | * >= Django 2.2 12 | 13 | Installation 14 | ------------ 15 | 16 | .. code:: bash 17 | 18 | pip install graphene-django 19 | 20 | **We strongly recommend pinning against a specific version of Graphene-Django because new versions could introduce breaking changes to your project.** 21 | 22 | Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project: 23 | 24 | .. code:: python 25 | 26 | INSTALLED_APPS = [ 27 | ... 28 | "django.contrib.staticfiles", # Required for GraphiQL 29 | "graphene_django" 30 | ] 31 | 32 | 33 | We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: 34 | 35 | For Django 2.2 and above: 36 | 37 | .. code:: python 38 | 39 | from django.urls import path 40 | from graphene_django.views import GraphQLView 41 | 42 | urlpatterns = [ 43 | # ... 44 | path("graphql", GraphQLView.as_view(graphiql=True)), 45 | ] 46 | 47 | (Change ``graphiql=True`` to ``graphiql=False`` if you do not want to use the GraphiQL API browser.) 48 | 49 | Finally, define the schema location for Graphene in the ``settings.py`` file of your Django project: 50 | 51 | .. code:: python 52 | 53 | GRAPHENE = { 54 | "SCHEMA": "django_root.schema.schema" 55 | } 56 | 57 | Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project. 58 | 59 | The most basic ``schema.py`` looks like this: 60 | 61 | .. code:: python 62 | 63 | import graphene 64 | 65 | class Query(graphene.ObjectType): 66 | hello = graphene.String(default_value="Hi!") 67 | 68 | schema = graphene.Schema(query=Query) 69 | 70 | 71 | To learn how to extend the schema object for your project, read the basic tutorial. 72 | 73 | CSRF exempt 74 | ----------- 75 | 76 | If you have enabled `CSRF protection `_ in your Django app 77 | you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either 78 | update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt`` 79 | decorator: 80 | 81 | .. code:: python 82 | 83 | # urls.py 84 | 85 | from django.urls import path 86 | from django.views.decorators.csrf import csrf_exempt 87 | 88 | from graphene_django.views import GraphQLView 89 | 90 | urlpatterns = [ 91 | # ... 92 | path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), 93 | ] 94 | -------------------------------------------------------------------------------- /docs/introspection.rst: -------------------------------------------------------------------------------- 1 | Introspection Schema 2 | ==================== 3 | 4 | Relay Modern uses `Babel Relay Plugin `__ which requires you to provide your GraphQL schema data. 5 | 6 | Graphene comes with a Django management command to dump your schema 7 | data to ``schema.json`` which is compatible with babel-relay-plugin. 8 | 9 | Usage 10 | ----- 11 | 12 | Include ``graphene_django`` to ``INSTALLED_APPS`` in your project 13 | settings: 14 | 15 | .. code:: python 16 | 17 | INSTALLED_APPS += ('graphene_django') 18 | 19 | Assuming your Graphene schema is at ``tutorial.quickstart.schema``, run 20 | the command: 21 | 22 | .. code:: bash 23 | 24 | ./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.json 25 | 26 | It dumps your full introspection schema to ``schema.json`` inside your 27 | project root directory. Point ``babel-relay-plugin`` to this file and 28 | you're ready to use Relay with Graphene GraphQL implementation. 29 | 30 | The schema file is sorted to create a reproducible canonical representation. 31 | 32 | GraphQL SDL Representation 33 | -------------------------- 34 | 35 | The schema can also be exported as a GraphQL SDL file by changing the file 36 | extension : 37 | 38 | .. code:: bash 39 | 40 | ./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql 41 | 42 | When exporting the schema as a ``.graphql`` file the ``--indent`` option is 43 | ignored. 44 | 45 | 46 | Advanced Usage 47 | -------------- 48 | 49 | The ``--indent`` option can be used to specify the number of indentation spaces to 50 | be used in the output. Defaults to `None` which displays all data on a single line. 51 | 52 | The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project 53 | 54 | To simplify the command to ``./manage.py graphql_schema``, you can 55 | specify the parameters in your settings.py: 56 | 57 | .. code:: python 58 | 59 | GRAPHENE = { 60 | 'SCHEMA': 'tutorial.quickstart.schema', 61 | 'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json, 62 | 'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line) 63 | } 64 | 65 | 66 | Running ``./manage.py graphql_schema`` dumps your schema to 67 | ``/data/schema.json``. 68 | 69 | Help 70 | ---- 71 | 72 | Run ``./manage.py graphql_schema -h`` for command usage. 73 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==7.0.0 2 | sphinx-autobuild==2021.3.14 3 | pygments-graphql-lexer==0.1.0 4 | # Docs template 5 | http://graphene-python.org/sphinx_graphene_theme.zip 6 | -------------------------------------------------------------------------------- /docs/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graphene_django.types import DjangoObjectType 4 | 5 | from cookbook.ingredients.models import Category, Ingredient 6 | 7 | 8 | class CategoryType(DjangoObjectType): 9 | class Meta: 10 | model = Category 11 | fields = "__all__" 12 | 13 | 14 | class IngredientType(DjangoObjectType): 15 | class Meta: 16 | model = Ingredient 17 | fields = "__all__" 18 | 19 | 20 | class Query: 21 | category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) 22 | all_categories = graphene.List(CategoryType) 23 | 24 | ingredient = graphene.Field( 25 | IngredientType, id=graphene.Int(), name=graphene.String() 26 | ) 27 | all_ingredients = graphene.List(IngredientType) 28 | 29 | def resolve_all_categories(self, info, **kwargs): 30 | return Category.objects.all() 31 | 32 | def resolve_all_ingredients(self, info, **kwargs): 33 | return Ingredient.objects.all() 34 | 35 | def resolve_category(self, info, **kwargs): 36 | id = kwargs.get("id") 37 | name = kwargs.get("name") 38 | 39 | if id is not None: 40 | return Category.objects.get(pk=id) 41 | 42 | if name is not None: 43 | return Category.objects.get(name=name) 44 | 45 | return None 46 | 47 | def resolve_ingredient(self, info, **kwargs): 48 | id = kwargs.get("id") 49 | name = kwargs.get("name") 50 | 51 | if id is not None: 52 | return Ingredient.objects.get(pk=id) 53 | 54 | if name is not None: 55 | return Ingredient.objects.get(name=name) 56 | 57 | return None 58 | -------------------------------------------------------------------------------- /docs/schema.rst: -------------------------------------------------------------------------------- 1 | Schema 2 | ====== 3 | 4 | The ``graphene.Schema`` object describes your data model and provides a GraphQL server with an associated set of resolve methods that know how to fetch data. The most basic schema you can create looks like this: 5 | 6 | .. code:: python 7 | 8 | import graphene 9 | 10 | class Query(graphene.ObjectType): 11 | pass 12 | 13 | class Mutation(graphene.ObjectType): 14 | pass 15 | 16 | schema = graphene.Schema(query=Query, mutation=Mutation) 17 | 18 | 19 | This schema doesn't do anything yet, but it is ready to accept new Query or Mutation fields. 20 | 21 | 22 | Adding to the schema 23 | -------------------- 24 | 25 | If you have defined a ``Query`` or ``Mutation``, you can register them with the schema: 26 | 27 | .. code:: python 28 | 29 | import graphene 30 | 31 | import my_app.schema.Query 32 | import my_app.schema.Mutation 33 | 34 | class Query( 35 | my_app.schema.Query, # Add your Query objects here 36 | graphene.ObjectType 37 | ): 38 | pass 39 | 40 | class Mutation( 41 | my_app.schema.Mutation, # Add your Mutation objects here 42 | graphene.ObjectType 43 | ): 44 | pass 45 | 46 | schema = graphene.Schema(query=Query, mutation=Mutation) 47 | 48 | You can add as many mixins to the base ``Query`` and ``Mutation`` objects as you like. 49 | 50 | Read more about Schema on the `core graphene docs `__ -------------------------------------------------------------------------------- /docs/subscriptions.rst: -------------------------------------------------------------------------------- 1 | Subscriptions 2 | ============= 3 | 4 | The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are 5 | several community-driven modules for adding subscription support, and the provided GraphiQL interface supports 6 | running subscription operations over a websocket. 7 | 8 | To implement websocket-based support for GraphQL subscriptions, you’ll need to do the following: 9 | 10 | 1. Install and configure `django-channels `_. 11 | 2. Install and configure* a third-party module for adding subscription support over websockets. A few options include: 12 | 13 | - `graphql-python/graphql-ws `_ 14 | - `datavance/django-channels-graphql-ws `_ 15 | - `jaydenwindle/graphene-subscriptions `_ 16 | 17 | 3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like 18 | daphne (built in to ``django-channels``), `uvicorn `_, or 19 | `hypercorn `_. 20 | 21 | .. 22 | 23 | *** Note:** By default, the GraphiQL interface that comes with 24 | ``graphene-django`` assumes that you are handling subscriptions at 25 | the same path as any other operation (i.e., you configured both 26 | ``urls.py`` and ``routing.py`` to handle GraphQL operations at the 27 | same path, like ``/graphql``). 28 | 29 | If these URLs differ, GraphiQL will try to run your subscription over 30 | HTTP, which will produce an error. If you need to use a different URL 31 | for handling websocket connections, you can configure 32 | ``SUBSCRIPTION_PATH`` in your ``settings.py``: 33 | 34 | .. code:: python 35 | 36 | GRAPHENE = { 37 | # ... 38 | "SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash. 39 | } 40 | 41 | Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test 42 | subscriptions like any other operation. 43 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing API calls with django 2 | ============================= 3 | 4 | Using unittest 5 | -------------- 6 | 7 | If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. 8 | 9 | The default endpoint for testing is `/graphql`. You can override this in the `settings `__. 10 | 11 | 12 | Usage: 13 | 14 | .. code:: python 15 | 16 | import json 17 | 18 | from graphene_django.utils.testing import GraphQLTestCase 19 | 20 | class MyFancyTestCase(GraphQLTestCase): 21 | def test_some_query(self): 22 | response = self.query( 23 | ''' 24 | query { 25 | myModel { 26 | id 27 | name 28 | } 29 | } 30 | ''', 31 | operation_name='myModel' 32 | ) 33 | 34 | content = json.loads(response.content) 35 | 36 | # This validates the status code and if you get errors 37 | self.assertResponseNoErrors(response) 38 | 39 | # Add some more asserts if you like 40 | ... 41 | 42 | def test_query_with_variables(self): 43 | response = self.query( 44 | ''' 45 | query myModel($id: Int!){ 46 | myModel(id: $id) { 47 | id 48 | name 49 | } 50 | } 51 | ''', 52 | operation_name='myModel', 53 | variables={'id': 1} 54 | ) 55 | 56 | content = json.loads(response.content) 57 | 58 | # This validates the status code and if you get errors 59 | self.assertResponseNoErrors(response) 60 | 61 | # Add some more asserts if you like 62 | ... 63 | 64 | def test_some_mutation(self): 65 | response = self.query( 66 | ''' 67 | mutation myMutation($input: MyMutationInput!) { 68 | myMutation(input: $input) { 69 | my-model { 70 | id 71 | name 72 | } 73 | } 74 | } 75 | ''', 76 | operation_name='myMutation', 77 | input_data={'my_field': 'foo', 'other_field': 'bar'} 78 | ) 79 | 80 | # This validates the status code and if you get errors 81 | self.assertResponseNoErrors(response) 82 | 83 | # Add some more asserts if you like 84 | ... 85 | 86 | 87 | For testing mutations that are executed within a transaction you should subclass `GraphQLTransactionTestCase` 88 | 89 | Usage: 90 | 91 | .. code:: python 92 | 93 | import json 94 | 95 | from graphene_django.utils.testing import GraphQLTransactionTestCase 96 | 97 | class MyFancyTransactionTestCase(GraphQLTransactionTestCase): 98 | 99 | def test_some_mutation_that_executes_within_a_transaction(self): 100 | response = self.query( 101 | ''' 102 | mutation myMutation($input: MyMutationInput!) { 103 | myMutation(input: $input) { 104 | my-model { 105 | id 106 | name 107 | } 108 | } 109 | } 110 | ''', 111 | operation_name='myMutation', 112 | input_data={'my_field': 'foo', 'other_field': 'bar'} 113 | ) 114 | 115 | # This validates the status code and if you get errors 116 | self.assertResponseNoErrors(response) 117 | 118 | # Add some more asserts if you like 119 | ... 120 | 121 | Using pytest 122 | ------------ 123 | 124 | To use pytest define a simple fixture using the query helper below 125 | 126 | .. code:: python 127 | 128 | # Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`. 129 | import json 130 | import pytest 131 | from graphene_django.utils.testing import graphql_query 132 | 133 | @pytest.fixture 134 | def client_query(client): 135 | def func(*args, **kwargs): 136 | return graphql_query(*args, **kwargs, client=client) 137 | 138 | return func 139 | 140 | # Test you query using the client_query fixture 141 | def test_some_query(client_query): 142 | response = client_query( 143 | ''' 144 | query { 145 | myModel { 146 | id 147 | name 148 | } 149 | } 150 | ''', 151 | operation_name='myModel' 152 | ) 153 | 154 | content = json.loads(response.content) 155 | assert 'errors' not in content 156 | -------------------------------------------------------------------------------- /docs/validation.rst: -------------------------------------------------------------------------------- 1 | Query Validation 2 | ================ 3 | 4 | Graphene-Django supports query validation by allowing passing a list of validation rules (subclasses of `ValidationRule `_ from graphql-core) to the ``validation_rules`` option in ``GraphQLView``. 5 | 6 | .. code:: python 7 | 8 | from django.urls import path 9 | from graphene.validation import DisableIntrospection 10 | from graphene_django.views import GraphQLView 11 | 12 | urlpatterns = [ 13 | path("graphql", GraphQLView.as_view(validation_rules=(DisableIntrospection,))), 14 | ] 15 | 16 | or 17 | 18 | .. code:: python 19 | 20 | from django.urls import path 21 | from graphene.validation import DisableIntrospection 22 | from graphene_django.views import GraphQLView 23 | 24 | class View(GraphQLView): 25 | validation_rules = (DisableIntrospection,) 26 | 27 | urlpatterns = [ 28 | path("graphql", View.as_view()), 29 | ] 30 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/README.md: -------------------------------------------------------------------------------- 1 | Cookbook Example Django Project 2 | =============================== 3 | 4 | This example project demos integration between Graphene and Django. 5 | The project contains two apps, one named `ingredients` and another 6 | named `recipes`. 7 | 8 | Getting started 9 | --------------- 10 | 11 | First you'll need to get the source of the project. Do this by cloning the 12 | whole Graphene repository: 13 | 14 | ```bash 15 | # Get the example project code 16 | git clone https://github.com/graphql-python/graphene-django.git 17 | cd graphene-django/examples/cookbook-plain 18 | ``` 19 | 20 | It is good idea (but not required) to create a virtual environment 21 | for this project. We'll do this using 22 | [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) 23 | to keep things simple, 24 | but you may also find something like 25 | [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) 26 | to be useful: 27 | 28 | ```bash 29 | # Create a virtualenv in which we can install the dependencies 30 | virtualenv env 31 | source env/bin/activate 32 | ``` 33 | 34 | Now we can install our dependencies: 35 | 36 | ```bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | Now setup our database: 41 | 42 | ```bash 43 | # Setup the database 44 | ./manage.py migrate 45 | 46 | # Load some example data 47 | ./manage.py loaddata ingredients 48 | 49 | # Create an admin user (useful for logging into the admin UI 50 | # at http://127.0.0.1:8000/admin) 51 | ./manage.py createsuperuser 52 | ``` 53 | 54 | Now you should be ready to start the server: 55 | 56 | ```bash 57 | ./manage.py runserver 58 | ``` 59 | 60 | Now head on over to 61 | [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) 62 | and run some queries! 63 | (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) 64 | for some example queries) 65 | 66 | Testing local graphene-django changes 67 | ------------------------------------- 68 | 69 | 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): 70 | 71 | ``` 72 | ../../ # graphene-django 73 | ``` 74 | -------------------------------------------------------------------------------- /examples/cookbook-plain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/cookbook/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/cookbook/ingredients/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cookbook.ingredients.models import Category, Ingredient 4 | 5 | 6 | @admin.register(Ingredient) 7 | class IngredientAdmin(admin.ModelAdmin): 8 | list_display = ("id", "name", "category") 9 | list_editable = ("name", "category") 10 | 11 | 12 | admin.site.register(Category) 13 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class IngredientsConfig(AppConfig): 5 | name = "cookbook.ingredients" 6 | label = "ingredients" 7 | verbose_name = "Ingredients" 8 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "Dairy" 5 | }, 6 | "model": "ingredients.category", 7 | "pk": 1 8 | }, 9 | { 10 | "fields": { 11 | "name": "Meat" 12 | }, 13 | "model": "ingredients.category", 14 | "pk": 2 15 | }, 16 | { 17 | "fields": { 18 | "category": 1, 19 | "name": "Eggs", 20 | "notes": "Good old eggs" 21 | }, 22 | "model": "ingredients.ingredient", 23 | "pk": 1 24 | }, 25 | { 26 | "fields": { 27 | "category": 1, 28 | "name": "Milk", 29 | "notes": "Comes from a cow" 30 | }, 31 | "model": "ingredients.ingredient", 32 | "pk": 2 33 | }, 34 | { 35 | "fields": { 36 | "category": 2, 37 | "name": "Beef", 38 | "notes": "Much like milk, this comes from a cow" 39 | }, 40 | "model": "ingredients.ingredient", 41 | "pk": 3 42 | }, 43 | { 44 | "fields": { 45 | "category": 2, 46 | "name": "Chicken", 47 | "notes": "Definitely doesn't come from a cow" 48 | }, 49 | "model": "ingredients.ingredient", 50 | "pk": 4 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2015-12-04 18:15 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Category", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(max_length=100)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name="Ingredient", 30 | fields=[ 31 | ( 32 | "id", 33 | models.AutoField( 34 | auto_created=True, 35 | primary_key=True, 36 | serialize=False, 37 | verbose_name="ID", 38 | ), 39 | ), 40 | ("name", models.CharField(max_length=100)), 41 | ("notes", models.TextField()), 42 | ( 43 | "category", 44 | models.ForeignKey( 45 | on_delete=django.db.models.deletion.CASCADE, 46 | related_name="ingredients", 47 | to="ingredients.Category", 48 | ), 49 | ), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2016-11-04 00:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ingredients", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="ingredient", 14 | name="notes", 15 | field=models.TextField(blank=True, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-10-18 17:46 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ingredients", "0002_auto_20161104_0050"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="category", 14 | options={"verbose_name_plural": "Categories"}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | class Meta: 6 | verbose_name_plural = "Categories" 7 | 8 | name = models.CharField(max_length=100) 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | 14 | class Ingredient(models.Model): 15 | name = models.CharField(max_length=100) 16 | notes = models.TextField(null=True, blank=True) 17 | category = models.ForeignKey( 18 | Category, related_name="ingredients", on_delete=models.CASCADE 19 | ) 20 | 21 | def __str__(self): 22 | return self.name 23 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | 4 | from .models import Category, Ingredient 5 | 6 | 7 | class CategoryType(DjangoObjectType): 8 | class Meta: 9 | model = Category 10 | fields = "__all__" 11 | 12 | 13 | class IngredientType(DjangoObjectType): 14 | class Meta: 15 | model = Ingredient 16 | fields = "__all__" 17 | 18 | 19 | class Query: 20 | category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) 21 | all_categories = graphene.List(CategoryType) 22 | 23 | ingredient = graphene.Field( 24 | IngredientType, id=graphene.Int(), name=graphene.String() 25 | ) 26 | all_ingredients = graphene.List(IngredientType) 27 | 28 | def resolve_all_categories(self, context): 29 | return Category.objects.all() 30 | 31 | def resolve_all_ingredients(self, context): 32 | # We can easily optimize query count in the resolve method 33 | return Ingredient.objects.select_related("category").all() 34 | 35 | def resolve_category(self, context, id=None, name=None): 36 | if id is not None: 37 | return Category.objects.get(pk=id) 38 | 39 | if name is not None: 40 | return Category.objects.get(name=name) 41 | 42 | return None 43 | 44 | def resolve_ingredient(self, context, id=None, name=None): 45 | if id is not None: 46 | return Ingredient.objects.get(pk=id) 47 | 48 | if name is not None: 49 | return Ingredient.objects.get(name=name) 50 | 51 | return None 52 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/ingredients/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/cookbook/recipes/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cookbook.recipes.models import Recipe, RecipeIngredient 4 | 5 | 6 | class RecipeIngredientInline(admin.TabularInline): 7 | model = RecipeIngredient 8 | 9 | 10 | @admin.register(Recipe) 11 | class RecipeAdmin(admin.ModelAdmin): 12 | inlines = [RecipeIngredientInline] 13 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RecipesConfig(AppConfig): 5 | name = "cookbook.recipes" 6 | label = "recipes" 7 | verbose_name = "Recipes" 8 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2015-12-04 18:20 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [ 11 | ("ingredients", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Recipe", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("title", models.CharField(max_length=100)), 28 | ("instructions", models.TextField()), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name="RecipeIngredient", 33 | fields=[ 34 | ( 35 | "id", 36 | models.AutoField( 37 | auto_created=True, 38 | primary_key=True, 39 | serialize=False, 40 | verbose_name="ID", 41 | ), 42 | ), 43 | ("amount", models.FloatField()), 44 | ( 45 | "unit", 46 | models.CharField( 47 | choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], 48 | max_length=20, 49 | ), 50 | ), 51 | ( 52 | "ingredient", 53 | models.ForeignKey( 54 | on_delete=django.db.models.deletion.CASCADE, 55 | related_name="used_by", 56 | to="ingredients.Ingredient", 57 | ), 58 | ), 59 | ( 60 | "recipes", 61 | models.ForeignKey( 62 | on_delete=django.db.models.deletion.CASCADE, 63 | related_name="amounts", 64 | to="recipes.Recipe", 65 | ), 66 | ), 67 | ], 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2016-11-04 01:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("recipes", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="recipeingredient", 14 | old_name="recipes", 15 | new_name="recipe", 16 | ), 17 | migrations.AlterField( 18 | model_name="recipeingredient", 19 | name="unit", 20 | field=models.CharField( 21 | choices=[ 22 | (b"unit", b"Units"), 23 | (b"kg", b"Kilograms"), 24 | (b"l", b"Litres"), 25 | (b"st", b"Shots"), 26 | ], 27 | max_length=20, 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-10-18 17:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("recipes", "0002_auto_20161104_0106"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="recipeingredient", 14 | name="unit", 15 | field=models.CharField( 16 | choices=[ 17 | ("unit", "Units"), 18 | ("kg", "Kilograms"), 19 | ("l", "Litres"), 20 | ("st", "Shots"), 21 | ], 22 | max_length=20, 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook-plain/cookbook/recipes/migrations/__init__.py -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from ..ingredients.models import Ingredient 4 | 5 | 6 | class Recipe(models.Model): 7 | title = models.CharField(max_length=100) 8 | instructions = models.TextField() 9 | 10 | def __str__(self): 11 | return self.title 12 | 13 | 14 | class RecipeIngredient(models.Model): 15 | recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) 16 | ingredient = models.ForeignKey( 17 | Ingredient, related_name="used_by", on_delete=models.CASCADE 18 | ) 19 | amount = models.FloatField() 20 | unit = models.CharField( 21 | max_length=20, 22 | choices=( 23 | ("unit", "Units"), 24 | ("kg", "Kilograms"), 25 | ("l", "Litres"), 26 | ("st", "Shots"), 27 | ), 28 | ) 29 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | 4 | from .models import Recipe, RecipeIngredient 5 | 6 | 7 | class RecipeType(DjangoObjectType): 8 | class Meta: 9 | model = Recipe 10 | fields = "__all__" 11 | 12 | 13 | class RecipeIngredientType(DjangoObjectType): 14 | class Meta: 15 | model = RecipeIngredient 16 | fields = "__all__" 17 | 18 | 19 | class Query: 20 | recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) 21 | all_recipes = graphene.List(RecipeType) 22 | 23 | recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int()) 24 | all_recipeingredients = graphene.List(RecipeIngredientType) 25 | 26 | def resolve_recipe(self, context, id=None, title=None): 27 | if id is not None: 28 | return Recipe.objects.get(pk=id) 29 | 30 | if title is not None: 31 | return Recipe.objects.get(title=title) 32 | 33 | return None 34 | 35 | def resolve_recipeingredient(self, context, id=None): 36 | if id is not None: 37 | return RecipeIngredient.objects.get(pk=id) 38 | 39 | return None 40 | 41 | def resolve_all_recipes(self, context): 42 | return Recipe.objects.all() 43 | 44 | def resolve_all_recipeingredients(self, context): 45 | related = ["recipe", "ingredient"] 46 | return RecipeIngredient.objects.select_related(*related).all() 47 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/recipes/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.debug import DjangoDebug 3 | 4 | import cookbook.ingredients.schema 5 | import cookbook.recipes.schema 6 | 7 | 8 | class Query( 9 | cookbook.ingredients.schema.Query, 10 | cookbook.recipes.schema.Query, 11 | graphene.ObjectType, 12 | ): 13 | debug = graphene.Field(DjangoDebug, name="_debug") 14 | 15 | 16 | schema = graphene.Schema(query=Query) 17 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/settings.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | """ 3 | Django settings for cookbook project. 4 | 5 | Generated by 'django-admin startproject' using Django 1.9. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/3.2/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/3.2/ref/settings/ 12 | """ 13 | 14 | import os 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "graphene_django", 42 | "cookbook.ingredients.apps.IngredientsConfig", 43 | "cookbook.recipes.apps.RecipesConfig", 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | "django.middleware.security.SecurityMiddleware", 48 | "django.contrib.sessions.middleware.SessionMiddleware", 49 | "django.middleware.common.CommonMiddleware", 50 | "django.middleware.csrf.CsrfViewMiddleware", 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", 52 | "django.contrib.messages.middleware.MessageMiddleware", 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 54 | ] 55 | 56 | GRAPHENE = { 57 | "SCHEMA": "cookbook.schema.schema", 58 | "SCHEMA_INDENT": 2, 59 | "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), 60 | } 61 | 62 | ROOT_URLCONF = "cookbook.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ] 76 | }, 77 | } 78 | ] 79 | 80 | WSGI_APPLICATION = "cookbook.wsgi.application" 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 85 | 86 | DATABASES = { 87 | "default": { 88 | "ENGINE": "django.db.backends.sqlite3", 89 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 90 | } 91 | } 92 | 93 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 94 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 102 | }, 103 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 104 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 105 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 111 | 112 | LANGUAGE_CODE = "en-us" 113 | 114 | TIME_ZONE = "UTC" 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 125 | 126 | STATIC_URL = "/static/" 127 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from graphene_django.views import GraphQLView 5 | 6 | urlpatterns = [ 7 | path("admin/", admin.site.urls), 8 | path("graphql/", GraphQLView.as_view(graphiql=True)), 9 | ] 10 | -------------------------------------------------------------------------------- /examples/cookbook-plain/cookbook/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cookbook project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/cookbook-plain/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/cookbook-plain/requirements.txt: -------------------------------------------------------------------------------- 1 | django~=3.2 2 | graphene 3 | graphene-django>=3.1 4 | -------------------------------------------------------------------------------- /examples/cookbook-plain/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=migrations,.git,__pycache__ 3 | -------------------------------------------------------------------------------- /examples/cookbook/README.md: -------------------------------------------------------------------------------- 1 | Cookbook Example (Relay) Django Project 2 | =============================== 3 | 4 | This example project demos integration between Graphene and Django. 5 | The project contains two apps, one named `ingredients` and another 6 | named `recipes`. 7 | 8 | Getting started 9 | --------------- 10 | 11 | First you'll need to get the source of the project. Do this by cloning the 12 | whole Graphene repository: 13 | 14 | ```bash 15 | # Get the example project code 16 | git clone https://github.com/graphql-python/graphene-django.git 17 | cd graphene-django/examples/cookbook 18 | ``` 19 | 20 | It is good idea (but not required) to create a virtual environment 21 | for this project. We'll do this using 22 | [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) 23 | to keep things simple, 24 | but you may also find something like 25 | [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) 26 | to be useful: 27 | 28 | ```bash 29 | # Create a virtualenv in which we can install the dependencies 30 | virtualenv env 31 | source env/bin/activate 32 | ``` 33 | 34 | Now we can install our dependencies: 35 | 36 | ```bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | Now setup our database: 41 | 42 | ```bash 43 | # Setup the database 44 | ./manage.py migrate 45 | 46 | # Load some example data 47 | ./manage.py loaddata ingredients 48 | 49 | # Create an admin user (useful for logging into the admin UI 50 | # at http://127.0.0.1:8000/admin) 51 | ./manage.py createsuperuser 52 | ``` 53 | 54 | Now you should be ready to start the server: 55 | 56 | ```bash 57 | ./manage.py runserver 58 | ``` 59 | 60 | Now head on over to 61 | [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) 62 | and run some queries! 63 | (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-relay/#testing-our-graphql-schema) 64 | for some example queries) 65 | -------------------------------------------------------------------------------- /examples/cookbook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/cookbook/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/cookbook/ingredients/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cookbook.ingredients.models import Category, Ingredient 4 | 5 | 6 | @admin.register(Ingredient) 7 | class IngredientAdmin(admin.ModelAdmin): 8 | list_display = ("id", "name", "category") 9 | list_editable = ("name", "category") 10 | 11 | 12 | admin.site.register(Category) 13 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class IngredientsConfig(AppConfig): 5 | name = "cookbook.ingredients" 6 | label = "ingredients" 7 | verbose_name = "Ingredients" 8 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/fixtures/ingredients.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "name": "Dairy" 5 | }, 6 | "model": "ingredients.category", 7 | "pk": 1 8 | }, 9 | { 10 | "fields": { 11 | "name": "Meat" 12 | }, 13 | "model": "ingredients.category", 14 | "pk": 2 15 | }, 16 | { 17 | "fields": { 18 | "category": 1, 19 | "name": "Eggs", 20 | "notes": "Good old eggs" 21 | }, 22 | "model": "ingredients.ingredient", 23 | "pk": 1 24 | }, 25 | { 26 | "fields": { 27 | "category": 1, 28 | "name": "Milk", 29 | "notes": "Comes from a cow" 30 | }, 31 | "model": "ingredients.ingredient", 32 | "pk": 2 33 | }, 34 | { 35 | "fields": { 36 | "category": 2, 37 | "name": "Beef", 38 | "notes": "Much like milk, this comes from a cow" 39 | }, 40 | "model": "ingredients.ingredient", 41 | "pk": 3 42 | }, 43 | { 44 | "fields": { 45 | "category": 2, 46 | "name": "Chicken", 47 | "notes": "Definitely doesn't come from a cow" 48 | }, 49 | "model": "ingredients.ingredient", 50 | "pk": 4 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2015-12-04 18:15 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Category", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(max_length=100)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name="Ingredient", 30 | fields=[ 31 | ( 32 | "id", 33 | models.AutoField( 34 | auto_created=True, 35 | primary_key=True, 36 | serialize=False, 37 | verbose_name="ID", 38 | ), 39 | ), 40 | ("name", models.CharField(max_length=100)), 41 | ("notes", models.TextField()), 42 | ( 43 | "category", 44 | models.ForeignKey( 45 | on_delete=django.db.models.deletion.CASCADE, 46 | related_name="ingredients", 47 | to="ingredients.Category", 48 | ), 49 | ), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2016-11-04 00:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ingredients", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="ingredient", 14 | name="notes", 15 | field=models.TextField(blank=True, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/cookbook/ingredients/migrations/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=100) 6 | 7 | def __str__(self): 8 | return self.name 9 | 10 | 11 | class Ingredient(models.Model): 12 | name = models.CharField(max_length=100) 13 | notes = models.TextField(null=True, blank=True) 14 | category = models.ForeignKey( 15 | Category, related_name="ingredients", on_delete=models.CASCADE 16 | ) 17 | 18 | def __str__(self): 19 | return self.name 20 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/schema.py: -------------------------------------------------------------------------------- 1 | from graphene import Node 2 | from graphene_django.filter import DjangoFilterConnectionField 3 | from graphene_django.types import DjangoObjectType 4 | 5 | from cookbook.ingredients.models import Category, Ingredient 6 | 7 | 8 | # Graphene will automatically map the Category model's fields onto the CategoryNode. 9 | # This is configured in the CategoryNode's Meta class (as you can see below) 10 | class CategoryNode(DjangoObjectType): 11 | class Meta: 12 | model = Category 13 | interfaces = (Node,) 14 | fields = "__all__" 15 | filter_fields = ["name", "ingredients"] 16 | 17 | 18 | class IngredientNode(DjangoObjectType): 19 | class Meta: 20 | model = Ingredient 21 | # Allow for some more advanced filtering here 22 | interfaces = (Node,) 23 | fields = "__all__" 24 | filter_fields = { 25 | "name": ["exact", "icontains", "istartswith"], 26 | "notes": ["exact", "icontains"], 27 | "category": ["exact"], 28 | "category__name": ["exact"], 29 | } 30 | 31 | 32 | class Query: 33 | category = Node.Field(CategoryNode) 34 | all_categories = DjangoFilterConnectionField(CategoryNode) 35 | 36 | ingredient = Node.Field(IngredientNode) 37 | all_ingredients = DjangoFilterConnectionField(IngredientNode) 38 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/ingredients/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/cookbook/recipes/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cookbook.recipes.models import Recipe, RecipeIngredient 4 | 5 | 6 | class RecipeIngredientInline(admin.TabularInline): 7 | model = RecipeIngredient 8 | 9 | 10 | @admin.register(Recipe) 11 | class RecipeAdmin(admin.ModelAdmin): 12 | inlines = [RecipeIngredientInline] 13 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RecipesConfig(AppConfig): 5 | name = "cookbook.recipes" 6 | label = "recipes" 7 | verbose_name = "Recipes" 8 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2015-12-04 18:20 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [ 11 | ("ingredients", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Recipe", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("title", models.CharField(max_length=100)), 28 | ("instructions", models.TextField()), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name="RecipeIngredient", 33 | fields=[ 34 | ( 35 | "id", 36 | models.AutoField( 37 | auto_created=True, 38 | primary_key=True, 39 | serialize=False, 40 | verbose_name="ID", 41 | ), 42 | ), 43 | ("amount", models.FloatField()), 44 | ( 45 | "unit", 46 | models.CharField( 47 | choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], 48 | max_length=20, 49 | ), 50 | ), 51 | ( 52 | "ingredient", 53 | models.ForeignKey( 54 | on_delete=django.db.models.deletion.CASCADE, 55 | related_name="used_by", 56 | to="ingredients.Ingredient", 57 | ), 58 | ), 59 | ( 60 | "recipes", 61 | models.ForeignKey( 62 | on_delete=django.db.models.deletion.CASCADE, 63 | related_name="amounts", 64 | to="recipes.Recipe", 65 | ), 66 | ), 67 | ], 68 | ), 69 | ] 70 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9 on 2016-11-04 01:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("recipes", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="recipeingredient", 14 | old_name="recipes", 15 | new_name="recipe", 16 | ), 17 | migrations.AlterField( 18 | model_name="recipeingredient", 19 | name="unit", 20 | field=models.CharField( 21 | choices=[ 22 | (b"unit", b"Units"), 23 | (b"kg", b"Kilograms"), 24 | (b"l", b"Litres"), 25 | (b"st", b"Shots"), 26 | ], 27 | max_length=20, 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/cookbook/cookbook/recipes/migrations/__init__.py -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from cookbook.ingredients.models import Ingredient 4 | 5 | 6 | class Recipe(models.Model): 7 | title = models.CharField(max_length=100) 8 | instructions = models.TextField() 9 | 10 | def __unicode__(self): 11 | return self.title 12 | 13 | 14 | class RecipeIngredient(models.Model): 15 | recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) 16 | ingredient = models.ForeignKey( 17 | Ingredient, related_name="used_by", on_delete=models.CASCADE 18 | ) 19 | amount = models.FloatField() 20 | unit = models.CharField( 21 | max_length=20, 22 | choices=( 23 | ("unit", "Units"), 24 | ("kg", "Kilograms"), 25 | ("l", "Litres"), 26 | ("st", "Shots"), 27 | ), 28 | ) 29 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/schema.py: -------------------------------------------------------------------------------- 1 | from graphene import Node 2 | from graphene_django.filter import DjangoFilterConnectionField 3 | from graphene_django.types import DjangoObjectType 4 | 5 | from cookbook.recipes.models import Recipe, RecipeIngredient 6 | 7 | 8 | class RecipeNode(DjangoObjectType): 9 | class Meta: 10 | model = Recipe 11 | interfaces = (Node,) 12 | fields = "__all__" 13 | filter_fields = ["title", "amounts"] 14 | 15 | 16 | class RecipeIngredientNode(DjangoObjectType): 17 | class Meta: 18 | model = RecipeIngredient 19 | # Allow for some more advanced filtering here 20 | interfaces = (Node,) 21 | fields = "__all__" 22 | filter_fields = { 23 | "ingredient__name": ["exact", "icontains", "istartswith"], 24 | "recipe": ["exact"], 25 | "recipe__title": ["icontains"], 26 | } 27 | 28 | 29 | class Query: 30 | recipe = Node.Field(RecipeNode) 31 | all_recipes = DjangoFilterConnectionField(RecipeNode) 32 | 33 | recipeingredient = Node.Field(RecipeIngredientNode) 34 | all_recipeingredients = DjangoFilterConnectionField(RecipeIngredientNode) 35 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/recipes/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.debug import DjangoDebug 3 | 4 | import cookbook.ingredients.schema 5 | import cookbook.recipes.schema 6 | 7 | 8 | class Query( 9 | cookbook.ingredients.schema.Query, 10 | cookbook.recipes.schema.Query, 11 | graphene.ObjectType, 12 | ): 13 | debug = graphene.Field(DjangoDebug, name="_debug") 14 | 15 | 16 | schema = graphene.Schema(query=Query) 17 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/settings.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | """ 3 | Django settings for cookbook project. 4 | 5 | Generated by 'django-admin startproject' using Django 1.9. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/1.9/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/1.9/ref/settings/ 12 | """ 13 | 14 | import os 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "graphene_django", 42 | "cookbook.ingredients.apps.IngredientsConfig", 43 | "cookbook.recipes.apps.RecipesConfig", 44 | "django_filters", 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | "django.middleware.security.SecurityMiddleware", 49 | "django.contrib.sessions.middleware.SessionMiddleware", 50 | "django.middleware.common.CommonMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 55 | ] 56 | 57 | GRAPHENE = { 58 | "SCHEMA": "cookbook.schema.schema", 59 | "SCHEMA_INDENT": 2, 60 | "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), 61 | } 62 | 63 | ROOT_URLCONF = "cookbook.urls" 64 | 65 | TEMPLATES = [ 66 | { 67 | "BACKEND": "django.template.backends.django.DjangoTemplates", 68 | "DIRS": [], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ] 77 | }, 78 | } 79 | ] 80 | 81 | WSGI_APPLICATION = "cookbook.wsgi.application" 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 86 | 87 | DATABASES = { 88 | "default": { 89 | "ENGINE": "django.db.backends.sqlite3", 90 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 101 | }, 102 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 103 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 104 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 110 | 111 | LANGUAGE_CODE = "en-us" 112 | 113 | TIME_ZONE = "UTC" 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 124 | 125 | STATIC_URL = "/static/" 126 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | from graphene_django.views import GraphQLView 5 | 6 | urlpatterns = [ 7 | url(r"^admin/", admin.site.urls), 8 | url(r"^graphql$", GraphQLView.as_view(graphiql=True)), 9 | ] 10 | -------------------------------------------------------------------------------- /examples/cookbook/cookbook/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cookbook project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/cookbook/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/cookbook/requirements.txt: -------------------------------------------------------------------------------- 1 | graphene>=2.1,<3 2 | graphene-django>=2.1,<3 3 | graphql-core>=2.1,<3 4 | django==3.2.25 5 | django-filter>=2 6 | -------------------------------------------------------------------------------- /examples/cookbook/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=migrations,.git,__pycache__ 3 | -------------------------------------------------------------------------------- /examples/django_test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) 5 | sys.path.insert(0, ROOT_PATH + "/examples/") 6 | 7 | SECRET_KEY = 1 8 | 9 | INSTALLED_APPS = [ 10 | "graphene_django", 11 | "graphene_django.rest_framework", 12 | "graphene_django.tests", 13 | "examples.starwars", 14 | ] 15 | 16 | DATABASES = { 17 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "django_test.sqlite"} 18 | } 19 | 20 | TEMPLATES = [ 21 | { 22 | "BACKEND": "django.template.backends.django.DjangoTemplates", 23 | "DIRS": [], 24 | "APP_DIRS": True, 25 | } 26 | ] 27 | 28 | GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"} 29 | 30 | ROOT_URLCONF = "graphene_django.tests.urls" 31 | 32 | USE_TZ = True 33 | -------------------------------------------------------------------------------- /examples/starwars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/starwars/__init__.py -------------------------------------------------------------------------------- /examples/starwars/data.py: -------------------------------------------------------------------------------- 1 | from .models import Character, Faction, Ship 2 | 3 | 4 | def initialize(): 5 | human = Character(name="Human") 6 | human.save() 7 | 8 | droid = Character(name="Droid") 9 | droid.save() 10 | 11 | rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human) 12 | rebels.save() 13 | 14 | empire = Faction(id="2", name="Galactic Empire", hero=droid) 15 | empire.save() 16 | 17 | xwing = Ship(id="1", name="X-Wing", faction=rebels) 18 | xwing.save() 19 | 20 | human.ship = xwing 21 | human.save() 22 | 23 | ywing = Ship(id="2", name="Y-Wing", faction=rebels) 24 | ywing.save() 25 | 26 | awing = Ship(id="3", name="A-Wing", faction=rebels) 27 | awing.save() 28 | 29 | # Yeah, technically it's Corellian. But it flew in the service of the rebels, 30 | # so for the purposes of this demo it's a rebel ship. 31 | falcon = Ship(id="4", name="Millennium Falcon", faction=rebels) 32 | falcon.save() 33 | 34 | homeOne = Ship(id="5", name="Home One", faction=rebels) 35 | homeOne.save() 36 | 37 | tieFighter = Ship(id="6", name="TIE Fighter", faction=empire) 38 | tieFighter.save() 39 | 40 | tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire) 41 | tieInterceptor.save() 42 | 43 | executor = Ship(id="8", name="Executor", faction=empire) 44 | executor.save() 45 | 46 | 47 | def create_ship(ship_name, faction_id): 48 | new_ship = Ship(name=ship_name, faction_id=faction_id) 49 | new_ship.save() 50 | return new_ship 51 | 52 | 53 | def get_ship(_id): 54 | return Ship.objects.get(id=_id) 55 | 56 | 57 | def get_ships(): 58 | return Ship.objects.all() 59 | 60 | 61 | def get_faction(_id): 62 | return Faction.objects.get(id=_id) 63 | 64 | 65 | def get_rebels(): 66 | return get_faction(1) 67 | 68 | 69 | def get_empire(): 70 | return get_faction(2) 71 | -------------------------------------------------------------------------------- /examples/starwars/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Character(models.Model): 5 | name = models.CharField(max_length=50) 6 | ship = models.ForeignKey( 7 | "Ship", 8 | on_delete=models.CASCADE, 9 | blank=True, 10 | null=True, 11 | related_name="characters", 12 | ) 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | 18 | class Faction(models.Model): 19 | name = models.CharField(max_length=50) 20 | hero = models.ForeignKey(Character, on_delete=models.CASCADE) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class Ship(models.Model): 27 | name = models.CharField(max_length=50) 28 | faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships") 29 | 30 | def __str__(self): 31 | return self.name 32 | -------------------------------------------------------------------------------- /examples/starwars/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import Schema, relay 3 | from graphene_django import DjangoConnectionField, DjangoObjectType 4 | 5 | from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships 6 | from .models import ( 7 | Character as CharacterModel, 8 | Faction as FactionModel, 9 | Ship as ShipModel, 10 | ) 11 | 12 | 13 | class Ship(DjangoObjectType): 14 | class Meta: 15 | model = ShipModel 16 | interfaces = (relay.Node,) 17 | fields = "__all__" 18 | 19 | @classmethod 20 | def get_node(cls, info, id): 21 | node = get_ship(id) 22 | return node 23 | 24 | 25 | class Character(DjangoObjectType): 26 | class Meta: 27 | model = CharacterModel 28 | fields = "__all__" 29 | 30 | 31 | class Faction(DjangoObjectType): 32 | class Meta: 33 | model = FactionModel 34 | interfaces = (relay.Node,) 35 | fields = "__all__" 36 | 37 | @classmethod 38 | def get_node(cls, info, id): 39 | return get_faction(id) 40 | 41 | 42 | class IntroduceShip(relay.ClientIDMutation): 43 | class Input: 44 | ship_name = graphene.String(required=True) 45 | faction_id = graphene.String(required=True) 46 | 47 | ship = graphene.Field(Ship) 48 | faction = graphene.Field(Faction) 49 | 50 | @classmethod 51 | def mutate_and_get_payload( 52 | cls, root, info, ship_name, faction_id, client_mutation_id=None 53 | ): 54 | ship = create_ship(ship_name, faction_id) 55 | faction = get_faction(faction_id) 56 | return IntroduceShip(ship=ship, faction=faction) 57 | 58 | 59 | class Query(graphene.ObjectType): 60 | rebels = graphene.Field(Faction) 61 | empire = graphene.Field(Faction) 62 | node = relay.Node.Field() 63 | ships = DjangoConnectionField(Ship, description="All the ships.") 64 | 65 | def resolve_ships(self, info): 66 | return get_ships() 67 | 68 | def resolve_rebels(self, info): 69 | return get_rebels() 70 | 71 | def resolve_empire(self, info): 72 | return get_empire() 73 | 74 | 75 | class Mutation(graphene.ObjectType): 76 | introduce_ship = IntroduceShip.Field() 77 | 78 | 79 | # We register the Character Model because if not would be 80 | # inaccessible for the schema 81 | schema = Schema(query=Query, mutation=Mutation, types=[Ship, Character]) 82 | -------------------------------------------------------------------------------- /examples/starwars/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/examples/starwars/tests/__init__.py -------------------------------------------------------------------------------- /examples/starwars/tests/test_connections.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..data import initialize 4 | from ..schema import schema 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_correct_fetch_first_ship_rebels(): 10 | initialize() 11 | query = """ 12 | query RebelsShipsQuery { 13 | rebels { 14 | name, 15 | hero { 16 | name 17 | } 18 | ships(first: 1) { 19 | edges { 20 | node { 21 | name 22 | } 23 | } 24 | } 25 | } 26 | } 27 | """ 28 | expected = { 29 | "rebels": { 30 | "name": "Alliance to Restore the Republic", 31 | "hero": {"name": "Human"}, 32 | "ships": {"edges": [{"node": {"name": "X-Wing"}}]}, 33 | } 34 | } 35 | result = schema.execute(query) 36 | assert not result.errors 37 | assert result.data == expected 38 | 39 | 40 | def test_correct_list_characters(): 41 | initialize() 42 | query = """ 43 | query RebelsShipsQuery { 44 | node(id: "U2hpcDox") { 45 | ... on Ship { 46 | name 47 | characters { 48 | name 49 | } 50 | } 51 | } 52 | } 53 | """ 54 | expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}} 55 | result = schema.execute(query) 56 | assert not result.errors 57 | assert result.data == expected 58 | -------------------------------------------------------------------------------- /examples/starwars/tests/test_mutation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..data import initialize 4 | from ..schema import schema 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_mutations(): 10 | initialize() 11 | 12 | query = """ 13 | mutation MyMutation { 14 | introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { 15 | ship { 16 | id 17 | name 18 | } 19 | faction { 20 | name 21 | ships { 22 | edges { 23 | node { 24 | id 25 | name 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | """ 33 | expected = { 34 | "introduceShip": { 35 | "ship": {"id": "U2hpcDo5", "name": "Peter"}, 36 | "faction": { 37 | "name": "Alliance to Restore the Republic", 38 | "ships": { 39 | "edges": [ 40 | {"node": {"id": "U2hpcDox", "name": "X-Wing"}}, 41 | {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}}, 42 | {"node": {"id": "U2hpcDoz", "name": "A-Wing"}}, 43 | {"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}}, 44 | {"node": {"id": "U2hpcDo1", "name": "Home One"}}, 45 | {"node": {"id": "U2hpcDo5", "name": "Peter"}}, 46 | ] 47 | }, 48 | }, 49 | } 50 | } 51 | result = schema.execute(query) 52 | assert not result.errors 53 | assert result.data == expected 54 | -------------------------------------------------------------------------------- /examples/starwars/tests/test_objectidentification.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..data import initialize 4 | from ..schema import schema 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_correctly_fetches_id_name_rebels(): 10 | initialize() 11 | query = """ 12 | query RebelsQuery { 13 | rebels { 14 | id 15 | name 16 | } 17 | } 18 | """ 19 | expected = { 20 | "rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} 21 | } 22 | result = schema.execute(query) 23 | assert not result.errors 24 | assert result.data == expected 25 | 26 | 27 | def test_correctly_refetches_rebels(): 28 | initialize() 29 | query = """ 30 | query RebelsRefetchQuery { 31 | node(id: "RmFjdGlvbjox") { 32 | id 33 | ... on Faction { 34 | name 35 | } 36 | } 37 | } 38 | """ 39 | expected = { 40 | "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} 41 | } 42 | result = schema.execute(query) 43 | assert not result.errors 44 | assert result.data == expected 45 | 46 | 47 | def test_correctly_fetches_id_name_empire(): 48 | initialize() 49 | query = """ 50 | query EmpireQuery { 51 | empire { 52 | id 53 | name 54 | } 55 | } 56 | """ 57 | expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} 58 | result = schema.execute(query) 59 | assert not result.errors 60 | assert result.data == expected 61 | 62 | 63 | def test_correctly_refetches_empire(): 64 | initialize() 65 | query = """ 66 | query EmpireRefetchQuery { 67 | node(id: "RmFjdGlvbjoy") { 68 | id 69 | ... on Faction { 70 | name 71 | } 72 | } 73 | } 74 | """ 75 | expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} 76 | result = schema.execute(query) 77 | assert not result.errors 78 | assert result.data == expected 79 | 80 | 81 | def test_correctly_refetches_xwing(): 82 | initialize() 83 | query = """ 84 | query XWingRefetchQuery { 85 | node(id: "U2hpcDox") { 86 | id 87 | ... on Ship { 88 | name 89 | } 90 | } 91 | } 92 | """ 93 | expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}} 94 | result = schema.execute(query) 95 | assert not result.errors 96 | assert result.data == expected 97 | -------------------------------------------------------------------------------- /graphene_django/__init__.py: -------------------------------------------------------------------------------- 1 | from .fields import DjangoConnectionField, DjangoListField 2 | from .types import DjangoObjectType 3 | from .utils import bypass_get_queryset 4 | 5 | __version__ = "3.2.3" 6 | 7 | __all__ = [ 8 | "__version__", 9 | "DjangoObjectType", 10 | "DjangoListField", 11 | "DjangoConnectionField", 12 | "bypass_get_queryset", 13 | ] 14 | -------------------------------------------------------------------------------- /graphene_django/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections.abc import Callable 3 | from pathlib import PurePath 4 | 5 | # For backwards compatibility, we import JSONField to have it available for import via 6 | # this compat module (https://github.com/graphql-python/graphene-django/issues/1428). 7 | # Django's JSONField is available in Django 3.2+ (the minimum version we support) 8 | from django.db.models import Choices, JSONField 9 | 10 | 11 | class MissingType: 12 | def __init__(self, *args, **kwargs): 13 | pass 14 | 15 | 16 | try: 17 | # Postgres fields are only available in Django with psycopg2 installed 18 | # and we cannot have psycopg2 on PyPy 19 | from django.contrib.postgres.fields import ( 20 | ArrayField, 21 | HStoreField, 22 | IntegerRangeField, 23 | RangeField, 24 | ) 25 | except ImportError: 26 | IntegerRangeField, HStoreField, RangeField = (MissingType,) * 3 27 | 28 | # For unit tests we fake ArrayField using JSONFields 29 | if any( 30 | PurePath(sys.argv[0]).match(p) 31 | for p in [ 32 | "**/pytest", 33 | "**/py.test", 34 | "**/pytest/__main__.py", 35 | ] 36 | ): 37 | 38 | class ArrayField(JSONField): 39 | def __init__(self, *args, **kwargs): 40 | if len(args) > 0: 41 | self.base_field = args[0] 42 | super().__init__(**kwargs) 43 | 44 | else: 45 | ArrayField = MissingType 46 | 47 | 48 | try: 49 | from django.utils.choices import normalize_choices 50 | except ImportError: 51 | 52 | def normalize_choices(choices): 53 | if isinstance(choices, type) and issubclass(choices, Choices): 54 | choices = choices.choices 55 | 56 | if isinstance(choices, Callable): 57 | choices = choices() 58 | 59 | # In restframework==3.15.0, choices are not passed 60 | # as OrderedDict anymore, so it's safer to check 61 | # for a dict 62 | if isinstance(choices, dict): 63 | choices = choices.items() 64 | 65 | return choices 66 | -------------------------------------------------------------------------------- /graphene_django/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from graphene_django.settings import graphene_settings as gsettings 4 | 5 | from .registry import reset_global_registry 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def reset_registry_fixture(db): 10 | yield None 11 | reset_global_registry() 12 | 13 | 14 | @pytest.fixture() 15 | def graphene_settings(): 16 | settings = dict(gsettings.__dict__) 17 | yield gsettings 18 | gsettings.__dict__ = settings 19 | -------------------------------------------------------------------------------- /graphene_django/constants.py: -------------------------------------------------------------------------------- 1 | MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors" 2 | -------------------------------------------------------------------------------- /graphene_django/debug/__init__.py: -------------------------------------------------------------------------------- 1 | from .middleware import DjangoDebugMiddleware 2 | from .types import DjangoDebug 3 | 4 | __all__ = ["DjangoDebugMiddleware", "DjangoDebug"] 5 | -------------------------------------------------------------------------------- /graphene_django/debug/exception/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/debug/exception/__init__.py -------------------------------------------------------------------------------- /graphene_django/debug/exception/formating.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from django.utils.encoding import force_str 4 | 5 | from .types import DjangoDebugException 6 | 7 | 8 | def wrap_exception(exception): 9 | return DjangoDebugException( 10 | message=force_str(exception), 11 | exc_type=force_str(type(exception)), 12 | stack="".join( 13 | traceback.format_exception( 14 | exception, value=exception, tb=exception.__traceback__ 15 | ) 16 | ), 17 | ) 18 | -------------------------------------------------------------------------------- /graphene_django/debug/exception/types.py: -------------------------------------------------------------------------------- 1 | from graphene import ObjectType, String 2 | 3 | 4 | class DjangoDebugException(ObjectType): 5 | class Meta: 6 | description = "Represents a single exception raised." 7 | 8 | exc_type = String(required=True, description="The class of the exception") 9 | message = String(required=True, description="The message of the exception") 10 | stack = String(required=True, description="The stack trace") 11 | -------------------------------------------------------------------------------- /graphene_django/debug/middleware.py: -------------------------------------------------------------------------------- 1 | from django.db import connections 2 | 3 | from .exception.formating import wrap_exception 4 | from .sql.tracking import unwrap_cursor, wrap_cursor 5 | from .types import DjangoDebug 6 | 7 | 8 | class DjangoDebugContext: 9 | def __init__(self): 10 | self.debug_result = None 11 | self.results = [] 12 | self.object = DjangoDebug(sql=[], exceptions=[]) 13 | self.enable_instrumentation() 14 | 15 | def get_debug_result(self): 16 | if not self.debug_result: 17 | self.debug_result = self.results 18 | self.results = [] 19 | return self.on_resolve_all_results() 20 | 21 | def on_resolve_error(self, value): 22 | if hasattr(self, "object"): 23 | self.object.exceptions.append(wrap_exception(value)) 24 | return value 25 | 26 | def on_resolve_all_results(self): 27 | if self.results: 28 | self.debug_result = None 29 | return self.get_debug_result() 30 | self.disable_instrumentation() 31 | return self.object 32 | 33 | def add_result(self, result): 34 | if self.debug_result: 35 | self.results.append(result) 36 | 37 | def enable_instrumentation(self): 38 | # This is thread-safe because database connections are thread-local. 39 | for connection in connections.all(): 40 | wrap_cursor(connection, self) 41 | 42 | def disable_instrumentation(self): 43 | for connection in connections.all(): 44 | unwrap_cursor(connection) 45 | 46 | 47 | class DjangoDebugMiddleware: 48 | def resolve(self, next, root, info, **args): 49 | context = info.context 50 | django_debug = getattr(context, "django_debug", None) 51 | if not django_debug: 52 | if context is None: 53 | raise Exception("DjangoDebug cannot be executed in None contexts") 54 | try: 55 | context.django_debug = DjangoDebugContext() 56 | except Exception: 57 | raise Exception( 58 | "DjangoDebug need the context to be writable, context received: {}.".format( 59 | context.__class__.__name__ 60 | ) 61 | ) 62 | if info.schema.get_type("DjangoDebug") == info.return_type: 63 | return context.django_debug.get_debug_result() 64 | try: 65 | result = next(root, info, **args) 66 | except Exception as e: 67 | return context.django_debug.on_resolve_error(e) 68 | context.django_debug.add_result(result) 69 | return result 70 | -------------------------------------------------------------------------------- /graphene_django/debug/sql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/debug/sql/__init__.py -------------------------------------------------------------------------------- /graphene_django/debug/sql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import Boolean, Float, ObjectType, String 2 | 3 | 4 | class DjangoDebugSQL(ObjectType): 5 | class Meta: 6 | description = "Represents a single database query made to a Django managed DB." 7 | 8 | vendor = String( 9 | required=True, 10 | description=( 11 | "The type of database being used (e.g. postrgesql, mysql, sqlite)." 12 | ), 13 | ) 14 | alias = String( 15 | required=True, description="The Django database alias (e.g. 'default')." 16 | ) 17 | sql = String(description="The actual SQL sent to this database.") 18 | duration = Float( 19 | required=True, description="Duration of this database query in seconds." 20 | ) 21 | raw_sql = String( 22 | required=True, description="The raw SQL of this query, without params." 23 | ) 24 | params = String( 25 | required=True, description="JSON encoded database query parameters." 26 | ) 27 | start_time = Float(required=True, description="Start time of this database query.") 28 | stop_time = Float(required=True, description="Stop time of this database query.") 29 | is_slow = Boolean( 30 | required=True, 31 | description="Whether this database query took more than 10 seconds.", 32 | ) 33 | is_select = Boolean( 34 | required=True, description="Whether this database query was a SELECT." 35 | ) 36 | 37 | # Postgres 38 | trans_id = String(description="Postgres transaction ID if available.") 39 | trans_status = String(description="Postgres transaction status if available.") 40 | iso_level = String(description="Postgres isolation level if available.") 41 | encoding = String(description="Postgres connection encoding if available.") 42 | -------------------------------------------------------------------------------- /graphene_django/debug/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/debug/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/debug/types.py: -------------------------------------------------------------------------------- 1 | from graphene import List, ObjectType 2 | 3 | from .exception.types import DjangoDebugException 4 | from .sql.types import DjangoDebugSQL 5 | 6 | 7 | class DjangoDebug(ObjectType): 8 | class Meta: 9 | description = "Debugging information for the current query." 10 | 11 | sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.") 12 | exceptions = List( 13 | DjangoDebugException, description="Raise exceptions for this API query." 14 | ) 15 | -------------------------------------------------------------------------------- /graphene_django/filter/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from ..utils import DJANGO_FILTER_INSTALLED 4 | 5 | if not DJANGO_FILTER_INSTALLED: 6 | warnings.warn( 7 | "Use of django filtering requires the django-filter package " 8 | "be installed. You can do so using `pip install django-filter`", 9 | ImportWarning, 10 | ) 11 | else: 12 | from .fields import DjangoFilterConnectionField 13 | from .filters import ( 14 | ArrayFilter, 15 | GlobalIDFilter, 16 | GlobalIDMultipleChoiceFilter, 17 | ListFilter, 18 | RangeFilter, 19 | TypedFilter, 20 | ) 21 | 22 | __all__ = [ 23 | "DjangoFilterConnectionField", 24 | "GlobalIDFilter", 25 | "GlobalIDMultipleChoiceFilter", 26 | "ArrayFilter", 27 | "ListFilter", 28 | "RangeFilter", 29 | "TypedFilter", 30 | ] 31 | -------------------------------------------------------------------------------- /graphene_django/filter/fields.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import partial 3 | 4 | from django.core.exceptions import ValidationError 5 | 6 | from graphene.types.argument import to_arguments 7 | from graphene.types.enum import EnumType 8 | from graphene.utils.str_converters import to_snake_case 9 | 10 | from ..fields import DjangoConnectionField 11 | from .utils import get_filtering_args_from_filterset, get_filterset_class 12 | 13 | 14 | def convert_enum(data): 15 | """ 16 | Check if the data is a enum option (or potentially nested list of enum option) 17 | and convert it to its value. 18 | 19 | This method is used to pre-process the data for the filters as they can take an 20 | graphene.Enum as argument, but filters (from django_filters) expect a simple value. 21 | """ 22 | if isinstance(data, list): 23 | return [convert_enum(item) for item in data] 24 | if isinstance(type(data), EnumType): 25 | return data.value 26 | else: 27 | return data 28 | 29 | 30 | class DjangoFilterConnectionField(DjangoConnectionField): 31 | def __init__( 32 | self, 33 | type_, 34 | fields=None, 35 | order_by=None, 36 | extra_filter_meta=None, 37 | filterset_class=None, 38 | *args, 39 | **kwargs, 40 | ): 41 | self._fields = fields 42 | self._provided_filterset_class = filterset_class 43 | self._filterset_class = None 44 | self._filtering_args = None 45 | self._extra_filter_meta = extra_filter_meta 46 | self._base_args = None 47 | super().__init__(type_, *args, **kwargs) 48 | 49 | @property 50 | def args(self): 51 | return to_arguments(self._base_args or OrderedDict(), self.filtering_args) 52 | 53 | @args.setter 54 | def args(self, args): 55 | self._base_args = args 56 | 57 | @property 58 | def filterset_class(self): 59 | if not self._filterset_class: 60 | fields = self._fields or self.node_type._meta.filter_fields 61 | meta = {"model": self.model, "fields": fields} 62 | if self._extra_filter_meta: 63 | meta.update(self._extra_filter_meta) 64 | 65 | filterset_class = ( 66 | self._provided_filterset_class or self.node_type._meta.filterset_class 67 | ) 68 | self._filterset_class = get_filterset_class(filterset_class, **meta) 69 | 70 | return self._filterset_class 71 | 72 | @property 73 | def filtering_args(self): 74 | if not self._filtering_args: 75 | self._filtering_args = get_filtering_args_from_filterset( 76 | self.filterset_class, self.node_type 77 | ) 78 | return self._filtering_args 79 | 80 | @classmethod 81 | def resolve_queryset( 82 | cls, connection, iterable, info, args, filtering_args, filterset_class 83 | ): 84 | def filter_kwargs(): 85 | kwargs = {} 86 | for k, v in args.items(): 87 | if k in filtering_args: 88 | if k == "order_by" and v is not None: 89 | v = to_snake_case(v) 90 | kwargs[k] = convert_enum(v) 91 | return kwargs 92 | 93 | qs = super().resolve_queryset(connection, iterable, info, args) 94 | 95 | filterset = filterset_class( 96 | data=filter_kwargs(), queryset=qs, request=info.context 97 | ) 98 | if filterset.is_valid(): 99 | return filterset.qs 100 | raise ValidationError(filterset.form.errors.as_json()) 101 | 102 | def get_queryset_resolver(self): 103 | return partial( 104 | self.resolve_queryset, 105 | filterset_class=self.filterset_class, 106 | filtering_args=self.filtering_args, 107 | ) 108 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from ...utils import DJANGO_FILTER_INSTALLED 4 | 5 | if not DJANGO_FILTER_INSTALLED: 6 | warnings.warn( 7 | "Use of django filtering requires the django-filter package " 8 | "be installed. You can do so using `pip install django-filter`", 9 | ImportWarning, 10 | ) 11 | else: 12 | from .array_filter import ArrayFilter 13 | from .global_id_filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter 14 | from .list_filter import ListFilter 15 | from .range_filter import RangeFilter 16 | from .typed_filter import TypedFilter 17 | 18 | __all__ = [ 19 | "DjangoFilterConnectionField", 20 | "GlobalIDFilter", 21 | "GlobalIDMultipleChoiceFilter", 22 | "ArrayFilter", 23 | "ListFilter", 24 | "RangeFilter", 25 | "TypedFilter", 26 | ] 27 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/array_filter.py: -------------------------------------------------------------------------------- 1 | from django_filters.constants import EMPTY_VALUES 2 | from django_filters.filters import FilterMethod 3 | 4 | from .typed_filter import TypedFilter 5 | 6 | 7 | class ArrayFilterMethod(FilterMethod): 8 | def __call__(self, qs, value): 9 | if value is None: 10 | return qs 11 | return self.method(qs, self.f.field_name, value) 12 | 13 | 14 | class ArrayFilter(TypedFilter): 15 | """ 16 | Filter made for PostgreSQL ArrayField. 17 | """ 18 | 19 | @TypedFilter.method.setter 20 | def method(self, value): 21 | """ 22 | Override method setter so that in case a custom `method` is provided 23 | (see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method), 24 | it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method 25 | of the `FilterMethod` class) and instead use our ArrayFilterMethod that consider empty lists as values. 26 | 27 | Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)` 28 | which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead. 29 | """ 30 | TypedFilter.method.fset(self, value) 31 | if value is not None: 32 | self.filter = ArrayFilterMethod(self) 33 | 34 | def filter(self, qs, value): 35 | """ 36 | Override the default filter class to check first whether the list is 37 | empty or not. 38 | This needs to be done as in this case we expect to get the filter applied with 39 | an empty list since it's a valid value but django_filter consider an empty list 40 | to be an empty input value (see `EMPTY_VALUES`) meaning that 41 | the filter does not need to be applied (hence returning the original 42 | queryset). 43 | """ 44 | if value in EMPTY_VALUES and value != []: 45 | return qs 46 | if self.distinct: 47 | qs = qs.distinct() 48 | lookup = f"{self.field_name}__{self.lookup_expr}" 49 | qs = self.get_method(qs)(**{lookup: value}) 50 | return qs 51 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/global_id_filter.py: -------------------------------------------------------------------------------- 1 | from django_filters import Filter, MultipleChoiceFilter 2 | from graphql_relay.node.node import from_global_id 3 | 4 | from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField 5 | 6 | 7 | class GlobalIDFilter(Filter): 8 | """ 9 | Filter for Relay global ID. 10 | """ 11 | 12 | field_class = GlobalIDFormField 13 | 14 | def filter(self, qs, value): 15 | """Convert the filter value to a primary key before filtering""" 16 | _id = None 17 | if value is not None: 18 | _, _id = from_global_id(value) 19 | return super().filter(qs, _id) 20 | 21 | 22 | class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): 23 | field_class = GlobalIDMultipleChoiceField 24 | 25 | def filter(self, qs, value): 26 | gids = [from_global_id(v)[1] for v in value] 27 | return super().filter(qs, gids) 28 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/list_filter.py: -------------------------------------------------------------------------------- 1 | from django_filters.filters import FilterMethod 2 | 3 | from .typed_filter import TypedFilter 4 | 5 | 6 | class ListFilterMethod(FilterMethod): 7 | def __call__(self, qs, value): 8 | if value is None: 9 | return qs 10 | return self.method(qs, self.f.field_name, value) 11 | 12 | 13 | class ListFilter(TypedFilter): 14 | """ 15 | Filter that takes a list of value as input. 16 | It is for example used for `__in` filters. 17 | """ 18 | 19 | @TypedFilter.method.setter 20 | def method(self, value): 21 | """ 22 | Override method setter so that in case a custom `method` is provided 23 | (see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method), 24 | it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method 25 | of the `FilterMethod` class) and instead use our ListFilterMethod that consider empty lists as values. 26 | 27 | Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)` 28 | which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead. 29 | """ 30 | TypedFilter.method.fset(self, value) 31 | if value is not None: 32 | self.filter = ListFilterMethod(self) 33 | 34 | def filter(self, qs, value): 35 | """ 36 | Override the default filter class to check first whether the list is 37 | empty or not. 38 | This needs to be done as in this case we expect to get an empty output 39 | (if not an exclude filter) but django_filter consider an empty list 40 | to be an empty input value (see `EMPTY_VALUES`) meaning that 41 | the filter does not need to be applied (hence returning the original 42 | queryset). 43 | """ 44 | if value is not None and len(value) == 0: 45 | if self.exclude: 46 | return qs 47 | else: 48 | return qs.none() 49 | else: 50 | return super().filter(qs, value) 51 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/range_filter.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.forms import Field 3 | 4 | from .typed_filter import TypedFilter 5 | 6 | 7 | def validate_range(value): 8 | """ 9 | Validator for range filter input: the list of value must be of length 2. 10 | Note that validators are only run if the value is not empty. 11 | """ 12 | if len(value) != 2: 13 | raise ValidationError( 14 | "Invalid range specified: it needs to contain 2 values.", code="invalid" 15 | ) 16 | 17 | 18 | class RangeField(Field): 19 | default_validators = [validate_range] 20 | empty_values = [None] 21 | 22 | 23 | class RangeFilter(TypedFilter): 24 | field_class = RangeField 25 | -------------------------------------------------------------------------------- /graphene_django/filter/filters/typed_filter.py: -------------------------------------------------------------------------------- 1 | from django_filters import Filter 2 | 3 | from graphene.types.utils import get_type 4 | 5 | 6 | class TypedFilter(Filter): 7 | """ 8 | Filter class for which the input GraphQL type can explicitly be provided. 9 | If it is not provided, when building the schema, it will try to guess 10 | it from the field. 11 | """ 12 | 13 | def __init__(self, input_type=None, *args, **kwargs): 14 | self._input_type = input_type 15 | super().__init__(*args, **kwargs) 16 | 17 | @property 18 | def input_type(self): 19 | input_type = get_type(self._input_type) 20 | if input_type is not None: 21 | if not callable(getattr(input_type, "get_type", None)): 22 | raise ValueError( 23 | "Wrong `input_type` for {}: it only accepts graphene types, got {}".format( 24 | self.__class__.__name__, input_type 25 | ) 26 | ) 27 | return input_type 28 | -------------------------------------------------------------------------------- /graphene_django/filter/filterset.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from django.db import models 4 | from django_filters.filterset import ( 5 | FILTER_FOR_DBFIELD_DEFAULTS, 6 | BaseFilterSet, 7 | FilterSet, 8 | ) 9 | 10 | from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter 11 | 12 | GRAPHENE_FILTER_SET_OVERRIDES = { 13 | models.AutoField: {"filter_class": GlobalIDFilter}, 14 | models.OneToOneField: {"filter_class": GlobalIDFilter}, 15 | models.ForeignKey: {"filter_class": GlobalIDFilter}, 16 | models.ManyToManyField: {"filter_class": GlobalIDMultipleChoiceFilter}, 17 | models.ManyToOneRel: {"filter_class": GlobalIDMultipleChoiceFilter}, 18 | models.ManyToManyRel: {"filter_class": GlobalIDMultipleChoiceFilter}, 19 | } 20 | 21 | 22 | class GrapheneFilterSetMixin(BaseFilterSet): 23 | """A django_filters.filterset.BaseFilterSet with default filter overrides 24 | to handle global IDs""" 25 | 26 | FILTER_DEFAULTS = dict( 27 | itertools.chain( 28 | FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items() 29 | ) 30 | ) 31 | 32 | 33 | def setup_filterset(filterset_class): 34 | """Wrap a provided filterset in Graphene-specific functionality""" 35 | return type( 36 | f"Graphene{filterset_class.__name__}", 37 | (filterset_class, GrapheneFilterSetMixin), 38 | {}, 39 | ) 40 | 41 | 42 | def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): 43 | """Create a filterset for the given model using the provided meta data""" 44 | meta.update({"model": model}) 45 | meta_class = type("Meta", (object,), meta) 46 | filterset = type( 47 | str("%sFilterSet" % model._meta.object_name), 48 | (filterset_base_class, GrapheneFilterSetMixin), 49 | {"Meta": meta_class}, 50 | ) 51 | return filterset 52 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/filter/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/filter/tests/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django_filters import OrderingFilter 3 | 4 | from graphene_django.tests.models import Article, Pet, Reporter 5 | 6 | 7 | class ArticleFilter(django_filters.FilterSet): 8 | class Meta: 9 | model = Article 10 | fields = { 11 | "headline": ["exact", "icontains"], 12 | "pub_date": ["gt", "lt", "exact"], 13 | "reporter": ["exact", "in"], 14 | } 15 | 16 | order_by = OrderingFilter(fields=("pub_date",)) 17 | 18 | 19 | class ReporterFilter(django_filters.FilterSet): 20 | class Meta: 21 | model = Reporter 22 | fields = ["first_name", "last_name", "email", "pets"] 23 | 24 | order_by = OrderingFilter(fields=("first_name",)) 25 | 26 | 27 | class PetFilter(django_filters.FilterSet): 28 | class Meta: 29 | model = Pet 30 | fields = ["name"] 31 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_array_field_contains_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ...compat import ArrayField, MissingType 4 | 5 | 6 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 7 | def test_array_field_contains_multiple(schema): 8 | """ 9 | Test contains filter on a array field of string. 10 | """ 11 | 12 | query = """ 13 | query { 14 | events (tags_Contains: ["concert", "music"]) { 15 | edges { 16 | node { 17 | name 18 | } 19 | } 20 | } 21 | } 22 | """ 23 | result = schema.execute(query) 24 | assert not result.errors 25 | assert result.data["events"]["edges"] == [ 26 | {"node": {"name": "Live Show"}}, 27 | ] 28 | 29 | 30 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 31 | def test_array_field_contains_one(schema): 32 | """ 33 | Test contains filter on a array field of string. 34 | """ 35 | 36 | query = """ 37 | query { 38 | events (tags_Contains: ["music"]) { 39 | edges { 40 | node { 41 | name 42 | } 43 | } 44 | } 45 | } 46 | """ 47 | result = schema.execute(query) 48 | assert not result.errors 49 | assert result.data["events"]["edges"] == [ 50 | {"node": {"name": "Live Show"}}, 51 | {"node": {"name": "Musical"}}, 52 | ] 53 | 54 | 55 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 56 | def test_array_field_contains_empty_list(schema): 57 | """ 58 | Test contains filter on a array field of string. 59 | """ 60 | 61 | query = """ 62 | query { 63 | events (tags_Contains: []) { 64 | edges { 65 | node { 66 | name 67 | } 68 | } 69 | } 70 | } 71 | """ 72 | result = schema.execute(query) 73 | assert not result.errors 74 | assert result.data["events"]["edges"] == [ 75 | {"node": {"name": "Live Show"}}, 76 | {"node": {"name": "Musical"}}, 77 | {"node": {"name": "Ballet"}}, 78 | {"node": {"name": "Speech"}}, 79 | ] 80 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_array_field_custom_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ...compat import ArrayField, MissingType 4 | 5 | 6 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 7 | def test_array_field_len_filter(schema): 8 | query = """ 9 | query { 10 | events (tags_Len: 2) { 11 | edges { 12 | node { 13 | name 14 | } 15 | } 16 | } 17 | } 18 | """ 19 | result = schema.execute(query) 20 | assert not result.errors 21 | assert result.data["events"]["edges"] == [ 22 | {"node": {"name": "Musical"}}, 23 | {"node": {"name": "Ballet"}}, 24 | ] 25 | 26 | query = """ 27 | query { 28 | events (tags_Len: 0) { 29 | edges { 30 | node { 31 | name 32 | } 33 | } 34 | } 35 | } 36 | """ 37 | result = schema.execute(query) 38 | assert not result.errors 39 | assert result.data["events"]["edges"] == [ 40 | {"node": {"name": "Speech"}}, 41 | ] 42 | 43 | query = """ 44 | query { 45 | events (tags_Len: 10) { 46 | edges { 47 | node { 48 | name 49 | } 50 | } 51 | } 52 | } 53 | """ 54 | result = schema.execute(query) 55 | assert not result.errors 56 | assert result.data["events"]["edges"] == [] 57 | 58 | query = """ 59 | query { 60 | events (tags_Len: "2") { 61 | edges { 62 | node { 63 | name 64 | } 65 | } 66 | } 67 | } 68 | """ 69 | result = schema.execute(query) 70 | assert len(result.errors) == 1 71 | assert result.errors[0].message == 'Int cannot represent non-integer value: "2"' 72 | 73 | query = """ 74 | query { 75 | events (tags_Len: True) { 76 | edges { 77 | node { 78 | name 79 | } 80 | } 81 | } 82 | } 83 | """ 84 | result = schema.execute(query) 85 | assert len(result.errors) == 1 86 | assert result.errors[0].message == "Int cannot represent non-integer value: True" 87 | 88 | 89 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 90 | def test_array_field_custom_filter(schema): 91 | query = """ 92 | query { 93 | events (tags_Len_In: 2) { 94 | edges { 95 | node { 96 | name 97 | } 98 | } 99 | } 100 | } 101 | """ 102 | result = schema.execute(query) 103 | assert not result.errors 104 | assert result.data["events"]["edges"] == [ 105 | {"node": {"name": "Ballet"}}, 106 | {"node": {"name": "Musical"}}, 107 | ] 108 | 109 | query = """ 110 | query { 111 | events (tags_Len_In: [0, 2]) { 112 | edges { 113 | node { 114 | name 115 | } 116 | } 117 | } 118 | } 119 | """ 120 | result = schema.execute(query) 121 | assert not result.errors 122 | assert result.data["events"]["edges"] == [ 123 | {"node": {"name": "Ballet"}}, 124 | {"node": {"name": "Musical"}}, 125 | {"node": {"name": "Speech"}}, 126 | ] 127 | 128 | query = """ 129 | query { 130 | events (tags_Len_In: [10]) { 131 | edges { 132 | node { 133 | name 134 | } 135 | } 136 | } 137 | } 138 | """ 139 | result = schema.execute(query) 140 | assert not result.errors 141 | assert result.data["events"]["edges"] == [] 142 | 143 | query = """ 144 | query { 145 | events (tags_Len_In: []) { 146 | edges { 147 | node { 148 | name 149 | } 150 | } 151 | } 152 | } 153 | """ 154 | result = schema.execute(query) 155 | assert not result.errors 156 | assert result.data["events"]["edges"] == [] 157 | 158 | query = """ 159 | query { 160 | events (tags_Len_In: "12") { 161 | edges { 162 | node { 163 | name 164 | } 165 | } 166 | } 167 | } 168 | """ 169 | result = schema.execute(query) 170 | assert len(result.errors) == 1 171 | assert result.errors[0].message == 'Int cannot represent non-integer value: "12"' 172 | 173 | query = """ 174 | query { 175 | events (tags_Len_In: True) { 176 | edges { 177 | node { 178 | name 179 | } 180 | } 181 | } 182 | } 183 | """ 184 | result = schema.execute(query) 185 | assert len(result.errors) == 1 186 | assert result.errors[0].message == "Int cannot represent non-integer value: True" 187 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_array_field_exact_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ...compat import ArrayField, MissingType 4 | 5 | 6 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 7 | def test_array_field_exact_no_match(schema): 8 | """ 9 | Test exact filter on a array field of string. 10 | """ 11 | 12 | query = """ 13 | query { 14 | events (tags: ["concert", "music"]) { 15 | edges { 16 | node { 17 | name 18 | } 19 | } 20 | } 21 | } 22 | """ 23 | result = schema.execute(query) 24 | assert not result.errors 25 | assert result.data["events"]["edges"] == [] 26 | 27 | 28 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 29 | def test_array_field_exact_match(schema): 30 | """ 31 | Test exact filter on a array field of string. 32 | """ 33 | 34 | query = """ 35 | query { 36 | events (tags: ["movie", "music"]) { 37 | edges { 38 | node { 39 | name 40 | } 41 | } 42 | } 43 | } 44 | """ 45 | result = schema.execute(query) 46 | assert not result.errors 47 | assert result.data["events"]["edges"] == [ 48 | {"node": {"name": "Musical"}}, 49 | ] 50 | 51 | 52 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 53 | def test_array_field_exact_empty_list(schema): 54 | """ 55 | Test exact filter on a array field of string. 56 | """ 57 | 58 | query = """ 59 | query { 60 | events (tags: []) { 61 | edges { 62 | node { 63 | name 64 | } 65 | } 66 | } 67 | } 68 | """ 69 | result = schema.execute(query) 70 | assert not result.errors 71 | assert result.data["events"]["edges"] == [ 72 | {"node": {"name": "Speech"}}, 73 | ] 74 | 75 | 76 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 77 | def test_array_field_filter_schema_type(schema): 78 | """ 79 | Check that the type in the filter is an array field like on the object type. 80 | """ 81 | schema_str = str(schema) 82 | 83 | assert ( 84 | '''type EventType implements Node { 85 | """The ID of the object""" 86 | id: ID! 87 | name: String! 88 | tags: [String!]! 89 | tagIds: [Int!]! 90 | randomField: [Boolean!]! 91 | }''' 92 | in schema_str 93 | ) 94 | 95 | filters = { 96 | "offset": "Int", 97 | "before": "String", 98 | "after": "String", 99 | "first": "Int", 100 | "last": "Int", 101 | "name": "String", 102 | "name_Contains": "String", 103 | "tags_Contains": "[String!]", 104 | "tags_Overlap": "[String!]", 105 | "tags": "[String!]", 106 | "tags_Len": "Int", 107 | "tags_Len_In": "[Int]", 108 | "tagsIds_Contains": "[Int!]", 109 | "tagsIds_Overlap": "[Int!]", 110 | "tagsIds": "[Int!]", 111 | "randomField_Contains": "[Boolean!]", 112 | "randomField_Overlap": "[Boolean!]", 113 | "randomField": "[Boolean!]", 114 | } 115 | filters_str = ", ".join( 116 | [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] 117 | ) 118 | assert ( 119 | f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str 120 | ) 121 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_array_field_overlap_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ...compat import ArrayField, MissingType 4 | 5 | 6 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 7 | def test_array_field_overlap_multiple(schema): 8 | """ 9 | Test overlap filter on a array field of string. 10 | """ 11 | 12 | query = """ 13 | query { 14 | events (tags_Overlap: ["concert", "music"]) { 15 | edges { 16 | node { 17 | name 18 | } 19 | } 20 | } 21 | } 22 | """ 23 | result = schema.execute(query) 24 | assert not result.errors 25 | assert result.data["events"]["edges"] == [ 26 | {"node": {"name": "Live Show"}}, 27 | {"node": {"name": "Musical"}}, 28 | {"node": {"name": "Ballet"}}, 29 | ] 30 | 31 | 32 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 33 | def test_array_field_overlap_one(schema): 34 | """ 35 | Test overlap filter on a array field of string. 36 | """ 37 | 38 | query = """ 39 | query { 40 | events (tags_Overlap: ["music"]) { 41 | edges { 42 | node { 43 | name 44 | } 45 | } 46 | } 47 | } 48 | """ 49 | result = schema.execute(query) 50 | assert not result.errors 51 | assert result.data["events"]["edges"] == [ 52 | {"node": {"name": "Live Show"}}, 53 | {"node": {"name": "Musical"}}, 54 | ] 55 | 56 | 57 | @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") 58 | def test_array_field_overlap_empty_list(schema): 59 | """ 60 | Test overlap filter on a array field of string. 61 | """ 62 | 63 | query = """ 64 | query { 65 | events (tags_Overlap: []) { 66 | edges { 67 | node { 68 | name 69 | } 70 | } 71 | } 72 | } 73 | """ 74 | result = schema.execute(query) 75 | assert not result.errors 76 | assert result.data["events"]["edges"] == [] 77 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_enum_filtering.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import graphene 4 | from graphene.relay import Node 5 | from graphene_django import DjangoConnectionField, DjangoObjectType 6 | from graphene_django.tests.models import Article, Reporter 7 | from graphene_django.utils import DJANGO_FILTER_INSTALLED 8 | 9 | pytestmark = [] 10 | 11 | if DJANGO_FILTER_INSTALLED: 12 | from graphene_django.filter import DjangoFilterConnectionField 13 | else: 14 | pytestmark.append( 15 | pytest.mark.skipif( 16 | True, reason="django_filters not installed or not compatible" 17 | ) 18 | ) 19 | 20 | 21 | @pytest.fixture 22 | def schema(): 23 | class ReporterType(DjangoObjectType): 24 | class Meta: 25 | model = Reporter 26 | interfaces = (Node,) 27 | fields = "__all__" 28 | 29 | class ArticleType(DjangoObjectType): 30 | class Meta: 31 | model = Article 32 | interfaces = (Node,) 33 | fields = "__all__" 34 | filter_fields = { 35 | "lang": ["exact", "in"], 36 | "reporter__a_choice": ["exact", "in"], 37 | } 38 | 39 | class Query(graphene.ObjectType): 40 | all_reporters = DjangoConnectionField(ReporterType) 41 | all_articles = DjangoFilterConnectionField(ArticleType) 42 | 43 | schema = graphene.Schema(query=Query) 44 | return schema 45 | 46 | 47 | @pytest.fixture 48 | def reporter_article_data(): 49 | john = Reporter.objects.create( 50 | first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 51 | ) 52 | jane = Reporter.objects.create( 53 | first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2 54 | ) 55 | Article.objects.create( 56 | headline="Article Node 1", reporter=john, editor=john, lang="es" 57 | ) 58 | Article.objects.create( 59 | headline="Article Node 2", reporter=john, editor=john, lang="en" 60 | ) 61 | Article.objects.create( 62 | headline="Article Node 3", reporter=jane, editor=jane, lang="en" 63 | ) 64 | 65 | 66 | def test_filter_enum_on_connection(schema, reporter_article_data): 67 | """ 68 | Check that we can filter with enums on a connection. 69 | """ 70 | query = """ 71 | query { 72 | allArticles(lang: ES) { 73 | edges { 74 | node { 75 | headline 76 | } 77 | } 78 | } 79 | } 80 | """ 81 | 82 | expected = { 83 | "allArticles": { 84 | "edges": [ 85 | {"node": {"headline": "Article Node 1"}}, 86 | ] 87 | } 88 | } 89 | 90 | result = schema.execute(query) 91 | assert not result.errors 92 | assert result.data == expected 93 | 94 | 95 | def test_filter_on_foreign_key_enum_field(schema, reporter_article_data): 96 | """ 97 | Check that we can filter with enums on a field from a foreign key. 98 | """ 99 | query = """ 100 | query { 101 | allArticles(reporter_AChoice: A_1) { 102 | edges { 103 | node { 104 | headline 105 | } 106 | } 107 | } 108 | } 109 | """ 110 | 111 | expected = { 112 | "allArticles": { 113 | "edges": [ 114 | {"node": {"headline": "Article Node 1"}}, 115 | {"node": {"headline": "Article Node 2"}}, 116 | ] 117 | } 118 | } 119 | 120 | result = schema.execute(query) 121 | assert not result.errors 122 | assert result.data == expected 123 | 124 | 125 | def test_filter_enum_field_schema_type(schema): 126 | """ 127 | Check that the type in the filter is an enum like on the object type. 128 | """ 129 | schema_str = str(schema) 130 | 131 | assert ( 132 | '''type ArticleType implements Node { 133 | """The ID of the object""" 134 | id: ID! 135 | headline: String! 136 | pubDate: Date! 137 | pubDateTime: DateTime! 138 | reporter: ReporterType! 139 | editor: ReporterType! 140 | 141 | """Language""" 142 | lang: TestsArticleLangChoices! 143 | importance: TestsArticleImportanceChoices 144 | }''' 145 | in schema_str 146 | ) 147 | 148 | filters = { 149 | "offset": "Int", 150 | "before": "String", 151 | "after": "String", 152 | "first": "Int", 153 | "last": "Int", 154 | "lang": "TestsArticleLangChoices", 155 | "lang_In": "[TestsArticleLangChoices]", 156 | "reporter_AChoice": "TestsReporterAChoiceChoices", 157 | "reporter_AChoice_In": "[TestsReporterAChoiceChoices]", 158 | } 159 | filters_str = ", ".join( 160 | [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] 161 | ) 162 | assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str 163 | -------------------------------------------------------------------------------- /graphene_django/filter/tests/test_range_filter.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from graphene import ObjectType, Schema 6 | from graphene.relay import Node 7 | from graphene_django import DjangoObjectType 8 | from graphene_django.tests.models import Pet 9 | from graphene_django.utils import DJANGO_FILTER_INSTALLED 10 | 11 | pytestmark = [] 12 | 13 | if DJANGO_FILTER_INSTALLED: 14 | from graphene_django.filter import DjangoFilterConnectionField 15 | else: 16 | pytestmark.append( 17 | pytest.mark.skipif( 18 | True, reason="django_filters not installed or not compatible" 19 | ) 20 | ) 21 | 22 | 23 | class PetNode(DjangoObjectType): 24 | class Meta: 25 | model = Pet 26 | interfaces = (Node,) 27 | fields = "__all__" 28 | filter_fields = { 29 | "name": ["exact", "in"], 30 | "age": ["exact", "in", "range"], 31 | } 32 | 33 | 34 | class Query(ObjectType): 35 | pets = DjangoFilterConnectionField(PetNode) 36 | 37 | 38 | def test_int_range_filter(): 39 | """ 40 | Test range filter on an integer field. 41 | """ 42 | Pet.objects.create(name="Brutus", age=12) 43 | Pet.objects.create(name="Mimi", age=8) 44 | Pet.objects.create(name="Jojo, the rabbit", age=3) 45 | Pet.objects.create(name="Picotin", age=5) 46 | 47 | schema = Schema(query=Query) 48 | 49 | query = """ 50 | query { 51 | pets (age_Range: [4, 9]) { 52 | edges { 53 | node { 54 | name 55 | } 56 | } 57 | } 58 | } 59 | """ 60 | result = schema.execute(query) 61 | assert not result.errors 62 | assert result.data["pets"]["edges"] == [ 63 | {"node": {"name": "Mimi"}}, 64 | {"node": {"name": "Picotin"}}, 65 | ] 66 | 67 | 68 | def test_range_filter_with_invalid_input(): 69 | """ 70 | Test range filter used with invalid inputs raise an error. 71 | """ 72 | Pet.objects.create(name="Brutus", age=12) 73 | Pet.objects.create(name="Mimi", age=8) 74 | Pet.objects.create(name="Jojo, the rabbit", age=3) 75 | Pet.objects.create(name="Picotin", age=5) 76 | 77 | schema = Schema(query=Query) 78 | 79 | query = """ 80 | query ($rangeValue: [Int]) { 81 | pets (age_Range: $rangeValue) { 82 | edges { 83 | node { 84 | name 85 | } 86 | } 87 | } 88 | } 89 | """ 90 | expected_error = json.dumps( 91 | { 92 | "age__range": [ 93 | { 94 | "message": "Invalid range specified: it needs to contain 2 values.", 95 | "code": "invalid", 96 | } 97 | ] 98 | } 99 | ) 100 | 101 | # Empty list 102 | result = schema.execute(query, variables={"rangeValue": []}) 103 | assert len(result.errors) == 1 104 | assert result.errors[0].message == expected_error 105 | 106 | # Only one item in the list 107 | result = schema.execute(query, variables={"rangeValue": [1]}) 108 | assert len(result.errors) == 1 109 | assert result.errors[0].message == expected_error 110 | 111 | # More than 2 items in the list 112 | result = schema.execute(query, variables={"rangeValue": [1, 2, 3]}) 113 | assert len(result.errors) == 1 114 | assert result.errors[0].message == expected_error 115 | -------------------------------------------------------------------------------- /graphene_django/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa 2 | -------------------------------------------------------------------------------- /graphene_django/forms/converter.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | 3 | from django import forms 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from graphene import ( 7 | ID, 8 | UUID, 9 | Boolean, 10 | Date, 11 | DateTime, 12 | Decimal, 13 | Float, 14 | Int, 15 | List, 16 | String, 17 | Time, 18 | ) 19 | 20 | from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField 21 | 22 | 23 | def get_form_field_description(field): 24 | return str(field.help_text) if field.help_text else None 25 | 26 | 27 | @singledispatch 28 | def convert_form_field(field): 29 | raise ImproperlyConfigured( 30 | f"Don't know how to convert the Django form field {field} ({field.__class__}) " 31 | "to Graphene type" 32 | ) 33 | 34 | 35 | @convert_form_field.register(forms.fields.BaseTemporalField) 36 | @convert_form_field.register(forms.CharField) 37 | @convert_form_field.register(forms.EmailField) 38 | @convert_form_field.register(forms.SlugField) 39 | @convert_form_field.register(forms.URLField) 40 | @convert_form_field.register(forms.ChoiceField) 41 | @convert_form_field.register(forms.RegexField) 42 | @convert_form_field.register(forms.Field) 43 | def convert_form_field_to_string(field): 44 | return String( 45 | description=get_form_field_description(field), required=field.required 46 | ) 47 | 48 | 49 | @convert_form_field.register(forms.UUIDField) 50 | def convert_form_field_to_uuid(field): 51 | return UUID(description=get_form_field_description(field), required=field.required) 52 | 53 | 54 | @convert_form_field.register(forms.IntegerField) 55 | @convert_form_field.register(forms.NumberInput) 56 | def convert_form_field_to_int(field): 57 | return Int(description=get_form_field_description(field), required=field.required) 58 | 59 | 60 | @convert_form_field.register(forms.BooleanField) 61 | def convert_form_field_to_boolean(field): 62 | return Boolean( 63 | description=get_form_field_description(field), required=field.required 64 | ) 65 | 66 | 67 | @convert_form_field.register(forms.NullBooleanField) 68 | def convert_form_field_to_nullboolean(field): 69 | return Boolean(description=get_form_field_description(field)) 70 | 71 | 72 | @convert_form_field.register(forms.FloatField) 73 | def convert_form_field_to_float(field): 74 | return Float(description=get_form_field_description(field), required=field.required) 75 | 76 | 77 | @convert_form_field.register(forms.DecimalField) 78 | def convert_form_field_to_decimal(field): 79 | return Decimal( 80 | description=get_form_field_description(field), required=field.required 81 | ) 82 | 83 | 84 | @convert_form_field.register(forms.MultipleChoiceField) 85 | def convert_form_field_to_string_list(field): 86 | return List( 87 | String, description=get_form_field_description(field), required=field.required 88 | ) 89 | 90 | 91 | @convert_form_field.register(forms.ModelMultipleChoiceField) 92 | @convert_form_field.register(GlobalIDMultipleChoiceField) 93 | def convert_form_field_to_id_list(field): 94 | return List(ID, required=field.required) 95 | 96 | 97 | @convert_form_field.register(forms.DateField) 98 | def convert_form_field_to_date(field): 99 | return Date(description=get_form_field_description(field), required=field.required) 100 | 101 | 102 | @convert_form_field.register(forms.DateTimeField) 103 | def convert_form_field_to_datetime(field): 104 | return DateTime( 105 | description=get_form_field_description(field), required=field.required 106 | ) 107 | 108 | 109 | @convert_form_field.register(forms.TimeField) 110 | def convert_form_field_to_time(field): 111 | return Time(description=get_form_field_description(field), required=field.required) 112 | 113 | 114 | @convert_form_field.register(forms.ModelChoiceField) 115 | @convert_form_field.register(GlobalIDFormField) 116 | def convert_form_field_to_id(field): 117 | return ID(required=field.required) 118 | -------------------------------------------------------------------------------- /graphene_django/forms/forms.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.forms import CharField, Field, MultipleChoiceField 5 | from django.utils.translation import gettext_lazy as _ 6 | from graphql_relay import from_global_id 7 | 8 | 9 | class GlobalIDFormField(Field): 10 | default_error_messages = {"invalid": _("Invalid ID specified.")} 11 | 12 | def clean(self, value): 13 | if not value and not self.required: 14 | return None 15 | 16 | try: 17 | _type, _id = from_global_id(value) 18 | except (TypeError, ValueError, UnicodeDecodeError, binascii.Error): 19 | raise ValidationError(self.error_messages["invalid"]) 20 | 21 | try: 22 | CharField().clean(_id) 23 | CharField().clean(_type) 24 | except ValidationError: 25 | raise ValidationError(self.error_messages["invalid"]) 26 | 27 | return value 28 | 29 | 30 | class GlobalIDMultipleChoiceField(MultipleChoiceField): 31 | default_error_messages = { 32 | "invalid_choice": _("One of the specified IDs was invalid (%(value)s)."), 33 | "invalid_list": _("Enter a list of values."), 34 | } 35 | 36 | def valid_value(self, value): 37 | # Clean will raise a validation error if there is a problem 38 | GlobalIDFormField().clean(value) 39 | return True 40 | -------------------------------------------------------------------------------- /graphene_django/forms/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/forms/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/forms/tests/test_converter.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as DJANGO_VERSION, forms 2 | from pytest import raises 3 | 4 | from graphene import ( 5 | ID, 6 | UUID, 7 | Boolean, 8 | Date, 9 | DateTime, 10 | Decimal, 11 | Float, 12 | Int, 13 | List, 14 | NonNull, 15 | String, 16 | Time, 17 | ) 18 | 19 | from ..converter import convert_form_field 20 | 21 | 22 | def assert_conversion(django_field, graphene_field, *args, **kwargs): 23 | # Arrange 24 | help_text = kwargs.setdefault("help_text", "Custom Help Text") 25 | field = django_field(*args, **kwargs) 26 | # Act 27 | graphene_type = convert_form_field(field) 28 | # Assert 29 | assert isinstance(graphene_type, graphene_field) 30 | field = graphene_type.Field() 31 | assert field.description == help_text 32 | return field 33 | 34 | 35 | def test_should_unknown_django_field_raise_exception(): 36 | with raises(Exception) as excinfo: 37 | convert_form_field(None) 38 | assert "Don't know how to convert the Django form field" in str(excinfo.value) 39 | 40 | 41 | def test_should_date_convert_date(): 42 | assert_conversion(forms.DateField, Date) 43 | 44 | 45 | def test_should_time_convert_time(): 46 | assert_conversion(forms.TimeField, Time) 47 | 48 | 49 | def test_should_date_time_convert_date_time(): 50 | assert_conversion(forms.DateTimeField, DateTime) 51 | 52 | 53 | def test_should_char_convert_string(): 54 | assert_conversion(forms.CharField, String) 55 | 56 | 57 | def test_should_email_convert_string(): 58 | assert_conversion(forms.EmailField, String) 59 | 60 | 61 | def test_should_slug_convert_string(): 62 | assert_conversion(forms.SlugField, String) 63 | 64 | 65 | def test_should_url_convert_string(): 66 | kwargs = {} 67 | if DJANGO_VERSION >= (5, 0): 68 | # silence RemovedInDjango60Warning 69 | kwargs["assume_scheme"] = "https" 70 | 71 | assert_conversion(forms.URLField, String, **kwargs) 72 | 73 | 74 | def test_should_choice_convert_string(): 75 | assert_conversion(forms.ChoiceField, String) 76 | 77 | 78 | def test_should_base_field_convert_string(): 79 | assert_conversion(forms.Field, String) 80 | 81 | 82 | def test_should_regex_convert_string(): 83 | assert_conversion(forms.RegexField, String, "[0-9]+") 84 | 85 | 86 | def test_should_uuid_convert_string(): 87 | assert_conversion(forms.UUIDField, UUID) 88 | 89 | 90 | def test_should_integer_convert_int(): 91 | assert_conversion(forms.IntegerField, Int) 92 | 93 | 94 | def test_should_boolean_convert_boolean(): 95 | field = assert_conversion(forms.BooleanField, Boolean) 96 | assert isinstance(field.type, NonNull) 97 | 98 | 99 | def test_should_nullboolean_convert_boolean(): 100 | field = assert_conversion(forms.NullBooleanField, Boolean) 101 | assert not isinstance(field.type, NonNull) 102 | 103 | 104 | def test_should_float_convert_float(): 105 | assert_conversion(forms.FloatField, Float) 106 | 107 | 108 | def test_should_decimal_convert_decimal(): 109 | assert_conversion(forms.DecimalField, Decimal) 110 | 111 | 112 | def test_should_multiple_choice_convert_list(): 113 | field = forms.MultipleChoiceField() 114 | graphene_type = convert_form_field(field) 115 | assert isinstance(graphene_type, List) 116 | assert graphene_type.of_type == String 117 | 118 | 119 | def test_should_model_multiple_choice_convert_connectionorlist(): 120 | field = forms.ModelMultipleChoiceField(queryset=None) 121 | graphene_type = convert_form_field(field) 122 | assert isinstance(graphene_type, List) 123 | assert graphene_type.of_type == ID 124 | 125 | 126 | def test_should_manytoone_convert_connectionorlist(): 127 | field = forms.ModelChoiceField(queryset=None) 128 | graphene_type = convert_form_field(field) 129 | assert isinstance(graphene_type, ID) 130 | -------------------------------------------------------------------------------- /graphene_django/forms/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import ID 3 | from graphene.types.inputobjecttype import InputObjectType 4 | from graphene.utils.str_converters import to_camel_case 5 | 6 | from ..converter import EnumValueField 7 | from ..types import ErrorType # noqa Import ErrorType for backwards compatibility 8 | from .mutation import fields_for_form 9 | 10 | 11 | class DjangoFormInputObjectType(InputObjectType): 12 | @classmethod 13 | def __init_subclass_with_meta__( 14 | cls, 15 | container=None, 16 | _meta=None, 17 | only_fields=(), 18 | exclude_fields=(), 19 | form_class=None, 20 | object_type=None, 21 | add_id_field_name=None, 22 | add_id_field_type=None, 23 | **options, 24 | ): 25 | """Retrieve fields from django form (Meta.form_class). Received 26 | fields are set to cls (they will be converted to input fields 27 | by InputObjectType). Type of fields with choices (converted 28 | to enum) is set to custom scalar type (using Meta.object_type) 29 | to dynamically convert enum values back. 30 | 31 | class MyDjangoFormInput(DjangoFormInputObjectType): 32 | # any other fields can be placed here and other inputobjectforms as well 33 | 34 | class Meta: 35 | form_class = MyDjangoModelForm 36 | object_type = MyModelType 37 | 38 | class SomeMutation(graphene.Mutation): 39 | class Arguments: 40 | data = MyDjangoFormInput(required=True) 41 | 42 | @staticmethod 43 | def mutate(_root, _info, data): 44 | form_inst = MyDjangoModelForm(data=data) 45 | if form_inst.is_valid(): 46 | django_model_instance = form_inst.save(commit=False) 47 | # ... etc ... 48 | """ 49 | 50 | if not form_class: 51 | raise Exception("form_class is required for DjangoFormInputObjectType") 52 | 53 | form = form_class() 54 | form_fields = fields_for_form(form, only_fields, exclude_fields) 55 | 56 | for name, field in form_fields.items(): 57 | if ( 58 | object_type 59 | and name in object_type._meta.fields 60 | and isinstance(object_type._meta.fields[name], EnumValueField) 61 | ): 62 | # Field type EnumValueField here means that field 63 | # with choices have been converted to enum 64 | setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type)) 65 | elif ( 66 | object_type 67 | and name in object_type._meta.fields 68 | and object_type._meta.convert_choices_to_enum is False 69 | and form.fields[name].__class__.__name__ == "TypedChoiceField" 70 | ): 71 | # FIXME 72 | # in case if convert_choices_to_enum is False 73 | # form field class is converted to String but original 74 | # model field type is needed here... (.converter.py bug?) 75 | # This is temp workaround to get field type from ObjectType field 76 | # TEST: test_enum_not_converted_and_field_type_as_in_model 77 | setattr(cls, name, object_type._meta.fields[name].type()) 78 | else: 79 | # set input field according to django form field 80 | setattr(cls, name, field) 81 | 82 | # explicitly adding id field (absent in django form fields) 83 | # with name and type from Meta or 'id' with graphene.ID by default 84 | if add_id_field_name: 85 | setattr(cls, add_id_field_name, add_id_field_type or ID(required=False)) 86 | elif "id" not in exclude_fields: 87 | cls.id = ID(required=False) 88 | 89 | super().__init_subclass_with_meta__(container=container, _meta=_meta, **options) 90 | 91 | @staticmethod 92 | def get_enum_cnv_cls_instance(field_name, object_type): 93 | """Saves args in context to convert enum values in 94 | Dynamically created Scalar derived class 95 | """ 96 | 97 | @staticmethod 98 | def parse_value(value): 99 | # field_name & object_type have been saved in context (closure) 100 | field = object_type._meta.fields[field_name] 101 | if isinstance(field.type, graphene.NonNull): 102 | val_before_convert = field.type._of_type[value].value 103 | else: 104 | val_before_convert = field.type[value].value 105 | return graphene.String.parse_value(val_before_convert) 106 | 107 | cls_doc = "String scalar to convert choice value back from enum to original" 108 | scalar_type = type( 109 | ( 110 | f"{field_name[0].upper()}{to_camel_case(field_name[1:])}" 111 | "EnumBackConvString" 112 | ), 113 | (graphene.String,), 114 | {"parse_value": parse_value, "__doc__": cls_doc}, 115 | ) 116 | return scalar_type() 117 | -------------------------------------------------------------------------------- /graphene_django/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/management/__init__.py -------------------------------------------------------------------------------- /graphene_django/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/management/commands/__init__.py -------------------------------------------------------------------------------- /graphene_django/management/commands/graphql_schema.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import importlib 3 | import json 4 | import os 5 | 6 | from django.core.management.base import BaseCommand, CommandError 7 | from django.utils import autoreload 8 | from graphql import print_schema 9 | 10 | from graphene_django.settings import graphene_settings 11 | 12 | 13 | class CommandArguments(BaseCommand): 14 | def add_arguments(self, parser): 15 | parser.add_argument( 16 | "--schema", 17 | type=str, 18 | dest="schema", 19 | default=graphene_settings.SCHEMA, 20 | help="Django app containing schema to dump, e.g. myproject.core.schema.schema", 21 | ) 22 | 23 | parser.add_argument( 24 | "--out", 25 | type=str, 26 | dest="out", 27 | default=graphene_settings.SCHEMA_OUTPUT, 28 | help="Output file, --out=- prints to stdout (default: schema.json)", 29 | ) 30 | 31 | parser.add_argument( 32 | "--indent", 33 | type=int, 34 | dest="indent", 35 | default=graphene_settings.SCHEMA_INDENT, 36 | help="Output file indent (default: None)", 37 | ) 38 | 39 | parser.add_argument( 40 | "--watch", 41 | dest="watch", 42 | default=False, 43 | action="store_true", 44 | help="Updates the schema on file changes (default: False)", 45 | ) 46 | 47 | 48 | class Command(CommandArguments): 49 | help = "Dump Graphene schema as a JSON or GraphQL file" 50 | can_import_settings = True 51 | requires_system_checks = [] 52 | 53 | def save_json_file(self, out, schema_dict, indent): 54 | with open(out, "w") as outfile: 55 | json.dump(schema_dict, outfile, indent=indent, sort_keys=True) 56 | 57 | def save_graphql_file(self, out, schema): 58 | with open(out, "w", encoding="utf-8") as outfile: 59 | outfile.write(print_schema(schema.graphql_schema)) 60 | 61 | def get_schema(self, schema, out, indent): 62 | schema_dict = {"data": schema.introspect()} 63 | if out == "-" or out == "-.json": 64 | self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) 65 | elif out == "-.graphql": 66 | self.stdout.write(print_schema(schema.graphql_schema)) 67 | else: 68 | # Determine format 69 | _, file_extension = os.path.splitext(out) 70 | 71 | if file_extension == ".graphql": 72 | self.save_graphql_file(out, schema) 73 | elif file_extension == ".json": 74 | self.save_json_file(out, schema_dict, indent) 75 | else: 76 | raise CommandError(f'Unrecognised file format "{file_extension}"') 77 | 78 | style = getattr(self, "style", None) 79 | success = getattr(style, "SUCCESS", lambda x: x) 80 | 81 | self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}")) 82 | 83 | def handle(self, *args, **options): 84 | options_schema = options.get("schema") 85 | 86 | if options_schema and isinstance(options_schema, str): 87 | module_str, schema_name = options_schema.rsplit(".", 1) 88 | mod = importlib.import_module(module_str) 89 | schema = getattr(mod, schema_name) 90 | 91 | elif options_schema: 92 | schema = options_schema 93 | 94 | else: 95 | schema = graphene_settings.SCHEMA 96 | 97 | out = options.get("out") or graphene_settings.SCHEMA_OUTPUT 98 | 99 | if not schema: 100 | raise CommandError( 101 | "Specify schema on GRAPHENE.SCHEMA setting or by using --schema" 102 | ) 103 | 104 | indent = options.get("indent") 105 | watch = options.get("watch") 106 | if watch: 107 | autoreload.run_with_reloader( 108 | functools.partial(self.get_schema, schema, out, indent) 109 | ) 110 | else: 111 | self.get_schema(schema, out, indent) 112 | -------------------------------------------------------------------------------- /graphene_django/registry.py: -------------------------------------------------------------------------------- 1 | class Registry: 2 | def __init__(self): 3 | self._registry = {} 4 | self._field_registry = {} 5 | 6 | def register(self, cls): 7 | from .types import DjangoObjectType 8 | 9 | assert issubclass( 10 | cls, DjangoObjectType 11 | ), f'Only DjangoObjectTypes can be registered, received "{cls.__name__}"' 12 | assert cls._meta.registry == self, "Registry for a Model have to match." 13 | # assert self.get_type_for_model(cls._meta.model) == cls, ( 14 | # 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) 15 | # ) 16 | if not getattr(cls._meta, "skip_registry", False): 17 | self._registry[cls._meta.model] = cls 18 | 19 | def get_type_for_model(self, model): 20 | return self._registry.get(model) 21 | 22 | def register_converted_field(self, field, converted): 23 | self._field_registry[field] = converted 24 | 25 | def get_converted_field(self, field): 26 | return self._field_registry.get(field) 27 | 28 | 29 | registry = None 30 | 31 | 32 | def get_global_registry(): 33 | global registry 34 | if not registry: 35 | registry = Registry() 36 | return registry 37 | 38 | 39 | def reset_global_registry(): 40 | global registry 41 | registry = None 42 | -------------------------------------------------------------------------------- /graphene_django/rest_framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/rest_framework/__init__.py -------------------------------------------------------------------------------- /graphene_django/rest_framework/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class MyFakeModel(models.Model): 5 | cool_name = models.CharField(max_length=50) 6 | created = models.DateTimeField(auto_now_add=True) 7 | 8 | 9 | class MyFakeModelWithPassword(models.Model): 10 | cool_name = models.CharField(max_length=50) 11 | password = models.CharField(max_length=50) 12 | 13 | 14 | class MyFakeModelWithDate(models.Model): 15 | cool_name = models.CharField(max_length=50) 16 | last_edited = models.DateField() 17 | 18 | 19 | class MyFakeModelWithChoiceField(models.Model): 20 | class ChoiceType(models.Choices): 21 | ASDF = "asdf" 22 | HI = "hi" 23 | 24 | choice_type = models.CharField( 25 | max_length=4, 26 | default=ChoiceType.HI.name, 27 | ) 28 | -------------------------------------------------------------------------------- /graphene_django/rest_framework/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/rest_framework/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/rest_framework/tests/test_multiple_model_serializers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from rest_framework import serializers 3 | 4 | import graphene 5 | from graphene import Schema 6 | from graphene_django import DjangoObjectType 7 | from graphene_django.rest_framework.mutation import SerializerMutation 8 | 9 | 10 | class MyFakeChildModel(models.Model): 11 | name = models.CharField(max_length=50) 12 | created = models.DateTimeField(auto_now_add=True) 13 | 14 | 15 | class MyFakeParentModel(models.Model): 16 | name = models.CharField(max_length=50) 17 | created = models.DateTimeField(auto_now_add=True) 18 | child1 = models.OneToOneField( 19 | MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE 20 | ) 21 | child2 = models.OneToOneField( 22 | MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE 23 | ) 24 | 25 | 26 | class ParentType(DjangoObjectType): 27 | class Meta: 28 | model = MyFakeParentModel 29 | interfaces = (graphene.relay.Node,) 30 | fields = "__all__" 31 | 32 | 33 | class ChildType(DjangoObjectType): 34 | class Meta: 35 | model = MyFakeChildModel 36 | interfaces = (graphene.relay.Node,) 37 | fields = "__all__" 38 | 39 | 40 | class MyModelChildSerializer(serializers.ModelSerializer): 41 | class Meta: 42 | model = MyFakeChildModel 43 | fields = "__all__" 44 | 45 | 46 | class MyModelParentSerializer(serializers.ModelSerializer): 47 | child1 = MyModelChildSerializer() 48 | child2 = MyModelChildSerializer() 49 | 50 | class Meta: 51 | model = MyFakeParentModel 52 | fields = "__all__" 53 | 54 | 55 | class MyParentModelMutation(SerializerMutation): 56 | class Meta: 57 | serializer_class = MyModelParentSerializer 58 | 59 | 60 | class Mutation(graphene.ObjectType): 61 | createParentWithChild = MyParentModelMutation.Field() 62 | 63 | 64 | def test_create_schema(): 65 | schema = Schema(mutation=Mutation, types=[ParentType, ChildType]) 66 | assert schema 67 | -------------------------------------------------------------------------------- /graphene_django/rest_framework/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.unmountedtype import UnmountedType 3 | 4 | 5 | class DictType(UnmountedType): 6 | key = graphene.String() 7 | value = graphene.String() 8 | -------------------------------------------------------------------------------- /graphene_django/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for Graphene are all namespaced in the GRAPHENE setting. 3 | For example your project's `settings.py` file might look like this: 4 | GRAPHENE = { 5 | 'SCHEMA': 'my_app.schema.schema' 6 | 'MIDDLEWARE': ( 7 | 'graphene_django.debug.DjangoDebugMiddleware', 8 | ) 9 | } 10 | This module provides the `graphene_settings` object, that is used to access 11 | Graphene settings, checking for user settings first, then falling 12 | back to the defaults. 13 | """ 14 | 15 | import importlib # Available in Python 3.1+ 16 | 17 | from django.conf import settings 18 | from django.test.signals import setting_changed 19 | 20 | # Copied shamelessly from Django REST Framework 21 | 22 | DEFAULTS = { 23 | "SCHEMA": None, 24 | "SCHEMA_OUTPUT": "schema.json", 25 | "SCHEMA_INDENT": 2, 26 | "MIDDLEWARE": (), 27 | # Set to True if the connection fields must have 28 | # either the first or last argument 29 | "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, 30 | # Max items returned in ConnectionFields / FilterConnectionFields 31 | "RELAY_CONNECTION_MAX_LIMIT": 100, 32 | "CAMELCASE_ERRORS": True, 33 | # Automatically convert Choice fields of Django into Enum fields 34 | "DJANGO_CHOICE_FIELD_ENUM_CONVERT": True, 35 | # Set to True to enable v2 naming convention for choice field Enum's 36 | "DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False, 37 | "DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None, 38 | # Use a separate path for handling subscriptions. 39 | "SUBSCRIPTION_PATH": None, 40 | # By default GraphiQL headers editor tab is enabled, set to False to hide it 41 | # This sets headerEditorEnabled GraphiQL option, for details go to 42 | # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options 43 | "GRAPHIQL_HEADER_EDITOR_ENABLED": True, 44 | "GRAPHIQL_SHOULD_PERSIST_HEADERS": False, 45 | "GRAPHIQL_INPUT_VALUE_DEPRECATION": False, 46 | "ATOMIC_MUTATIONS": False, 47 | "TESTING_ENDPOINT": "/graphql", 48 | "MAX_VALIDATION_ERRORS": None, 49 | } 50 | 51 | if settings.DEBUG: 52 | DEFAULTS["MIDDLEWARE"] += ("graphene_django.debug.DjangoDebugMiddleware",) 53 | 54 | # List of settings that may be in string import notation. 55 | IMPORT_STRINGS = ("MIDDLEWARE", "SCHEMA") 56 | 57 | 58 | def perform_import(val, setting_name): 59 | """ 60 | If the given setting is a string import notation, 61 | then perform the necessary import or imports. 62 | """ 63 | if val is None: 64 | return None 65 | elif isinstance(val, str): 66 | return import_from_string(val, setting_name) 67 | elif isinstance(val, (list, tuple)): 68 | return [import_from_string(item, setting_name) for item in val] 69 | return val 70 | 71 | 72 | def import_from_string(val, setting_name): 73 | """ 74 | Attempt to import a class from a string representation. 75 | """ 76 | try: 77 | # Nod to tastypie's use of importlib. 78 | parts = val.split(".") 79 | module_path, class_name = ".".join(parts[:-1]), parts[-1] 80 | module = importlib.import_module(module_path) 81 | return getattr(module, class_name) 82 | except (ImportError, AttributeError) as e: 83 | msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format( 84 | val, 85 | setting_name, 86 | e.__class__.__name__, 87 | e, 88 | ) 89 | raise ImportError(msg) 90 | 91 | 92 | class GrapheneSettings: 93 | """ 94 | A settings object, that allows API settings to be accessed as properties. 95 | For example: 96 | from graphene_django.settings import settings 97 | print(settings.SCHEMA) 98 | Any setting with string import paths will be automatically resolved 99 | and return the class, rather than the string literal. 100 | """ 101 | 102 | def __init__(self, user_settings=None, defaults=None, import_strings=None): 103 | if user_settings: 104 | self._user_settings = user_settings 105 | self.defaults = defaults or DEFAULTS 106 | self.import_strings = import_strings or IMPORT_STRINGS 107 | 108 | @property 109 | def user_settings(self): 110 | if not hasattr(self, "_user_settings"): 111 | self._user_settings = getattr(settings, "GRAPHENE", {}) 112 | return self._user_settings 113 | 114 | def __getattr__(self, attr): 115 | if attr not in self.defaults: 116 | raise AttributeError("Invalid Graphene setting: '%s'" % attr) 117 | 118 | try: 119 | # Check if present in user settings 120 | val = self.user_settings[attr] 121 | except KeyError: 122 | # Fall back to defaults 123 | val = self.defaults[attr] 124 | 125 | # Coerce import strings into classes 126 | if attr in self.import_strings: 127 | val = perform_import(val, attr) 128 | 129 | # Cache the result 130 | setattr(self, attr, val) 131 | return val 132 | 133 | 134 | graphene_settings = GrapheneSettings(None, DEFAULTS, IMPORT_STRINGS) 135 | 136 | 137 | def reload_graphene_settings(*args, **kwargs): 138 | global graphene_settings 139 | setting, value = kwargs["setting"], kwargs["value"] 140 | if setting == "GRAPHENE": 141 | graphene_settings = GrapheneSettings(value, DEFAULTS, IMPORT_STRINGS) 142 | 143 | 144 | setting_changed.connect(reload_graphene_settings) 145 | -------------------------------------------------------------------------------- /graphene_django/static/graphene_django/graphiql.js: -------------------------------------------------------------------------------- 1 | (function ( 2 | document, 3 | 4 | GRAPHENE_SETTINGS, 5 | GraphiQL, 6 | React, 7 | ReactDOM, 8 | graphqlWs, 9 | GraphiQLPluginExplorer, 10 | fetch, 11 | history, 12 | location, 13 | ) { 14 | 15 | // Collect the URL parameters 16 | var parameters = {}; 17 | location.hash 18 | .substr(1) 19 | .split("&") 20 | .forEach(function (entry) { 21 | var eq = entry.indexOf("="); 22 | if (eq >= 0) { 23 | parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent( 24 | entry.slice(eq + 1), 25 | ); 26 | } 27 | }); 28 | // Produce a Location fragment string from a parameter object. 29 | function locationQuery(params) { 30 | return ( 31 | "#" + 32 | Object.keys(params) 33 | .map(function (key) { 34 | return ( 35 | encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) 36 | ); 37 | }) 38 | .join("&") 39 | ); 40 | } 41 | // Derive a fetch URL from the current URL, sans the GraphQL parameters. 42 | var graphqlParamNames = { 43 | query: true, 44 | variables: true, 45 | operationName: true, 46 | }; 47 | var otherParams = {}; 48 | for (var k in parameters) { 49 | if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) { 50 | otherParams[k] = parameters[k]; 51 | } 52 | } 53 | 54 | var fetchURL = locationQuery(otherParams); 55 | 56 | // Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise 57 | // assumes the current window location with an appropriate websocket protocol. 58 | var subscribeURL = 59 | location.origin.replace(/^http/, "ws") + 60 | (GRAPHENE_SETTINGS.subscriptionPath || location.pathname); 61 | 62 | function trueLambda() { return true; }; 63 | 64 | var headers = {}; 65 | var cookies = ("; " + document.cookie).split("; csrftoken="); 66 | if (cookies.length == 2) { 67 | csrftoken = cookies.pop().split(";").shift(); 68 | } else { 69 | csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; 70 | } 71 | if (csrftoken) { 72 | headers['X-CSRFToken'] = csrftoken 73 | } 74 | 75 | var graphQLFetcher = GraphiQL.createFetcher({ 76 | url: fetchURL, 77 | wsClient: graphqlWs.createClient({ 78 | url: subscribeURL, 79 | shouldRetry: trueLambda, 80 | lazy: true, 81 | }), 82 | headers: headers 83 | }) 84 | 85 | // When the query and variables string is edited, update the URL bar so 86 | // that it can be easily shared. 87 | function onEditQuery(newQuery) { 88 | parameters.query = newQuery; 89 | updateURL(); 90 | } 91 | function onEditVariables(newVariables) { 92 | parameters.variables = newVariables; 93 | updateURL(); 94 | } 95 | function onEditOperationName(newOperationName) { 96 | parameters.operationName = newOperationName; 97 | updateURL(); 98 | } 99 | function updateURL() { 100 | history.replaceState(null, null, locationQuery(parameters)); 101 | } 102 | 103 | function GraphiQLWithExplorer() { 104 | var [query, setQuery] = React.useState(parameters.query); 105 | 106 | function handleQuery(query) { 107 | setQuery(query); 108 | onEditQuery(query); 109 | } 110 | 111 | var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ 112 | query: query, 113 | onEdit: handleQuery, 114 | }); 115 | 116 | var options = { 117 | fetcher: graphQLFetcher, 118 | plugins: [explorerPlugin], 119 | defaultEditorToolsVisibility: true, 120 | onEditQuery: handleQuery, 121 | onEditVariables: onEditVariables, 122 | onEditOperationName: onEditOperationName, 123 | isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, 124 | shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, 125 | inputValueDeprecation: GRAPHENE_SETTINGS.graphiqlInputValueDeprecation, 126 | query: query, 127 | }; 128 | if (parameters.variables) { 129 | options.variables = parameters.variables; 130 | } 131 | if (parameters.operation_name) { 132 | options.operationName = parameters.operation_name; 133 | } 134 | 135 | return React.createElement(GraphiQL, options); 136 | } 137 | 138 | // Render into the body. 139 | ReactDOM.render( 140 | React.createElement(GraphiQLWithExplorer), 141 | document.getElementById("editor"), 142 | ); 143 | })( 144 | document, 145 | 146 | window.GRAPHENE_SETTINGS, 147 | window.GraphiQL, 148 | window.React, 149 | window.ReactDOM, 150 | window.graphqlWs, 151 | window.GraphiQLPluginExplorer, 152 | window.fetch, 153 | window.history, 154 | window.location, 155 | ); 156 | -------------------------------------------------------------------------------- /graphene_django/templates/graphene/graphiql.html: -------------------------------------------------------------------------------- 1 | 8 | {% load static %} 9 | 10 | 11 | 12 | 20 | 24 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 47 | 48 |
49 | {% csrf_token %} 50 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /graphene_django/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | 4 | from .models import Pet 5 | 6 | 7 | class PetForm(forms.ModelForm): 8 | class Meta: 9 | model = Pet 10 | fields = "__all__" 11 | 12 | def clean_age(self): 13 | age = self.cleaned_data["age"] 14 | if age >= 99: 15 | raise ValidationError("Too old") 16 | return age 17 | -------------------------------------------------------------------------------- /graphene_django/tests/issues/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/tests/issues/__init__.py -------------------------------------------------------------------------------- /graphene_django/tests/issues/test_520.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene-django/issues/520 2 | 3 | 4 | from django import forms 5 | from rest_framework import serializers 6 | 7 | import graphene 8 | 9 | from ...forms.mutation import DjangoFormMutation 10 | from ...rest_framework.models import MyFakeModel 11 | from ...rest_framework.mutation import SerializerMutation 12 | 13 | 14 | class MyModelSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = MyFakeModel 17 | fields = "__all__" 18 | 19 | 20 | class MyForm(forms.Form): 21 | text = forms.CharField() 22 | 23 | 24 | def test_can_use_form_and_serializer_mutations(): 25 | class MyMutation(SerializerMutation): 26 | class Meta: 27 | serializer_class = MyModelSerializer 28 | 29 | class MyFormMutation(DjangoFormMutation): 30 | class Meta: 31 | form_class = MyForm 32 | 33 | class Mutation(graphene.ObjectType): 34 | my_mutation = MyMutation.Field() 35 | my_form_mutation = MyFormMutation.Field() 36 | 37 | graphene.Schema(mutation=Mutation) 38 | -------------------------------------------------------------------------------- /graphene_django/tests/mutations.py: -------------------------------------------------------------------------------- 1 | from graphene import Field 2 | from graphene_django.forms.mutation import DjangoFormMutation, DjangoModelFormMutation 3 | 4 | from .forms import PetForm 5 | from .types import PetType 6 | 7 | 8 | class PetFormMutation(DjangoFormMutation): 9 | class Meta: 10 | form_class = PetForm 11 | 12 | 13 | class PetMutation(DjangoModelFormMutation): 14 | pet = Field(PetType) 15 | 16 | class Meta: 17 | form_class = PetForm 18 | -------------------------------------------------------------------------------- /graphene_django/tests/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import Schema, relay 3 | 4 | from ..types import DjangoObjectType 5 | from .models import Article, Reporter 6 | 7 | 8 | class Character(DjangoObjectType): 9 | class Meta: 10 | model = Reporter 11 | interfaces = (relay.Node,) 12 | fields = "__all__" 13 | 14 | def get_node(self, info, id): 15 | pass 16 | 17 | 18 | class Human(DjangoObjectType): 19 | raises = graphene.String() 20 | 21 | class Meta: 22 | model = Article 23 | interfaces = (relay.Node,) 24 | fields = "__all__" 25 | 26 | def resolve_raises(self, info): 27 | raise Exception("This field should raise exception") 28 | 29 | def get_node(self, info, id): 30 | pass 31 | 32 | 33 | class Query(graphene.ObjectType): 34 | human = graphene.Field(Human) 35 | 36 | def resolve_human(self, info): 37 | return Human() 38 | 39 | 40 | schema = Schema(query=Query) 41 | -------------------------------------------------------------------------------- /graphene_django/tests/schema_view.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import ObjectType, Schema 3 | 4 | from .mutations import PetFormMutation, PetMutation 5 | 6 | 7 | class QueryRoot(ObjectType): 8 | thrower = graphene.String(required=True) 9 | request = graphene.String(required=True) 10 | test = graphene.String(who=graphene.String()) 11 | 12 | def resolve_thrower(self, info): 13 | raise Exception("Throws!") 14 | 15 | def resolve_request(self, info): 16 | return info.context.GET.get("q") 17 | 18 | def resolve_test(self, info, who=None): 19 | return "Hello %s" % (who or "World") 20 | 21 | 22 | class MutationRoot(ObjectType): 23 | pet_form_mutation = PetFormMutation.Field() 24 | pet_mutation = PetMutation.Field() 25 | write_test = graphene.Field(QueryRoot) 26 | 27 | def resolve_write_test(self, info): 28 | return QueryRoot() 29 | 30 | 31 | schema = Schema(query=QueryRoot, mutation=MutationRoot) 32 | -------------------------------------------------------------------------------- /graphene_django/tests/test_command.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from textwrap import dedent 3 | from unittest.mock import mock_open, patch 4 | 5 | from django.core import management 6 | 7 | from graphene import ObjectType, Schema, String 8 | 9 | 10 | @patch("graphene_django.management.commands.graphql_schema.Command.save_json_file") 11 | def test_generate_json_file_on_call_graphql_schema(savefile_mock): 12 | out = StringIO() 13 | management.call_command("graphql_schema", schema="", stdout=out) 14 | assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() 15 | 16 | 17 | @patch("json.dump") 18 | def test_json_files_are_canonical(dump_mock): 19 | open_mock = mock_open() 20 | with patch("graphene_django.management.commands.graphql_schema.open", open_mock): 21 | management.call_command("graphql_schema", schema="") 22 | 23 | open_mock.assert_called_once() 24 | 25 | dump_mock.assert_called_once() 26 | assert dump_mock.call_args[1][ 27 | "sort_keys" 28 | ], "json.mock() should be used to sort the output" 29 | assert ( 30 | dump_mock.call_args[1]["indent"] > 0 31 | ), "output should be pretty-printed by default" 32 | 33 | 34 | def test_generate_graphql_file_on_call_graphql_schema(): 35 | class Query(ObjectType): 36 | hi = String() 37 | 38 | mock_schema = Schema(query=Query) 39 | 40 | open_mock = mock_open() 41 | with patch("graphene_django.management.commands.graphql_schema.open", open_mock): 42 | management.call_command( 43 | "graphql_schema", schema=mock_schema, out="schema.graphql" 44 | ) 45 | 46 | open_mock.assert_called_once() 47 | 48 | handle = open_mock() 49 | handle.write.assert_called_once() 50 | 51 | schema_output = handle.write.call_args[0][0] 52 | assert schema_output == dedent( 53 | """\ 54 | type Query { 55 | hi: String 56 | }""" 57 | ) 58 | -------------------------------------------------------------------------------- /graphene_django/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from pytest import raises 3 | 4 | from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField 5 | 6 | # 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc' 7 | 8 | 9 | def test_global_id_valid(): 10 | field = GlobalIDFormField() 11 | field.clean("TXlUeXBlOmFiYw==") 12 | 13 | 14 | def test_global_id_invalid(): 15 | field = GlobalIDFormField() 16 | with raises(ValidationError): 17 | field.clean("badvalue") 18 | 19 | 20 | def test_global_id_multiple_valid(): 21 | field = GlobalIDMultipleChoiceField() 22 | field.clean(["TXlUeXBlOmFiYw==", "TXlUeXBlOmFiYw=="]) 23 | 24 | 25 | def test_global_id_multiple_invalid(): 26 | field = GlobalIDMultipleChoiceField() 27 | with raises(ValidationError): 28 | field.clean(["badvalue", "another bad avue"]) 29 | 30 | 31 | def test_global_id_none(): 32 | field = GlobalIDFormField() 33 | with raises(ValidationError): 34 | field.clean(None) 35 | 36 | 37 | def test_global_id_none_optional(): 38 | field = GlobalIDFormField(required=False) 39 | field.clean(None) 40 | -------------------------------------------------------------------------------- /graphene_django/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from ..registry import Registry 4 | from ..types import DjangoObjectType 5 | from .models import Reporter 6 | 7 | 8 | def test_should_raise_if_no_model(): 9 | with raises(Exception) as excinfo: 10 | 11 | class Character1(DjangoObjectType): 12 | fields = "__all__" 13 | 14 | assert "valid Django Model" in str(excinfo.value) 15 | 16 | 17 | def test_should_raise_if_model_is_invalid(): 18 | with raises(Exception) as excinfo: 19 | 20 | class Character2(DjangoObjectType): 21 | class Meta: 22 | model = 1 23 | fields = "__all__" 24 | 25 | assert "valid Django Model" in str(excinfo.value) 26 | 27 | 28 | def test_should_map_fields_correctly(): 29 | class ReporterType2(DjangoObjectType): 30 | class Meta: 31 | model = Reporter 32 | registry = Registry() 33 | fields = "__all__" 34 | 35 | fields = list(ReporterType2._meta.fields.keys()) 36 | assert fields[:-3] == [ 37 | "id", 38 | "first_name", 39 | "last_name", 40 | "email", 41 | "pets", 42 | "a_choice", 43 | "typed_choice", 44 | "class_choice", 45 | "callable_choice", 46 | "fans", 47 | "reporter_type", 48 | ] 49 | 50 | assert sorted(fields[-3:]) == ["apnewsreporter", "articles", "films"] 51 | 52 | 53 | def test_should_map_only_few_fields(): 54 | class Reporter2(DjangoObjectType): 55 | class Meta: 56 | model = Reporter 57 | fields = ("id", "email") 58 | 59 | assert list(Reporter2._meta.fields.keys()) == ["id", "email"] 60 | -------------------------------------------------------------------------------- /graphene_django/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | from django.utils.translation import gettext_lazy 6 | 7 | from ..utils import GraphQLTestCase, camelize, get_model_fields, get_reverse_fields 8 | from ..utils.testing import graphql_query 9 | from .models import APNewsReporter, CNNReporter, Film, Reporter 10 | 11 | 12 | def test_get_model_fields_no_duplication(): 13 | reporter_fields = get_model_fields(Reporter) 14 | reporter_name_set = {field[0] for field in reporter_fields} 15 | assert len(reporter_fields) == len(reporter_name_set) 16 | 17 | film_fields = get_model_fields(Film) 18 | film_name_set = {field[0] for field in film_fields} 19 | assert len(film_fields) == len(film_name_set) 20 | 21 | 22 | def test_get_reverse_fields_includes_proxied_models(): 23 | reporter_fields = get_reverse_fields(Reporter, []) 24 | cnn_reporter_fields = get_reverse_fields(CNNReporter, []) 25 | ap_news_reporter_fields = get_reverse_fields(APNewsReporter, []) 26 | 27 | assert ( 28 | len(list(reporter_fields)) 29 | == len(list(cnn_reporter_fields)) 30 | == len(list(ap_news_reporter_fields)) 31 | ) 32 | 33 | 34 | def test_camelize(): 35 | assert camelize({}) == {} 36 | assert camelize("value_a") == "value_a" 37 | assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"} 38 | assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} 39 | assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} 40 | assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == { 41 | "nestedField": {"valueA": ["error"], "valueB": ["error"]} 42 | } 43 | assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"} 44 | assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]} 45 | assert camelize(gettext_lazy("value_a")) == "value_a" 46 | assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == { 47 | "valueA": "value_b" 48 | } 49 | assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}} 50 | 51 | 52 | @pytest.mark.django_db 53 | @patch("graphene_django.utils.testing.Client.post") 54 | def test_graphql_test_case_operation_name(post_mock): 55 | """ 56 | Test that `GraphQLTestCase.query()`'s `operation_name` argument produces an `operationName` field. 57 | """ 58 | 59 | class TestClass(GraphQLTestCase): 60 | GRAPHQL_SCHEMA = True 61 | 62 | def runTest(self): 63 | pass 64 | 65 | tc = TestClass() 66 | tc._pre_setup() 67 | tc.setUpClass() 68 | tc.query("query { }", operation_name="QueryName") 69 | body = json.loads(post_mock.call_args[0][1]) 70 | # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request 71 | assert ( 72 | "operationName", 73 | "QueryName", 74 | ) in body.items(), "Field 'operationName' is not present in the final request." 75 | 76 | 77 | @pytest.mark.django_db 78 | @patch("graphene_django.utils.testing.Client.post") 79 | def test_graphql_query_case_operation_name(post_mock): 80 | graphql_query("query { }", operation_name="QueryName") 81 | body = json.loads(post_mock.call_args[0][1]) 82 | # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request 83 | assert ( 84 | "operationName", 85 | "QueryName", 86 | ) in body.items(), "Field 'operationName' is not present in the final request." 87 | 88 | 89 | @pytest.fixture 90 | def client_query(client): 91 | def func(*args, **kwargs): 92 | return graphql_query(*args, client=client, **kwargs) 93 | 94 | return func 95 | 96 | 97 | def test_pytest_fixture_usage(client_query): 98 | response = client_query("query { test }") 99 | content = json.loads(response.content) 100 | assert content == {"data": {"test": "Hello World"}} 101 | -------------------------------------------------------------------------------- /graphene_django/tests/types.py: -------------------------------------------------------------------------------- 1 | from graphene_django.types import DjangoObjectType 2 | 3 | from .models import Pet 4 | 5 | 6 | class PetType(DjangoObjectType): 7 | class Meta: 8 | model = Pet 9 | fields = "__all__" 10 | -------------------------------------------------------------------------------- /graphene_django/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ..views import GraphQLView 4 | 5 | urlpatterns = [ 6 | path("graphql/batch", GraphQLView.as_view(batch=True)), 7 | path("graphql", GraphQLView.as_view(graphiql=True)), 8 | ] 9 | -------------------------------------------------------------------------------- /graphene_django/tests/urls_inherited.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ..views import GraphQLView 4 | from .schema_view import schema 5 | 6 | 7 | class CustomGraphQLView(GraphQLView): 8 | schema = schema 9 | graphiql = True 10 | pretty = True 11 | 12 | 13 | urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())] 14 | -------------------------------------------------------------------------------- /graphene_django/tests/urls_pretty.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ..views import GraphQLView 4 | from .schema_view import schema 5 | 6 | urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))] 7 | -------------------------------------------------------------------------------- /graphene_django/tests/urls_validation.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from graphene.validation import DisableIntrospection 4 | 5 | from ..views import GraphQLView 6 | from .schema_view import schema 7 | 8 | 9 | class View(GraphQLView): 10 | schema = schema 11 | 12 | 13 | class NoIntrospectionView(View): 14 | validation_rules = (DisableIntrospection,) 15 | 16 | 17 | class NoIntrospectionViewInherited(NoIntrospectionView): 18 | pass 19 | 20 | 21 | urlpatterns = [ 22 | path("graphql/", View.as_view()), 23 | path("graphql/validation/", View.as_view(validation_rules=(DisableIntrospection,))), 24 | path("graphql/validation/alternative/", NoIntrospectionView.as_view()), 25 | path("graphql/validation/inherited/", NoIntrospectionViewInherited.as_view()), 26 | ] 27 | -------------------------------------------------------------------------------- /graphene_django/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .testing import GraphQLTestCase 2 | from .utils import ( 3 | DJANGO_FILTER_INSTALLED, 4 | bypass_get_queryset, 5 | camelize, 6 | get_model_fields, 7 | get_reverse_fields, 8 | is_valid_django_model, 9 | maybe_queryset, 10 | ) 11 | 12 | __all__ = [ 13 | "DJANGO_FILTER_INSTALLED", 14 | "get_reverse_fields", 15 | "maybe_queryset", 16 | "get_model_fields", 17 | "camelize", 18 | "is_valid_django_model", 19 | "GraphQLTestCase", 20 | "bypass_get_queryset", 21 | ] 22 | -------------------------------------------------------------------------------- /graphene_django/utils/str_converters.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from text_unidecode import unidecode 4 | 5 | 6 | def to_const(string): 7 | return re.sub(r"[\W|^]+", "_", unidecode(string)).upper() 8 | -------------------------------------------------------------------------------- /graphene_django/utils/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene-django/c52cf2b0458e9c3082fc30cf260a87212d1a67b5/graphene_django/utils/tests/__init__.py -------------------------------------------------------------------------------- /graphene_django/utils/tests/test_str_converters.py: -------------------------------------------------------------------------------- 1 | from ..str_converters import to_const 2 | 3 | 4 | def test_to_const(): 5 | assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE" 6 | 7 | 8 | def test_to_const_unicode(): 9 | assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" 10 | -------------------------------------------------------------------------------- /graphene_django/utils/tests/test_testing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import Client 3 | 4 | from ...settings import graphene_settings 5 | from ...tests.test_types import with_local_registry 6 | from .. import GraphQLTestCase 7 | 8 | 9 | @with_local_registry 10 | def test_graphql_test_case_deprecated_client_getter(): 11 | """ 12 | `GraphQLTestCase._client`' getter should raise pending deprecation warning. 13 | """ 14 | 15 | class TestClass(GraphQLTestCase): 16 | GRAPHQL_SCHEMA = True 17 | 18 | def runTest(self): 19 | pass 20 | 21 | tc = TestClass() 22 | tc._pre_setup() 23 | tc.setUpClass() 24 | 25 | with pytest.warns(PendingDeprecationWarning): 26 | tc._client # noqa: B018 27 | 28 | 29 | @with_local_registry 30 | def test_graphql_test_case_deprecated_client_setter(): 31 | """ 32 | `GraphQLTestCase._client`' setter should raise pending deprecation warning. 33 | """ 34 | 35 | class TestClass(GraphQLTestCase): 36 | GRAPHQL_SCHEMA = True 37 | 38 | def runTest(self): 39 | pass 40 | 41 | tc = TestClass() 42 | tc._pre_setup() 43 | tc.setUpClass() 44 | 45 | with pytest.warns(PendingDeprecationWarning): 46 | tc._client = Client() 47 | 48 | 49 | def test_graphql_test_case_imports_endpoint(): 50 | """ 51 | GraphQLTestCase class should import the default endpoint from settings file 52 | """ 53 | 54 | assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT 55 | -------------------------------------------------------------------------------- /graphene_django/utils/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import django 4 | from django.db import connection, models, transaction 5 | from django.db.models.manager import Manager 6 | from django.utils.encoding import force_str 7 | from django.utils.functional import Promise 8 | 9 | from graphene.utils.str_converters import to_camel_case 10 | 11 | try: 12 | import django_filters # noqa 13 | 14 | DJANGO_FILTER_INSTALLED = True 15 | except ImportError: 16 | DJANGO_FILTER_INSTALLED = False 17 | 18 | 19 | def isiterable(value): 20 | try: 21 | iter(value) 22 | except TypeError: 23 | return False 24 | return True 25 | 26 | 27 | def _camelize_django_str(s): 28 | if isinstance(s, Promise): 29 | s = force_str(s) 30 | return to_camel_case(s) if isinstance(s, str) else s 31 | 32 | 33 | def camelize(data): 34 | if isinstance(data, dict): 35 | return {_camelize_django_str(k): camelize(v) for k, v in data.items()} 36 | if isiterable(data) and not isinstance(data, (str, Promise)): 37 | return [camelize(d) for d in data] 38 | return data 39 | 40 | 41 | def _get_model_ancestry(model): 42 | model_ancestry = [model] 43 | 44 | for base in model.__bases__: 45 | if is_valid_django_model(base) and getattr(base, "_meta", False): 46 | model_ancestry.append(base) 47 | return model_ancestry 48 | 49 | 50 | def get_reverse_fields(model, local_field_names): 51 | """ 52 | Searches through the model's ancestry and gets reverse relationships the models 53 | Yields a tuple of (field.name, field) 54 | """ 55 | model_ancestry = _get_model_ancestry(model) 56 | 57 | for _model in model_ancestry: 58 | for name, attr in _model.__dict__.items(): 59 | # Don't duplicate any local fields 60 | if name in local_field_names: 61 | continue 62 | 63 | # "rel" for FK and M2M relations and "related" for O2O Relations 64 | related = getattr(attr, "rel", None) or getattr(attr, "related", None) 65 | if isinstance(related, models.ManyToOneRel): 66 | yield (name, related) 67 | elif isinstance(related, models.ManyToManyRel) and not related.symmetrical: 68 | yield (name, related) 69 | 70 | 71 | def get_local_fields(model): 72 | """ 73 | Searches through the model's ancestry and gets the fields on the models 74 | Returns a dict of {field.name: field} 75 | """ 76 | model_ancestry = _get_model_ancestry(model) 77 | 78 | local_fields_dict = {} 79 | for _model in model_ancestry: 80 | for field in sorted( 81 | list(_model._meta.fields) + list(_model._meta.local_many_to_many) 82 | ): 83 | if field.name not in local_fields_dict: 84 | local_fields_dict[field.name] = field 85 | 86 | return list(local_fields_dict.items()) 87 | 88 | 89 | def maybe_queryset(value): 90 | if isinstance(value, Manager): 91 | value = value.get_queryset() 92 | return value 93 | 94 | 95 | def get_model_fields(model): 96 | """ 97 | Gets all the fields and relationships on the Django model and its ancestry. 98 | Prioritizes local fields and relationships over the reverse relationships of the same name 99 | Returns a tuple of (field.name, field) 100 | """ 101 | local_fields = get_local_fields(model) 102 | local_field_names = {field[0] for field in local_fields} 103 | reverse_fields = get_reverse_fields(model, local_field_names) 104 | all_fields = local_fields + list(reverse_fields) 105 | 106 | return all_fields 107 | 108 | 109 | def is_valid_django_model(model): 110 | return inspect.isclass(model) and issubclass(model, models.Model) 111 | 112 | 113 | def import_single_dispatch(): 114 | from functools import singledispatch 115 | 116 | return singledispatch 117 | 118 | 119 | def set_rollback(): 120 | atomic_requests = connection.settings_dict.get("ATOMIC_REQUESTS", False) 121 | if atomic_requests and connection.in_atomic_block: 122 | transaction.set_rollback(True) 123 | 124 | 125 | def bypass_get_queryset(resolver): 126 | """ 127 | Adds a bypass_get_queryset attribute to the resolver, which is used to 128 | bypass any custom get_queryset method of the DjangoObjectType. 129 | """ 130 | resolver._bypass_get_queryset = True 131 | return resolver 132 | 133 | 134 | _DJANGO_VERSION_AT_LEAST_4_2 = django.VERSION[0] > 4 or ( 135 | django.VERSION[0] >= 4 and django.VERSION[1] >= 2 136 | ) 137 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | [coverage:run] 8 | omit = */tests/* 9 | 10 | [tool:pytest] 11 | DJANGO_SETTINGS_MODULE = examples.django_test_settings 12 | addopts = --random-order 13 | filterwarnings = 14 | error 15 | # we can't do anything about the DeprecationWarning about typing.ByteString in graphql 16 | default:'typing\.ByteString' is deprecated:DeprecationWarning:graphql\.pyutils\.is_iterable 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | _version_re = re.compile(r"__version__\s+=\s+(.*)") 7 | 8 | with open("graphene_django/__init__.py", "rb") as f: 9 | version = str( 10 | ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)) 11 | ) 12 | 13 | rest_framework_require = ["djangorestframework>=3.6.3"] 14 | 15 | 16 | tests_require = [ 17 | "pytest>=7.3.1", 18 | "pytest-cov", 19 | "pytest-random-order", 20 | "coveralls", 21 | "mock", 22 | "pytz", 23 | "django-filter>=22.1", 24 | "pytest-django>=4.5.2", 25 | ] + rest_framework_require 26 | 27 | 28 | dev_requires = [ 29 | "ruff==0.1.2", 30 | "pre-commit", 31 | ] + tests_require 32 | 33 | setup( 34 | name="graphene-django", 35 | version=version, 36 | description="Graphene Django integration", 37 | long_description=open("README.md").read(), 38 | long_description_content_type="text/markdown", 39 | url="https://github.com/graphql-python/graphene-django", 40 | author="Syrus Akbary", 41 | author_email="me@syrusakbary.com", 42 | license="MIT", 43 | classifiers=[ 44 | "Development Status :: 5 - Production/Stable", 45 | "Intended Audience :: Developers", 46 | "Topic :: Software Development :: Libraries", 47 | "Programming Language :: Python :: 3", 48 | "Programming Language :: Python :: 3.8", 49 | "Programming Language :: Python :: 3.9", 50 | "Programming Language :: Python :: 3.10", 51 | "Programming Language :: Python :: 3.11", 52 | "Programming Language :: Python :: 3.12", 53 | "Programming Language :: Python :: Implementation :: PyPy", 54 | "Framework :: Django", 55 | "Framework :: Django :: 3.2", 56 | "Framework :: Django :: 4.1", 57 | "Framework :: Django :: 4.2", 58 | ], 59 | keywords="api graphql protocol rest relay graphene", 60 | packages=find_packages(exclude=["tests", "examples", "examples.*"]), 61 | install_requires=[ 62 | "graphene>=3.0,<4", 63 | "graphql-core>=3.1.0,<4", 64 | "graphql-relay>=3.1.1,<4", 65 | "Django>=3.2", 66 | "promise>=2.1", 67 | "text-unidecode", 68 | ], 69 | setup_requires=["pytest-runner"], 70 | tests_require=tests_require, 71 | rest_framework_require=rest_framework_require, 72 | extras_require={ 73 | "test": tests_require, 74 | "rest_framework": rest_framework_require, 75 | "dev": dev_requires, 76 | }, 77 | include_package_data=True, 78 | zip_safe=False, 79 | platforms="any", 80 | ) 81 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{38,39,310}-django32 4 | py{38,39}-django42 5 | py{310,311,312}-django{42,50,51,main} 6 | pre-commit 7 | 8 | [gh-actions] 9 | python = 10 | 3.8: py38 11 | 3.9: py39 12 | 3.10: py310 13 | 3.11: py311 14 | 3.12: py312 15 | 16 | [gh-actions:env] 17 | DJANGO = 18 | 3.2: django32 19 | 4.2: django42 20 | 5.0: django50 21 | 5.1: django51 22 | main: djangomain 23 | 24 | [testenv] 25 | passenv = * 26 | usedevelop = True 27 | setenv = 28 | DJANGO_SETTINGS_MODULE=examples.django_test_settings 29 | PYTHONPATH=. 30 | deps = 31 | -e.[test] 32 | psycopg2-binary 33 | django32: Django>=3.2,<4.0 34 | django42: Django>=4.2,<4.3 35 | django50: Django>=5.0,<5.1 36 | django51: Django>=5.1,<5.2 37 | djangomain: https://github.com/django/django/archive/main.zip 38 | commands = {posargs:pytest --cov=graphene_django graphene_django examples} 39 | 40 | [testenv:pre-commit] 41 | skip_install = true 42 | deps = pre-commit 43 | commands = 44 | pre-commit run {posargs:--all-files --show-diff-on-failure} 45 | --------------------------------------------------------------------------------